PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.8
VikBooking Hotel Booking Engine & PMS v1.6.8
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / site / helpers / lib.vikbooking.php

lib.vikbooking.php in VikBooking Hotel Booking Engine & PMS 1.6.8, at site/helpers/lib.vikbooking.php

13,330 lines 450.3 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').'-'.date('m').'-'.date('d'));
323
324 if (!$filters) {
325 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `dto` = 0 OR `dto` >= ".$limts.";";
326 } else {
327 if (count($rooms) == 0) {
328 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.");";
329 } else {
330 $clause = array();
331 foreach ($rooms as $idr) {
332 if (empty($idr)) continue;
333 $clause[] = "`idrooms` LIKE '%-".intval($idr)."-%'";
334 }
335 if (count($clause) > 0) {
336 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE (`dto` = 0 OR `dto` >= ".$limts.") AND (`allrooms` = 1 OR (`allrooms` = 0 AND (".implode(" OR ", $clause).")));";
337 } else {
338 $q = "SELECT * FROM `#__vikbooking_restrictions` WHERE `allrooms` = 1 AND (`dto` = 0 OR `dto` >= ".$limts.");";
339 }
340 }
341 }
342 $dbo->setQuery($q);
343 $allrestrictions = $dbo->loadAssocList();
344
345 if ($allrestrictions) {
346 foreach ($allrestrictions as $k => $res) {
347 if (!empty($res['month'])) {
348 $restrictions[$res['month']] = $res;
349 } else {
350 if (!isset($restrictions['range'])) {
351 $restrictions['range'] = array();
352 }
353 $restrictions['range'][$k] = $res;
354 }
355 }
356 }
357
358 return $restrictions;
359 }
360
361 public static function globalRestrictions($restrictions = [])
362 {
363 $ret = [];
364 foreach ($restrictions as $kr => $rr) {
365 if ($kr == 'range') {
366 foreach ($rr as $kd => $dr) {
367 if ($dr['allrooms'] == 1) {
368 $ret['range'][$kd] = $restrictions[$kr][$kd];
369 }
370 }
371 continue;
372 }
373 if ($rr['allrooms'] == 1) {
374 $ret[$kr] = $restrictions[$kr];
375 }
376 }
377 return $ret;
378 }
379
380 /**
381 * From the given restrictions, check-in, check-out and nights, looks for
382 * a restriction to be returned and applied over these stays.
383 * In order to give priority to newer Restriction IDs, the order of the
384 * $restrictions array should be ascending, so that the newer restrictions
385 * will overwrite the array with the last ID (more recent). Only for date ranges.
386 * Returns an array with the record of the restriction found. The loop is not
387 * broken when the first valid restriction is found to give priority to
388 * newer restriction IDs to overwrite older records.
389 *
390 * @param int $first the unix timestamp for the check-in date
391 * @param int $second the unix timestamp for the check-out date
392 * @param int $daysdiff the number of nights of stay
393 * @param array $restrictions the list of restrictions loaded
394 *
395 * @return array the restriction found, or an empty array.
396 */
397 public static function parseSeasonRestrictions($first, $second, $daysdiff, $restrictions) {
398 $season_restrictions = array();
399 $restrcheckin = getdate($first);
400 $restrcheckout = getdate($second);
401 if (array_key_exists($restrcheckin['mon'], $restrictions)) {
402 //restriction found for this month, checking:
403 $season_restrictions['id'] = $restrictions[$restrcheckin['mon']]['id'];
404 $season_restrictions['name'] = $restrictions[$restrcheckin['mon']]['name'];
405 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
406 if (strlen($restrictions[$restrcheckin['mon']]['wday']) > 0) {
407 //Week Day Arrival Restriction
408 $rvalidwdays = array($restrictions[$restrcheckin['mon']]['wday']);
409 if (strlen($restrictions[$restrcheckin['mon']]['wdaytwo']) > 0) {
410 $rvalidwdays[] = $restrictions[$restrcheckin['mon']]['wdaytwo'];
411 }
412 $season_restrictions['wdays'] = $rvalidwdays;
413 } elseif (!empty($restrictions[$restrcheckin['mon']]['ctad']) || !empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
414 if (!empty($restrictions[$restrcheckin['mon']]['ctad'])) {
415 $season_restrictions['cta'] = explode(',', $restrictions[$restrcheckin['mon']]['ctad']);
416 }
417 if (!empty($restrictions[$restrcheckin['mon']]['ctdd'])) {
418 $season_restrictions['ctd'] = explode(',', $restrictions[$restrcheckin['mon']]['ctdd']);
419 }
420 }
421 if (!empty($restrictions[$restrcheckin['mon']]['maxlos']) && $restrictions[$restrcheckin['mon']]['maxlos'] > 0 && $restrictions[$restrcheckin['mon']]['maxlos'] > $restrictions[$restrcheckin['mon']]['minlos']) {
422 $season_restrictions['maxlos'] = $restrictions[$restrcheckin['mon']]['maxlos'];
423 if ($daysdiff > $restrictions[$restrcheckin['mon']]['maxlos']) {
424 $season_restrictions['allowed'] = false;
425 }
426 }
427 if ($daysdiff < $restrictions[$restrcheckin['mon']]['minlos']) {
428 $season_restrictions['allowed'] = false;
429 }
430 $season_restrictions['minlos'] = $restrictions[$restrcheckin['mon']]['minlos'];
431 } elseif (array_key_exists('range', $restrictions)) {
432 foreach ($restrictions['range'] as $restr) {
433 if ($restr['dfrom'] <= $first && $restr['dto'] >= $first) {
434 //restriction found for this date range, checking:
435 $season_restrictions['id'] = $restr['id'];
436 $season_restrictions['name'] = $restr['name'];
437 $season_restrictions['allowed'] = true; //set to false when these nights are not allowed
438 if (strlen((string)$restr['wday']) > 0) {
439 //Week Day Arrival Restriction
440 $rvalidwdays = array($restr['wday']);
441 if (strlen((string)$restr['wdaytwo']) > 0) {
442 $rvalidwdays[] = $restr['wdaytwo'];
443 }
444 $season_restrictions['wdays'] = $rvalidwdays;
445 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
446 if (!empty($restr['ctad'])) {
447 $season_restrictions['cta'] = explode(',', $restr['ctad']);
448 }
449 if (!empty($restr['ctdd'])) {
450 $season_restrictions['ctd'] = explode(',', $restr['ctdd']);
451 }
452 }
453 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
454 $season_restrictions['maxlos'] = $restr['maxlos'];
455 if ($daysdiff > $restr['maxlos']) {
456 $season_restrictions['allowed'] = false;
457 }
458 }
459 if ($daysdiff < $restr['minlos']) {
460 $season_restrictions['allowed'] = false;
461 }
462 $season_restrictions['minlos'] = $restr['minlos'];
463 }
464 }
465 }
466
467 return $season_restrictions;
468 }
469
470 public static function compareSeasonRestrictionsNights($restrictions)
471 {
472 $base_compare = array();
473 $base_nights = 0;
474 foreach ($restrictions as $nights => $restr) {
475 $base_compare = $restr;
476 $base_nights = $nights;
477 break;
478 }
479
480 /**
481 * Prepare 1st hypothetical multi dimension array for string-comparison with array_diff().
482 * Keys "cta" and "ctd" may be the 2nd dimension array variables which cannot be casted to string.
483 *
484 * @since 1.15.0 (J) - 1.5.0 (WP)
485 */
486 list($casted_base_compare, $use_base_compare) = self::prepareMultiDimArrayDiff($base_compare);
487
488 foreach ($restrictions as $nights => $restr) {
489 if ($nights == $base_nights) {
490 continue;
491 }
492
493 // prepare 2nd hypothetical multi dimension array for string-comparison with array_diff().
494 list($casted_restr, $use_restr) = self::prepareMultiDimArrayDiff($restr);
495
496 // get associative array of differences
497 $diff = array_diff($use_base_compare, $use_restr);
498
499 if (count($diff) > 0 && array_key_exists('id', $diff)) {
500 // return differences only if the Restriction ID is different: ignore allowed, wdays, minlos, maxlos.
501 // only one Restriction per time should be applied to certain Season Dates but check just in case.
502 return self::restoreMultiDimArrayDiff($casted_base_compare, $casted_restr, $diff);
503 }
504 }
505
506 return array();
507 }
508
509 /**
510 * Methods using array_diff() may need one-dimension arrays, because this
511 * native functions applies a string comparison, and so casting an array
512 * value to a string would generate a Notice message. This method simply
513 * converts any sub-array into a restorable string into a non-scalar var.
514 * This only works when comparing 2 array variables.
515 *
516 * @param array $arr the hypothetical multi dimension array.
517 *
518 * @return array the one dimension array to be passed to array_diff()
519 * and the list of keys converted to strings for comparison.
520 *
521 * @since 1.15.0 (J) - 1.5.0 (WP)
522 */
523 public static function prepareMultiDimArrayDiff($arr)
524 {
525 $casted_keys = array();
526
527 if (!is_array($arr) || !count($arr)) {
528 return array($casted_keys, $arr);
529 }
530
531 foreach ($arr as $key => $val) {
532 if (!is_scalar($val)) {
533 // memorize original value
534 $casted_keys[$key] = $val;
535 // cast to string must be applied
536 $arr[$key] = json_encode($val);
537 }
538 }
539
540 return array($casted_keys, $arr);
541 }
542
543 /**
544 * Methods using array_diff() may need one-dimension arrays, because this
545 * native functions applies a string comparison, and so casting an array
546 * value to a string would generate a Notice message. This method restores
547 * the original values of the "prepared" arrays by getting the list of the
548 * modified keys from non-scalar to scalar values.
549 * This only works when comparing 2 array variables.
550 *
551 * @param array $keys_one associative array of casted key-value pairs from 1st arg.
552 * @param array $keys_two associative array of casted key-value pairs from 2nd arg.
553 * @param array $arr the array result of array_diff().
554 *
555 * @return array the array result of array_diff() with its original values.
556 *
557 * @since 1.15.0 (J) - 1.5.0 (WP)
558 */
559 public static function restoreMultiDimArrayDiff($keys_one, $keys_two, $arr)
560 {
561 if (!is_array($arr) || !count($arr)) {
562 return $arr;
563 }
564
565 foreach ($arr as $key => $val) {
566 // keys returned by array_diff can be present in both $keys_one and $keys_two
567 if (isset($keys_one[$key])) {
568 // always give higher priority to the 1st argument keys for array_diff()
569 $arr[$key] = json_decode($keys_one[$key]);
570 } elseif (isset($keys_two[$key])) {
571 // fallback to 2nd argument keys
572 $arr[$key] = json_decode($keys_two[$key]);
573 }
574 }
575
576 return $arr;
577 }
578
579 public static function roomRestrictions($roomid, $restrictions) {
580 $ret = array();
581 if (!empty($roomid) && count($restrictions) > 0) {
582 foreach ($restrictions as $kr => $rr) {
583 if ($kr == 'range') {
584 foreach ($rr as $kd => $dr) {
585 if ($dr['allrooms'] == 0 && !empty($dr['idrooms'])) {
586 $allrooms = explode(';', $dr['idrooms']);
587 if (in_array('-'.$roomid.'-', $allrooms)) {
588 $ret['range'][$kd] = $restrictions[$kr][$kd];
589 }
590 }
591 }
592 } else {
593 if ($rr['allrooms'] == 0 && !empty($rr['idrooms'])) {
594 $allrooms = explode(';', $rr['idrooms']);
595 if (in_array('-'.$roomid.'-', $allrooms)) {
596 $ret[$kr] = $restrictions[$kr];
597 }
598 }
599 }
600 }
601 }
602 return $ret;
603 }
604
605 public static function validateRoomRestriction($roomrestr, $restrcheckin, $restrcheckout, $daysdiff) {
606 $restrictionerrmsg = '';
607 $restrictions_affcount = 0;
608 if (array_key_exists($restrcheckin['mon'], $roomrestr)) {
609 //restriction found for this month, checking:
610 $restrictions_affcount++;
611 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wday'])) {
612 $rvalidwdays = array($roomrestr[$restrcheckin['mon']]['wday']);
613 if (strlen((string)$roomrestr[$restrcheckin['mon']]['wdaytwo'])) {
614 $rvalidwdays[] = $roomrestr[$restrcheckin['mon']]['wdaytwo'];
615 }
616 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
617 $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']) : ''));
618 } elseif ($roomrestr[$restrcheckin['mon']]['multiplyminlos'] == 1) {
619 if (($daysdiff % $roomrestr[$restrcheckin['mon']]['minlos']) != 0) {
620 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOS', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
621 }
622 }
623 $comborestr = self::parseJsDrangeWdayCombo($roomrestr[$restrcheckin['mon']]);
624 if (count($comborestr) > 0) {
625 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
626 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
627 $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']));
628 }
629 }
630 }
631 } elseif (!empty($roomrestr[$restrcheckin['mon']]['ctad']) || !empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
632 if (!empty($roomrestr[$restrcheckin['mon']]['ctad'])) {
633 $ctarestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctad']);
634 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
635 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTAMONTH', self::sayWeekDay($restrcheckin['wday']), self::sayMonth($restrcheckin['mon']));
636 }
637 }
638 if (!empty($roomrestr[$restrcheckin['mon']]['ctdd'])) {
639 $ctdrestrictions = explode(',', $roomrestr[$restrcheckin['mon']]['ctdd']);
640 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
641 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDMONTH', self::sayWeekDay($restrcheckout['wday']), self::sayMonth($restrcheckin['mon']));
642 }
643 }
644 }
645 if (!empty($roomrestr[$restrcheckin['mon']]['maxlos']) && $roomrestr[$restrcheckin['mon']]['maxlos'] > 0 && $roomrestr[$restrcheckin['mon']]['maxlos'] > $roomrestr[$restrcheckin['mon']]['minlos']) {
646 if ($daysdiff > $roomrestr[$restrcheckin['mon']]['maxlos']) {
647 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['maxlos']);
648 }
649 }
650 if ($daysdiff < $roomrestr[$restrcheckin['mon']]['minlos']) {
651 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDED', self::sayMonth($restrcheckin['mon']), $roomrestr[$restrcheckin['mon']]['minlos']);
652 }
653 } elseif (array_key_exists('range', $roomrestr)) {
654 $restrictionsvalid = true;
655 /**
656 * We use this map to know which restriction IDs are okay or not okay with the Min LOS.
657 * The most recent restrictions will have a higher priority over the oldest ones.
658 *
659 * @since 1.12.1
660 */
661 $minlos_priority = array(
662 'ok' => array(),
663 'nok' => array()
664 );
665 foreach ($roomrestr['range'] as $restr) {
666 /**
667 * We should not always add 82799 seconds to the end date of the restriction
668 * because if they only last for one day (like a Saturday), then $restr['dto']
669 * will be already set to the time 23:59:59.
670 *
671 * @since 1.13 (J) - 1.2.18 (WP)
672 */
673 $end_operator = date('Y-m-d', $restr['dfrom']) != date('Y-m-d', $restr['dto']) ? 82799 : 0;
674 //
675 if ($restr['dfrom'] <= $restrcheckin[0] && ($restr['dto'] + $end_operator) >= $restrcheckin[0]) {
676 // restriction found for this date range based on arrival date, check if compliant
677 $restrictions_affcount++;
678 if (strlen((string)$restr['wday']) > 0) {
679 $rvalidwdays = array($restr['wday']);
680 if (strlen((string)$restr['wdaytwo']) > 0) {
681 $rvalidwdays[] = $restr['wdaytwo'];
682 }
683 if (!in_array($restrcheckin['wday'], $rvalidwdays)) {
684 $restrictionsvalid = false;
685 $restrictionerrmsg = JText::sprintf('VBRESTRTIPWDAYARRIVALRANGE', self::sayWeekDay($restr['wday']).(strlen((string)$restr['wdaytwo']) ? '/'.self::sayWeekDay($restr['wdaytwo']) : ''));
686 } elseif ($restr['multiplyminlos'] == 1) {
687 if (($daysdiff % $restr['minlos']) != 0) {
688 $restrictionsvalid = false;
689 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMULTIPLYMINLOSRANGE', $restr['minlos']);
690 }
691 }
692 $comborestr = self::parseJsDrangeWdayCombo($restr);
693 if ($comborestr) {
694 if (array_key_exists($restrcheckin['wday'], $comborestr)) {
695 if (!in_array($restrcheckout['wday'], $comborestr[$restrcheckin['wday']])) {
696 $restrictionsvalid = false;
697 $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']));
698 }
699 }
700 }
701 } elseif (!empty($restr['ctad']) || !empty($restr['ctdd'])) {
702 if (!empty($restr['ctad'])) {
703 $ctarestrictions = explode(',', $restr['ctad']);
704 if (in_array('-'.$restrcheckin['wday'].'-', $ctarestrictions)) {
705 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTARANGE', self::sayWeekDay($restrcheckin['wday']));
706 }
707 }
708 if (!empty($restr['ctdd'])) {
709 $ctdrestrictions = explode(',', $restr['ctdd']);
710 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions) && $restrcheckout[0] <= ($restr['dto'] + $end_operator)) {
711 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', self::sayWeekDay($restrcheckout['wday']));
712 }
713 }
714 }
715 if (!empty($restr['maxlos']) && $restr['maxlos'] > 0 && $restr['maxlos'] > $restr['minlos']) {
716 if ($daysdiff > $restr['maxlos']) {
717 $restrictionsvalid = false;
718 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMAXLOSEXCEEDEDRANGE', $restr['maxlos']);
719 }
720 }
721 if ($daysdiff < $restr['minlos']) {
722 $restrictionsvalid = false;
723 $restrictionerrmsg = JText::sprintf('VBRESTRTIPMINLOSEXCEEDEDRANGE', $restr['minlos']);
724 array_push($minlos_priority['nok'], (int)$restr['id']);
725 } else {
726 array_push($minlos_priority['ok'], (int)$restr['id']);
727 }
728 } elseif ($restr['dfrom'] <= $restrcheckout[0] && ($restr['dto'] + $end_operator) >= $restrcheckout[0] && !empty($restr['ctdd'])) {
729 /**
730 * We validate the CTD restrictions depending on the check-out date.
731 *
732 * @since 1.16.3 (J) - 1.6.3 (WP)
733 */
734 $ctdrestrictions = explode(',', $restr['ctdd']);
735 if (in_array('-'.$restrcheckout['wday'].'-', $ctdrestrictions)) {
736 $restrictions_affcount++;
737 $restrictionsvalid = false;
738 $restrictionerrmsg = JText::sprintf('VBRESTRERRWDAYCTDRANGE', VikBooking::sayWeekDay($restrcheckout['wday']));
739 }
740 }
741 }
742 if (!$restrictionsvalid && count($minlos_priority['ok']) && count($minlos_priority['nok']) && max($minlos_priority['ok']) > max($minlos_priority['nok'])) {
743 // we unset the error message because a more recent restriction is allowing this MinLOS
744 $restrictionerrmsg = '';
745 }
746 }
747 //April 2017 - Check global restriction of Min LOS for TAC functions in VBO and VCM
748 if (empty($restrictionerrmsg) && count($roomrestr) && $restrictions_affcount <= 0) {
749 //Check global MinLOS (only in case there are no restrictions affecting these dates or no restrictions at all)
750 $globminlos = self::getDefaultNightsCalendar();
751 if ($globminlos > 1 && $daysdiff < $globminlos) {
752 $restrictionerrmsg = JText::sprintf('VBRESTRERRMINLOSEXCEEDEDRANGE', $globminlos);
753 }
754 }
755 //
756
757 return $restrictionerrmsg;
758 }
759
760 public static function parseJsDrangeWdayCombo($drestr) {
761 $combo = array();
762 if (strlen((string)$drestr['wday']) && strlen((string)$drestr['wdaytwo']) && !empty($drestr['wdaycombo'])) {
763 $cparts = explode(':', $drestr['wdaycombo']);
764 foreach ($cparts as $kc => $cw) {
765 if (!empty($cw)) {
766 $nowcombo = explode('-', $cw);
767 $combo[intval($nowcombo[0])][] = intval($nowcombo[1]);
768 }
769 }
770 }
771 return $combo;
772 }
773
774 public static function validateRoomPackage($pkg_id, $rooms, $numnights, $checkints, $checkoutts) {
775 $dbo = JFactory::getDbo();
776 $pkg = array();
777 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
778 $dbo->setQuery($q);
779 $dbo->execute();
780 if ($dbo->getNumRows() == 1) {
781 $pkg = $dbo->loadAssoc();
782 $vbo_tn = self::getTranslator();
783 $vbo_tn->translateContents($pkg, '#__vikbooking_packages');
784 } else {
785 return JText::translate('VBOPKGERRNOTFOUND');
786 }
787 $rooms_req = array();
788 foreach ($rooms as $num => $room) {
789 if (!empty($room['id']) && !in_array($room['id'], $rooms_req)) {
790 $rooms_req[] = $room['id'];
791 }
792 }
793 $q = "SELECT `id` FROM `#__vikbooking_packages_rooms` WHERE `idpackage`=".$pkg['id']." AND `idroom` IN (".implode(', ', $rooms_req).");";
794 $dbo->setQuery($q);
795 $dbo->execute();
796 if ($dbo->getNumRows() != count($rooms_req)) {
797 //error, not all the rooms requested are available for this package
798 return JText::translate('VBOPKGERRNOTROOM');
799 }
800 if ($numnights < $pkg['minlos'] || ($pkg['maxlos'] > 0 && $numnights > $pkg['maxlos'])) {
801 return JText::translate('VBOPKGERRNUMNIGHTS');
802 }
803 if ($checkints < $pkg['dfrom'] || $checkints > $pkg['dto']) {
804 return JText::translate('VBOPKGERRCHECKIND');
805 }
806 if ($checkoutts < $pkg['dfrom'] || ($checkoutts > $pkg['dto'] && date('Y-m-d', $pkg['dfrom']) != date('Y-m-d', $pkg['dto']))) {
807 //VBO 1.10 - we allow a check-out date after the pkg validity-end-date only if the validity dates are equal (dfrom & dto)
808 return JText::translate('VBOPKGERRCHECKOUTD');
809 }
810 if (!empty($pkg['excldates'])) {
811 //this would check if any stay date is excluded
812 //$bookdates_ts = self::getGroupDays($checkints, $checkoutts, $numnights);
813 //check just the arrival and departure dates
814 $bookdates_ts = array($checkints, $checkoutts);
815 $bookdates = array();
816 foreach ($bookdates_ts as $bookdate_ts) {
817 $info_d = getdate($bookdate_ts);
818 $bookdates[] = $info_d['mon'].'-'.$info_d['mday'].'-'.$info_d['year'];
819 }
820 $edates = explode(';', $pkg['excldates']);
821 foreach ($edates as $edate) {
822 if (!empty($edate) && in_array($edate, $bookdates)) {
823 return JText::sprintf('VBOPKGERREXCLUDEDATE', $edate);
824 }
825 }
826 }
827 return $pkg;
828 }
829
830 public static function getPackage($pkg_id) {
831 $dbo = JFactory::getDbo();
832 $pkg = array();
833 $q = "SELECT * FROM `#__vikbooking_packages` WHERE `id`='".intval($pkg_id)."';";
834 $dbo->setQuery($q);
835 $dbo->execute();
836 if ($dbo->getNumRows() == 1) {
837 $pkg = $dbo->loadAssoc();
838 }
839 return $pkg;
840 }
841
842 public static function getRoomParam($paramname, $paramstr)
843 {
844 if (empty($paramstr)) {
845 return '';
846 }
847
848 $paramarr = json_decode($paramstr, true);
849
850 if (is_array($paramarr) && isset($paramarr[$paramname])) {
851 return $paramarr[$paramname];
852 }
853
854 return '';
855 }
856
857 public static function filterNightsSeasonsCal($arr_nights) {
858 $nights = array();
859 foreach ($arr_nights as $night) {
860 if (intval(trim($night)) > 0) {
861 $nights[] = intval(trim($night));
862 }
863 }
864 sort($nights);
865 return array_unique($nights);
866 }
867
868 public static function getSeasonRangeTs($from, $to, $year) {
869 $sfrom = 0;
870 $sto = 0;
871 $tsbase = mktime(0, 0, 0, 1, 1, $year);
872 $curyear = $year;
873 $tsbasetwo = $tsbase;
874 $curyeartwo = $year;
875 if ($from > $to) {
876 //between two years
877 $curyeartwo += 1;
878 $tsbasetwo = mktime(0, 0, 0, 1, 1, $curyeartwo);
879 }
880 $sfrom = ($tsbase + $from);
881 $sto = ($tsbasetwo + $to);
882 if ($curyear % 4 == 0 && ($curyear % 100 != 0 || $curyear % 400 == 0)) {
883 //leap years
884 $infoseason = getdate($sfrom);
885 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
886 if ($infoseason[0] > $leapts) {
887 /**
888 * Timestamp must be greater than the leap-day of Feb 29th.
889 * It used to be checked for >= $leapts.
890 *
891 * @since July 3rd 2019
892 */
893 $sfrom += 86400;
894 if ($curyear == $curyeartwo) {
895 $sto += 86400;
896 }
897 }
898 } elseif ($curyeartwo % 4 == 0 && ($curyeartwo % 100 != 0 || $curyeartwo % 400 == 0)) {
899 //leap years
900 $infoseason = getdate($sto);
901 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
902 if ($infoseason[0] > $leapts) {
903 /**
904 * Timestamp must be greater than the leap-day of Feb 29th.
905 * It used to be checked for >= $leapts.
906 *
907 * @since July 3rd 2019
908 */
909 $sto += 86400;
910 }
911 }
912 return array($sfrom, $sto);
913 }
914
915 public static function sortSeasonsRangeTs ($all_seasons) {
916 $sorted = array();
917 $map = array();
918 foreach ($all_seasons as $key => $season) {
919 $map[$key] = $season['from_ts'];
920 }
921 asort($map);
922 foreach ($map as $key => $s) {
923 $sorted[] = $all_seasons[$key];
924 }
925 return $sorted;
926 }
927
928 public static function formatSeasonDates ($from_ts, $to_ts) {
929 $one = getdate($from_ts);
930 $two = getdate($to_ts);
931 $months_map = array(
932 1 => JText::translate('VBSHORTMONTHONE'),
933 2 => JText::translate('VBSHORTMONTHTWO'),
934 3 => JText::translate('VBSHORTMONTHTHREE'),
935 4 => JText::translate('VBSHORTMONTHFOUR'),
936 5 => JText::translate('VBSHORTMONTHFIVE'),
937 6 => JText::translate('VBSHORTMONTHSIX'),
938 7 => JText::translate('VBSHORTMONTHSEVEN'),
939 8 => JText::translate('VBSHORTMONTHEIGHT'),
940 9 => JText::translate('VBSHORTMONTHNINE'),
941 10 => JText::translate('VBSHORTMONTHTEN'),
942 11 => JText::translate('VBSHORTMONTHELEVEN'),
943 12 => JText::translate('VBSHORTMONTHTWELVE')
944 );
945 $mday_map = array(
946 1 => JText::translate('VBMDAYFRIST'),
947 2 => JText::translate('VBMDAYSECOND'),
948 3 => JText::translate('VBMDAYTHIRD'),
949 'generic' => JText::translate('VBMDAYNUMGEN')
950 );
951 if ($one['year'] == $two['year']) {
952 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>';
953 }
954 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'];
955 }
956
957 public static function getFirstCustDataField($custdata) {
958 $first_field = '';
959 if (strpos($custdata, JText::translate('VBDBTEXTROOMCLOSED')) !== false) {
960 //Room is closed with this booking
961 return '----';
962 }
963 $parts = explode("\n", $custdata);
964 foreach ($parts as $part) {
965 if (!empty($part)) {
966 $field = explode(':', trim($part));
967 if (!empty($field[1])) {
968 return trim($field[1]);
969 }
970 }
971 }
972 return $first_field;
973 }
974
975 /**
976 * This method composes a string to be logged for the admin
977 * to keep track of what was inside the booking before the
978 * modification. Returns a string and it uses language definitions
979 * that should be available on the front-end and back-end INI files.
980 *
981 * @param array $old_booking the array of the booking prior to the modification
982 * @param array $room_stay_dates optional list of room stay information in case of split stays.
983 *
984 * @return string text describing the situation with the booking before the changes.
985 *
986 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
987 */
988 public static function getLogBookingModification($old_booking, $room_stay_dates = [])
989 {
990 $vbo_df = self::getDateFormat();
991 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
992 $wdays_map = array(
993 JText::translate('VBWEEKDAYZERO'),
994 JText::translate('VBWEEKDAYONE'),
995 JText::translate('VBWEEKDAYTWO'),
996 JText::translate('VBWEEKDAYTHREE'),
997 JText::translate('VBWEEKDAYFOUR'),
998 JText::translate('VBWEEKDAYFIVE'),
999 JText::translate('VBWEEKDAYSIX'),
1000 );
1001 $now_info = getdate();
1002 $checkin_info = getdate($old_booking['checkin']);
1003 $checkout_info = getdate($old_booking['checkout']);
1004
1005 $datemod = $wdays_map[$now_info['wday']].', '.date($df.' H:i', $now_info[0]);
1006 $prev_nights = $old_booking['days'].' '.($old_booking['days'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1007 $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]);
1008 $prev_rooms = '';
1009
1010 $orooms_map = [];
1011 $orooms_arr = [];
1012
1013 if (isset($old_booking['rooms_info'])) {
1014 foreach ($old_booking['rooms_info'] as $oroom) {
1015 $orooms_arr[] = $oroom['name'].', '.JText::translate('VBMAILADULTS').': '.$oroom['adults'].', '.JText::translate('VBMAILCHILDREN').': '.$oroom['children'];
1016 if (!empty($oroom['idroom'])) {
1017 $orooms_map[$oroom['idroom']] = $oroom['name'];
1018 }
1019 }
1020 $prev_rooms = implode("\n", $orooms_arr);
1021 }
1022
1023 if (!empty($old_booking['split_stay']) && !empty($room_stay_dates) && count($orooms_map)) {
1024 $split_stay_prev_infos = [];
1025 foreach ($room_stay_dates as $rs_ind => $room_stay) {
1026 if (empty($room_stay['idroom']) || !isset($orooms_map[$room_stay['idroom']])) {
1027 continue;
1028 }
1029 $room_checkin = date('Y-m-d', $room_stay['checkin']);
1030 $room_checkout = date('Y-m-d', $room_stay['checkout']);
1031 $room_nights = !empty($room_stay['nights']) ? $room_stay['nights'] : 0;
1032
1033 $split_stay_descr = $orooms_map[$room_stay['idroom']];
1034 if (!empty($room_nights)) {
1035 $split_stay_descr .= ': ' . $room_nights . ' ' . ($room_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
1036 }
1037 $split_stay_descr .= ', ';
1038 $split_stay_descr .= $room_checkin . ' - ' . $room_checkout;
1039
1040 // push split stay description string
1041 $split_stay_prev_infos[] = $split_stay_descr;
1042 }
1043 if (count($split_stay_prev_infos)) {
1044 $prev_rooms .= "\n" . JText::translate('VBO_SPLIT_STAY') . ":\n" . implode("\n", $split_stay_prev_infos);
1045 }
1046 }
1047
1048 $currencyname = self::getCurrencyName();
1049 $prev_total = $currencyname.' '.self::numberFormat($old_booking['total']);
1050
1051 return JText::sprintf('VBOBOOKMODLOGSTR', $datemod, $prev_dates, $prev_rooms, $prev_total);
1052 }
1053
1054 /**
1055 * This method invokes the class
1056 * VikChannelManagerLogos (new in VCM 1.6.4) to
1057 * map the name of a channel to its corresponding logo.
1058 * The method can also be used to get an istance of the class.
1059 *
1060 * @param mixed $provenience either a string or an array with main and sub channels.
1061 * @param boolean $get_istance
1062 *
1063 * @return mixed boolean if the Class doesn't exist or if the provenience cannot be matched. Instance otherwise.
1064 */
1065 public static function getVcmChannelsLogo($provenience, $get_istance = false) {
1066 if (!file_exists(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php')) {
1067 return false;
1068 }
1069 if (!class_exists('VikChannelManagerLogos')) {
1070 require_once(VCM_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'logos.php');
1071 }
1072
1073 /**
1074 * Due to the new iCal channel we now support main and sub channels.
1075 *
1076 * @since 1.13 - VCM 1.7.0
1077 */
1078 if (is_string($provenience) && strpos($provenience, '_') !== false) {
1079 $provenience = explode('_', $provenience);
1080 }
1081 $main_channel = '';
1082 $full_channel = '';
1083 if (is_array($provenience)) {
1084 if (count($provenience) > 1) {
1085 $main_channel = $provenience[1];
1086 } else {
1087 $main_channel = $provenience[0];
1088 }
1089 if (stripos($provenience[0], 'ical') !== false) {
1090 $full_channel = implode('_', $provenience);
1091 }
1092 } else {
1093 $main_channel = $provenience;
1094 }
1095
1096 // get object instance by passing the main provenience
1097 $obj = new VikChannelManagerLogos($main_channel);
1098
1099 // update provenience with main and full channel
1100 if (!empty($full_channel)) {
1101 $obj->setProvenience($main_channel, $full_channel);
1102 }
1103
1104 // return either the instance or the logo URL for this channel source
1105 return $get_istance ? $obj : $obj->getLogoURL();
1106 }
1107
1108 public static function vcmAutoUpdate()
1109 {
1110 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1111 return -1;
1112 }
1113
1114 $vcm_auto_upd = (int)VBOFactory::getConfig()->get('vcmautoupd', 0);
1115
1116 return $vcm_auto_upd ? 1 : 0;
1117 }
1118
1119 public static function getVcmInvoker()
1120 {
1121 if (!class_exists('VboVcmInvoker')) {
1122 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "vcm.php");
1123 }
1124
1125 return new VboVcmInvoker();
1126 }
1127
1128 /**
1129 * Returns an instance of VikBooking History object.
1130 *
1131 * @param int $bid optional booking ID to bind.
1132 *
1133 * @return VboBookingHistory
1134 *
1135 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
1136 */
1137 public static function getBookingHistoryInstance($bid = 0)
1138 {
1139 if (!class_exists('VboBookingHistory')) {
1140 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "history.php");
1141 }
1142
1143 return new VboBookingHistory($bid);
1144 }
1145
1146 /**
1147 * Returns an instance of the VCMChatHandler class to handle
1148 * the messaging/chat for the given reservation ID.
1149 *
1150 * @param int $oid the ID of the booking in VBO
1151 * @param string $channel the name of the source channel
1152 *
1153 * @return mixed null if VCM is not available, VCMChatHandler instance otherwise
1154 *
1155 * @since 1.11.2 (J) - 1.1.2 (WP)
1156 * @since 1.16.0 (J) - 1.6.0 (WP) the method can also be used to require the chat handler.
1157 * @since 1.16.4 (J) - 1.6.4 (WP) preloading widgets on WP when VCM is inactive is prevented.
1158 */
1159 public static function getVcmChatInstance($oid, $channel = null)
1160 {
1161 $vcm_messaging_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'chat' . DIRECTORY_SEPARATOR . 'handler.php';
1162 if (!is_file($vcm_messaging_helper)) {
1163 // VCM is not available
1164 return null;
1165 }
1166
1167 if (VBOPlatformDetection::isWordPress() && !defined('VIKCHANNELMANAGER_LIBRARIES')) {
1168 // VCM must be inactive, prevent the loading of the instance
1169 return null;
1170 }
1171
1172 // make sure the channel name is correct as it may contain the sub-network
1173 if (!empty($channel)) {
1174 $segments = explode('_', $channel);
1175 if (count($segments) > 1) {
1176 // we take the first segment, as the second could be the source sub-network (expedia_Hotels.com)
1177 $channel = $segments[0];
1178 }
1179 }
1180
1181 // always require main file of the abstract class even if arguments are empty/invalid
1182 require_once $vcm_messaging_helper;
1183
1184 // return the instance of the class for this channel handler
1185 return VCMChatHandler::getInstance($oid, $channel);
1186 }
1187
1188 /**
1189 * Returns an instance of the VCMOpportunityHandler class to handle
1190 * the various opportunities through VCM.
1191 *
1192 * @return mixed null if VCM is not available, VCMOpportunityHandler instance otherwise
1193 *
1194 * @since 1.2.0
1195 */
1196 public static function getVcmOpportunityInstance() {
1197 $vcm_opp_helper = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'opportunity.php';
1198 if (!is_file($vcm_opp_helper)) {
1199 // VCM is not available
1200 return null;
1201 }
1202 // require main file of the class
1203 require_once $vcm_opp_helper;
1204 // return the instance of the class
1205 return VCMOpportunityHandler::getInstance();
1206 }
1207
1208 /**
1209 * Tells if the Channel Manager can handle certain reporting operations with Booking.com.
1210 *
1211 * @param array $data optional data to bind to the object.
1212 *
1213 * @return bool
1214 *
1215 * @since 1.16.8 (J) - 1.6.8 (WP) the method now relies on a recent VCM class.
1216 *
1217 * @requires VCM >= 1.8.24
1218 */
1219 public static function vcmBcomReportingSupported(array $data = [])
1220 {
1221 return class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($data)->reportingAllowed();
1222 }
1223
1224 /**
1225 * Gets a list of all channels supporting the promotions.
1226 *
1227 * @param string $key the key of the handler.
1228 *
1229 * @return mixed false if VCM is not installed, empty array otherwise.
1230 *
1231 * @requires Vik Channel Manager 1.7.1
1232 *
1233 * @since 1.3.0
1234 */
1235 public static function getPromotionHandlers($key = null)
1236 {
1237 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1238 // VCM not installed
1239 return false;
1240 }
1241
1242 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1243 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1244 return [];
1245 }
1246
1247 return VikChannelManager::getPromotionHandlers($key);
1248 }
1249
1250 /**
1251 * Gets the factors for suggesting the application of the promotions.
1252 *
1253 * @param mixed $data some optional instructions to be passed as argument.
1254 *
1255 * @return mixed false if VCM is not installed, associative array otherwise.
1256 *
1257 * @requires Vik Channel Manager 1.7.1
1258 *
1259 * @since 1.3.0
1260 */
1261 public static function getPromotionFactors($data = null)
1262 {
1263 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1264 // VCM not installed
1265 return false;
1266 }
1267
1268 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1269 // VCM is outdated
1270 return false;
1271 }
1272
1273 return VikChannelManager::getPromotionFactors($data);
1274 }
1275
1276 public static function getTheme()
1277 {
1278 return VBOFactory::getConfig()->get('theme', '');
1279 }
1280
1281 public static function getFooterOrdMail($vbo_tn = null)
1282 {
1283 $dbo = JFactory::getDbo();
1284
1285 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1286 $dbo->setQuery($q);
1287 $ft = $dbo->loadAssocList();
1288
1289 if (!is_object($vbo_tn)) {
1290 $vbo_tn = self::getTranslator();
1291 }
1292
1293 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1294
1295 return $ft ? $ft[0]['setting'] : '';
1296 }
1297
1298 public static function requireLogin()
1299 {
1300 return VBOFactory::getConfig()->getBool('requirelogin', false);
1301 }
1302
1303 public static function autoRoomUnit()
1304 {
1305 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1306 }
1307
1308 public static function todayBookings()
1309 {
1310 static $todayBookings = null;
1311
1312 if ($todayBookings === null) {
1313 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1314 }
1315
1316 return $todayBookings;
1317 }
1318
1319 public static function couponsEnabled()
1320 {
1321 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1322 }
1323
1324 public static function customersPinEnabled()
1325 {
1326 return VBOFactory::getConfig()->getBool('enablepin', false);
1327 }
1328
1329 /**
1330 * Detects the type of visitor from the user agent.
1331 * Known types are: computer, smartphone, tablet.
1332 *
1333 * @param boolean $returnua whether the type of visitor should be returned. If false
1334 * boolean is returned in case of mobile device detected.
1335 * @param boolean $loadassets whether the system should load an apposite CSS file.
1336 *
1337 * @return mixed string for the type of visitor or boolean if mobile detected.
1338 *
1339 * @since 1.0.13 - Revision September 2018
1340 */
1341 public static function detectUserAgent($returnua = false, $loadassets = true) {
1342 $session = JFactory::getSession();
1343 $sval = $session->get('vbuseragent', '');
1344 $mobiles = array('tablet', 'smartphone');
1345 if (!empty($sval)) {
1346 if ($loadassets) {
1347 self::userAgentStyleSheet($sval);
1348 }
1349 return $returnua ? $sval : in_array($sval, $mobiles);
1350 }
1351
1352 // detect visitor (device) type
1353 $visitoris = 'computer';
1354 try {
1355 if (!class_exists('MobileDetector')) {
1356 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1357 }
1358 $detector = new MobileDetector;
1359 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1360 } catch (Exception $e) {
1361 // do nothing
1362 }
1363
1364 $session->set('vbuseragent', $visitoris);
1365 if ($loadassets) {
1366 self::userAgentStyleSheet($visitoris);
1367 }
1368
1369 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1370 }
1371
1372 public static function userAgentStyleSheet($ua)
1373 {
1374 /**
1375 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1376 */
1377 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1378 return;
1379 }
1380 //
1381
1382 $document = JFactory::getDocument();
1383 /**
1384 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1385 */
1386 if ($ua == 'smartphone') {
1387 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1388 } elseif ($ua == 'tablet') {
1389 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1390 }
1391 return true;
1392 }
1393
1394 public static function loadJquery()
1395 {
1396 return VBOFactory::getConfig()->getBool('loadjquery', true);
1397 }
1398
1399 public static function loadBootstrap()
1400 {
1401 return VBOFactory::getConfig()->getBool('bootstrap', true);
1402 }
1403
1404 public static function allowMultiLanguage()
1405 {
1406 return VBOFactory::getConfig()->getBool('multilang', true);
1407 }
1408
1409 public static function getTranslator()
1410 {
1411 if (!class_exists('VikBookingTranslator')) {
1412 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1413 }
1414
1415 return new VikBookingTranslator();
1416 }
1417
1418 /**
1419 * New method's name spelled correctly. The object will be cached by default.
1420 *
1421 * @return VikBookingCustomersPin
1422 *
1423 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1424 */
1425 public static function getCPinInstance()
1426 {
1427 static $cpin_instance = null;
1428
1429 if (!$cpin_instance) {
1430 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1431
1432 $cpin_instance = new VikBookingCustomersPin();
1433 }
1434
1435 return $cpin_instance;
1436 }
1437
1438 /**
1439 * Old method's name spelled wrongly for BC.
1440 *
1441 * @return VikBookingCustomersPin
1442 */
1443 public static function getCPinIstance()
1444 {
1445 return self::getCPinInstance();
1446 }
1447
1448 /**
1449 * Returns an instance of the VikBookingTracker Class.
1450 * It is also possible to call this method to just require the library.
1451 * This is useful for the back-end to access some static methods
1452 * without tracking any data.
1453 *
1454 * @param boolean $require_only whether to return the object.
1455 *
1456 * @return VikBookingTracker
1457 *
1458 * @since 1.11
1459 */
1460 public static function getTracker($require_only = false) {
1461 if (!class_exists('VikBookingTracker')) {
1462 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1463 }
1464 return $require_only ? true : VikBookingTracker::getInstance();
1465 }
1466
1467 /**
1468 * Returns an instance of the VikBookingOperator Class.
1469 *
1470 * @return VikBookingOperator
1471 *
1472 * @since 1.11
1473 */
1474 public static function getOperatorInstance() {
1475 if (!class_exists('VikBookingOperator')) {
1476 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1477 }
1478 return VikBookingOperator::getInstance();
1479 }
1480
1481 /**
1482 * Returns an instance of the VikBookingFestivities Class.
1483 *
1484 * @return VikBookingFestivities
1485 *
1486 * @since 1.12
1487 */
1488 public static function getFestivitiesInstance() {
1489 if (!class_exists('VikBookingFestivities')) {
1490 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1491 }
1492 return VikBookingFestivities::getInstance();
1493 }
1494
1495 /**
1496 * Returns an instance of the VikBookingCriticalDates Class.
1497 *
1498 * @return VikBookingCriticalDates
1499 *
1500 * @since 1.13.5
1501 */
1502 public static function getCriticalDatesInstance() {
1503 if (!class_exists('VikBookingCriticalDates')) {
1504 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1505 }
1506 return VikBookingCriticalDates::getInstance();
1507 }
1508
1509 /**
1510 * Checks whether the chat is enabled.
1511 *
1512 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1513 *
1514 * @since 1.12
1515 */
1516 public static function chatEnabled() {
1517 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1518 return -1;
1519 }
1520 $dbo = JFactory::getDbo();
1521 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1522 $dbo->setQuery($q);
1523 $dbo->execute();
1524 if (!$dbo->getNumRows()) {
1525 // enable the chat by default if VCM is installed
1526 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1527 $dbo->setQuery($q);
1528 $dbo->execute();
1529 return 1;
1530 }
1531 $s = $dbo->loadResult();
1532 return intval($s);
1533 }
1534
1535 /**
1536 * Loads the chat parameters from the configuration.
1537 *
1538 * @return object stdClass object from decoded JSON string
1539 *
1540 * @since 1.12
1541 */
1542 public static function getChatParams() {
1543 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1544 return new stdClass;
1545 }
1546 $dbo = JFactory::getDbo();
1547 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1548 $dbo->setQuery($q);
1549 $dbo->execute();
1550 if (!$dbo->getNumRows()) {
1551 // compose default and basic chat params for the first time
1552 $basic_params = new stdClass;
1553 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1554 $basic_params->av_type = 'checkin';
1555 $basic_params->av_days = 0;
1556
1557 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1558 $dbo->setQuery($q);
1559 $dbo->execute();
1560
1561 return $basic_params;
1562 }
1563 return json_decode($dbo->loadResult());
1564 }
1565
1566 /**
1567 * Checks whether the pre-checkin is enabled.
1568 *
1569 * @return int 0 if disabled, 1 otherwise
1570 *
1571 * @since 1.12
1572 */
1573 public static function precheckinEnabled() {
1574 $dbo = JFactory::getDbo();
1575 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinenabled';";
1576 $dbo->setQuery($q);
1577 $dbo->execute();
1578 if (!$dbo->getNumRows()) {
1579 // enable the pre-checkin by default
1580 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinenabled', '1');";
1581 $dbo->setQuery($q);
1582 $dbo->execute();
1583 return 1;
1584 }
1585 $s = $dbo->loadResult();
1586 return intval($s);
1587 }
1588
1589 /**
1590 * Returns the minimum number of days in advance to
1591 * enable the pre-checkin via front-end.
1592 *
1593 * @return int the min number of days in advance for pre-checkin.
1594 *
1595 * @since 1.12
1596 */
1597 public static function precheckinMinOffset() {
1598 $dbo = JFactory::getDbo();
1599 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinminoffset';";
1600 $dbo->setQuery($q);
1601 $dbo->execute();
1602 if (!$dbo->getNumRows()) {
1603 // set the limit to 1 day before arrival by default
1604 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinminoffset', '1');";
1605 $dbo->setQuery($q);
1606 $dbo->execute();
1607 return 1;
1608 }
1609 $s = $dbo->loadResult();
1610 return intval($s);
1611 }
1612
1613 /**
1614 * Whether upselling extra services is enabled.
1615 *
1616 * @return boolean true if enabled, false otherwise.
1617 *
1618 * @since 1.13
1619 */
1620 public static function upsellingEnabled() {
1621 $dbo = JFactory::getDbo();
1622 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='upselling';";
1623 $dbo->setQuery($q);
1624 $dbo->execute();
1625 if (!$dbo->getNumRows()) {
1626 // enable the upselling feature by default
1627 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('upselling', '1');";
1628 $dbo->setQuery($q);
1629 $dbo->execute();
1630 return true;
1631 }
1632 return (intval($dbo->loadResult()) > 0);
1633 }
1634
1635 /**
1636 * Collects a list of options/extra that can be upsold for the
1637 * the rooms booked.
1638 *
1639 * @param array $upsell_data list of stdClass objects for each room booked.
1640 * @param array $info some booking details (id, checkin, checkout).
1641 * @param object $vbo_tn the translation object.
1642 *
1643 * @return array list of upsellable options for each room booked.
1644 *
1645 * @since 1.3.0
1646 */
1647 public static function loadUpsellingData($upsell_data, $info, $vbo_tn) {
1648 $dbo = JFactory::getDbo();
1649 // get all options for all rooms booked
1650 $all_room_ids = array();
1651 foreach ($upsell_data as $v) {
1652 if (!in_array($v->id, $all_room_ids)) {
1653 array_push($all_room_ids, $v->id);
1654 }
1655 }
1656 $q = "SELECT `id`, `idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $all_room_ids) . ");";
1657 $dbo->setQuery($q);
1658 $dbo->execute();
1659 if (!$dbo->getNumRows()) {
1660 // no rooms found
1661 return array();
1662 }
1663 $records = $dbo->loadAssocList();
1664 $all_options = array();
1665 $rooms_options = array();
1666 foreach ($records as $v) {
1667 $allopts = explode(';', $v['idopt']);
1668 $room_opt = array();
1669 foreach ($allopts as $o) {
1670 if (empty($o)) {
1671 continue;
1672 }
1673 if (!in_array((int)$o, $all_options)) {
1674 array_push($all_options, (int)$o);
1675 }
1676 array_push($room_opt, (int)$o);
1677 }
1678 $rooms_options[$v['id']] = $room_opt;
1679 }
1680 if (!count($all_options)) {
1681 // no options found
1682 return array();
1683 }
1684 // load all options that could be used by the booked rooms no matter what was already booked
1685 $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;";
1686 $dbo->setQuery($q);
1687 $dbo->execute();
1688 if (!$dbo->getNumRows()) {
1689 // no upsell-able options found
1690 return array();
1691 }
1692 $records = $dbo->loadAssocList();
1693 // filter options by available date and translate records
1694 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1695 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1696 $records = !is_array($records) ? array() : $records;
1697 //
1698 $tot_upsellable = 0;
1699 foreach ($upsell_data as $k => $rdata) {
1700 if (!isset($upsell_data[$k]->upsellable)) {
1701 $upsell_data[$k]->upsellable = array();
1702 }
1703 foreach ($records as $opt) {
1704 if (!empty($opt['ageintervals'])) {
1705 // upsellable options should not contain age intervals for children
1706 continue;
1707 }
1708 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
1709 // this option is not assigned to this room
1710 continue;
1711 }
1712 // check if the option is suited for this room party
1713 $clone_opt = array($opt);
1714 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
1715 if (!is_array($clone_opt) || !count($clone_opt)) {
1716 // this option is not suited for this room party
1717 continue;
1718 }
1719 //
1720 if (in_array($opt['id'], $rdata->options)) {
1721 // this option has already been booked
1722 continue;
1723 }
1724 // push this option and increase counter
1725 array_push($upsell_data[$k]->upsellable, $opt);
1726 $tot_upsellable++;
1727 }
1728 }
1729
1730 // if no upsellable options were found, we return an empty array
1731 return $tot_upsellable > 0 ? $upsell_data : array();
1732 }
1733
1734 /**
1735 * Returns the minimum days in advance for booking, by considering by default
1736 * also the property closing dates. If the property is currently closed, then
1737 * the minimum number of days in advance will be increased.
1738 *
1739 * @param bool $no_closing_dates whether to skip checking closing dates.
1740 *
1741 * @return int the number of days in advance for booking.
1742 *
1743 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
1744 * getMinDaysAdvance($skipsession = false) to:
1745 * getMinDaysAdvance($no_closing_dates = false)
1746 * This was made to return the proper number of days in advance
1747 * in case the property is currently closed.
1748 */
1749 public static function getMinDaysAdvance($no_closing_dates = false) {
1750 // cache value in static var
1751 static $getMinDaysAdvance = null;
1752
1753 if ($getMinDaysAdvance) {
1754 return $getMinDaysAdvance;
1755 }
1756 //
1757
1758 $dbo = JFactory::getDbo();
1759
1760 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
1761 $dbo->setQuery($q);
1762 $dbo->execute();
1763 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
1764
1765 // update cached var
1766 $getMinDaysAdvance = $mind;
1767
1768 if ($no_closing_dates) {
1769 // do not check the closing dates
1770 return $mind;
1771 }
1772
1773 // check if the property is currently closed
1774 $cur_closed_dates = self::getClosingDates();
1775 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
1776 $today_midnight = mktime(0, 0, 0);
1777 $closed_until = null;
1778 foreach ($cur_closed_dates as $kcd => $vcd) {
1779 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
1780 // closing period found
1781 $closed_until = $vcd['to'];
1782 break;
1783 }
1784 }
1785 if ($closed_until !== null) {
1786 // count the number of days until property is closed
1787 $mind = 0;
1788 $today_info = getdate($today_midnight);
1789 while ($today_info[0] <= $closed_until) {
1790 $mind++;
1791 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
1792 }
1793 }
1794 }
1795
1796 // update cached var
1797 $getMinDaysAdvance = $mind;
1798
1799 return $mind;
1800 }
1801
1802 public static function getDefaultNightsCalendar($skipsession = false) {
1803 if ($skipsession) {
1804 $dbo = JFactory::getDbo();
1805 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1806 $dbo->setQuery($q);
1807 $dbo->execute();
1808 $s = $dbo->loadAssocList();
1809 return (int)$s[0]['setting'];
1810 } else {
1811 $session = JFactory::getSession();
1812 $sval = $session->get('vbdefaultNightsCalendar', '');
1813 if (!empty($sval)) {
1814 return (int)$sval;
1815 } else {
1816 $dbo = JFactory::getDbo();
1817 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1818 $dbo->setQuery($q);
1819 $dbo->execute();
1820 $s = $dbo->loadAssocList();
1821 $session->set('vbdefaultNightsCalendar', $s[0]['setting']);
1822 return (int)$s[0]['setting'];
1823 }
1824 }
1825 }
1826
1827 public static function getSearchNumRooms($skipsession = true)
1828 {
1829 $dbo = JFactory::getDbo();
1830 if ($skipsession) {
1831 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1832 $dbo->setQuery($q);
1833 $dbo->execute();
1834 $s = $dbo->loadAssocList();
1835 return (int)$s[0]['setting'];
1836 }
1837 $session = JFactory::getSession();
1838 $sval = $session->get('vbsearchNumRooms', '');
1839 if (!empty($sval)) {
1840 return (int)$sval;
1841 }
1842 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1843 $dbo->setQuery($q);
1844 $dbo->execute();
1845 $s = $dbo->loadAssocList();
1846 $session->set('vbsearchNumRooms', $s[0]['setting']);
1847 return (int)$s[0]['setting'];
1848 }
1849
1850 public static function getSearchNumAdults($skipsession = true)
1851 {
1852 $dbo = JFactory::getDbo();
1853 if ($skipsession) {
1854 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1855 $dbo->setQuery($q);
1856 $dbo->execute();
1857 $s = $dbo->loadAssocList();
1858 return $s[0]['setting'];
1859 }
1860 $session = JFactory::getSession();
1861 $sval = $session->get('vbsearchNumAdults', '');
1862 if (!empty($sval)) {
1863 return $sval;
1864 }
1865 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1866 $dbo->setQuery($q);
1867 $dbo->execute();
1868 $s = $dbo->loadAssocList();
1869 $session->set('vbsearchNumAdults', $s[0]['setting']);
1870 return $s[0]['setting'];
1871 }
1872
1873 public static function getSearchNumChildren($skipsession = true)
1874 {
1875 $dbo = JFactory::getDbo();
1876 if ($skipsession) {
1877 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1878 $dbo->setQuery($q);
1879 $dbo->execute();
1880 $s = $dbo->loadAssocList();
1881 return $s[0]['setting'];
1882 }
1883 $session = JFactory::getSession();
1884 $sval = $session->get('vbsearchNumChildren', '');
1885 if (!empty($sval)) {
1886 return $sval;
1887 }
1888 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1889 $dbo->setQuery($q);
1890 $dbo->execute();
1891 $s = $dbo->loadAssocList();
1892 $session->set('vbsearchNumChildren', $s[0]['setting']);
1893 return $s[0]['setting'];
1894 }
1895
1896 public static function getSmartSearchType($skipsession = true)
1897 {
1898 $dbo = JFactory::getDbo();
1899 if ($skipsession) {
1900 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1901 $dbo->setQuery($q);
1902 $dbo->execute();
1903 $s = $dbo->loadAssocList();
1904 return $s[0]['setting'];
1905 }
1906 $session = JFactory::getSession();
1907 $sval = $session->get('vbsmartSearchType', '');
1908 if (!empty($sval)) {
1909 return $sval;
1910 }
1911 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1912 $dbo->setQuery($q);
1913 $dbo->execute();
1914 $s = $dbo->loadAssocList();
1915 $session->set('vbsmartSearchType', $s[0]['setting']);
1916 return $s[0]['setting'];
1917 }
1918
1919 /**
1920 * Returns the maximum advance booking notice (offset) as a period.
1921 * It either returns the global configuration setting, or the value
1922 * defined at room-level, if any and if requested.
1923 *
1924 * @param int $idroom optional room ID for room-level setting.
1925 *
1926 * @return string the maximum booking notice period (i.e. +1Y).
1927 *
1928 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
1929 * $idroom to support room-level settings.
1930 */
1931 public static function getMaxDateFuture($idroom = 0)
1932 {
1933 static $max_adv_book_pool = [];
1934
1935 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
1936
1937 if (isset($max_adv_book_pool[$pool_id])) {
1938 return $max_adv_book_pool[$pool_id];
1939 }
1940
1941 $config = VBOFactory::getConfig();
1942
1943 if ($pool_id === 'global') {
1944 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
1945 } else {
1946 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
1947 if (!empty($room_level_max_notice)) {
1948 // setting is defined at room-level
1949 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
1950 } else {
1951 // recurse for global setting
1952 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
1953 }
1954 }
1955
1956 return $max_adv_book_pool[$pool_id];
1957 }
1958
1959 /**
1960 * Validates the maximum advance booking notice against the requested check-in
1961 * timestamp. The method supports the validation at both room and property levels.
1962 *
1963 * @param int $checkints the check-in date timestamp.
1964 * @param int $idroom the optional room ID for room-level validation.
1965 *
1966 * @return string error date string in case of validation failure.
1967 *
1968 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
1969 */
1970 public static function validateMaxDateBookings($checkints, $idroom = 0)
1971 {
1972 $datelim = self::getMaxDateFuture($idroom);
1973 $datelim = empty($datelim) ? '+2y' : $datelim;
1974 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
1975 $quantlim = substr($datelim, -1, 1);
1976
1977 $now = getdate();
1978 if ($quantlim == 'w') {
1979 $until_ts = strtotime("+$numlim weeks") + 86399;
1980 } else {
1981 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
1982 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
1983 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
1984 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
1985 }
1986
1987 if ($until_ts > $now[0] && $checkints > $until_ts) {
1988 $vbo_df = self::getDateFormat();
1989 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1990
1991 // error, return the maximum date in the future allowed
1992 return date($df, $until_ts);
1993 }
1994
1995 // validation passed
1996 return '';
1997 }
1998
1999 public static function validateMinDaysAdvance($checkints) {
2000 $mindadv = self::getMinDaysAdvance(true);
2001 if ($mindadv > 0) {
2002 $tsinfo = getdate($checkints);
2003 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
2004 if ($checkints < $limit_ts) {
2005 return $mindadv;
2006 }
2007 }
2008
2009 return '';
2010 }
2011
2012 /**
2013 * The only supported calendar type has been changed to jQuery UI.
2014 *
2015 * @since 1.13
2016 */
2017 public static function calendarType($skipsession = false)
2018 {
2019 return 'jqueryui';
2020 }
2021
2022 public static function getSiteLogo()
2023 {
2024 static $siteLogo = null;
2025
2026 if ($siteLogo !== null) {
2027 return $siteLogo;
2028 }
2029
2030 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2031
2032 return $siteLogo;
2033 }
2034
2035 public static function getBackendLogo()
2036 {
2037 static $backLogo = null;
2038
2039 if ($backLogo !== null) {
2040 return $backLogo;
2041 }
2042
2043 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2044
2045 return $backLogo;
2046 }
2047
2048 public static function numCalendars()
2049 {
2050 static $numCalendars = null;
2051
2052 if ($numCalendars !== null) {
2053 return $numCalendars;
2054 }
2055
2056 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2057
2058 return $numCalendars;
2059 }
2060
2061 public static function getFirstWeekDay()
2062 {
2063 static $firstWeekDay = null;
2064
2065 if ($firstWeekDay !== null) {
2066 return $firstWeekDay;
2067 }
2068
2069 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2070
2071 return $firstWeekDay;
2072 }
2073
2074 public static function showPartlyReserved()
2075 {
2076 return VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2077 }
2078
2079 public static function showStatusCheckinoutOnly()
2080 {
2081 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2082 }
2083
2084 public static function getDisclaimer($vbo_tn = null)
2085 {
2086 $dbo = JFactory::getDbo();
2087
2088 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2089 $dbo->setQuery($q);
2090 $ft = $dbo->loadAssocList();
2091 if (!$ft) {
2092 return '';
2093 }
2094
2095 if (!is_object($vbo_tn)) {
2096 $vbo_tn = self::getTranslator();
2097 }
2098 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2099
2100 return $ft[0]['setting'];
2101 }
2102
2103 public static function showFooter()
2104 {
2105 return VBOFactory::getConfig()->getBool('showfooter', false);
2106 }
2107
2108 public static function getPriceName($idp, $vbo_tn = null)
2109 {
2110 $dbo = JFactory::getDbo();
2111
2112 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2113 $dbo->setQuery($q, 0, 1);
2114 $n = $dbo->loadAssocList();
2115 if ($n) {
2116 if (is_object($vbo_tn)) {
2117 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2118 }
2119 return $n[0]['name'];
2120 }
2121
2122 return "";
2123 }
2124
2125 public static function getPriceAttr($idp, $vbo_tn = null)
2126 {
2127 $dbo = JFactory::getDbo();
2128
2129 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2130 $dbo->setQuery($q, 0, 1);
2131 $n = $dbo->loadAssocList();
2132 if ($n) {
2133 if (is_object($vbo_tn)) {
2134 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2135 }
2136 return $n[0]['attr'];
2137 }
2138
2139 return "";
2140 }
2141
2142 /**
2143 * Fetches the requested rate plan information.
2144 *
2145 * @param int $idp the rate plan ID.
2146 * @param object $vbo_tn the Vik Booking translator object.
2147 *
2148 * @return array empty array or associative record.
2149 */
2150 public static function getPriceInfo($idp, $vbo_tn = null)
2151 {
2152 // cache values
2153 static $priceInfos = [];
2154
2155 $idp = (int)$idp;
2156
2157 if ($priceInfos && isset($priceInfos[$idp])) {
2158 return $priceInfos[$idp];
2159 }
2160
2161 $dbo = JFactory::getDbo();
2162
2163 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2164 $dbo->setQuery($q, 0, 1);
2165 $rplan = $dbo->loadAssocList();
2166 if (!$rplan) {
2167 return [];
2168 }
2169
2170 if (is_object($vbo_tn)) {
2171 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2172 }
2173
2174 // set cached value
2175 $priceInfos[$idp] = $rplan[0];
2176
2177 return $rplan[0];
2178 }
2179
2180 public static function getAliq($idal)
2181 {
2182 $dbo = JFactory::getDbo();
2183
2184 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2185 $dbo->setQuery($q);
2186 $aliq = $dbo->loadResult();
2187 if ($aliq) {
2188 return $aliq;
2189 }
2190
2191 return 0;
2192 }
2193
2194 public static function getTimeOpenStore($skipsession = false)
2195 {
2196 // cache value in static var
2197 static $getTimeOpenStore = null;
2198
2199 if ($getTimeOpenStore) {
2200 return $getTimeOpenStore;
2201 }
2202 //
2203
2204 $dbo = JFactory::getDbo();
2205 $session = JFactory::getSession();
2206
2207 if ($skipsession) {
2208 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2209 if (empty($setting) && $setting != "0") {
2210 return false;
2211 } else {
2212 $x = explode("-", $setting);
2213 if (!empty($x[1]) && $x[1] != "0") {
2214 $getTimeOpenStore = $x;
2215 return $x;
2216 }
2217 }
2218 } else {
2219 $sval = $session->get('vbgetTimeOpenStore', '');
2220 if (!empty($sval)) {
2221 return $sval;
2222 } else {
2223 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2224 if (empty($setting) && $setting != "0") {
2225 return false;
2226 } else {
2227 $x = explode("-", $setting);
2228 if (!empty($x[1]) && $x[1] != "0") {
2229 $session->set('vbgetTimeOpenStore', $x);
2230 $getTimeOpenStore = $x;
2231 return $x;
2232 }
2233 }
2234 }
2235 }
2236 return false;
2237 }
2238
2239 public static function getHoursMinutes($secs) {
2240 if ($secs >= 3600) {
2241 $op = $secs / 3600;
2242 $hours = floor($op);
2243 $less = $hours * 3600;
2244 $newsec = $secs - $less;
2245 $optwo = $newsec / 60;
2246 $minutes = floor($optwo);
2247 } else {
2248 $hours = "0";
2249 $optwo = $secs / 60;
2250 $minutes = floor($optwo);
2251 }
2252 $x[] = $hours;
2253 $x[] = $minutes;
2254 return $x;
2255 }
2256
2257 public static function getClosingDates()
2258 {
2259 // cache value in static var
2260 static $getClosingDates = null;
2261
2262 if (is_array($getClosingDates)) {
2263 return $getClosingDates;
2264 }
2265
2266 $dbo = JFactory::getDbo();
2267
2268 $allcd = [];
2269
2270 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2271 $dbo->setQuery($q);
2272 $s = $dbo->loadAssocList();
2273 if ($s && !empty($s[0]['setting'])) {
2274 $allcd = json_decode($s[0]['setting'], true);
2275 $allcd = is_array($allcd) ? $allcd : array();
2276 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2277 foreach ($allcd as $k => $v) {
2278 if ($v['to'] < $base_ts) {
2279 unset($allcd[$k]);
2280 }
2281 }
2282 $allcd = array_values($allcd);
2283 }
2284
2285 $getClosingDates = $allcd;
2286
2287 return $allcd;
2288 }
2289
2290 public static function parseJsClosingDates() {
2291 $cd = self::getClosingDates();
2292 if (count($cd) > 0) {
2293 $cdjs = array();
2294 foreach ($cd as $k => $v) {
2295 $cdjs[] = array(date('Y-m-d', $v['from']), date('Y-m-d', $v['to']));
2296 }
2297 return $cdjs;
2298 }
2299 return array();
2300 }
2301
2302 public static function validateClosingDates($checkints, $checkoutts, $df = null)
2303 {
2304 $cd = self::getClosingDates();
2305 if (!count($cd)) {
2306 return '';
2307 }
2308 $df = empty($df) ? 'Y-m-d' : $df;
2309 $margin_seconds = 22 * 60 * 60;
2310 foreach ($cd as $k => $v) {
2311 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2312 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2313 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2314 if ($inner_closed || $outer_closed || $middle_closed) {
2315 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2316 }
2317 }
2318 return '';
2319 }
2320
2321 /**
2322 * Whether the categories dropdown filter menu should be displayed.
2323 *
2324 * @param boolean $skipsession [optional] re-read the configuration setting.
2325 *
2326 * @return boolean
2327 */
2328 public static function showCategoriesFront($skipsession = true)
2329 {
2330 $dbo = JFactory::getDbo();
2331 if ($skipsession) {
2332 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2333 $dbo->setQuery($q);
2334 $dbo->execute();
2335 if ($dbo->getNumRows() > 0) {
2336 $s = $dbo->loadAssocList();
2337 return (intval($s[0]['setting']) == 1);
2338 }
2339 return false;
2340 }
2341 $session = JFactory::getSession();
2342 $sval = $session->get('vbshowCategoriesFront', '');
2343 if (strlen($sval)) {
2344 return (intval($sval) == 1 ? true : false);
2345 }
2346 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2347 $dbo->setQuery($q);
2348 $dbo->execute();
2349 if ($dbo->getNumRows() > 0) {
2350 $s = $dbo->loadAssocList();
2351 $session->set('vbshowCategoriesFront', $s[0]['setting']);
2352 return (intval($s[0]['setting']) == 1);
2353 }
2354 return false;
2355 }
2356
2357 /**
2358 * Whether the number of children dropdown menu should be displayed.
2359 * Defaults to skip the session values and to re-read the configuration setting.
2360 *
2361 * @param boolean $skipsession [optional] re-read the configuration setting.
2362 *
2363 * @return boolean
2364 */
2365 public static function showChildrenFront($skipsession = true)
2366 {
2367 $dbo = JFactory::getDbo();
2368 if ($skipsession) {
2369 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2370 $dbo->setQuery($q);
2371 $dbo->execute();
2372 if ($dbo->getNumRows() > 0) {
2373 $s = $dbo->loadAssocList();
2374 return (intval($s[0]['setting']) == 1);
2375 }
2376 return false;
2377 }
2378 $session = JFactory::getSession();
2379 $sval = $session->get('vbshowChildrenFront', '');
2380 if (strlen($sval)) {
2381 return (intval($sval) == 1 ? true : false);
2382 }
2383 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2384 $dbo->setQuery($q);
2385 $dbo->execute();
2386 if ($dbo->getNumRows() > 0) {
2387 $s = $dbo->loadAssocList();
2388 $session->set('vbshowChildrenFront', $s[0]['setting']);
2389 return (intval($s[0]['setting']) == 1);
2390 }
2391 return false;
2392 }
2393
2394 public static function allowBooking() {
2395 $dbo = JFactory::getDbo();
2396 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='allowbooking';";
2397 $dbo->setQuery($q);
2398 $dbo->execute();
2399 if ($dbo->getNumRows() > 0) {
2400 $s = $dbo->loadAssocList();
2401 return (intval($s[0]['setting']) == 1 ? true : false);
2402 } else {
2403 return false;
2404 }
2405 }
2406
2407 public static function getDisabledBookingMsg($vbo_tn = null) {
2408 $dbo = JFactory::getDbo();
2409 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2410 $dbo->setQuery($q);
2411 $dbo->execute();
2412 $s = $dbo->loadAssocList();
2413 if (!is_object($vbo_tn)) {
2414 $vbo_tn = self::getTranslator();
2415 }
2416 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2417 return $s[0]['setting'];
2418 }
2419
2420 /**
2421 * Returns the text for the guests allowed policy.
2422 *
2423 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2424 *
2425 * @return string either the raw or the translated policy text.
2426 *
2427 * @since 1.16.5 (WP) - 1.6.5 (WP)
2428 */
2429 public static function getGuestsAllowedPolicy($vbo_tn = null)
2430 {
2431 $dbo = JFactory::getDbo();
2432
2433 $q = $dbo->getQuery(true)
2434 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2435 ->from($dbo->qn('#__vikbooking_texts'))
2436 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2437
2438 $dbo->setQuery($q);
2439 $text_record = $dbo->loadAssocList();
2440
2441 if (!$text_record) {
2442 // missing record, insert it with empty values
2443 if (self::isAdmin()) {
2444 $record = new stdClass;
2445 $record->param = 'guests_allowed_policy';
2446 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2447 $record->setting = '';
2448 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2449 }
2450
2451 return '';
2452 }
2453
2454 if (is_object($vbo_tn)) {
2455 // translate the record only if requested
2456 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2457 }
2458
2459 return $text_record[0]['setting'];
2460 }
2461
2462 public static function getDateFormat($skipsession = true)
2463 {
2464 // cache value in static var
2465 static $getDateFormat = null;
2466
2467 if ($getDateFormat) {
2468 return $getDateFormat;
2469 }
2470
2471 $dbo = JFactory::getDbo();
2472
2473 if ($skipsession) {
2474 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2475 $dbo->setQuery($q, 0, 1);
2476 $getDateFormat = $dbo->loadResult();
2477
2478 return $getDateFormat;
2479 }
2480
2481 $session = JFactory::getSession();
2482 $sval = $session->get('vbgetDateFormat', '');
2483 if (!empty($sval)) {
2484 return $sval;
2485 }
2486
2487 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2488 $dbo->setQuery($q, 0, 1);
2489 $getDateFormat = $dbo->loadResult();
2490
2491 $session->set('vbgetDateFormat', $getDateFormat);
2492
2493 return $getDateFormat;
2494 }
2495
2496 public static function getDateSeparator($skipsession = true)
2497 {
2498 // cache value in static var
2499 static $getDateSeparator = null;
2500
2501 if ($getDateSeparator) {
2502 return $getDateSeparator;
2503 }
2504
2505 $dbo = JFactory::getDbo();
2506
2507 if ($skipsession) {
2508 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2509 $dbo->setQuery($q, 0, 1);
2510 $val = $dbo->loadResult();
2511
2512 $getDateSeparator = empty($val) ? "/" : $val;
2513
2514 return $getDateSeparator;
2515 }
2516
2517 $session = JFactory::getSession();
2518 $sval = $session->get('vbgetDateSep', '');
2519 if (!empty($sval)) {
2520 return $sval;
2521 }
2522
2523 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2524 $dbo->setQuery($q, 0, 1);
2525 $val = $dbo->loadResult();
2526
2527 $getDateSeparator = empty($val) ? "/" : $val;
2528
2529 $session->set('vbgetDateSep', $getDateSeparator);
2530
2531 return $getDateSeparator;
2532 }
2533
2534 public static function getHoursMoreRb($skipsession = false)
2535 {
2536 // cache value in static var
2537 static $getHoursMoreRb = null;
2538
2539 if ($getHoursMoreRb !== null) {
2540 return $getHoursMoreRb;
2541 }
2542
2543 $dbo = JFactory::getDbo();
2544
2545 if ($skipsession) {
2546 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2547 $dbo->setQuery($q, 0, 1);
2548 $getHoursMoreRb = (int)$dbo->loadResult();
2549
2550 return $getHoursMoreRb;
2551 }
2552
2553 $session = JFactory::getSession();
2554 $sval = $session->get('getHoursMoreRb', '');
2555 if (strlen($sval)) {
2556 return $sval;
2557 }
2558
2559 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2560 $dbo->setQuery($q, 0, 1);
2561 $getHoursMoreRb = (int)$dbo->loadResult();
2562
2563 $session->set('getHoursMoreRb', $getHoursMoreRb);
2564
2565 return $getHoursMoreRb;
2566 }
2567
2568 public static function getHoursRoomAvail()
2569 {
2570 $dbo = JFactory::getDbo();
2571
2572 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2573 $dbo->setQuery($q, 0, 1);
2574
2575 return (int)$dbo->loadResult();
2576 }
2577
2578 public static function getFrontTitle($vbo_tn = null) {
2579 // cache value in static var
2580 static $getFrontTitle = null;
2581
2582 if ($getFrontTitle) {
2583 return $getFrontTitle;
2584 }
2585 //
2586
2587 $dbo = JFactory::getDbo();
2588 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2589 $dbo->setQuery($q);
2590 $dbo->execute();
2591 $ft = $dbo->loadAssocList();
2592 if (!is_object($vbo_tn)) {
2593 $vbo_tn = self::getTranslator();
2594 }
2595 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2596
2597 $getFrontTitle = $ft[0]['setting'];
2598
2599 return $ft[0]['setting'];
2600 }
2601
2602 public static function getFrontTitleTag()
2603 {
2604 return VBOFactory::getConfig()->get('fronttitletag', '');
2605 }
2606
2607 public static function getFrontTitleTagClass()
2608 {
2609 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2610 }
2611
2612 public static function getCurrencyName()
2613 {
2614 static $currencyName = null;
2615
2616 if ($currencyName !== null) {
2617 return $currencyName;
2618 }
2619
2620 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2621
2622 return $currencyName;
2623 }
2624
2625 public static function getCurrencySymb($skipsession = false) {
2626 // cache value in static var
2627 static $getCurrencySymb = null;
2628
2629 if ($getCurrencySymb) {
2630 return $getCurrencySymb;
2631 }
2632
2633 $dbo = JFactory::getDbo();
2634
2635 if ($skipsession) {
2636 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2637 $dbo->setQuery($q, 0, 1);
2638 $csymb = $dbo->loadResult();
2639
2640 $getCurrencySymb = $csymb;
2641
2642 return $csymb;
2643 }
2644
2645 $session = JFactory::getSession();
2646 $sval = $session->get('vbgetCurrencySymb', '');
2647 if (!empty($sval)) {
2648 return $sval;
2649 }
2650
2651 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2652 $dbo->setQuery($q, 0, 1);
2653 $csymb = $dbo->loadResult();
2654
2655 // update session value, cache it and return it
2656 $session->set('vbgetCurrencySymb', $csymb);
2657 $getCurrencySymb = $csymb;
2658
2659 return $csymb;
2660 }
2661
2662 public static function getNumberFormatData($skipsession = false) {
2663 // cache value in static var
2664 static $getNumberFormatData = null;
2665
2666 if ($getNumberFormatData) {
2667 return $getNumberFormatData;
2668 }
2669 //
2670
2671 $dbo = JFactory::getDbo();
2672
2673 if ($skipsession) {
2674 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2675 $dbo->setQuery($q, 0, 1);
2676 $dbo->execute();
2677 $numfdata = $dbo->loadResult();
2678
2679 // cache value and return it
2680 $getNumberFormatData = $numfdata;
2681
2682 return $numfdata;
2683 }
2684
2685 $session = JFactory::getSession();
2686 $sval = $session->get('getNumberFormatData', '');
2687 if (!empty($sval)) {
2688 return $sval;
2689 }
2690
2691 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2692 $dbo->setQuery($q, 0, 1);
2693 $dbo->execute();
2694 $numfdata = $dbo->loadResult();
2695
2696 // update session value, cache it and return it
2697 $session->set('getNumberFormatData', $numfdata);
2698 $getNumberFormatData = $numfdata;
2699
2700 return $numfdata;
2701 }
2702
2703 /**
2704 * It is possible to hide the decimals if they are like "N.00".
2705 *
2706 * @return int 0 if disabled, 1 if enabled (default).
2707 *
2708 * @since 1.14 (J) - 1.4.0 (WP)
2709 */
2710 public static function hideEmptyDecimals()
2711 {
2712 // cache value in static var
2713 static $hideEmptyDecimals = null;
2714
2715 if ($hideEmptyDecimals !== null) {
2716 return $hideEmptyDecimals;
2717 }
2718 //
2719
2720 $dbo = JFactory::getDbo();
2721
2722 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='noemptydecimals';";
2723 $dbo->setQuery($q);
2724 $hideEmptyDecimals = $dbo->loadResult();
2725 if ($hideEmptyDecimals !== null) {
2726 $hideEmptyDecimals = (int)$hideEmptyDecimals;
2727 return $hideEmptyDecimals;
2728 }
2729
2730 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('noemptydecimals', '1');";
2731 $dbo->setQuery($q);
2732 $dbo->execute();
2733
2734 $hideEmptyDecimals = 1;
2735
2736 return $hideEmptyDecimals;
2737 }
2738
2739 public static function numberFormat($num, $skipsession = false)
2740 {
2741 $formatvals = self::getNumberFormatData($skipsession);
2742 $formatparts = explode(':', $formatvals);
2743
2744 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
2745 // number has got no decimals
2746 $formatparts[0] = 0;
2747 }
2748
2749 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
2750 }
2751
2752 /**
2753 * Given a date timestamp, returns the formatted date.
2754 *
2755 * @param int|string $ts the date representation in Unix timestamp.
2756 * @param bool $wtime if true, a formatted date-time will be returned.
2757 *
2758 * @return string the formatted date according to settings.
2759 *
2760 * @since 1.16.0 (J) - 1.6.0 (WP)
2761 */
2762 public static function formatDateTs($ts, $wtime = false)
2763 {
2764 $nowdf = self::getDateFormat();
2765 if ($nowdf == "%d/%m/%Y") {
2766 $df = 'd/m/Y';
2767 } elseif ($nowdf == "%m/%d/%Y") {
2768 $df = 'm/d/Y';
2769 } else {
2770 $df = 'Y/m/d';
2771 }
2772 $datesep = self::getDateSeparator();
2773
2774 $time_f = $wtime ? ' H:i' : '';
2775
2776 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
2777 }
2778
2779 /**
2780 * Given a country name, 3-char or 2-char code, this method attempts
2781 * to guess the best language to assign to the booking according to
2782 * what languages are installed on the website. This way, cron jobs
2783 * and any other email notification will be correctly and automatically
2784 * sent to the guest in the proper language without any manual action.
2785 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
2786 *
2787 * @param string $country the country name, 3-char or 2-char code.
2788 * @param string $locale optional locale lang tag of the guest.
2789 *
2790 * @return mixed the best lang-tag string to use or null.
2791 *
2792 * @since 1.16.0 (J) - 1.6.0 (WP)
2793 */
2794 public static function guessBookingLangFromCountry($country, $locale = null)
2795 {
2796 if (empty($country)) {
2797 return null;
2798 }
2799
2800 // get all the available languages
2801 $known_langs = self::getVboApplication()->getKnownLanguages();
2802 if (!is_array($known_langs) || !count($known_langs)) {
2803 return null;
2804 }
2805
2806 // check if the booking included a supported "locale" for the guest
2807 if (!empty($locale)) {
2808 // make the locale compatible with the language tags format
2809 $locale = str_replace('_', '-', $locale);
2810 foreach ($known_langs as $ltag => $ldet) {
2811 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
2812 // we support this language tag, so we just use it
2813 return $ltag;
2814 }
2815 }
2816 }
2817
2818 // build similarities with country-languages
2819 $similarities = array(
2820 'AU' => 'en',
2821 'GB' => 'en',
2822 'IE' => 'en',
2823 'NZ' => 'en',
2824 'US' => 'en',
2825 'CA' => array(
2826 'en',
2827 'fr',
2828 ),
2829 'CL' => 'es',
2830 'AR' => 'es',
2831 'PE' => 'es',
2832 'MX' => 'es',
2833 'CR' => 'es',
2834 'CO' => 'es',
2835 'EC' => 'es',
2836 'BO' => 'es',
2837 'CU' => 'es',
2838 'VE' => 'es',
2839 'BE' => 'fr',
2840 'LU' => 'fr',
2841 'CH' => array(
2842 'de',
2843 'it',
2844 'fr',
2845 ),
2846 'AT' => 'de',
2847 'GR' => 'el',
2848 'GL' => 'dk',
2849 );
2850
2851 // fetch values from db
2852 $dbo = JFactory::getDbo();
2853 $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);
2854 $dbo->setQuery($q, 0, 1);
2855 $dbo->execute();
2856 if (!$dbo->getNumRows()) {
2857 return null;
2858 }
2859 $country_record = $dbo->loadAssoc();
2860
2861 // assign country name/code versions
2862 $country_name = $country_record['country_name'];
2863 $country_3char = strtoupper($country_record['country_3_code']);
2864 $country_2char = strtoupper($country_record['country_2_code']);
2865
2866 // build an associative array of language tags and related match-score
2867 $langtags_score = array();
2868 foreach ($known_langs as $ltag => $ldet) {
2869 // default language tag score is 0 for no matches
2870 $langtags_score[$ltag] = 0;
2871 // get language and country codes
2872 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
2873
2874 // check matches with the installed language details
2875 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
2876 // increase language tag score
2877 $langtags_score[$ltag]++;
2878 }
2879 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
2880 // increase language tag score
2881 $langtags_score[$ltag]++;
2882 }
2883 if (!empty($ldet['locale'])) {
2884 // sanitize locale for matching the 2-char code safely
2885 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
2886 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
2887 // increase language tag score
2888 $langtags_score[$ltag]++;
2889 }
2890 }
2891 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
2892 // increase language tag score
2893 $langtags_score[$ltag]++;
2894 }
2895 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
2896 // increase language tag score
2897 $langtags_score[$ltag]++;
2898 }
2899
2900 // check language similarities between countries
2901 if (isset($similarities[$country_2char])) {
2902 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
2903 // check if language tag(s) is available for this spoken language
2904 foreach ($spoken_tags as $spoken_tag) {
2905 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
2906 // increase language tag score
2907 $langtags_score[$ltag]++;
2908 }
2909 }
2910 }
2911 }
2912
2913 // make sure at least one language tag has got some points
2914 if (max($langtags_score) === 0) {
2915 // no languages installed to honor this country
2916 return null;
2917 }
2918
2919 // sort language tag scores
2920 arsort($langtags_score);
2921
2922 // reset array pointer to the first (highest) element
2923 reset($langtags_score);
2924
2925 // return the language tag with the highest score
2926 return key($langtags_score);
2927 }
2928
2929 public static function getCurrencyCodePp() {
2930 $dbo = JFactory::getDbo();
2931 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencycodepp';";
2932 $dbo->setQuery($q);
2933 $dbo->execute();
2934 $ft = $dbo->loadAssocList();
2935 return $ft[0]['setting'];
2936 }
2937
2938 public static function getIntroMain($vbo_tn = null) {
2939 $dbo = JFactory::getDbo();
2940 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
2941 $dbo->setQuery($q);
2942 $dbo->execute();
2943 $ft = $dbo->loadAssocList();
2944 if (!is_object($vbo_tn)) {
2945 $vbo_tn = self::getTranslator();
2946 }
2947 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2948 return $ft[0]['setting'];
2949 }
2950
2951 public static function getClosingMain($vbo_tn = null) {
2952 $dbo = JFactory::getDbo();
2953 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
2954 $dbo->setQuery($q);
2955 $dbo->execute();
2956 $ft = $dbo->loadAssocList();
2957 if (!is_object($vbo_tn)) {
2958 $vbo_tn = self::getTranslator();
2959 }
2960 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2961 return $ft[0]['setting'];
2962 }
2963
2964 public static function getFullFrontTitle($vbo_tn = null) {
2965 $dbo = JFactory::getDbo();
2966 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2967 $dbo->setQuery($q);
2968 $dbo->execute();
2969 $ft = $dbo->loadAssocList();
2970 if (!is_object($vbo_tn)) {
2971 $vbo_tn = self::getTranslator();
2972 }
2973 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2974 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
2975 $dbo->setQuery($q);
2976 $dbo->execute();
2977 $fttag = $dbo->loadAssocList();
2978 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
2979 $dbo->setQuery($q);
2980 $dbo->execute();
2981 $fttagclass = $dbo->loadAssocList();
2982 if (empty($ft[0]['setting'])) {
2983 return "";
2984 } else {
2985 if (empty($fttag[0]['setting'])) {
2986 return $ft[0]['setting'] . "<br/>\n";
2987 } else {
2988 $tag = str_replace("<", "", $fttag[0]['setting']);
2989 $tag = str_replace(">", "", $tag);
2990 $tag = str_replace("/", "", $tag);
2991 $tag = trim($tag);
2992 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
2993 }
2994 }
2995 }
2996
2997 public static function dateIsValid($date) {
2998 $df = self::getDateFormat();
2999 $datesep = self::getDateSeparator();
3000 if (strlen($date) != 10) {
3001 return false;
3002 }
3003 $cur_dsep = "/";
3004 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3005 $cur_dsep = $datesep;
3006 }
3007 $x = explode($cur_dsep, $date);
3008 if ($df == "%d/%m/%Y") {
3009 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3010 return false;
3011 }
3012 } elseif ($df == "%m/%d/%Y") {
3013 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3014 return false;
3015 }
3016 } else {
3017 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3018 return false;
3019 }
3020 }
3021 return true;
3022 }
3023
3024 public static function sayDateFormat() {
3025 $dbo = JFactory::getDbo();
3026 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3027 $dbo->setQuery($q);
3028 $dbo->execute();
3029 $s = $dbo->loadAssocList();
3030 if ($s[0]['setting'] == "%d/%m/%Y") {
3031 return JText::translate('VBCONFIGONETWELVE');
3032 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3033 return JText::translate('VBCONFIGONEMDY');
3034 } else {
3035 return JText::translate('VBCONFIGONETENTHREE');
3036 }
3037 }
3038
3039 /**
3040 * Calculates the Unix timestamp from the given date and
3041 * time. Avoids DST issues thanks to mktime. With prior releases,
3042 * DST issues may occur due to the sum of seconds.
3043 *
3044 * @param string $date the date string formatted with the current settings
3045 * @param int $h hours from 0 to 23 for check-in/check-out
3046 * @param int $m minutes from 0 to 59 for check-in/check-out
3047 * @param int $s seconds from 0 to 59 for check-in/check-out
3048 *
3049 * @return int the Unix timestamp of the date
3050 *
3051 * @since 1.0.14
3052 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3053 * signature modified for $h and $m that now take 0 as default value.
3054 */
3055 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3056 $df = self::getDateFormat();
3057 $datesep = self::getDateSeparator();
3058 $cur_dsep = "/";
3059 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3060 $cur_dsep = $datesep;
3061 }
3062 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3063 // date is in Y-m-d format (with no time)
3064 $cur_dsep = '-';
3065 $df = "%Y/%m/%d";
3066 }
3067 $x = explode($cur_dsep, $date);
3068 if (!(count($x) > 2)) {
3069 return 0;
3070 }
3071 if ($df == "%d/%m/%Y") {
3072 $month = (int)$x[1];
3073 $mday = (int)$x[0];
3074 $year = (int)$x[2];
3075 } elseif ($df == "%m/%d/%Y") {
3076 $month = (int)$x[0];
3077 $mday = (int)$x[1];
3078 $year = (int)$x[2];
3079 } else {
3080 $month = (int)$x[1];
3081 $mday = (int)$x[2];
3082 $year = (int)$x[0];
3083 }
3084 $h = empty($h) ? 0 : (int)$h;
3085 $m = empty($m) ? 0 : (int)$m;
3086 $s = $s > 0 && $s <= 59 ? $s : 0;
3087
3088 return mktime($h, $m, $s, $month, $mday, $year);
3089 }
3090
3091 public static function ivaInclusa($skipsession = false) {
3092 // cache value in static var
3093 static $getTaxIncluded = null;
3094
3095 if ($getTaxIncluded !== null) {
3096 return (bool)$getTaxIncluded;
3097 }
3098
3099 $dbo = JFactory::getDbo();
3100
3101 if ($skipsession) {
3102 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3103 $dbo->setQuery($q, 0, 1);
3104 $dbo->execute();
3105 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3106
3107 // cache value and return it
3108 $getTaxIncluded = $vat_incl_data;
3109
3110 return (bool)$vat_incl_data;
3111 }
3112
3113 $session = JFactory::getSession();
3114 $sval = $session->get('getTaxIncluded', '');
3115 if (strlen($sval)) {
3116 return (bool)$sval;
3117 }
3118
3119 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3120 $dbo->setQuery($q, 0, 1);
3121 $dbo->execute();
3122 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3123
3124 // update session value, cache it and return it
3125 $session->set('getTaxIncluded', $vat_incl_data);
3126 $getTaxIncluded = $vat_incl_data;
3127
3128 return (bool)$vat_incl_data;
3129 }
3130
3131 public static function showTaxOnSummaryOnly($skipsession = false) {
3132 if ($skipsession) {
3133 $dbo = JFactory::getDbo();
3134 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3135 $dbo->setQuery($q);
3136 $dbo->execute();
3137 $s = $dbo->loadAssocList();
3138 return (intval($s[0]['setting']) == 1 ? true : false);
3139 } else {
3140 $session = JFactory::getSession();
3141 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3142 if (strlen($sval) > 0) {
3143 return (intval($sval) == 1 ? true : false);
3144 } else {
3145 $dbo = JFactory::getDbo();
3146 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3147 $dbo->setQuery($q);
3148 $dbo->execute();
3149 $s = $dbo->loadAssocList();
3150 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3151 return (intval($s[0]['setting']) == 1 ? true : false);
3152 }
3153 }
3154 }
3155
3156 public static function tokenForm() {
3157 $dbo = JFactory::getDbo();
3158 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3159 $dbo->setQuery($q);
3160 $dbo->execute();
3161 $s = $dbo->loadAssocList();
3162 return (intval($s[0]['setting']) == 1 ? true : false);
3163 }
3164
3165 public static function getPaypalAcc() {
3166 $dbo = JFactory::getDbo();
3167 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3168 $dbo->setQuery($q);
3169 $dbo->execute();
3170 $s = $dbo->loadAssocList();
3171 return $s[0]['setting'];
3172 }
3173
3174 public static function getAccPerCent() {
3175 $dbo = JFactory::getDbo();
3176 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3177 $dbo->setQuery($q);
3178 $dbo->execute();
3179 $s = $dbo->loadAssocList();
3180 return $s[0]['setting'];
3181 }
3182
3183 public static function getTypeDeposit($skipsession = false) {
3184 if ($skipsession) {
3185 $dbo = JFactory::getDbo();
3186 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3187 $dbo->setQuery($q);
3188 $dbo->execute();
3189 $s = $dbo->loadAssocList();
3190 return $s[0]['setting'];
3191 } else {
3192 $session = JFactory::getSession();
3193 $sval = $session->get('getTypeDeposit', '');
3194 if (strlen($sval) > 0) {
3195 return $sval;
3196 } else {
3197 $dbo = JFactory::getDbo();
3198 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3199 $dbo->setQuery($q);
3200 $dbo->execute();
3201 $s = $dbo->loadAssocList();
3202 $session->set('getTypeDeposit', $s[0]['setting']);
3203 return $s[0]['setting'];
3204 }
3205 }
3206 }
3207
3208 public static function multiplePayments()
3209 {
3210 return VBOFactory::getConfig()->getBool('multipay', false);
3211 }
3212
3213 public static function getAdminMail()
3214 {
3215 return VBOFactory::getConfig()->getString('adminemail', '');
3216 }
3217
3218 public static function getSenderMail()
3219 {
3220 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3221
3222 return empty($sender) ? self::getAdminMail() : $sender;
3223 }
3224
3225 public static function getPaymentName($vbo_tn = null) {
3226 $dbo = JFactory::getDbo();
3227 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3228 $dbo->setQuery($q);
3229 $dbo->execute();
3230 $s = $dbo->loadAssocList();
3231 if (!is_object($vbo_tn)) {
3232 $vbo_tn = self::getTranslator();
3233 }
3234 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3235 return $s[0]['setting'];
3236 }
3237
3238 public static function getTermsConditions($vbo_tn = null) {
3239 //VBO 1.10
3240 $dbo = JFactory::getDbo();
3241 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3242 $dbo->setQuery($q);
3243 $dbo->execute();
3244 if ($dbo->getNumRows() > 0) {
3245 $s = $dbo->loadAssocList();
3246 if (!is_object($vbo_tn)) {
3247 $vbo_tn = self::getTranslator();
3248 }
3249 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3250 } else {
3251 //the record has never been saved. Compose it with the default lang definition
3252 $timeopst = self::getTimeOpenStore(true);
3253 if (is_array($timeopst)) {
3254 $openat = self::getHoursMinutes($timeopst[0]);
3255 $closeat = self::getHoursMinutes($timeopst[1]);
3256 } else {
3257 $openat = array(12, 0);
3258 $closeat = array(10, 0);
3259 }
3260 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3261 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3262 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3263 }
3264
3265 return $s[0]['setting'];
3266 }
3267
3268 public static function getMinutesLock($conv = false)
3269 {
3270 static $minutesLock = null;
3271
3272 if ($minutesLock === null) {
3273 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3274 }
3275
3276 if (!$minutesLock) {
3277 return 0;
3278 }
3279
3280 if ($conv) {
3281 return (time() + ($minutesLock * 60));
3282 }
3283
3284 return $minutesLock;
3285 }
3286
3287 /**
3288 * Checks if the room units are temporarily locked on the given dates.
3289 *
3290 * @param int $idroom the room ID.
3291 * @param int $units the total number of room units.
3292 * @param int $first the check-in timestamp.
3293 * @param int $second the check-out timestamp.
3294 * @param bool $occupied if true, the system will also consider the booked units.
3295 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3296 *
3297 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3298 *
3299 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3300 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3301 */
3302 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3303 {
3304 $dbo = JFactory::getDbo();
3305
3306 if ($units < 1) {
3307 return false;
3308 }
3309
3310 $actnow = time();
3311
3312 // clean up expired records
3313 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3314 $dbo->setQuery($q);
3315 $dbo->execute();
3316
3317 $secdiff = $second - $first;
3318 $daysdiff = $secdiff / 86400;
3319 if (is_int($daysdiff)) {
3320 if ($daysdiff < 1) {
3321 $daysdiff = 1;
3322 }
3323 } else {
3324 if ($daysdiff < 1) {
3325 $daysdiff = 1;
3326 } else {
3327 $sum = floor($daysdiff) * 86400;
3328 $newdiff = $secdiff - $sum;
3329 $maxhmore = self::getHoursMoreRb() * 3600;
3330 if ($maxhmore >= $newdiff) {
3331 $daysdiff = floor($daysdiff);
3332 } else {
3333 $daysdiff = ceil($daysdiff);
3334 }
3335 }
3336 }
3337 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3338 $tmplock_map = [];
3339
3340 // check temporarily locked records
3341 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3342 $dbo->setQuery($check);
3343 $busy = $dbo->loadAssocList();
3344 if ($busy) {
3345 foreach ($groupdays as $kg => $gday) {
3346 $bfound = 0;
3347 foreach ($busy as $bu) {
3348 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3349 $bfound++;
3350 }
3351 }
3352 $tmplock_map[$kg] = $bfound;
3353 if ($bfound >= $units) {
3354 return false;
3355 }
3356 }
3357 }
3358
3359 if ($occupied) {
3360 // check also the busy records and some the occupied units to the temporarily locked ones
3361 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3362 $dbo->setQuery($check);
3363 $busy = $dbo->loadAssocList();
3364 if (!$busy) {
3365 return true;
3366 }
3367
3368 foreach ($groupdays as $kg => $gday) {
3369 $bfound = 0;
3370 foreach ($busy as $bu) {
3371 if (in_array($bu['id'], $skip_busy_ids)) {
3372 // Booking modification
3373 continue;
3374 }
3375 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3376 $bfound++;
3377 }
3378 }
3379 // sum units occupied to the units temporarily locked
3380 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3381 if ($bfound >= $units) {
3382 return false;
3383 }
3384 }
3385 }
3386
3387 return true;
3388 }
3389
3390 public static function getGroupDays($first, $second, $daysdiff) {
3391 $ret = array();
3392 $ret[] = $first;
3393 if ($daysdiff > 1) {
3394 $start = getdate($first);
3395 $end = getdate($second);
3396 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3397 for($i = 1; $i < $daysdiff; $i++) {
3398 $checkday = $start['mday'] + $i;
3399 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3400 if ($dayts != $endcheck) {
3401 $ret[] = $dayts;
3402 }
3403 }
3404 }
3405 $ret[] = $second;
3406 return $ret;
3407 }
3408
3409 /**
3410 * Counts the hours of difference between the current
3411 * server time and the selected check-in date and time.
3412 *
3413 * @param int $checkin_ts
3414 * @param [int] $now_ts
3415 *
3416 * @return int
3417 */
3418 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3419 $hoursdiff = 0;
3420
3421 if (empty($now_ts)) {
3422 $now_ts = time();
3423 }
3424
3425 if ($now_ts >= $checkin_ts) {
3426 return $hoursdiff;
3427 }
3428
3429 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3430
3431 return $hoursdiff;
3432 }
3433
3434 /**
3435 * Loads all the occupied records for the given rooms and offset.
3436 *
3437 * @param array $roomids the optional list of room IDs to filter.
3438 * @param int $from_ts the optional base date to use.
3439 * @param int $max_ts the optional max date to use.
3440 * @param bool $closures whether to ignore, include or exclude closures.
3441 *
3442 * @return array
3443 *
3444 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3445 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3446 */
3447 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3448 {
3449 $base_ts = empty($from_ts) ? time() : $from_ts;
3450 $busy = [];
3451 $clauses = [];
3452
3453 if (is_array($roomids) && count($roomids)) {
3454 // filter busy records only by the requested room ids
3455 $roomids = array_map(function($rid) {
3456 return (int)$rid;
3457 }, $roomids);
3458 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3459 }
3460
3461 // exclude past reservations
3462 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3463
3464 if (!empty($max_ts)) {
3465 // exclude future reservations (recommended for large datasets)
3466 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3467 }
3468
3469 if (is_bool($closures)) {
3470 // include or exclude closures
3471 if ($closures) {
3472 $clauses[] = "`o`.`closure` = 1";
3473 } else {
3474 $clauses[] = "`o`.`closure` != 1";
3475 }
3476 }
3477
3478 $dbo = JFactory::getDbo();
3479
3480 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3481 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3482 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3483 WHERE " . implode(' AND ', $clauses) . ";";
3484 $dbo->setQuery($q);
3485 $allbusy = $dbo->loadAssocList();
3486 if ($allbusy) {
3487 foreach ($allbusy as $br) {
3488 if (!isset($busy[$br['idroom']])) {
3489 $busy[$br['idroom']] = [];
3490 }
3491 $busy[$br['idroom']][] = $br;
3492 }
3493 }
3494
3495 return $busy;
3496 }
3497
3498 /**
3499 * Loads all the busy records by excluding the closures.
3500 * Built for the page dashboard to calculate the actual
3501 * rooms occupancy by excluding the rooms closed.
3502 *
3503 * @param array $roomids the optional list of room IDs to filter.
3504 * @param int $from_ts the optional base date to use.
3505 * @param int $max_ts the optional max date to use.
3506 *
3507 * @return array the list of busy records.
3508 *
3509 * @since 1.12
3510 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3511 */
3512 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3513 {
3514 if (!is_array($roomids) || !count($roomids)) {
3515 return [];
3516 }
3517
3518 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3519 }
3520
3521 public static function loadBookingBusyIds($idorder) {
3522 $busy = array();
3523 if (empty($idorder)) {
3524 return $busy;
3525 }
3526 $dbo = JFactory::getDbo();
3527 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$idorder.";";
3528 $dbo->setQuery($q);
3529 $allbusy = $dbo->loadAssocList();
3530 if ($allbusy) {
3531 foreach ($allbusy as $b) {
3532 array_push($busy, $b['idbusy']);
3533 }
3534 }
3535 return $busy;
3536 }
3537
3538 public static function loadLockedRecords($roomids, $ts = 0) {
3539 $dbo = JFactory::getDbo();
3540 $actnow = empty($ts) ? time() : $ts;
3541 $locked = array();
3542 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3543 $dbo->setQuery($q);
3544 $dbo->execute();
3545 if (!is_array($roomids) || !(count($roomids) > 0)) {
3546 return $locked;
3547 }
3548 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3549 $dbo->setQuery($check);
3550 $all_locked = $dbo->loadAssocList();
3551 if ($all_locked) {
3552 foreach ($all_locked as $kb => $br) {
3553 $locked[$br['idroom']][$kb] = $br;
3554 }
3555 }
3556 return $locked;
3557 }
3558
3559 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3560 $bookings = [];
3561 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3562 return $bookings;
3563 }
3564 $dbo = JFactory::getDbo();
3565 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3566 $dbo->setQuery($q);
3567 $all_booking_ids = $dbo->loadAssocList();
3568 if ($all_booking_ids) {
3569 $oids = array();
3570 foreach ($all_booking_ids as $bid) {
3571 $oids[] = $bid['idorder'];
3572 }
3573 $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` ".
3574 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3575 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3576 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3577 "ORDER BY `o`.`checkout` ASC;";
3578 $dbo->setQuery($q);
3579 $bookings = $dbo->loadAssocList();
3580 if (!$bookings) {
3581 $bookings = [];
3582 }
3583 }
3584 return $bookings;
3585 }
3586
3587 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3588 {
3589 $dbo = JFactory::getDbo();
3590
3591 if ($units < 1) {
3592 return false;
3593 }
3594
3595 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units', 'img']);
3596 if (!$room_info) {
3597 return false;
3598 }
3599
3600 $secdiff = $second - $first;
3601 $daysdiff = $secdiff / 86400;
3602 if (is_int($daysdiff)) {
3603 if ($daysdiff < 1) {
3604 $daysdiff = 1;
3605 }
3606 } else {
3607 if ($daysdiff < 1) {
3608 $daysdiff = 1;
3609 } else {
3610 $sum = floor($daysdiff) * 86400;
3611 $newdiff = $secdiff - $sum;
3612 $maxhmore = self::getHoursMoreRb() * 3600;
3613 if ($maxhmore >= $newdiff) {
3614 $daysdiff = floor($daysdiff);
3615 } else {
3616 $daysdiff = ceil($daysdiff);
3617 }
3618 }
3619 }
3620
3621 $q = $dbo->getQuery(true)
3622 ->select($dbo->qn([
3623 'id',
3624 'checkin',
3625 'realback',
3626 ]))
3627 ->from($dbo->qn('#__vikbooking_busy'))
3628 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3629 ->where($dbo->qn('realback') . ' >= ' . $first);
3630
3631 $dbo->setQuery($q);
3632 $busy = $dbo->loadAssocList();
3633 if (!$busy) {
3634 return ($units <= $room_info['units']);
3635 }
3636
3637 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3638 foreach ($groupdays as $gday) {
3639 $bfound = 0;
3640 foreach ($busy as $bu) {
3641 if (in_array($bu['id'], $skip_busy_ids)) {
3642 // booking modification
3643 continue;
3644 }
3645 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3646 $bfound++;
3647 }
3648 }
3649 if ($bfound >= $units) {
3650 return false;
3651 }
3652 }
3653
3654 return ($units <= $room_info['units']);
3655 }
3656
3657 public static function payTotal()
3658 {
3659 return VBOFactory::getConfig()->getBool('paytotal', true);
3660 }
3661
3662 public static function getDepositIfDays()
3663 {
3664 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3665 }
3666
3667 public static function depositAllowedDaysAdv($checkints) {
3668 $days_adv = self::getDepositIfDays();
3669 if (!($days_adv > 0) || !($checkints > 0)) {
3670 return true;
3671 }
3672 $now_info = getdate();
3673 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3674 return $maxts > $checkints ? false : true;
3675 }
3676
3677 public static function depositCustomerChoice() {
3678 $dbo = JFactory::getDbo();
3679 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3680 $dbo->setQuery($q);
3681 $dbo->execute();
3682 $s = $dbo->loadAssocList();
3683 return (intval($s[0]['setting']) == 1 ? true : false);
3684 }
3685
3686 public static function getDepositOverrides($getjson = false) {
3687 $dbo = JFactory::getDbo();
3688 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3689 $dbo->setQuery($q);
3690 $dbo->execute();
3691 if ($dbo->getNumRows() > 0) {
3692 $s = $dbo->loadResult();
3693 return $getjson ? $s : json_decode($s, true);
3694 }
3695 //count of this array will be at least 1 to store the "more" property
3696 $def_arr = array('more' => '');
3697 $def_val = json_encode($def_arr);
3698 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
3699 $dbo->setQuery($q);
3700 $dbo->execute();
3701 return $getjson ? $def_val : $def_arr;
3702 }
3703
3704 public static function calcDepositOverride($amount_deposit, $nights) {
3705 $overrides = self::getDepositOverrides();
3706 $nights = intval($nights);
3707 $andmore = intval($overrides['more']);
3708 if (!(count($overrides) > 1)) {
3709 //no overrides
3710 return $amount_deposit;
3711 }
3712 foreach ($overrides as $k => $v) {
3713 if ((int)$k == $nights && strlen($v) > 0) {
3714 //exact override found
3715 return (float)$v;
3716 }
3717 }
3718 if ($andmore > 0 && $andmore <= $nights) {
3719 foreach ($overrides as $k => $v) {
3720 if ((int)$k == $andmore && strlen($v) > 0) {
3721 //"and more" nights found
3722 return (float)$v;
3723 }
3724 }
3725 }
3726 //nothing was found
3727 return $amount_deposit;
3728 }
3729
3730 public static function noDepositForNonRefund() {
3731 $dbo = JFactory::getDbo();
3732 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
3733 $dbo->setQuery($q);
3734 $dbo->execute();
3735 if ($dbo->getNumRows() > 0) {
3736 $s = $dbo->loadResult();
3737 return ((int)$s == 1);
3738 }
3739 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
3740 $dbo->setQuery($q);
3741 $dbo->execute();
3742 //default to false
3743 return false;
3744 }
3745
3746 /**
3747 * This method returns the room-rate array with the lowest price
3748 * that matches the preferred rate plan parameter (if available).
3749 * The array $website_rates could not be an array, or it could be
3750 * an array with the error string (response from the TACVBO Class).
3751 * The method has been introduced in VBO 1.10 and it's mainly used
3752 * by the module mod_vikbooking_channelrates and its ajax requests.
3753 *
3754 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
3755 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
3756 *
3757 * @return array
3758 */
3759 public static function getBestRoomRate($website_rates, $def_rplan) {
3760 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
3761 return array();
3762 }
3763 $best_room_rate = array();
3764 foreach ($website_rates as $rid => $tars) {
3765 foreach ($tars as $tar) {
3766 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
3767 //the array $website_rates is already sorted by price ASC, so we take the first useful array
3768 $best_room_rate = $tar;
3769 break 2;
3770 }
3771 }
3772 }
3773 if (!(count($best_room_rate) > 0)) {
3774 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
3775 foreach ($website_rates as $rid => $tars) {
3776 foreach ($tars as $tar) {
3777 $best_room_rate = $tar;
3778 break 2;
3779 }
3780 }
3781 }
3782
3783 return $best_room_rate;
3784 }
3785
3786 /**
3787 * This method returns an array with the details
3788 * of all channels in VCM that supports AV requests,
3789 * and that have at least one room type mapped.
3790 * The method invokes the Logos Class to return details
3791 * about the name and logo URL of the channel.
3792 * The method has been introduced in VBO 1.10 and it's mainly used
3793 * by the module mod_vikbooking_channelrates and its ajax requests.
3794 *
3795 * @param array $channels an array of channel IDs to be mapped on the VCM relations
3796 *
3797 * @return array
3798 */
3799 public static function getChannelsMap($channels) {
3800 if (!is_array($channels) || !(count($channels))) {
3801 return array();
3802 }
3803 $vcm_logos = self::getVcmChannelsLogo('', true);
3804 if (!is_object($vcm_logos)) {
3805 return array();
3806 }
3807 $channels_ids = array();
3808 foreach ($channels as $chid) {
3809 $ichid = (int)$chid;
3810 if ($ichid < 1) {
3811 continue;
3812 }
3813 array_push($channels_ids, $ichid);
3814 }
3815 if (!(count($channels_ids))) {
3816 return array();
3817 }
3818 $dbo = JFactory::getDbo();
3819 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
3820 $dbo->setQuery($q);
3821 $dbo->execute();
3822 if ($dbo->getNumRows() < 1) {
3823 return array();
3824 }
3825 $channels_names = $dbo->loadAssocList();
3826 $channels_map = array();
3827 foreach ($channels_names as $ch) {
3828 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
3829 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
3830 $chdata = array(
3831 'id' => $ch['idchannel'],
3832 'name' => ucwords($ch['channel']),
3833 'logo' => $ota_logo_url
3834 );
3835 array_push($channels_map, $chdata);
3836 }
3837 return $channels_map;
3838 }
3839
3840 /**
3841 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
3842 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
3843 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
3844 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
3845 * module/widget and its ajax requests.
3846 *
3847 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
3848 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
3849 *
3850 * @return mixed string (empty or indicating the alteration), or associative array per channel.
3851 *
3852 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
3853 */
3854 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
3855 {
3856 $otas_rates_val = '';
3857 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
3858 return $otas_rates_val;
3859 }
3860 if (!class_exists('VikChannelManager')) {
3861 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
3862 }
3863 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
3864 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
3865 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
3866 // the Bulk Rates Cache contains data for this room type and rate plan
3867 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
3868 // build rates modification string
3869 if ((int)$data_cont['rmod'] > 0) {
3870 // rates were modified for the OTAs, check how
3871 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
3872 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
3873 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
3874 // check alterations per channel, if requested
3875 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
3876 // we compose an associative array with the alteration for each channel (0 = generic rule)
3877 $channel_alterations = array($otas_rates_val);
3878 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
3879 if ((int)$ch_mods['rmod'] > 0) {
3880 // custom rate alteration for this channel
3881 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
3882 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
3883 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
3884 } else {
3885 // no rates alteration for this channel identifier
3886 $ch_alter_string = '+0%';
3887 }
3888 $channel_alterations[$ch_id] = $ch_alter_string;
3889 }
3890 // overwrite return value with associative array
3891 $otas_rates_val = $channel_alterations;
3892 }
3893 }
3894 }
3895 }
3896
3897 return $otas_rates_val;
3898 }
3899
3900 /**
3901 * This method checks if some non-refundable rates were selected
3902 * (`free_cancellation`=0), the only argument is an array of tariffs.
3903 * The property 'idprice' must be defined on each sub-array.
3904 *
3905 * @param $tars array
3906 *
3907 * @return boolean
3908 **/
3909 public static function findNonRefundableRates($tars = [])
3910 {
3911 $id_prices = [];
3912
3913 foreach ($tars as $tar) {
3914 if (isset($tar['idprice'])) {
3915 if (!in_array($tar['idprice'], $id_prices)) {
3916 array_push($id_prices, (int)$tar['idprice']);
3917 }
3918 continue;
3919 }
3920 foreach ($tar as $t) {
3921 if (isset($t['idprice'])) {
3922 if (!in_array($t['idprice'], $id_prices)) {
3923 array_push($id_prices, (int)$t['idprice']);
3924 }
3925 }
3926 }
3927 }
3928
3929 if (!count($id_prices)) {
3930 // no price IDs found (probably a package)
3931 return false;
3932 }
3933
3934 $dbo = JFactory::getDbo();
3935
3936 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
3937 $dbo->setQuery($q);
3938 $dbo->execute();
3939
3940 return (bool)($dbo->getNumRows() > 0);
3941 }
3942
3943 /**
3944 * This method checks if the deposit is allowed depending on
3945 * the selected rate plans (idprice) for the rooms reserved.
3946 * If the configuration setting is enabled, and if some
3947 * non-refundable rates were selected (`free_cancellation`=0),
3948 * the method will return false, because the deposit is not allowed.
3949 * The only argument is an array of tariffs. The property 'idprice'
3950 * must be defined on each sub-array (multi-dimension supported)
3951 * throgh the method findNonRefundableRates();
3952 *
3953 * @param $tars array
3954 *
3955 * @return boolean
3956 **/
3957 public static function allowDepositFromRates($tars) {
3958 if (!self::noDepositForNonRefund()) {
3959 //deposit can be paid also if non-refundable rates
3960 return true;
3961 }
3962 return !self::findNonRefundableRates($tars);
3963 }
3964
3965 public static function showSearchSuggestions() {
3966 $dbo = JFactory::getDbo();
3967 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
3968 $dbo->setQuery($q);
3969 $dbo->execute();
3970 if ($dbo->getNumRows() > 0) {
3971 return (int)$dbo->loadResult();
3972 }
3973 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
3974 $dbo->setQuery($q);
3975 $dbo->execute();
3976 return 1;
3977 }
3978
3979 /**
3980 * Given a code, returns the coupon information array.
3981 *
3982 * @param string $code the coupon code.
3983 *
3984 * @return array empty array or coupon record.
3985 *
3986 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
3987 */
3988 public static function getCouponInfo($code)
3989 {
3990 $dbo = JFactory::getDbo();
3991
3992 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
3993 $dbo->setQuery($q, 0, 1);
3994 $coupon = $dbo->loadAssoc();
3995 if (!$coupon) {
3996 return [];
3997 }
3998
3999 // check for customers assigned
4000 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
4001 $dbo->setQuery($q);
4002 $coupon_customers = $dbo->loadAssocList();
4003 if ($coupon_customers) {
4004 // set customers and automatic properties
4005 $coupon['customers'] = [];
4006 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4007 foreach ($coupon_customers as $coupon_customer) {
4008 // push customer ID
4009 $coupon['customers'][] = $coupon_customer['idcustomer'];
4010 }
4011 }
4012
4013 return $coupon;
4014 }
4015
4016 public static function getRoomInfo($idroom, $columns = [])
4017 {
4018 static $room_infos = [];
4019
4020 if (isset($room_infos[$idroom])) {
4021 return $room_infos[$idroom];
4022 }
4023
4024 $dbo = JFactory::getDbo();
4025
4026 if ($columns) {
4027 $columns = array_map([$dbo, 'qn'], $columns);
4028 }
4029
4030 $q = $dbo->getQuery(true)
4031 ->select($columns ? $columns : '*')
4032 ->from($dbo->qn('#__vikbooking_rooms'))
4033 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4034
4035 $dbo->setQuery($q);
4036 $room = $dbo->loadAssoc();
4037
4038 $room_infos[$idroom] = $room ? $room : [];
4039
4040 return $room_infos[$idroom];
4041 }
4042
4043 public static function loadOrdersRoomsData($idorder)
4044 {
4045 $dbo = JFactory::getDbo();
4046
4047 $q = $dbo->getQuery(true)
4048 ->select($dbo->qn('or') . '.*')
4049 ->select($dbo->qn('r.name', 'room_name'))
4050 ->select($dbo->qn('r.params'))
4051 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4052 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4053 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4054 ->order($dbo->qn('or.id') . ' ASC');
4055
4056 $dbo->setQuery($q);
4057
4058 return $dbo->loadAssocList();
4059 }
4060
4061 public static function sayCategory($ids, $vbo_tn = null)
4062 {
4063 $dbo = JFactory::getDbo();
4064 $split = explode(";", $ids);
4065 $say = "";
4066 foreach ($split as $k => $s) {
4067 if (!strlen($s)) {
4068 continue;
4069 }
4070 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4071 $dbo->setQuery($q);
4072 $nam = $dbo->loadAssocList();
4073 if (!$nam) {
4074 continue;
4075 }
4076 if (is_object($vbo_tn)) {
4077 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4078 }
4079 $say .= $nam[0]['name'];
4080 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4081 }
4082 return $say;
4083 }
4084
4085 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4086 {
4087 $dbo = JFactory::getDbo();
4088 $split = explode(";", $idc);
4089 $carat = "";
4090 $arr = array();
4091 $where = array();
4092 foreach ($split as $s) {
4093 if (!empty($s)) {
4094 $where[] = $s;
4095 }
4096 }
4097 if ($where) {
4098 $q = "SELECT `c`.* FROM `#__vikbooking_characteristics` AS `c` WHERE `c`.`id` IN (" . implode(",", $where) . ") ORDER BY `c`.`ordering` ASC;";
4099 $dbo->setQuery($q);
4100 $arr = $dbo->loadAssocList();
4101 if ($arr && is_object($vbo_tn)) {
4102 $vbo_tn->translateContents($arr, '#__vikbooking_characteristics');
4103 }
4104 }
4105 if ($arr) {
4106 $carat .= "<div class=\"vbo-room-carats\">\n";
4107 foreach ($arr as $a) {
4108 if (!empty($a['textimg'])) {
4109 //tooltip icon text is not empty
4110 if (!empty($a['icon'])) {
4111 //an icon has been uploaded: display the image
4112 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['textimg']."\"><img src=\"".VBO_SITE_URI."resources/uploads/".$a['icon']."\" alt=\"" . $a['name'] . "\" /></span></span>\n";
4113 } else {
4114 if (strpos($a['textimg'], '</i>') !== false) {
4115 //the tooltip icon text is a font-icon, we can use the name as tooltip
4116 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['name']."\">".$a['textimg']."</span></span>\n";
4117 } else {
4118 //display just the text
4119 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4120 }
4121 }
4122 } else {
4123 $carat .= (!empty($a['icon']) ? "<span class=\"vbo-room-carat\"><img src=\"".VBO_SITE_URI."resources/uploads/" . $a['icon'] . "\" alt=\"" . $a['name'] . "\" title=\"" . $a['name'] . "\"/></span>\n" : "<span class=\"vbo-room-carat\">".$a['name']."</span>\n");
4124 }
4125 }
4126 $carat .= "</div>\n";
4127 }
4128 return $carat;
4129 }
4130
4131 public static function getRoomOptionals($idopts, $vbo_tn = null)
4132 {
4133 $dbo = JFactory::getDbo();
4134 $split = explode(";", $idopts);
4135 $fetch = array();
4136 foreach ($split as $s) {
4137 if (!empty($s)) {
4138 $fetch[] = $s;
4139 }
4140 }
4141 if ($fetch) {
4142 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4143 $dbo->setQuery($q);
4144 $arr = $dbo->loadAssocList();
4145 if ($arr) {
4146 if (is_object($vbo_tn)) {
4147 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4148 }
4149 return $arr;
4150 }
4151 }
4152 return [];
4153 }
4154
4155 public static function getSingleOption($idopt, $vbo_tn = null)
4156 {
4157 $dbo = JFactory::getDbo();
4158 $opt = array();
4159 if (!empty($idopt)) {
4160 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4161 $dbo->setQuery($q);
4162 $opt = $dbo->loadAssoc();
4163 if ($opt && is_object($vbo_tn)) {
4164 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4165 }
4166 }
4167 return $opt;
4168 }
4169
4170 /**
4171 * Unsets the options that are not available in the requested dates.
4172 *
4173 * @param array $optionals the array of options passed by reference
4174 * @param int $checkin the timestamp of the check-in date and time
4175 * @param int $checkout the timestamp of the check-out date and time
4176 *
4177 * @return void
4178 *
4179 * @since 1.11
4180 */
4181 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4182 {
4183 if (!$optionals || empty($checkin) || empty($checkout)) {
4184 return;
4185 }
4186
4187 foreach ($optionals as $k => $v) {
4188 if (!empty($v['alwaysav'])) {
4189 $dates = explode(';', $v['alwaysav']);
4190 if (empty($dates[0]) || empty($dates[1])) {
4191 continue;
4192 }
4193 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4194 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4195 unset($optionals[$k]);
4196 }
4197 }
4198 }
4199 }
4200
4201 /**
4202 * Unsets the options that are not suited for the requested room party.
4203 *
4204 * @param array $optionals the array of options passed by reference.
4205 * @param int $adults the number of adults in the room party.
4206 * @param int $children the number of children in the room party.
4207 *
4208 * @return void
4209 *
4210 * @since 1.13.5
4211 */
4212 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4213 {
4214 if (!$optionals || (empty($adults) && empty($children))) {
4215 return;
4216 }
4217
4218 foreach ($optionals as $k => $v) {
4219 if (empty($v['oparams'])) {
4220 continue;
4221 }
4222 $v['oparams'] = json_decode($v['oparams'], true);
4223 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4224 continue;
4225 }
4226 if (!empty($v['oparams']['minguestsnum'])) {
4227 // filter by minimum adults/guests
4228 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4229 // minimum number of adults not sufficient, unset option
4230 unset($optionals[$k]);
4231 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4232 // minimum number of total guests not sufficient, unset option
4233 unset($optionals[$k]);
4234 }
4235 }
4236 if (!empty($v['oparams']['maxguestsnum'])) {
4237 // filter by maximum adults/guests
4238 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4239 // maximum number of adults exceeded, unset option
4240 unset($optionals[$k]);
4241 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4242 // maximum number of total guests exceeded, unset option
4243 unset($optionals[$k]);
4244 }
4245 }
4246 }
4247 }
4248
4249 /**
4250 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4251 *
4252 * @param array $id_rooms list of room IDs.
4253 * @param int $num_adults number of adults.
4254 * @param int $num_nights number of nights of stay.
4255 * @param int $checkin optional check-in timestamp.
4256 * @param int $checkout optional check-out timestamp.
4257 *
4258 * @return array associative information of city taxes, fees and options.
4259 *
4260 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4261 */
4262 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4263 {
4264 $dbo = JFactory::getDbo();
4265
4266 $taxes = 0;
4267 $fees = 0;
4268
4269 $options_data = [];
4270 $id_options = [];
4271
4272 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4273 $dbo->setQuery($q);
4274 $assocs = $dbo->loadAssocList();
4275 if ($assocs) {
4276 foreach ($assocs as $opts) {
4277 if (!empty($opts['idopt'])) {
4278 $r_ido = explode(';', rtrim($opts['idopt']));
4279 foreach ($r_ido as $ido) {
4280 if (!empty($ido) && !in_array($ido, $id_options)) {
4281 $id_options[] = (int)$ido;
4282 }
4283 }
4284 }
4285 }
4286 }
4287
4288 if ($id_options) {
4289 $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);";
4290 $dbo->setQuery($q);
4291 $alltaxesfees = $dbo->loadAssocList();
4292
4293 if ($checkin && $checkout) {
4294 // filter the records by stay dates to support, for example, seasonal city taxes
4295 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4296 }
4297
4298 foreach ($alltaxesfees as $tf) {
4299 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4300 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4301 $realcost = $tf['maxprice'];
4302 }
4303 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4304 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4305 if ($tf['is_citytax'] == 1) {
4306 $taxes += $realcost;
4307 } elseif ($tf['is_fee'] == 1) {
4308 $fees += $realcost;
4309 }
4310 $optsett = explode('-', $tf['forceval']);
4311 $options_data[] = $tf['id'].':'.$optsett[0];
4312 }
4313 }
4314
4315 return [
4316 'city_taxes' => $taxes,
4317 'fees' => $fees,
4318 'options' => $options_data
4319 ];
4320 }
4321
4322 /**
4323 * From a list of option/extras records, it returns an array with the options/extras
4324 * that do not support age intervals, and options for the children age intervals.
4325 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4326 *
4327 * @param array $optionals the full list of options/extras records.
4328 * @param int $adults the number of adults in the room requested.
4329 * @param int $children the number of children in the room requested.
4330 *
4331 * @return array the filtered regular and children age intervals options.
4332 */
4333 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4334 {
4335 // container for age intervals
4336 $ageintervals = [];
4337
4338 // regular options should start as an array
4339 if (!is_array($optionals)) {
4340 $optionals = [];
4341 }
4342
4343 // check for the first valid age intervals option
4344 foreach ($optionals as $kopt => $opt) {
4345 if (!empty($opt['ageintervals'])) {
4346 $intervals = explode(';;', $opt['ageintervals']);
4347 foreach ($intervals as $intv) {
4348 if (empty($intv)) {
4349 continue;
4350 }
4351 $parts = explode('_', $intv);
4352 if (count($parts) >= 3) {
4353 // age intervals option found, get the first and only one
4354 $ageintervals = $optionals[$kopt];
4355 break 2;
4356 }
4357 }
4358 }
4359 }
4360
4361 if ($ageintervals) {
4362 /**
4363 * We allow price adjustments for a minimum total guests by overriding the costs
4364 * for each age interval for specific child numbers when the party covers some children.
4365 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4366 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4367 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4368 *
4369 * @since 1.13.5
4370 */
4371 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4372 $oparams = !is_array($oparams) ? array() : $oparams;
4373 $adults = (int)$adults;
4374 $children = (int)$children;
4375 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4376 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4377 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4378 if ($children_to_pay < $children) {
4379 // compose string for free age intervals
4380 $intervals = explode(';;', $ageintervals['ageintervals']);
4381 foreach ($intervals as $k => $v) {
4382 $parts = explode('_', $v);
4383 // set cost to 0 (key = 2)
4384 $parts[2] = '0';
4385 $intervals[$k] = implode('_', $parts);
4386 }
4387 $free_ageintervals = implode(';;', $intervals);
4388 for ($i = 1; $i <= $children; $i++) {
4389 if (($children - $i + 1) <= $children_to_pay) {
4390 // costs for this Nth last child should be regular as this child should pay
4391 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4392 } else {
4393 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4394 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4395 }
4396 }
4397 }
4398 }
4399
4400 // remove age intervals from regular options
4401 foreach ($optionals as $kopt => $opt) {
4402 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4403 // unset the option of type age intervals from the regular options
4404 unset($optionals[$kopt]);
4405 }
4406 }
4407 }
4408
4409 // return the filtered list of regular options and age intervals options
4410 return [$optionals, $ageintervals];
4411 }
4412
4413 /**
4414 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4415 * This is because some children in the party may not need to pay anything due to the min guests.
4416 *
4417 * @param array $optional the option full record to parse.
4418 * @param int $adults the number of adults in the room requested.
4419 * @param int $children the number of children in the room requested.
4420 *
4421 * @return array associative array with default age intervals and override strings.
4422 *
4423 * @since 1.13.5
4424 */
4425 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4426 {
4427 if (!is_array($optional)) {
4428 $optional = array();
4429 }
4430
4431 $overrides = array(
4432 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4433 );
4434
4435 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4436 $oparams = !is_array($oparams) ? array() : $oparams;
4437 $adults = (int)$adults;
4438 $children = (int)$children;
4439
4440 if (!count($oparams)) {
4441 return $overrides;
4442 }
4443
4444 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4445 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4446 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4447 if ($children_to_pay < $children) {
4448 // compose string for free age intervals
4449 $intervals = explode(';;', $optional['ageintervals']);
4450 foreach ($intervals as $k => $v) {
4451 $parts = explode('_', $v);
4452 // set cost to 0 (key = 2)
4453 $parts[2] = '0';
4454 $intervals[$k] = implode('_', $parts);
4455 }
4456 $free_ageintervals = implode(';;', $intervals);
4457 for ($i = 1; $i <= $children; $i++) {
4458 if (($children - $i + 1) <= $children_to_pay) {
4459 // costs for this Nth last child should be regular as this child should pay
4460 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4461 } else {
4462 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4463 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4464 }
4465 }
4466 }
4467 }
4468
4469 return $overrides;
4470 }
4471
4472 /**
4473 * Returns the number/index of the children being parsed given the full list of room options string,
4474 * the current key in the array of the split room options string, the ID of the ageintervals option,
4475 * and the total number of children. This is useful for later applying the cost overrides for the
4476 * children ages through the method getOptionIntervalChildOverrides().
4477 *
4478 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4479 * @param int $optid the ID of the option of type age intervals to check.
4480 * @param int $roptkey the current position in the loop of the room options string.
4481 * @param int $children the number of children in the room requested.
4482 *
4483 * @return int the position/number of the children being parsed, -1 if not found.
4484 *
4485 * @since 1.13.5
4486 */
4487 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4488 {
4489 // default to -1 for not found
4490 $child_num = -1;
4491
4492 $valid_opt_counter = 0;
4493
4494 $roptions = explode(";", $roptstr);
4495 foreach ($roptions as $k => $opt) {
4496 if (empty($opt)) {
4497 continue;
4498 }
4499 $optvals = explode(":", $opt);
4500 /**
4501 * In some cases, like the "saveorder" task, the room option string may contain
4502 * the room number beside the option ID, separated with an underscore.
4503 * If an underscore is present, we need to split it to find the option ID.
4504 */
4505 if (strpos($optvals[0], '_') !== false) {
4506 // underscore found in room number portion, extract the option ID
4507 $rn_parts = explode('_', $optvals[0]);
4508 // 0th element is the room number in the party, 1st elem is the option ID
4509 $optvals[0] = $rn_parts[1];
4510 }
4511 //
4512 if ((int)$optvals[0] != (int)$optid) {
4513 // we are not interested into this option ID
4514 continue;
4515 }
4516 // increase counter for this option ID
4517 $valid_opt_counter++;
4518
4519 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4520 // children position found, we need to return it as a 0th base
4521 $child_num = ($valid_opt_counter - 1);
4522 break;
4523 }
4524 }
4525
4526 return $child_num;
4527 }
4528
4529 public static function getOptionIntervalsCosts($intvstr) {
4530 $optcosts = array();
4531 $intervals = explode(';;', $intvstr);
4532 foreach ($intervals as $kintv => $intv) {
4533 if (empty($intv)) continue;
4534 $parts = explode('_', $intv);
4535 if (count($parts) >= 3) {
4536 $optcosts[$kintv] = (float)$parts[2];
4537 }
4538 }
4539 return $optcosts;
4540 }
4541
4542 public static function getOptionIntervalsAges($intvstr) {
4543 $optages = array();
4544 $intervals = explode(';;', $intvstr);
4545 foreach ($intervals as $kintv => $intv) {
4546 if (empty($intv)) continue;
4547 $parts = explode('_', $intv);
4548 if (count($parts) >= 3) {
4549 $optages[$kintv] = $parts[0].' - '.$parts[1];
4550 }
4551 }
4552 return $optages;
4553 }
4554
4555 public static function getOptionIntervalsPercentage($intvstr) {
4556 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4557 $optcostspcent = array();
4558 $intervals = explode(';;', $intvstr);
4559 foreach ($intervals as $kintv => $intv) {
4560 if (empty($intv)) continue;
4561 $parts = explode('_', $intv);
4562 if (count($parts) >= 3) {
4563 //fixed amount
4564 $setval = 0;
4565 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4566 //percentage value of the room base cost (VBO 1.10)
4567 $setval = 2;
4568 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4569 //percentage value of the adults tariff
4570 $setval = 1;
4571 }
4572 $optcostspcent[$kintv] = $setval;
4573 }
4574 }
4575 return $optcostspcent;
4576 }
4577
4578 public static function dayValidTs($days, $first, $second)
4579 {
4580 $secdiff = $second - $first;
4581 $daysdiff = $secdiff / 86400;
4582 if (is_int($daysdiff)) {
4583 if ($daysdiff < 1) {
4584 $daysdiff = 1;
4585 }
4586 } else {
4587 if ($daysdiff < 1) {
4588 $daysdiff = 1;
4589 } else {
4590 $sum = floor($daysdiff) * 86400;
4591 $newdiff = $secdiff - $sum;
4592 $maxhmore = self::getHoursMoreRb() * 3600;
4593 if ($maxhmore >= $newdiff) {
4594 $daysdiff = floor($daysdiff);
4595 } else {
4596 $daysdiff = ceil($daysdiff);
4597 }
4598 }
4599 }
4600
4601 return ($daysdiff == $days);
4602 }
4603
4604 /**
4605 * Calculates the stay fee inclusive of taxes.
4606 *
4607 * @param float $cost the cost to evaluate.
4608 * @param int $idprice the rate plan ID associated.
4609 *
4610 * @return float the given cost after tax, if taxes had to be applied.
4611 */
4612 public static function sayCostPlusIva($cost, $idprice)
4613 {
4614 $ivainclusa = self::ivaInclusa();
4615 if ($ivainclusa) {
4616 return $cost;
4617 }
4618
4619 static $tax_rplans_map = [];
4620
4621 if (array_key_exists($idprice, $tax_rplans_map)) {
4622 $tax_rate = $tax_rplans_map[$idprice];
4623 } else {
4624 $dbo = JFactory::getDbo();
4625 $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 . ";";
4626 $dbo->setQuery($q);
4627 $tax_rate = $dbo->loadAssoc();
4628
4629 // cache value
4630 $tax_rplans_map[$idprice] = $tax_rate;
4631 }
4632
4633 if (!$tax_rate || empty($tax_rate['aliq'])) {
4634 return $cost;
4635 }
4636
4637 $subt = 100 + $tax_rate['aliq'];
4638 $op = ($cost * $subt / 100);
4639
4640 /**
4641 * Tax Cap implementation for prices tax excluded (most common).
4642 *
4643 * @since 1.12
4644 */
4645 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4646 $op = ($cost + $tax_rate['taxcap']);
4647 }
4648
4649 // apply rounding to avoid issues with multiple tax rates when tax excluded
4650 $formatvals = self::getNumberFormatData();
4651 $formatparts = explode(':', $formatvals);
4652 $rounded_op = round($op, (int)$formatparts[0]);
4653 if ($rounded_op > $op) {
4654 return $rounded_op;
4655 }
4656
4657 /**
4658 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4659 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4660 *
4661 * @since 1.11.1
4662 */
4663 return round($op, (int)$formatparts[0]);
4664 }
4665
4666 /**
4667 * Calculates the stay fee exclusive of taxes.
4668 *
4669 * @param float $cost the cost to evaluate.
4670 * @param int $idprice the rate plan ID associated.
4671 *
4672 * @return float the given cost before tax.
4673 */
4674 public static function sayCostMinusIva($cost, $idprice)
4675 {
4676 $ivainclusa = self::ivaInclusa();
4677 if (!$ivainclusa) {
4678 return $cost;
4679 }
4680
4681 static $tax_rplans_map = [];
4682
4683 if (array_key_exists($idprice, $tax_rplans_map)) {
4684 $tax_rate = $tax_rplans_map[$idprice];
4685 } else {
4686 $dbo = JFactory::getDbo();
4687 $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 . ";";
4688 $dbo->setQuery($q);
4689 $tax_rate = $dbo->loadAssoc();
4690
4691 // cache value
4692 $tax_rplans_map[$idprice] = $tax_rate;
4693 }
4694
4695 if (!$tax_rate || empty($tax_rate['aliq'])) {
4696 return $cost;
4697 }
4698
4699 $subt = 100 + $tax_rate['aliq'];
4700 $op = ($cost * 100 / $subt);
4701
4702 /**
4703 * Tax Cap implementation also when prices tax included.
4704 *
4705 * @since 1.12
4706 */
4707 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4708 $op = ($cost - $tax_rate['taxcap']);
4709 }
4710
4711 // apply rounding to avoid issues with multiple tax rates when tax included
4712 $formatvals = self::getNumberFormatData();
4713 $formatparts = explode(':', $formatvals);
4714 $rounded_op = round($op, (int)$formatparts[0]);
4715 if ($rounded_op < $op) {
4716 return $rounded_op;
4717 }
4718
4719 /**
4720 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4721 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4722 *
4723 * @since 1.11.1
4724 */
4725 return round($op, (int)$formatparts[0]);
4726 }
4727
4728 /**
4729 * Given an option/extra cost, determines whether taxes should be applied over it.
4730 * Can also be used to calculate taxes on the extra costs per room in the bookings
4731 *
4732 * @param float $cost the cost to evaluate, option or extra service fee.
4733 * @param int $idiva the ID of the tax rate to use to apply taxes.
4734 * @param bool $force if true, taxes will always be applied.
4735 *
4736 * @return float the given option/extra cost plus tax, if taxes had to be applied.
4737 *
4738 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4739 */
4740 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
4741 {
4742 $ivainclusa = self::ivaInclusa();
4743 if ($ivainclusa && $force !== true) {
4744 return $cost;
4745 }
4746
4747 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4748 if (!$tax_rate) {
4749 return $cost;
4750 }
4751
4752 $subt = 100 + $tax_rate['aliq'];
4753 $op = ($cost * $subt / 100);
4754
4755 /**
4756 * Tax Cap implementation for prices tax excluded (most common).
4757 *
4758 * @since 1.12
4759 */
4760 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4761 $op = ($cost + $tax_rate['taxcap']);
4762 }
4763
4764 // apply rounding to avoid issues with multiple tax rates when tax excluded
4765 $formatvals = self::getNumberFormatData();
4766 $formatparts = explode(':', $formatvals);
4767 $rounded_op = round($op, (int)$formatparts[0]);
4768 if ($rounded_op > $op) {
4769 return $rounded_op;
4770 }
4771
4772 return $op;
4773 }
4774
4775 /**
4776 * Calculates the net amount for an option/extra service.
4777 *
4778 * @param float $cost the cost to evaluate, option or extra service fee.
4779 * @param int $idiva the ID of the tax rate to use to apply taxes.
4780 *
4781 * @return float the given option/extra cost before tax, if taxes were inclusive.
4782 */
4783 public static function sayOptionalsMinusIva($cost, $idiva)
4784 {
4785 if (empty($idiva)) {
4786 return $cost;
4787 }
4788
4789 $ivainclusa = self::ivaInclusa();
4790 if (!$ivainclusa) {
4791 return $cost;
4792 }
4793
4794 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4795 if (!$tax_rate) {
4796 return $cost;
4797 }
4798
4799 $subt = 100 + $tax_rate['aliq'];
4800 $op = ($cost * 100 / $subt);
4801
4802 /**
4803 * Tax Cap implementation also when prices tax included.
4804 *
4805 * @since 1.12
4806 */
4807 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4808 $op = ($cost - $tax_rate['taxcap']);
4809 }
4810
4811 // apply rounding to avoid issues with multiple tax rates when tax included
4812 $formatvals = self::getNumberFormatData();
4813 $formatparts = explode(':', $formatvals);
4814 $rounded_op = round($op, (int)$formatparts[0]);
4815 if ($rounded_op < $op) {
4816 return $rounded_op;
4817 }
4818
4819 return $op;
4820 }
4821
4822 /**
4823 * Given a cost, determines whether taxes should be applied over it.
4824 *
4825 * @param float $cost the cost to evaluate, could be a room custom price.
4826 * @param int $idiva the ID of the tax rate to use to apply taxes.
4827 * @param bool $force if true, taxes will always be applied.
4828 *
4829 * @return float the given cost plus tax, if taxes had to be applied.
4830 *
4831 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4832 */
4833 public static function sayPackagePlusIva($cost, $idiva, $force = false)
4834 {
4835 $ivainclusa = self::ivaInclusa();
4836 if ($ivainclusa && $force !== true) {
4837 return $cost;
4838 }
4839
4840 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4841 if (!$tax_rate) {
4842 return $cost;
4843 }
4844
4845 $subt = 100 + $tax_rate['aliq'];
4846 $op = ($cost * $subt / 100);
4847
4848 /**
4849 * Tax Cap implementation for prices tax excluded (most common).
4850 *
4851 * @since 1.12
4852 */
4853 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4854 $op = ($cost + $tax_rate['taxcap']);
4855 }
4856
4857 // apply rounding to avoid issues with multiple tax rates when tax excluded
4858 $formatvals = self::getNumberFormatData();
4859 $formatparts = explode(':', $formatvals);
4860 $rounded_op = round($op, (int)$formatparts[0]);
4861 if ($rounded_op > $op) {
4862 return $rounded_op;
4863 }
4864
4865 return $op;
4866 }
4867
4868 /**
4869 * Calculates the net amount for a package.
4870 *
4871 * @param float $cost the cost to evaluate, option or extra service fee.
4872 * @param int $idiva the ID of the tax rate to use to apply taxes.
4873 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
4874 *
4875 * @return float the given cost before tax.
4876 */
4877 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
4878 {
4879 $ivainclusa = self::ivaInclusa();
4880 if (!$ivainclusa && $force_invoice_excltax !== true) {
4881 return $cost;
4882 }
4883
4884 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4885 if (!$tax_rate) {
4886 return $cost;
4887 }
4888
4889 $subt = 100 + $tax_rate['aliq'];
4890 $op = ($cost * 100 / $subt);
4891
4892 /**
4893 * Tax Cap implementation also when prices tax included.
4894 *
4895 * @since 1.12
4896 */
4897 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4898 $op = ($cost - $tax_rate['taxcap']);
4899 }
4900
4901 // apply rounding to avoid issues with multiple tax rates when tax included
4902 $formatvals = self::getNumberFormatData();
4903 $formatparts = explode(':', $formatvals);
4904 $rounded_op = round($op, (int)$formatparts[0]);
4905 if ($rounded_op < $op) {
4906 return $rounded_op;
4907 }
4908
4909 return $op;
4910 }
4911
4912 public static function getSecretLink()
4913 {
4914 $dbo = JFactory::getDbo();
4915
4916 $sid = mt_rand();
4917
4918 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
4919 $dbo->setQuery($q);
4920 $all = $dbo->loadAssocList();
4921 if ($all) {
4922 $list = [];
4923 foreach ($all as $s) {
4924 $list[] = $s['sid'];
4925 }
4926 if (in_array($sid, $list)) {
4927 while(in_array($sid, $list)) {
4928 $sid++;
4929 }
4930 }
4931 }
4932
4933 return $sid;
4934 }
4935
4936 /**
4937 * Generates a confirmation number for the given booking.
4938 * Modified the way random string-numbers are obtained to
4939 * avoid any possible brute-force attempt.
4940 *
4941 * @since 1.15.1 (J) - 1.5.4 (WP)
4942 */
4943 public static function generateConfirmNumber($oid, $update = true)
4944 {
4945 $dbo = JFactory::getDbo();
4946
4947 $confirmnumb = date('y') . $oid;
4948 for ($i = 0; $i < 8; $i++) {
4949 $confirmnumb .= rand(0, 9);
4950 }
4951
4952 if ($update) {
4953 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
4954 $dbo->setQuery($q);
4955 $dbo->execute();
4956 }
4957
4958 return $confirmnumb;
4959 }
4960
4961 public static function buildCustData($arr, $sep) {
4962 $cdata = "";
4963 foreach ($arr as $k => $e) {
4964 if (strlen($e)) {
4965 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
4966 }
4967 }
4968 return $cdata;
4969 }
4970
4971 /**
4972 * This method parses the Joomla menu object to see if a menu item of a
4973 * specific type is available, to get its ID. Useful when links should be
4974 * displayed in pages where there is no Itemid set (booking details pages).
4975 *
4976 * @param array $viewtypes list of accepted menu items
4977 * @param string $lang the optional language to use.
4978 *
4979 * @return int
4980 *
4981 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
4982 */
4983 public static function findProperItemIdType($viewtypes, $lang = null)
4984 {
4985 if (VBOPlatformDetection::isWordPress()) {
4986 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
4987
4988 $itemid = $model->best($viewtypes, $lang);
4989
4990 if ($itemid) {
4991 return $itemid;
4992 }
4993
4994 return 0;
4995 }
4996
4997 $bestitemid = 0;
4998
4999 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
5000
5001 $app = JFactory::getApplication();
5002
5003 $menu = $app->getMenu('site');
5004
5005 if (!$menu) {
5006 return 0;
5007 }
5008
5009 $menu_items = $menu->getMenu();
5010
5011 if (!$menu_items) {
5012 return 0;
5013 }
5014
5015 foreach ($menu_items as $itemid => $item) {
5016 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5017 // proper menu item type found
5018 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5019
5020 if (isset($item->language) && $item->language == $current_lang) {
5021 // we found the exact menu item type for the given language
5022 return $itemid;
5023 }
5024 }
5025 }
5026
5027 return $bestitemid;
5028 }
5029
5030 /**
5031 * Rewrites an internal URI that needs to be used outside of the website.
5032 * This means that the routed URI MUST start with the base path of the site.
5033 *
5034 * @param mixed $query The query string or an associative array of data.
5035 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5036 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5037 *
5038 * @return string The complete routed URI.
5039 *
5040 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5041 * routing from back-end (if available on the CMS version).
5042 */
5043 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5044 {
5045 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5046 }
5047
5048 /**
5049 * Generates an iCal file to be attached to the email message for the
5050 * customer or the administrator with some basic booking details.
5051 *
5052 * @param string $recip either admin or customer.
5053 * @param array $booking the booking array or some keys.
5054 *
5055 * @return mixed string in case of success, false otherwise.
5056 *
5057 * @since 1.12.0
5058 */
5059 public static function getEmailIcal($recip, $booking) {
5060 // load configuration setting
5061 $attachical = self::attachIcal();
5062
5063 if ($attachical === 0) {
5064 // do not attach any iCal file
5065 return false;
5066 }
5067
5068 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5069 // skip the iCal for the admin
5070 return false;
5071 }
5072
5073 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5074 // skip the iCal for the customer
5075 return false;
5076 }
5077
5078 if (strpos($recip, 'admin') !== false) {
5079 // prepare event description and summary for the admin
5080 $description = $booking['custdata'];
5081 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5082 $fname = $booking['ts'] . '.ics';
5083 } else {
5084 // event description and summary for the customer
5085 $description = '';
5086 $summary = self::getFrontTitle();
5087 $fname = 'reservation_reminder.ics';
5088 }
5089
5090 // prepare iCal head
5091 $company_name = self::getFrontTitle();
5092 $ics_str = "BEGIN:VCALENDAR\r\n" .
5093 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5094 "CALSCALE:GREGORIAN\r\n" .
5095 "VERSION:2.0\r\n";
5096 // compose iCal body
5097 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5098 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5099 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5100 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5101 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5102 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5103 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5104 $ics_str .= 'END:VEVENT'."\r\n";
5105 // close iCal file content
5106 $ics_str .= "END:VCALENDAR";
5107
5108 /**
5109 * Trigger event to allow third party plugins to overwrite the iCal file.
5110 *
5111 * @since 1.16.0 (J) - 1.6.0 (WP)
5112 */
5113 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5114
5115 if (empty($ics_str)) {
5116 return false;
5117 }
5118
5119 // store the event onto a .ics file. We use the resources folder in back-end.
5120 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5121 $fp = fopen($fpath, 'w+');
5122 $bytes = fwrite($fp, $ics_str);
5123 fclose($fp);
5124
5125 return $bytes ? $fpath : false;
5126 }
5127
5128 public static function loadEmailTemplate($booking_info = array()) {
5129 define('_VIKBOOKINGEXEC', '1');
5130 ob_start();
5131 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5132 $content = ob_get_contents();
5133 ob_end_clean();
5134 return $content;
5135 }
5136
5137 /**
5138 * Parses the raw HTML content of the booking email template.
5139 *
5140 * @param string $tmpl the raw content of the template.
5141 * @param mixed $bid int for the booking ID or booking array.
5142 * @param array $rooms list of rooms booked and translated.
5143 * @param array $rates list of translated rates for the booked rooms.
5144 * @param array $options list of translated options booked.
5145 * @param float [$total] the booking total amount (in case it has changed).
5146 * @param string [$link] the booking link can be passed for the no-deposit.
5147 *
5148 * @return string the HTML content of the parsed email template.
5149 *
5150 * @since 1.13 with different arguments.
5151 */
5152 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5153 {
5154 $app = JFactory::getApplication();
5155 $dbo = JFactory::getDbo();
5156 $vbo_tn = self::getTranslator();
5157
5158 // availability helper
5159 $av_helper = self::getAvailabilityInstance();
5160
5161 // get necessary values
5162 if (is_array($bid)) {
5163 // we got the full booking record
5164 $order_info = $bid;
5165 $bid = $order_info['id'];
5166 } else {
5167 $order_info = array();
5168 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5169 $dbo->setQuery($q);
5170 $order_info = $dbo->loadAssoc();
5171 if (!$order_info) {
5172 throw new Exception('Booking not found', 404);
5173 }
5174 }
5175
5176 $tars_info = array();
5177 $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'] . ";";
5178 $dbo->setQuery($q);
5179 $tars_info = $dbo->loadAssocList();
5180 if (!$tars_info) {
5181 throw new Exception('No rooms found', 404);
5182 }
5183
5184 /**
5185 * Split stay reservation.
5186 *
5187 * @since 1.16.0 (J) - 1.6.0 (WP)
5188 */
5189 $room_stay_dates = [];
5190 if ($order_info['split_stay']) {
5191 if ($order_info['status'] == 'confirmed') {
5192 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5193 } else {
5194 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5195 }
5196 // immediately count the number of nights of stay for each split room
5197 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5198 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5199 // overwrite values for compatibility with non-confirmed bookings
5200 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5201 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5202 }
5203 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5204 // overwrite the whole array
5205 $room_stay_dates[$sps_r_k] = $sps_r_v;
5206 }
5207 }
5208
5209 // check if the language in use is the same as the one used during the checkout
5210 $lang = JFactory::getLanguage();
5211 if (!empty($order_info['lang'])) {
5212 if ($lang->getTag() != $order_info['lang']) {
5213 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5214 if (VBOPlatformDetection::isJoomla()) {
5215 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5216 }
5217 }
5218 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5219 // force the translation to start because contents should be translated
5220 $vbo_tn::$force_tolang = $order_info['lang'];
5221 }
5222 }
5223
5224 // values for replacements
5225 $company_name = self::getFrontTitle();
5226 $currencyname = self::getCurrencyName();
5227 $sitelogo = self::getSiteLogo();
5228 $footermess = self::getFooterOrdMail($vbo_tn);
5229 $dateformat = self::getDateFormat();
5230 $datesep = self::getDateSeparator();
5231 if ($dateformat == "%d/%m/%Y") {
5232 $df = 'd/m/Y';
5233 } elseif ($dateformat == "%m/%d/%Y") {
5234 $df = 'm/d/Y';
5235 } else {
5236 $df = 'Y/m/d';
5237 }
5238 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5239 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5240 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5241 $customer_info = nl2br($order_info['custdata']);
5242 $company_logo = '';
5243 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5244 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5245 }
5246 if ($order_info['status'] == 'cancelled') {
5247 $confirmnumber = '';
5248 $status_str = JText::translate('VBCANCELLED');
5249 } elseif ($order_info['status'] == 'standby') {
5250 $confirmnumber = '';
5251 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5252 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5253 // this is an inquiry reservation
5254 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5255 }
5256 } else {
5257 $confirmnumber = $order_info['confirmnumber'];
5258 $status_str = JText::translate('VBCOMPLETED');
5259 }
5260 // booking total amount
5261 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5262 // booking link
5263 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5264 if (is_null($link)) {
5265 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5266 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5267 }
5268
5269 // raw HTML content
5270 $parsed = $tmpl;
5271
5272 /**
5273 * Parse all conditional text rules.
5274 *
5275 * @since 1.14 (J) - 1.4.0 (WP)
5276 */
5277 self::getConditionalRulesInstance()
5278 ->set(array('booking', 'rooms'), array($order_info, $rooms))
5279 ->parseTokens($parsed);
5280 //
5281
5282 // special tokens (tags) replacement
5283 $parsed = str_replace("{logo}", $company_logo, $parsed);
5284 $parsed = str_replace("{company_name}", $company_name, $parsed);
5285 $parsed = str_replace("{order_id}", $order_info['id'], $parsed);
5286 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5287 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5288 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5289 $parsed = str_replace("{order_status}", $status_str, $parsed);
5290 $parsed = str_replace("{order_date}", $create_date, $parsed);
5291
5292 // PIN Code
5293 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5294 $cpin = self::getCPinInstance();
5295 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5296 if (!empty($customer_pin)) {
5297 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5298 }
5299 }
5300
5301 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5302 // Confirmation Number
5303 if (strlen($confirmnumber) > 0) {
5304 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5305 } else {
5306 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5307 }
5308 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5309 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5310
5311 $roomsnum = count($rooms);
5312 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5313 $roomstr = "";
5314 // Rooms Distinctive Features
5315 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5316 //
5317 foreach ($rooms as $num => $r) {
5318 $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/>";
5319 // Rooms Distinctive Features
5320 if (is_array($matches[1]) && count($matches[1])) {
5321 $distinctive_features = array();
5322 $rparams = (array)json_decode($r['params'], true);
5323 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'])) {
5324 $distinctive_features = $rparams['features'][$r['roomindex']];
5325 }
5326 $docheck = (count($distinctive_features) > 0);
5327 foreach ($matches[1] as $reqf) {
5328 $feature_found = false;
5329 if ($docheck) {
5330 foreach ($distinctive_features as $dfk => $dfv) {
5331 if (stripos($dfk, $reqf) !== false) {
5332 $feature_found = $dfk;
5333 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5334 break;
5335 }
5336 }
5337 }
5338 }
5339 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5340 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5341 }
5342 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5343 }
5344 }
5345 }
5346
5347 // custom fields replace
5348 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5349 if (is_array($cmatches[1]) && count($cmatches[1])) {
5350 $cfids = array();
5351 foreach ($cmatches[1] as $cfid ) {
5352 $cfids[] = $cfid;
5353 }
5354 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5355 $dbo->setQuery($q);
5356 $cfields = $dbo->loadAssocList();
5357 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5358 $cfmap = array();
5359 foreach ($cfields as $cf) {
5360 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5361 }
5362 $cfmapreplace = array();
5363 $partsreceived = explode("\n", $order_info['custdata']);
5364 if (count($partsreceived) > 0) {
5365 foreach ($partsreceived as $pst) {
5366 if (!empty($pst)) {
5367 $tmpdata = explode(":", $pst);
5368 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5369 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5370 }
5371 }
5372 }
5373 }
5374 foreach ($cmatches[1] as $cfid ) {
5375 if (array_key_exists($cfid, $cfmapreplace)) {
5376 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5377 } else {
5378 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5379 }
5380 }
5381 }
5382
5383 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5384 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5385 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5386
5387 // order details
5388 $orderdetails = "";
5389 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5390 foreach ($rooms as $num => $r) {
5391 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5392 $use_room_ind = $num - $room_loop_ind;
5393 $split_stay_str = '';
5394 $split_stay_info = [];
5395 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) {
5396 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5397 $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']);
5398 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5399 }
5400
5401 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5402 $faredets = explode(":", $expdet[0]);
5403 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5404 if (!empty($expdet[1])) {
5405 $attrfaredets = explode(":", $expdet[1]);
5406 if (strlen($attrfaredets[1]) > 0) {
5407 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5408 }
5409 }
5410 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5411 $orderdetails .= '<div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($fareprice).'</span></div></div>';
5412 // options
5413 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5414 foreach ($options[$num] as $oo) {
5415 $expopts = explode("\n", $oo);
5416 foreach ($expopts as $optinfo) {
5417 if (!empty($optinfo)) {
5418 $splitopt = explode(":", $optinfo);
5419 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5420 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($optprice).'</span></div></div>';
5421 }
5422 }
5423 }
5424 }
5425 //
5426 if ($roomsnum > 1 && $num < $roomsnum) {
5427 $orderdetails .= '<br/>';
5428 }
5429 }
5430 //
5431 // coupon
5432 if (!empty($order_info['coupon'])) {
5433 $expcoupon = explode(";", $order_info['coupon']);
5434 $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>';
5435 }
5436 //
5437 // discount payment method
5438 if ($order_info['status'] != 'cancelled') {
5439 $idpayment = $order_info['idpayment'];
5440 if (!empty($idpayment)) {
5441 $exppay = explode('=', $idpayment);
5442 $payment = self::getPayment($exppay[0], $vbo_tn);
5443 if (is_array($payment)) {
5444 if ($payment['charge'] > 0.00 && $payment['ch_disc'] != 1) {
5445 // Discount (not charge)
5446 if ($payment['val_pcent'] == 1) {
5447 // fixed value
5448 $total -= $payment['charge'];
5449 $orderdetails .= '<br/><div class="discount"><span>'.$payment['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($payment['charge']).'</span></div></div>';
5450 } else {
5451 // percent value
5452 $percent_disc = $total * $payment['charge'] / 100;
5453 $total -= $percent_disc;
5454 $orderdetails .= '<br/><div class="discount"><span>'.$payment['name'].'</span><div class="service-amount" style="float: right;"><span>- '.$currencyname.' '.self::numberFormat($percent_disc).'</span></div></div>';
5455 }
5456 }
5457 }
5458 }
5459 }
5460 //
5461 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5462 //
5463 $parsed = str_replace("{order_total}", $currencyname.' '.self::numberFormat($total), $parsed);
5464 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5465 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5466 $parsed = str_replace("{booking_link}", $link, $parsed);
5467
5468 // deposit
5469 $deposit_str = '';
5470 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5471 $percentdeposit = self::getAccPerCent();
5472 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5473 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5474 if (self::getTypeDeposit() == "fixed") {
5475 $deposit_amount = $percentdeposit;
5476 } else {
5477 $deposit_amount = $total * $percentdeposit / 100;
5478 }
5479 if ($deposit_amount > 0) {
5480 $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>';
5481 }
5482 }
5483 }
5484 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5485 //
5486 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5487 $totpaid_str = '';
5488 if ($order_info['refund'] > 0) {
5489 $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>';
5490 }
5491 if ($order_info['status'] != 'cancelled') {
5492 $tot_paid = $order_info['totpaid'];
5493 $diff_topay = (float)$total - (float)$tot_paid;
5494 if ((float)$tot_paid > 0) {
5495 $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>';
5496 // only in case the remaining balance is greater than 1 to avoid commissions issues
5497 if ($diff_topay > 1) {
5498 $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>';
5499 }
5500 }
5501 }
5502 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5503 $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>';
5504 }
5505 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5506
5507 /**
5508 * Language direction (LTR or RTL).
5509 *
5510 * @since 1.16.4 (J) - 1.6.4 (WP)
5511 */
5512 $lang_direction = 'ltr';
5513 if (!empty($order_info['lang'])) {
5514 // attempt to define the language direction and replace the special tag
5515 try {
5516 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5517 } catch (Throwable $e) {
5518 // do nothing, default to ltr
5519 $lang_direction = 'ltr';
5520 }
5521 // static map for Arabic and Hebrew
5522 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5523 }
5524 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5525 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5526 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5527
5528 /**
5529 * Replace static floating styles depending on language direction (LTR or RTL).
5530 * For BC with older email template sources, we need to keep the inline style
5531 * for the floating of certain elements, which looks good only on LTR.
5532 *
5533 * @since 1.16.4 (J) - 1.6.4 (WP)
5534 */
5535 if ($lang_direction === 'rtl') {
5536 // get rid of inline static style declaration because the template uses CSS classes
5537 $parsed = str_replace("float: right;", '', $parsed);
5538 }
5539
5540 return $parsed;
5541 }
5542
5543 /**
5544 * New method for sending booking email messages
5545 * to the guest or to the administrator(s).
5546 *
5547 * @param int $bid the booking ID.
5548 * @param array $for guest, admin or a custom email address.
5549 * @param bool $send whether to send or return the HTML message.
5550 * @param bool $no_config if true, no configuration settings will be used for the status.
5551 * @param string $type optional type to indicate a booking modification.
5552 *
5553 * @return mixed True or False depending on the result or HTML string for the preview.
5554 *
5555 * @since 1.13 (J) - 1.3 (WP)
5556 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5557 * the reservation status is pending and against the configuration.
5558 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
5559 */
5560 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
5561 {
5562 $dbo = JFactory::getDbo();
5563 $app = JFactory::getApplication();
5564
5565 $vbo_tn = self::getTranslator();
5566 $av_helper = self::getAvailabilityInstance();
5567
5568 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5569 $dbo->setQuery($q);
5570 $booking = $dbo->loadAssoc();
5571 if (!$booking) {
5572 return false;
5573 }
5574
5575 $result = false;
5576
5577 // check if the language in use is the same as the one used during the checkout
5578 $lang = JFactory::getLanguage();
5579 if (!empty($booking['lang'])) {
5580 if ($lang->getTag() != $booking['lang']) {
5581 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
5582 if (VBOPlatformDetection::isJoomla()) {
5583 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
5584 }
5585 }
5586 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
5587 // force the translation to start because contents should be translated
5588 $vbo_tn::$force_tolang = $booking['lang'];
5589 }
5590 }
5591
5592 // load rooms booked
5593 $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;";
5594 $dbo->setQuery($q);
5595 $ordersrooms = $dbo->loadAssocList();
5596 if (!$ordersrooms) {
5597 return false;
5598 }
5599 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
5600
5601 /**
5602 * Split stay reservation.
5603 *
5604 * @since 1.16.0 (J) - 1.6.0 (WP)
5605 */
5606 $room_stay_dates = [];
5607 if ($booking['split_stay']) {
5608 if ($booking['status'] == 'confirmed') {
5609 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
5610 } else {
5611 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
5612 }
5613 // immediately count the number of nights of stay for each split room
5614 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5615 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5616 // overwrite values for compatibility with non-confirmed bookings
5617 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5618 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5619 }
5620 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5621 // overwrite the whole array
5622 $room_stay_dates[$sps_r_k] = $sps_r_v;
5623 }
5624 }
5625
5626 $rooms = array();
5627 $tars = array();
5628 $is_package = !empty($booking['pkg']) ? true : false;
5629 $ftitle = self::getFrontTitle();
5630 $currencyname = self::getCurrencyName();
5631 foreach ($ordersrooms as $kor => $or) {
5632 $num = $kor + 1;
5633 $rooms[$num] = $or;
5634 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5635 // package or custom cost set from the back-end
5636 continue;
5637 }
5638
5639 // determine the days to consider for the count of the availability
5640 $room_nights = $booking['days'];
5641 $room_checkin = $booking['checkin'];
5642 $room_checkout = $booking['checkout'];
5643 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5644 $room_nights = $room_stay_dates[$kor]['nights'];
5645 $room_checkin = $room_stay_dates[$kor]['checkin'];
5646 $room_checkout = $room_stay_dates[$kor]['checkout'];
5647 }
5648
5649 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
5650 $dbo->setQuery($q);
5651 $tar = $dbo->loadAssocList();
5652 if (!$tar) {
5653 // tariff not found
5654 if (self::isAdmin()) {
5655 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
5656 }
5657 continue;
5658 }
5659
5660 // apply seasonal rates
5661 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
5662
5663 // different usage
5664 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
5665 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
5666 // Occupancy Override
5667 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
5668 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
5669 //
5670 if (is_array($diffusageprice)) {
5671 // set a charge or discount to the price(s) for the different usage of the room
5672 foreach ($tar as $kpr => $vpr) {
5673 $tar[$kpr]['diffusage'] = $or['adults'];
5674 if ($diffusageprice['chdisc'] == 1) {
5675 // charge
5676 if ($diffusageprice['valpcent'] == 1) {
5677 // fixed value
5678 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5679 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5680 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
5681 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5682 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
5683 } else {
5684 // percentage value
5685 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5686 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
5687 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
5688 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5689 $tar[$kpr]['cost'] = $aduseval;
5690 }
5691 } else {
5692 // discount
5693 if ($diffusageprice['valpcent'] == 1) {
5694 // fixed value
5695 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5696 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5697 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
5698 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5699 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
5700 } else {
5701 // percentage value
5702 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5703 $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);
5704 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
5705 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5706 $tar[$kpr]['cost'] = $aduseval;
5707 }
5708 }
5709 }
5710 }
5711 }
5712 //
5713 $tars[$num] = $tar[0];
5714 }
5715
5716 $secdiff = $booking['checkout'] - $booking['checkin'];
5717 $daysdiff = $secdiff / 86400;
5718 if (is_int($daysdiff)) {
5719 if ($daysdiff < 1) {
5720 $daysdiff = 1;
5721 }
5722 } else {
5723 if ($daysdiff < 1) {
5724 $daysdiff = 1;
5725 } else {
5726 $sum = floor($daysdiff) * 86400;
5727 $newdiff = $secdiff - $sum;
5728 $maxhmore = self::getHoursMoreRb() * 3600;
5729 if ($maxhmore >= $newdiff) {
5730 $daysdiff = floor($daysdiff);
5731 } else {
5732 $daysdiff = ceil($daysdiff);
5733 }
5734 }
5735 }
5736
5737 $isdue = 0;
5738 $pricestr = array();
5739 $optstr = array();
5740 foreach ($ordersrooms as $kor => $or) {
5741 $num = $kor + 1;
5742
5743 // determine the days to consider for the count of the availability
5744 $room_nights = $booking['days'];
5745 $room_checkin = $booking['checkin'];
5746 $room_checkout = $booking['checkout'];
5747 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5748 $room_nights = $room_stay_dates[$kor]['nights'];
5749 $room_checkin = $room_stay_dates[$kor]['checkin'];
5750 $room_checkout = $room_stay_dates[$kor]['checkout'];
5751 }
5752
5753 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5754 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
5755 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
5756 $isdue += $calctar;
5757 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
5758 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
5759 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5760 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
5761 $tars[$num]['calctar'] = $calctar;
5762 $isdue += $calctar;
5763 $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'] : "");
5764 }
5765
5766 if (!empty($or['optionals'])) {
5767 $stepo = explode(";", $or['optionals']);
5768 foreach ($stepo as $roptkey => $oo) {
5769 if (empty($oo)) {
5770 continue;
5771 }
5772 $stept = explode(":", $oo);
5773 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
5774 $dbo->setQuery($q);
5775 $actopt = $dbo->loadAssocList();
5776 if ($actopt) {
5777 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
5778 $chvar = '';
5779 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
5780 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
5781 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
5782 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
5783 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
5784 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
5785 $agestept = explode('-', $stept[1]);
5786 $stept[1] = $agestept[0];
5787 $chvar = $agestept[1];
5788 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
5789 // percentage value of the adults tariff
5790 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5791 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5792 } else {
5793 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5794 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5795 }
5796 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
5797 // VBO 1.10 - percentage value of room base cost
5798 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5799 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5800 } else {
5801 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
5802 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5803 }
5804 }
5805 $actopt[0]['chageintv'] = $chvar;
5806 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
5807 $actopt[0]['quan'] = $stept[1];
5808 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
5809 } else {
5810 $actopt[0]['quan'] = $stept[1];
5811 // VBO 1.11 - options percentage cost of the room total fee
5812 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5813 $deftar_basecosts = $or['cust_cost'];
5814 } else {
5815 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5816 }
5817 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
5818 //
5819 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
5820 }
5821 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
5822 $realcost = $actopt[0]['maxprice'];
5823 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
5824 $realcost = $actopt[0]['maxprice'] * $stept[1];
5825 }
5826 }
5827 if ($actopt[0]['perperson'] == 1) {
5828 $realcost = $realcost * $or['adults'];
5829 }
5830 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
5831 $isdue += $tmpopr;
5832 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
5833 }
5834 }
5835 }
5836
5837 // custom extra costs
5838 if (!empty($or['extracosts'])) {
5839 $cur_extra_costs = json_decode($or['extracosts'], true);
5840 foreach ($cur_extra_costs as $eck => $ecv) {
5841 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
5842 $isdue += $ecplustax;
5843 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
5844 }
5845 }
5846 }
5847
5848 // force the original total amount if rates have changed
5849 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
5850 $isdue = $booking['total'];
5851 }
5852
5853 // mail subject
5854 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
5855 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
5856
5857 // inject the recipient of the message for the template
5858 $booking['for'] = $for;
5859
5860 // load template file that will get $booking as variable
5861 $tmpl = self::loadEmailTemplate($booking);
5862
5863 // parse email template
5864 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
5865 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
5866
5867 if ($send !== true) {
5868 // return the content of the email message parsed
5869 return $hmess;
5870 }
5871
5872 // when the message can be sent
5873 $sendwhen = self::getSendEmailWhen();
5874
5875 if ($no_config) {
5876 // force the configuration setting to send the message no matter what's the booking status
5877 $sendwhen = 1;
5878 }
5879
5880 // send the message
5881 foreach ($for as $who) {
5882 $use_subject = $subject;
5883 $recipients = array();
5884 $attachments = self::addEmailAttachment(null);
5885 $attach_ical = false;
5886 $force_replyto = null;
5887 if (strpos($who, '@') !== false) {
5888 // send email to custom email address
5889 array_push($recipients, trim($who));
5890 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
5891 // send email to the customer
5892 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5893 continue;
5894 }
5895 array_push($recipients, $booking['custmail']);
5896 /**
5897 * Check whether an iCal should be attached for the customer.
5898 *
5899 * @since 1.12.0
5900 */
5901 $attach_ical = self::getEmailIcal('customer', $booking);
5902 } elseif (stripos($who, 'admin') !== false) {
5903 // send email to the administrator(s)
5904 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5905 continue;
5906 }
5907 $use_subject = $subject . ' #' . $booking['id'];
5908 if (!strcasecmp($type, 'modified')) {
5909 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
5910 }
5911 $adminemail = self::getAdminMail();
5912 $extra_admin_recipients = self::addAdminEmailRecipient(null);
5913 if (empty($adminemail) && empty($extra_admin_recipients)) {
5914 // Prevent Joomla Exceptions that would stop the script execution
5915 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
5916 continue;
5917 }
5918 if (strpos($adminemail, ',') !== false) {
5919 // multiple addresses
5920 $adminemails = explode(',', $adminemail);
5921 foreach ($adminemails as $am) {
5922 if (strpos($am, '@') !== false) {
5923 array_push($recipients, trim($am));
5924 }
5925 }
5926 } else {
5927 // single address
5928 array_push($recipients, trim($adminemail));
5929 }
5930
5931 // merge extra recipients
5932 $recipients = array_merge($recipients, $extra_admin_recipients);
5933
5934 // admin should reply to the customer
5935 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
5936
5937 /**
5938 * Check whether an iCal should be attached for the admin.
5939 *
5940 * @since 1.2.0
5941 */
5942 $attach_ical = self::getEmailIcal('admin', array(
5943 'ts' => $booking['ts'],
5944 'custdata' => $booking['custdata'],
5945 'checkin' => $booking['checkin'],
5946 'checkout' => $booking['checkout'],
5947 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
5948 ));
5949 }
5950
5951 // send the message, recipients should always be an array to support multiple admin addresses
5952
5953 // get sender e-mail
5954 $adsendermail = VBOFactory::getConfig()->get('senderemail');
5955
5956 // init mail data
5957 $mail = new VBOMailWrapper([
5958 'sender' => [$adsendermail, $ftitle],
5959 'recipient' => $recipients,
5960 'bcc' => self::addAdminEmailRecipient(null, true),
5961 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
5962 'subject' => $use_subject,
5963 'content' => $hmess,
5964 'attachments' => $attachments,
5965 ]);
5966
5967 if ($attach_ical !== false && $booking['status'] != 'standby') {
5968 // attach iCal file
5969 $mail->addAttachment($attach_ical);
5970 }
5971
5972 /**
5973 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
5974 *
5975 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
5976 *
5977 * @since 1.15.4 (J) - 1.5.10 (WP)
5978 */
5979 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
5980
5981 // send e-mail
5982 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
5983
5984 // unlink iCal file
5985 if ($attach_ical !== false) {
5986 @unlink($attach_ical);
5987 }
5988 }
5989
5990 return $result;
5991 }
5992
5993 /**
5994 * This method allows to add one or more recipient email
5995 * addresses for the next queue of email sending for the admin.
5996 * This method can be used in the template file for the customer
5997 * email to register an additional email address, maybe when a
5998 * specific room-type is booked.
5999 * The methods sending the email messages are supposed to call this
6000 * method by passing no arguments to obtain the extra addresses set.
6001 *
6002 * @param mixed $email null, string or array of email address(es).
6003 * @param bool $bcc if true, addresses will be used as bcc.
6004 * @param bool $reset if true, the queues will be emptied.
6005 *
6006 * @return array the current extra recipients or bcc addresses set.
6007 *
6008 * @since 1.13 (J) - 1.3.0 (WP)
6009 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6010 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6011 */
6012 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6013 {
6014 static $extra_recipients = [];
6015 static $extra_bcc = [];
6016
6017 if ($reset) {
6018 $extra_recipients = [];
6019 $extra_bcc = [];
6020 }
6021
6022 if (!empty($email)) {
6023 if (is_scalar($email)) {
6024 if ($bcc) {
6025 array_push($extra_bcc, $email);
6026 } else {
6027 array_push($extra_recipients, $email);
6028 }
6029 } else {
6030 if ($bcc) {
6031 $extra_bcc = array_merge($extra_bcc, $email);
6032 } else {
6033 $extra_recipients = array_merge($extra_recipients, $email);
6034 }
6035 }
6036 }
6037
6038 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6039 }
6040
6041 /**
6042 * This method serves to add one or more attachments to the
6043 * next queue of email sending for the admin.
6044 * The methods sending the email messages are supposed to call this
6045 * method by passing a null argument to obtain the attachments set.
6046 *
6047 * @param mixed $file null or string with path to file to attach.
6048 * @param bool $reset if true, the queue will be emptied.
6049 *
6050 * @return array the current attachments set.
6051 *
6052 * @since 1.14
6053 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6054 */
6055 public static function addEmailAttachment($file, $reset = false)
6056 {
6057 static $extra_attachments = [];
6058
6059 if ($reset) {
6060 $extra_attachments = [];
6061 }
6062
6063 if (!empty($file)) {
6064 if (is_scalar($file)) {
6065 array_push($extra_attachments, $file);
6066 } else {
6067 $extra_attachments = array_merge($extra_attachments, $file);
6068 }
6069 }
6070
6071 return array_unique($extra_attachments);
6072 }
6073
6074 /**
6075 * This method is called whenever some rooms get booked.
6076 * It checks whether the rooms involved have shared calendars.
6077 * If some are found, then also such rooms will be occupied.
6078 *
6079 * @param int $bid the booking ID.
6080 * @param array $roomids the list of rooms booked.
6081 * @param int $checkin checkin timestamp.
6082 * @param int $checkout checkout timestamp.
6083 *
6084 * @return boolean true if some other cals were occupied, false otherwise.
6085 *
6086 * @since 1.13 (J) - 1.3.0 (WP)
6087 *
6088 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6089 */
6090 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6091 {
6092 $dbo = JFactory::getDbo();
6093 $bid = (int)$bid;
6094
6095 // availability helper
6096 $av_helper = self::getAvailabilityInstance();
6097
6098 // check for split stay rooms reservation
6099 $known_split_stay = false;
6100 $is_split_stay = 0;
6101
6102 if (empty($checkin) || empty($checkout)) {
6103 // get checkin and checkout timestamps from booking
6104 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6105 $dbo->setQuery($q);
6106 $dbo->execute();
6107 if (!$dbo->getNumRows()) {
6108 // booking not found
6109 return false;
6110 }
6111 $bdata = $dbo->loadAssoc();
6112 $checkin = $bdata['checkin'];
6113 $checkout = $bdata['checkout'];
6114 $is_split_stay = $bdata['split_stay'];
6115 // turn flag on
6116 $known_split_stay = true;
6117 }
6118
6119 if (!$known_split_stay) {
6120 // we need to query the db to access this information
6121 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6122 $dbo->setQuery($q, 0, 1);
6123 $dbo->execute();
6124 if ($dbo->getNumRows()) {
6125 $is_split_stay = (int)$dbo->loadResult();
6126 }
6127 // turn flag on
6128 $known_split_stay = true;
6129 }
6130
6131 // check split stay room records
6132 $room_stay_dates = [];
6133 if ($is_split_stay) {
6134 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6135 // immediately count the number of nights of stay for each split room
6136 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6137 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6138 // overwrite values for compatibility with non-confirmed bookings
6139 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6140 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6141 }
6142 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6143 // overwrite the whole array
6144 $room_stay_dates[$sps_r_k] = $sps_r_v;
6145 }
6146 }
6147
6148 // get the rooms booked
6149 if (!count($roomids)) {
6150 // get the IDs of all rooms booked
6151 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6152 $dbo->setQuery($q);
6153 $orr = $dbo->loadAssocList();
6154 if ($orr) {
6155 foreach ($orr as $or) {
6156 array_push($roomids, $or['idroom']);
6157 }
6158 }
6159 }
6160 if (!count($roomids) || empty($bid)) {
6161 // unable to proceed
6162 return false;
6163 }
6164 $roomids = array_unique($roomids);
6165
6166 // build room stay dates map in case of split stay
6167 $room_split_stay_map = [];
6168 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6169 // we have unique room IDs in a split stay reservation for more than one room
6170 foreach ($room_stay_dates as $room_stay_date) {
6171 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6172 }
6173 }
6174
6175 // get rooms involved
6176 $involved = [];
6177 $involved_stay_map = [];
6178 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6179 $dbo->setQuery($q);
6180 $rooms_found = $dbo->loadAssocList();
6181 if (!$rooms_found) {
6182 // no rooms involved that need their calendars updated
6183 return false;
6184 }
6185 foreach ($rooms_found as $rf) {
6186 if (!in_array($rf['mainroom'], $roomids)) {
6187 // push room ID
6188 array_push($involved, $rf['mainroom']);
6189 // check for specific stay dates in connected room
6190 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6191 // split stay information available
6192 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6193 }
6194 }
6195 if (!in_array($rf['childroom'], $roomids)) {
6196 // push room ID
6197 array_push($involved, $rf['childroom']);
6198 // check for specific stay dates in connected room
6199 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6200 // split stay information available
6201 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6202 }
6203 }
6204 }
6205 $involved = array_unique($involved);
6206 if (!count($involved)) {
6207 // no rooms involved
6208 return false;
6209 }
6210
6211 // turnover seconds
6212 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6213
6214 // occupy the calendars for the involved rooms found
6215 $bids_generated = [];
6216 foreach ($involved as $rid) {
6217 // determine the values to use
6218 $room_checkin = (int)$checkin;
6219 $room_checkout = (int)$checkout;
6220 $room_realback = ($checkout + $turnover_secs);
6221 if (isset($involved_stay_map[$rid])) {
6222 // split stay dates available
6223 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6224 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6225 $room_realback = ($room_checkout + $turnover_secs);
6226 }
6227
6228 // occupy record in room with shared calendar
6229 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6230 $dbo->setQuery($q);
6231 $dbo->execute();
6232 array_push($bids_generated, $dbo->insertid());
6233 }
6234
6235 // store busy relations created
6236 foreach ($bids_generated as $busyid) {
6237 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6238 $dbo->setQuery($q);
6239 $dbo->execute();
6240 }
6241
6242 return true;
6243 }
6244
6245 /**
6246 * This method is needed whenever a booking gets modified by
6247 * adding or removing rooms. This way we reset (remove) all
6248 * busy records that were previously stored due to a shared
6249 * calendar. The correct relations should then be re-created
6250 * by calling updateSharedCalendars().
6251 *
6252 * @param int $bid the booking ID.
6253 *
6254 * @return boolean true if some records were cleaned.
6255 *
6256 * @see updateSharedCalendars() should be called after.
6257 *
6258 * @since 1.3.0
6259 */
6260 public static function cleanSharedCalendarsBusy($bid) {
6261 $dbo = JFactory::getDbo();
6262 $bid = (int)$bid;
6263 // get all the occupied records due to shared calendars for this booking
6264 $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;";
6265 $dbo->setQuery($q);
6266 $allbusy = $dbo->loadAssocList();
6267 if (!$allbusy) {
6268 return false;
6269 }
6270
6271 $busy_ids = array();
6272 foreach ($allbusy as $b) {
6273 // push busy ID to be removed later
6274 array_push($busy_ids, $b['id']);
6275 // delete the current busy ID-booking ID relation for this shared calendar
6276 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6277 $dbo->setQuery($q);
6278 $dbo->execute();
6279 }
6280 if (count($busy_ids)) {
6281 // delete all busy records due to shared calendars
6282 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6283 $dbo->setQuery($q);
6284 $dbo->execute();
6285 }
6286
6287 return true;
6288 }
6289
6290 public static function sendJutility() {
6291 //deprecated in VBO 1.10
6292 $dbo = JFactory::getDbo();
6293 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6294 $dbo->setQuery($q);
6295 $dbo->execute();
6296 $s = $dbo->loadAssocList();
6297 return (intval($s[0]['setting']) == 1 ? true : false);
6298 }
6299
6300 public static function getCategoryName($idcat, $vbo_tn = null) {
6301 $dbo = JFactory::getDbo();
6302 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6303 $dbo->setQuery($q);
6304 $dbo->execute();
6305 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6306 if (is_object($vbo_tn) && count($p) > 0) {
6307 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6308 }
6309 return count($p) > 0 ? $p[0]['name'] : '';
6310 }
6311
6312 public static function loadAdultsDiff($idroom, $adults)
6313 {
6314 static $obp_map = [];
6315
6316 $map_sign = "{$idroom}:{$adults}";
6317
6318 if (array_key_exists($map_sign, $obp_map)) {
6319 return $obp_map[$map_sign];
6320 }
6321
6322 $dbo = JFactory::getDbo();
6323
6324 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6325 $dbo->setQuery($q, 0, 1);
6326 $obp_diff = $dbo->loadAssoc();
6327 if ($obp_diff) {
6328 // cache value and return it
6329 $obp_map[$map_sign] = $obp_diff;
6330
6331 return $obp_diff;
6332 }
6333
6334 // cache value and return it
6335 $obp_map[$map_sign] = null;
6336
6337 return null;
6338 }
6339
6340 public static function loadRoomAdultsDiff($idroom)
6341 {
6342 $dbo = JFactory::getDbo();
6343
6344 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6345 $dbo->setQuery($q);
6346 $diff = $dbo->loadAssocList();
6347 if ($diff) {
6348 $roomdiff = [];
6349 foreach ($diff as $v) {
6350 $roomdiff[$v['adults']] = $v;
6351 }
6352
6353 return $roomdiff;
6354 }
6355
6356 return [];
6357 }
6358
6359 public static function occupancyOverrideExists($tar, $adults) {
6360 foreach ($tar as $k => $v) {
6361 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6362 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6363 return $v['occupancy_ovr'][$adults];
6364 }
6365 }
6366 }
6367 return false;
6368 }
6369
6370 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6371 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6372 $charges = array();
6373 if (!($children > 0) || !(count($ages) > 0)) {
6374 return $charges;
6375 }
6376 $dbo = JFactory::getDbo();
6377 $id_options = array();
6378 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6379 $dbo->setQuery($q);
6380 $dbo->execute();
6381 if ($dbo->getNumRows() > 0) {
6382 $assocs = $dbo->loadAssocList();
6383 foreach ($assocs as $opts) {
6384 if (!empty($opts['idopt'])) {
6385 $r_ido = explode(';', rtrim($opts['idopt']));
6386 foreach ($r_ido as $ido) {
6387 if (!empty($ido) && !in_array($ido, $id_options)) {
6388 $id_options[] = $ido;
6389 }
6390 }
6391 }
6392 }
6393 }
6394 if (count($id_options) > 0) {
6395 $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;";
6396 $dbo->setQuery($q);
6397 $dbo->execute();
6398 if ($dbo->getNumRows() > 0) {
6399 $ageintervals = $dbo->loadAssocList();
6400 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6401 $age_range = array();
6402 foreach ($split_ages as $kg => $spage) {
6403 if (empty($spage)) {
6404 continue;
6405 }
6406 $parts = explode('_', $spage);
6407 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6408 $ind = count($age_range);
6409 $age_range[$ind]['from'] = intval($parts[0]);
6410 $age_range[$ind]['to'] = intval($parts[1]);
6411 //taxes are calculated later in VCM
6412 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6413 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6414 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6415 }
6416 }
6417 if (count($age_range) > 0) {
6418 $tot_charge = 0;
6419 $affected = array();
6420 $option_str = '';
6421 foreach ($ages as $age) {
6422 if (strlen($age) == 0) {
6423 continue;
6424 }
6425 foreach ($age_range as $range) {
6426 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6427 $tot_charge += $range['cost'];
6428 $affected[] = $age;
6429 $option_str .= $range['option_str'].';';
6430 break;
6431 }
6432 }
6433 }
6434 if ($tot_charge > 0) {
6435 $charges['total'] = $tot_charge;
6436 $charges['affected'] = $affected;
6437 $charges['options'] = $option_str;
6438 }
6439 }
6440 }
6441 }
6442
6443 return $charges;
6444 }
6445
6446 public static function sortRoomPrices($arr) {
6447 $newarr = array();
6448 foreach ($arr as $k => $v) {
6449 $newarr[$k] = $v['cost'];
6450 }
6451 asort($newarr);
6452 $sorted = array();
6453 foreach ($newarr as $k => $v) {
6454 $sorted[$k] = $arr[$k];
6455 }
6456 return $sorted;
6457 }
6458
6459 public static function sortResults($arr) {
6460 $newarr = array();
6461 foreach ($arr as $k => $v) {
6462 $newarr[$k] = $v[0]['cost'];
6463 }
6464 asort($newarr);
6465 $sorted = array();
6466 foreach ($newarr as $k => $v) {
6467 $sorted[$k] = $arr[$k];
6468 }
6469 return $sorted;
6470 }
6471
6472 public static function sortMultipleResults($arr) {
6473 foreach ($arr as $k => $v) {
6474 $newarr = array();
6475 foreach ($v as $subk => $subv) {
6476 $newarr[$subk] = $subv[0]['cost'];
6477 }
6478 asort($newarr);
6479 $sorted = array();
6480 foreach ($newarr as $nk => $v) {
6481 $sorted[$nk] = $arr[$k][$nk];
6482 }
6483 $arr[$k] = $sorted;
6484 }
6485 return $arr;
6486 }
6487
6488 /**
6489 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6490 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6491 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6492 *
6493 * @param array $promos optional list of promotion IDs to register in the execution flow.
6494 *
6495 * @return array list of cached promotion IDs (if any).
6496 *
6497 * @since 1.16.4 (J) - 1.6.4 (WP)
6498 */
6499 public static function registerPromotionIds(array $promos = null)
6500 {
6501 static $flagged_promos = [];
6502
6503 if ($promos) {
6504 // set cached value
6505 $flagged_promos = $promos;
6506 }
6507
6508 return $flagged_promos;
6509 }
6510
6511 /**
6512 * Getter and setter for seasons cache allowed.
6513 *
6514 * @param bool $enabled true or false for setter, null for getter.
6515 *
6516 * @return bool|null
6517 *
6518 * @since 1.16.5 (J) - 1.6.5 (WP)
6519 */
6520 public static function setSeasonsCache($enabled = null)
6521 {
6522 static $seasons_cache = null;
6523
6524 if ($enabled !== null) {
6525 // set value
6526 $seasons_cache = (bool)$enabled;
6527 }
6528
6529 return $seasons_cache;
6530 }
6531
6532 /**
6533 * Applies the seasonal rates over a list of room rate records.
6534 *
6535 * @param array $arr list of room rate records.
6536 * @param int $from unix timestamp for start date.
6537 * @param int $to unix timestamp for end date.
6538 *
6539 * @return array list of manipulated room rate records.
6540 */
6541 public static function applySeasonalPrices(array $arr, $from, $to)
6542 {
6543 $dbo = JFactory::getDbo();
6544 $vbo_tn = self::getTranslator();
6545
6546 $roomschange = [];
6547 $one = getdate($from);
6548
6549 // leap years
6550 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6551 $isleap = true;
6552 } else {
6553 $isleap = false;
6554 }
6555
6556 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6557 $tomidnightone = intval($one['hours']) * 3600;
6558 $tomidnightone += intval($one['minutes']) * 60;
6559 $sfrom = $from - $baseone - $tomidnightone;
6560 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6561 $two = getdate($to);
6562 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6563 $tomidnighttwo = intval($two['hours']) * 3600;
6564 $tomidnighttwo += intval($two['minutes']) * 60;
6565 $sto = $to - $basetwo - $tomidnighttwo;
6566
6567 // leap years, check what dates to manipulate
6568 if ($isleap) {
6569 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6570 if ($one[0] > $leapts) {
6571 /**
6572 * Timestamp must be greater than the leap-day of Feb 29th.
6573 * It used to be checked for >= $leapts.
6574 *
6575 * @since July 3rd 2019
6576 */
6577 $sfrom -= 86400;
6578 }
6579 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6580 // lower checkin date when in leap year but not for checkout
6581 $sto -= 86400;
6582 }
6583 }
6584
6585 // check for DST changes to adjust the query values to fetch
6586 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6587 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6588 // we are parsing the day when the DST changed (probably a Sunday)
6589 if (!date('I', $from)) {
6590 // DST was just turned off
6591 $sfrom -= 3600;
6592 }
6593 }
6594 }
6595
6596 // count nights requested
6597 $booking_nights = 1;
6598 foreach ($arr as $k => $a) {
6599 if (!isset($a[0])) {
6600 continue;
6601 }
6602 if (isset($a[0]['booking_nights'])) {
6603 // this value may be set when displaying pricing calendars
6604 $booking_nights = $a[0]['booking_nights'];
6605 break;
6606 }
6607 if (isset($a[0]['days'])) {
6608 $booking_nights = $a[0]['days'];
6609 break;
6610 }
6611 }
6612
6613 /**
6614 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
6615 *
6616 * @since 1.16.4 (J) - 1.6.4 (WP)
6617 */
6618 $skip_promo_ids = self::registerPromotionIds();
6619
6620 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6621 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6622 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6623 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6624 ($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`)") .
6625 ($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`)") .
6626 ($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`)") .
6627 ($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`)") .
6628 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6629 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6630
6631 $dbo->setQuery($q);
6632 $seasons = $dbo->loadAssocList();
6633
6634 if ($seasons) {
6635 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
6636 $applyseasons = false;
6637 $mem = [];
6638 foreach ($arr as $k => $a) {
6639 $mem[$k]['daysused'] = 0;
6640 $mem[$k]['sum'] = [];
6641 /**
6642 * The keys below are all needed to apply the promotions on the room's final cost.
6643 *
6644 * @since 1.13.5
6645 */
6646 $mem[$k]['diffs'] = [];
6647 $mem[$k]['trans_keys'] = [];
6648 $mem[$k]['trans_factors'] = [];
6649 $mem[$k]['trans_affdays'] = [];
6650 }
6651 foreach ($seasons as $s) {
6652 // check if this is a promotion registered for skipping
6653 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
6654 continue;
6655 }
6656
6657 // Special Price tied to the year
6658 if (!empty($s['year']) && $s['year'] > 0) {
6659 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
6660 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
6661 //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
6662 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
6663 continue;
6664 }
6665 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
6666 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
6667 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
6668 if ($calc_ends < ($from - $tomidnightone)) {
6669 continue;
6670 }
6671 }
6672 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
6673 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
6674 if ($s['from'] > $s['to']) {
6675 continue;
6676 }
6677 if (($basetwo + $s['from'] + 86399) > $to) {
6678 /**
6679 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
6680 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
6681 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
6682 *
6683 * @since 1.14.3 (J) - 1.4.3 (WP)
6684 */
6685 continue;
6686 }
6687 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
6688 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
6689 /**
6690 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
6691 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
6692 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
6693 *
6694 * @since 1.14.3 (J) - 1.4.3 (WP)
6695 */
6696 continue;
6697 }
6698 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
6699 // 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)
6700 if (($baseone + $s['from']) > $to) {
6701 continue;
6702 }
6703 }
6704 }
6705 //
6706 $allrooms = explode(",", $s['idrooms']);
6707 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
6708 $inits = $baseone + $s['from'];
6709 if ($s['from'] < $s['to']) {
6710 $ends = $basetwo + $s['to'];
6711 // check if the inits must be set to the year after
6712 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
6713 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
6714 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6715 $inits = $tmpbase + $s['from'];
6716 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6717 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
6718 $ends = $baseone + $s['to'];
6719 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6720 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
6721 $ends = $baseone + $s['to'];
6722 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
6723 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
6724 $inits = $basetwo + $s['from'];
6725 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
6726 // booking dates across two years for a season tied to a specific year
6727 if ($one['year'] == $s['year']) {
6728 $inits = $baseone + $s['from'];
6729 } else {
6730 $inits = $basetwo + $s['from'];
6731 }
6732 if ($two['year'] == $s['year']) {
6733 $ends = $basetwo + $s['to'];
6734 } else {
6735 $ends = $baseone + $s['to'];
6736 }
6737 }
6738 } else {
6739 // between 2 years
6740 if ($baseone < $basetwo) {
6741 // ex. 29/12/2012 - 14/01/2013
6742 $ends = $basetwo + $s['to'];
6743 } else {
6744 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
6745 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
6746 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6747 $ends = $tmpbase + $s['to'];
6748 } else {
6749 // ex. 03/01 - 09/01
6750 $ends = $basetwo + $s['to'];
6751 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
6752 $inits = $tmpbase + $s['from'];
6753 }
6754 }
6755 }
6756
6757 // leap years
6758 if ($isleap == true) {
6759 $infoseason = getdate($inits);
6760 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
6761 // leap years, check what dates to manipulate
6762 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
6763 /**
6764 * Timestamp must be greater than the leap-day of Feb 29th.
6765 * It used to be checked for >= $leapts.
6766 *
6767 * @since July 3rd 2019
6768 */
6769 $inits += 86400;
6770 if ($s['from'] < $s['to']) {
6771 // increase season end date only if still on the same leap year
6772 $ends += 86400;
6773 }
6774 }
6775 }
6776
6777 // promotions
6778 $promotion = [];
6779 if ($s['promo'] == 1) {
6780 $daysadv = (($inits - time()) / 86400);
6781 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
6782 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
6783 continue;
6784 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
6785 $secstocheckin = ($from - time());
6786 if ($s['promolastmin'] < $secstocheckin) {
6787 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
6788 continue;
6789 }
6790 }
6791 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
6792 /**
6793 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
6794 *
6795 * @since 1.13.5
6796 */
6797 continue;
6798 }
6799 $promotion['todaydaysadv'] = $daysadv;
6800 $promotion['promodaysadv'] = $s['promodaysadv'];
6801 $promotion['promotxt'] = $s['promotxt'];
6802 }
6803
6804 // occupancy override
6805 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
6806
6807 // week days
6808 $filterwdays = !empty($s['wdays']) ? true : false;
6809 $wdays = $filterwdays ? explode(';', $s['wdays']) : '';
6810 if (is_array($wdays) && $wdays) {
6811 foreach ($wdays as $kw => $wd) {
6812 if (strlen($wd) == 0) {
6813 unset($wdays[$kw]);
6814 }
6815 }
6816 }
6817
6818 // checkin must be after the beginning of the season
6819 $checkininclok = true;
6820 if ($s['checkinincl'] == 1) {
6821 $checkininclok = false;
6822 if ($s['from'] < $s['to']) {
6823 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
6824 $checkininclok = true;
6825 }
6826 } else {
6827 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
6828 $checkininclok = true;
6829 }
6830 }
6831 }
6832 if ($checkininclok !== true) {
6833 continue;
6834 }
6835
6836 foreach ($arr as $k => $a) {
6837 // applied only to some types of price
6838 if ($allprices && !empty($allprices[0])) {
6839 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
6840 continue;
6841 }
6842 }
6843 // applied only to some room types
6844 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
6845 continue;
6846 }
6847
6848 // count affected nights of stay
6849 $affdays = 0;
6850 $season_fromdayts = $fromdayts;
6851 $is_dst = date('I', $season_fromdayts);
6852 for ($i = 0; $i < $a[0]['days']; $i++) {
6853 $todayts = $season_fromdayts + ($i * 86400);
6854 $is_now_dst = date('I', $todayts);
6855 if ($is_dst != $is_now_dst) {
6856 // Daylight Saving Time has changed, check how
6857 if ((bool)$is_dst === true) {
6858 $todayts += 3600;
6859 $season_fromdayts += 3600;
6860 } else {
6861 $todayts -= 3600;
6862 $season_fromdayts -= 3600;
6863 }
6864 $is_dst = $is_now_dst;
6865 }
6866 if ($todayts >= $inits && $todayts <= $ends) {
6867 // week days
6868 if ($filterwdays == true) {
6869 $checkwday = getdate($todayts);
6870 if (in_array($checkwday['wday'], $wdays)) {
6871 $affdays++;
6872 }
6873 } else {
6874 $affdays++;
6875 }
6876 //
6877 }
6878 }
6879 if (!($affdays > 0)) {
6880 // no nights affected
6881 continue;
6882 }
6883
6884 // apply the rule
6885 $applyseasons = true;
6886 $dailyprice = $a[0]['cost'] / $a[0]['days'];
6887
6888 // modification factor object
6889 $factor = new stdClass;
6890
6891 // calculate new price progressively
6892 if (intval($s['val_pcent']) == 2) {
6893 // percentage value
6894 $factor->pcent = 1;
6895 $pctval = $s['diffcost'];
6896 if (strlen((string)$s['losoverride'])) {
6897 // values overrides
6898 $arrvaloverrides = [];
6899 $valovrparts = explode('_', $s['losoverride']);
6900 foreach ($valovrparts as $valovr) {
6901 if (!empty($valovr)) {
6902 $ovrinfo = explode(':', $valovr);
6903 if (strstr($ovrinfo[0], '-i') != false) {
6904 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6905 if ((int)$ovrinfo[0] < $a[0]['days']) {
6906 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6907 }
6908 }
6909 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6910 }
6911 }
6912 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6913 $pctval = $arrvaloverrides[$a[0]['days']];
6914 }
6915 }
6916 if (intval($s['type']) == 1) {
6917 // charge
6918 $factor->type = '+';
6919 $cpercent = 100 + $pctval;
6920 } else {
6921 // discount
6922 $factor->type = '-';
6923 $cpercent = 100 - $pctval;
6924 }
6925 $factor->amount = $pctval;
6926 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
6927 } else {
6928 // absolute value
6929 $factor->pcent = 0;
6930 $absval = $s['diffcost'];
6931 if (strlen((string)$s['losoverride'])) {
6932 // values overrides
6933 $arrvaloverrides = [];
6934 $valovrparts = explode('_', $s['losoverride']);
6935 foreach ($valovrparts as $valovr) {
6936 if (!empty($valovr)) {
6937 $ovrinfo = explode(':', $valovr);
6938 if (strstr($ovrinfo[0], '-i') != false) {
6939 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6940 if ((int)$ovrinfo[0] < $a[0]['days']) {
6941 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6942 }
6943 }
6944 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6945 }
6946 }
6947 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6948 $absval = $arrvaloverrides[$a[0]['days']];
6949 }
6950 }
6951 if (intval($s['type']) == 1) {
6952 // charge
6953 $factor->type = '+';
6954 $newprice = ($dailyprice + $absval) * $affdays;
6955 } else {
6956 // discount
6957 $factor->type = '-';
6958 $newprice = ($dailyprice - $absval) * $affdays;
6959 }
6960 $factor->amount = $absval;
6961 }
6962
6963 // apply rounding
6964 $factor->roundmode = $s['roundmode'];
6965 if (!empty($s['roundmode'])) {
6966 $newprice = round($newprice, 0, constant($s['roundmode']));
6967 } else {
6968 $newprice = round($newprice, 2);
6969 }
6970
6971 // define the promotion (only if no value overrides set the amount to 0)
6972 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
6973 /**
6974 * Include the discount information (if any). The cost re-calculated may not be
6975 * precise if multiple special prices were applied over the same dates.
6976 *
6977 * @since 1.13
6978 */
6979 if ($s['type'] == 2 && $s['diffcost'] > 0) {
6980 $promotion['discount'] = [
6981 'amount' => $s['diffcost'],
6982 'pcent' => (int)($s['val_pcent'] == 2),
6983 ];
6984 }
6985 //
6986 $mem[$k]['promotion'] = $promotion;
6987 }
6988
6989 // define the occupancy override
6990 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
6991 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
6992 }
6993
6994 // push difference generated only if to be applied progressively
6995 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
6996 /**
6997 * Push the difference generated by this special price for later transliteration of final price,
6998 * only if the special price is calculated progressively and not on the final price.
6999 *
7000 * @since 1.13.5
7001 */
7002 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7003 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7004 /**
7005 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7006 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7007 * all differences applied by the previous special pricing rules.
7008 *
7009 * @since 1.13.5
7010 */
7011 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7012 array_push($mem[$k]['trans_factors'], $factor);
7013 array_push($mem[$k]['trans_affdays'], $affdays);
7014 }
7015
7016 // push values in memory array
7017 array_push($mem[$k]['sum'], $newprice);
7018 $mem[$k]['daysused'] += $affdays;
7019 array_push($roomschange, $a[0]['idroom']);
7020 }
7021 }
7022 if ($applyseasons) {
7023 foreach ($mem as $k => $v) {
7024 if ($v['daysused'] > 0 && $v['sum']) {
7025 $newprice = 0;
7026 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7027 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7028 $addrest = $restdays * $dailyprice;
7029 $newprice += $addrest;
7030
7031 // calculate new final cost
7032 $redo_rounding = null;
7033 foreach ($v['sum'] as $sum_index => $add) {
7034 /**
7035 * The application of the various special pricing rules is made in a progressive and cumulative way
7036 * by always starting from the room base cost or its average daily cost. However, promotions may need
7037 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7038 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7039 * onto the differences generated by all the regular and progressively applied special pricing rules.
7040 *
7041 * @since 1.13.5
7042 */
7043 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7044 /**
7045 * This progressive price difference must be applied on the room final cost, so we need to
7046 * apply the transliteration over the other differences applied by other special prices.
7047 */
7048 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7049 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7050 // this is the % promotion we are looking for applying it on the final cost
7051 $factor = $v['trans_factors'][$transliterate_key];
7052 if (is_object($factor) && $factor->pcent) {
7053 $final_factor = 0;
7054 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7055 $final_factor += $prog_diff * $factor->amount / 100;
7056 }
7057 // update rounding
7058 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7059
7060 /**
7061 * Leverage the final factor depending on how many nights this affected.
7062 *
7063 * @since 1.15.0 (J) - 1.5.0 (WP)
7064 */
7065 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7066 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7067 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7068 }
7069
7070 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7071 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7072 }
7073 }
7074 }
7075
7076 // apply new price progressively
7077 $newprice += $add;
7078 }
7079
7080 // apply rounding from factor
7081 if (!empty($redo_rounding)) {
7082 $newprice = round($newprice, 0, constant($redo_rounding));
7083 }
7084
7085 // set promotion (if any)
7086 if (isset($v['promotion'])) {
7087 $arr[$k][0]['promotion'] = $v['promotion'];
7088 }
7089
7090 // set occupancy overrides (if any)
7091 if (isset($v['occupancy_ovr'])) {
7092 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7093 }
7094
7095 // set new final cost and update nights affected
7096 $arr[$k][0]['cost'] = $newprice;
7097 $arr[$k][0]['affdays'] = $v['daysused'];
7098 }
7099 }
7100 }
7101 }
7102
7103 // week days with no season
7104 $roomschange = array_unique($roomschange);
7105 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7106 $dbo->setQuery($q);
7107 $specials = $dbo->loadAssocList();
7108 if ($specials) {
7109 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7110 $applyseasons = false;
7111 unset($mem);
7112 $mem = [];
7113 foreach ($arr as $k => $a) {
7114 $mem[$k]['daysused'] = 0;
7115 $mem[$k]['sum'] = [];
7116 }
7117 foreach ($specials as $s) {
7118 // check if this is a promotion registered for skipping
7119 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7120 continue;
7121 }
7122
7123 // Special Price tied to the year
7124 if (!empty($s['year']) && $s['year'] > 0) {
7125 if ($one['year'] != $s['year']) {
7126 continue;
7127 }
7128 }
7129
7130 $allrooms = explode(",", $s['idrooms']);
7131 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7132 // week days
7133 $filterwdays = !empty($s['wdays']) ? true : false;
7134 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7135 if (is_array($wdays) && $wdays) {
7136 foreach ($wdays as $kw => $wd) {
7137 if (strlen($wd) == 0) {
7138 unset($wdays[$kw]);
7139 }
7140 }
7141 }
7142
7143 foreach ($arr as $k => $a) {
7144 // only rooms with no price modifications from seasons
7145
7146 // applied only to some types of price
7147 if ($allprices && !empty($allprices[0])) {
7148 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7149 continue;
7150 }
7151 }
7152
7153 /**
7154 * We should not exclude the rooms that already had a modification of the price through a season
7155 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7156 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7157 *
7158 * @since 1.11
7159 */
7160 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7161 continue;
7162 }
7163
7164 $affdays = 0;
7165 $season_fromdayts = $fromdayts;
7166 $is_dst = date('I', $season_fromdayts);
7167 for ($i = 0; $i < $a[0]['days']; $i++) {
7168 $todayts = $season_fromdayts + ($i * 86400);
7169 $is_now_dst = date('I', $todayts);
7170 if ($is_dst != $is_now_dst) {
7171 // Daylight Saving Time has changed, check how
7172 if ((bool)$is_dst === true) {
7173 $todayts += 3600;
7174 $season_fromdayts += 3600;
7175 } else {
7176 $todayts -= 3600;
7177 $season_fromdayts -= 3600;
7178 }
7179 $is_dst = $is_now_dst;
7180 }
7181 // week days
7182 if ($filterwdays == true) {
7183 $checkwday = getdate($todayts);
7184 if (in_array($checkwday['wday'], $wdays)) {
7185 $affdays++;
7186 }
7187 }
7188 }
7189 if (!($affdays > 0)) {
7190 // no nights affected
7191 continue;
7192 }
7193
7194 // apply the rule
7195 $applyseasons = true;
7196 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7197
7198 if (intval($s['val_pcent']) == 2) {
7199 // percentage value
7200 $pctval = $s['diffcost'];
7201 if (strlen((string)$s['losoverride'])) {
7202 // values overrides
7203 $arrvaloverrides = [];
7204 $valovrparts = explode('_', $s['losoverride']);
7205 foreach ($valovrparts as $valovr) {
7206 if (!empty($valovr)) {
7207 $ovrinfo = explode(':', $valovr);
7208 if (strstr($ovrinfo[0], '-i') != false) {
7209 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7210 if ((int)$ovrinfo[0] < $a[0]['days']) {
7211 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7212 }
7213 }
7214 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7215 }
7216 }
7217 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7218 $pctval = $arrvaloverrides[$a[0]['days']];
7219 }
7220 }
7221 if (intval($s['type']) == 1) {
7222 // charge
7223 $cpercent = 100 + $pctval;
7224 } else {
7225 // discount
7226 $cpercent = 100 - $pctval;
7227 }
7228 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7229 } else {
7230 // absolute value
7231 $absval = $s['diffcost'];
7232 if (strlen((string)$s['losoverride'])) {
7233 // values overrides
7234 $arrvaloverrides = [];
7235 $valovrparts = explode('_', $s['losoverride']);
7236 foreach ($valovrparts as $valovr) {
7237 if (!empty($valovr)) {
7238 $ovrinfo = explode(':', $valovr);
7239 if (strstr($ovrinfo[0], '-i') != false) {
7240 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7241 if ((int)$ovrinfo[0] < $a[0]['days']) {
7242 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7243 }
7244 }
7245 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7246 }
7247 }
7248 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7249 $absval = $arrvaloverrides[$a[0]['days']];
7250 }
7251 }
7252 if (intval($s['type']) == 1) {
7253 // charge
7254 $newprice = ($dailyprice + $absval) * $affdays;
7255 } else {
7256 // discount
7257 $newprice = ($dailyprice - $absval) * $affdays;
7258 }
7259 }
7260
7261 // apply rounding
7262 if (!empty($s['roundmode'])) {
7263 $newprice = round($newprice, 0, constant($s['roundmode']));
7264 } else {
7265 $newprice = round($newprice, 2);
7266 }
7267
7268 // push values in memory array
7269 array_push($mem[$k]['sum'], $newprice);
7270 $mem[$k]['daysused'] += $affdays;
7271 }
7272 }
7273 if ($applyseasons) {
7274 foreach ($mem as $k => $v) {
7275 if ($v['daysused'] > 0 && $v['sum']) {
7276 $newprice = 0;
7277 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7278 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7279 $addrest = $restdays * $dailyprice;
7280 $newprice += $addrest;
7281 foreach ($v['sum'] as $add) {
7282 $newprice += $add;
7283 }
7284 $arr[$k][0]['cost'] = $newprice;
7285 $arr[$k][0]['affdays'] = $v['daysused'];
7286 }
7287 }
7288 }
7289 }
7290 // end week days with no season
7291
7292 return $arr;
7293 }
7294
7295 /**
7296 * Applies the special prices over an array of tariffs.
7297 * The function is also used by VCM (>= 1.6.5) with specific arguments.
7298 *
7299 * @param array $arr array of tariffs taken from the DB
7300 * @param int $from start timestamp
7301 * @param int $to end timestamp
7302 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
7303 * @param array $seasons_dates (VBO 1.10) array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7304 * @param array $seasons_wdays (VBO 1.10) array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7305 *
7306 * @return array
7307 */
7308 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], array $seasons_wdays = [])
7309 {
7310 static $cached_seasons = [];
7311
7312 $cache_signature = '';
7313 $cache_signature_wdays = '';
7314 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates && !$seasons_wdays) {
7315 // enable season records caching
7316 $cache_signature = "{$from}_{$to}";
7317 $cache_signature_wdays = "wdays";
7318 }
7319
7320 $dbo = JFactory::getDbo();
7321 $vbo_tn = self::getTranslator();
7322
7323 $roomschange = [];
7324 $one = getdate($from);
7325
7326 // leap years
7327 if ($one['year'] % 4 == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7328 $isleap = true;
7329 } else {
7330 $isleap = false;
7331 }
7332
7333 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7334 $tomidnightone = intval($one['hours']) * 3600;
7335 $tomidnightone += intval($one['minutes']) * 60;
7336 $sfrom = $from - $baseone - $tomidnightone;
7337 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7338 $two = getdate($to);
7339 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7340 $tomidnighttwo = intval($two['hours']) * 3600;
7341 $tomidnighttwo += intval($two['minutes']) * 60;
7342 $sto = $to - $basetwo - $tomidnighttwo;
7343
7344 // leap years, check what dates to manipulate
7345 if ($isleap) {
7346 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7347 if ($one[0] > $leapts) {
7348 /**
7349 * Timestamp must be greater than the leap-day of Feb 29th.
7350 * It used to be checked for >= $leapts.
7351 *
7352 * @since July 3rd 2019
7353 */
7354 $sfrom -= 86400;
7355 }
7356 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7357 // lower checkin date when in leap year but not for checkout
7358 $sto -= 86400;
7359 }
7360 }
7361
7362 // check for DST changes to adjust the query values to fetch
7363 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7364 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7365 // we are parsing the day when the DST changed (probably a Sunday)
7366 if (!date('I', $from)) {
7367 // DST was just turned off
7368 $sfrom -= 3600;
7369 }
7370 }
7371 }
7372
7373 // count nights requested
7374 $booking_nights = 1;
7375 foreach ($arr as $k => $a) {
7376 if (isset($a['booking_nights'])) {
7377 // this value may be set when displaying pricing calendars
7378 $booking_nights = $a['booking_nights'];
7379 break;
7380 }
7381 if (isset($a['days'])) {
7382 $booking_nights = $a['days'];
7383 break;
7384 }
7385 }
7386
7387 /**
7388 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7389 *
7390 * @since 1.16.4 (J) - 1.6.4 (WP)
7391 */
7392 $skip_promo_ids = self::registerPromotionIds();
7393
7394 $totseasons = 0;
7395 if (!$parsed_season && !$seasons_dates) {
7396 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7397 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7398 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7399 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7400 ($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`)") .
7401 ($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`)") .
7402 ($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`)") .
7403 ($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`)") .
7404 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7405 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7406
7407 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
7408 // avoid making a query
7409 $seasons = $cached_seasons[$cache_signature];
7410 } else {
7411 // get the season records by running the query
7412 $dbo->setQuery($q);
7413 $seasons = $dbo->loadAssocList();
7414 }
7415
7416 // count total seasons
7417 $totseasons = $seasons ? count($seasons) : 0;
7418
7419 if ($cache_signature) {
7420 // cache season records
7421 $cached_seasons[$cache_signature] = $seasons;
7422 }
7423 }
7424
7425 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
7426 if ($totseasons > 0) {
7427 $seasons = $seasons;
7428 } elseif ($parsed_season) {
7429 $seasons = [$parsed_season];
7430 } else {
7431 $seasons = $seasons_dates;
7432 }
7433 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7434 $applyseasons = false;
7435 $mem = [];
7436 foreach ($arr as $k => $a) {
7437 $mem[$k]['daysused'] = 0;
7438 $mem[$k]['sum'] = [];
7439 $mem[$k]['spids'] = [];
7440 /**
7441 * The keys below are all needed to apply the promotions on the room's final cost.
7442 *
7443 * @since 1.13.5
7444 */
7445 $mem[$k]['diffs'] = [];
7446 $mem[$k]['trans_keys'] = [];
7447 $mem[$k]['trans_factors'] = [];
7448 $mem[$k]['trans_affdays'] = [];
7449 }
7450 $affdayslistless = [];
7451 foreach ($seasons as $s) {
7452 // 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
7453 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
7454 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
7455 continue;
7456 }
7457
7458 /**
7459 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
7460 * We need to skip such fake seasons as they do not need any parsing.
7461 *
7462 * @since 1.13
7463 */
7464 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
7465 continue;
7466 }
7467
7468 // check if this is a promotion registered for skipping
7469 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7470 continue;
7471 }
7472
7473 // Special Price tied to the year
7474 if (!empty($s['year']) && $s['year'] > 0) {
7475 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7476 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7477 //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
7478 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7479 continue;
7480 }
7481 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7482 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7483 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7484 if ($calc_ends < ($from - $tomidnightone)) {
7485 continue;
7486 }
7487 }
7488 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7489 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7490 if ($s['from'] > $s['to']) {
7491 continue;
7492 }
7493 if (($basetwo + $s['from'] + 86399) > $to) {
7494 /**
7495 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7496 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7497 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7498 *
7499 * @since 1.14.3 (J) - 1.4.3 (WP)
7500 */
7501 continue;
7502 }
7503 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7504 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7505 /**
7506 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7507 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7508 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7509 *
7510 * @since 1.14.3 (J) - 1.4.3 (WP)
7511 */
7512 continue;
7513 }
7514 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7515 // 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)
7516 if (($baseone + $s['from']) > $to) {
7517 continue;
7518 }
7519 }
7520 }
7521 //
7522 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7523 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7524 $inits = $baseone + $s['from'];
7525 if ($s['from'] < $s['to']) {
7526 $ends = $basetwo + $s['to'];
7527 // check if the inits must be set to the year after
7528 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7529 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7530 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7531 $inits = $tmpbase + $s['from'];
7532 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7533 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7534 $ends = $baseone + $s['to'];
7535 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7536 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7537 $ends = $baseone + $s['to'];
7538 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7539 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7540 $inits = $basetwo + $s['from'];
7541 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7542 // booking dates across two years for a season tied to a specific year
7543 if ($one['year'] == $s['year']) {
7544 $inits = $baseone + $s['from'];
7545 } else {
7546 $inits = $basetwo + $s['from'];
7547 }
7548 if ($two['year'] == $s['year']) {
7549 $ends = $basetwo + $s['to'];
7550 } else {
7551 $ends = $baseone + $s['to'];
7552 }
7553 }
7554 } else {
7555 // between 2 years
7556 if ($baseone < $basetwo) {
7557 // ex. 29/12/2012 - 14/01/2013
7558 $ends = $basetwo + $s['to'];
7559 } else {
7560 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7561 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7562 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7563 $ends = $tmpbase + $s['to'];
7564 } else {
7565 // ex. 03/01 - 09/01
7566 $ends = $basetwo + $s['to'];
7567 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7568 $inits = $tmpbase + $s['from'];
7569 }
7570 }
7571 }
7572 // leap years
7573 if ($isleap == true) {
7574 $infoseason = getdate($inits);
7575 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7576 // leap years, check what dates to manipulate
7577 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7578 /**
7579 * Timestamp must be greater than the leap-day of Feb 29th.
7580 * It used to be checked for >= $leapts.
7581 *
7582 * @since July 3rd 2019
7583 */
7584 $inits += 86400;
7585 if ($s['from'] < $s['to']) {
7586 // increase season end date only if still on the same leap year
7587 $ends += 86400;
7588 }
7589 }
7590 }
7591
7592 // promotions
7593 $promotion = [];
7594 if ($s['promo'] == 1) {
7595 $daysadv = (($inits - time()) / 86400);
7596 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7597 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7598 continue;
7599 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7600 $secstocheckin = ($from - time());
7601 if ($s['promolastmin'] < $secstocheckin) {
7602 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7603 continue;
7604 }
7605 }
7606 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7607 /**
7608 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7609 *
7610 * @since 1.13.5
7611 */
7612 continue;
7613 }
7614 $promotion['todaydaysadv'] = $daysadv;
7615 $promotion['promodaysadv'] = $s['promodaysadv'];
7616 $promotion['promotxt'] = $s['promotxt'];
7617 }
7618
7619 // occupancy override
7620 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7621
7622 //week days
7623 $filterwdays = !empty($s['wdays']) ? true : false;
7624 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7625 if (is_array($wdays) && $wdays) {
7626 foreach ($wdays as $kw => $wd) {
7627 if (strlen($wd) == 0) {
7628 unset($wdays[$kw]);
7629 }
7630 }
7631 }
7632
7633 // checkin must be after the begin of the season
7634 $checkininclok = true;
7635 if ($s['checkinincl'] == 1) {
7636 $checkininclok = false;
7637 if ($s['from'] < $s['to']) {
7638 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7639 $checkininclok = true;
7640 }
7641 } else {
7642 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7643 $checkininclok = true;
7644 }
7645 }
7646 }
7647 if ($checkininclok !== true) {
7648 continue;
7649 }
7650
7651 foreach ($arr as $k => $a) {
7652 // applied only to some types of price
7653 if ($allprices && !empty($allprices[0])) {
7654 // VikBooking 1.6: Price Calendar sets the idprice to -1
7655 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
7656 continue;
7657 }
7658 }
7659 // applied only to some room types
7660 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
7661 continue;
7662 }
7663
7664 if (isset($a['days']) && is_float($a['days'])) {
7665 // prevent any possible warning with array_key_exists()
7666 $a['days'] = (int)$a['days'];
7667 }
7668
7669 $affdays = 0;
7670 $season_fromdayts = $fromdayts;
7671 $is_dst = date('I', $season_fromdayts);
7672 for ($i = 0; $i < $a['days']; $i++) {
7673 $todayts = $season_fromdayts + ($i * 86400);
7674 $is_now_dst = date('I', $todayts);
7675 if ($is_dst != $is_now_dst) {
7676 // Daylight Saving Time has changed, check how
7677 if ((bool)$is_dst === true) {
7678 $todayts += 3600;
7679 $season_fromdayts += 3600;
7680 } else {
7681 $todayts -= 3600;
7682 $season_fromdayts -= 3600;
7683 }
7684 $is_dst = $is_now_dst;
7685 }
7686 if ($todayts >= $inits && $todayts <= $ends) {
7687 $checkwday = getdate($todayts);
7688 // week days
7689 if ($filterwdays == true) {
7690 if (in_array($checkwday['wday'], $wdays)) {
7691 if (!isset($arr[$k]['affdayslist'])) {
7692 $arr[$k]['affdayslist'] = [];
7693 }
7694 $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;
7695 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7696 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7697 $affdays++;
7698 }
7699 } else {
7700 if (!isset($arr[$k]['affdayslist'])) {
7701 $arr[$k]['affdayslist'] = [];
7702 }
7703 $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;
7704 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7705 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7706 $affdays++;
7707 }
7708 //
7709 }
7710 }
7711
7712 if (!($affdays > 0)) {
7713 // no nights affected
7714 continue;
7715 }
7716
7717 // apply the rule
7718 $applyseasons = true;
7719 $dailyprice = $a['cost'] / $a['days'];
7720
7721 // modification factor object
7722 $factor = new stdClass;
7723
7724 // calculate new price progressively
7725 if (intval($s['val_pcent']) == 2) {
7726 // percentage value
7727 $factor->pcent = 1;
7728 $pctval = $s['diffcost'];
7729 if (strlen((string)$s['losoverride'])) {
7730 // values overrides
7731 $arrvaloverrides = [];
7732 $valovrparts = explode('_', $s['losoverride']);
7733 foreach ($valovrparts as $valovr) {
7734 if (!empty($valovr)) {
7735 $ovrinfo = explode(':', $valovr);
7736 if (strstr($ovrinfo[0], '-i') != false) {
7737 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7738 if ((int)$ovrinfo[0] < $a['days']) {
7739 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7740 }
7741 }
7742 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7743 }
7744 }
7745 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7746 $pctval = $arrvaloverrides[$a['days']];
7747 }
7748 }
7749 if (intval($s['type']) == 1) {
7750 // charge
7751 $factor->type = '+';
7752 $cpercent = 100 + $pctval;
7753 } else {
7754 // discount
7755 $factor->type = '-';
7756 $cpercent = 100 - $pctval;
7757 }
7758 $factor->amount = $pctval;
7759 $dailysum = ($dailyprice * $cpercent / 100);
7760 $newprice = $dailysum * $affdays;
7761 } else {
7762 // absolute value
7763 $factor->pcent = 0;
7764 $absval = $s['diffcost'];
7765 if (strlen((string)$s['losoverride'])) {
7766 // values overrides
7767 $arrvaloverrides = [];
7768 $valovrparts = explode('_', $s['losoverride']);
7769 foreach ($valovrparts as $valovr) {
7770 if (!empty($valovr)) {
7771 $ovrinfo = explode(':', $valovr);
7772 if (strstr($ovrinfo[0], '-i') != false) {
7773 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7774 if ((int)$ovrinfo[0] < $a['days']) {
7775 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7776 }
7777 }
7778 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7779 }
7780 }
7781 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7782 $absval = $arrvaloverrides[$a['days']];
7783 }
7784 }
7785 if (intval($s['type']) == 1) {
7786 // charge
7787 $factor->type = '+';
7788 $dailysum = ($dailyprice + $absval);
7789 $newprice = $dailysum * $affdays;
7790 } else {
7791 // discount
7792 $factor->type = '-';
7793 $dailysum = ($dailyprice - $absval);
7794 $newprice = $dailysum * $affdays;
7795 }
7796 $factor->amount = $absval;
7797 }
7798
7799 // apply rounding
7800 $factor->roundmode = $s['roundmode'];
7801 if (!empty($s['roundmode'])) {
7802 $newprice = round($newprice, 0, constant($s['roundmode']));
7803 } else {
7804 $newprice = round($newprice, 2);
7805 }
7806
7807 // define the promotion (only if no value overrides set the amount to 0)
7808 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
7809 /**
7810 * Include the discount information (if any). The cost re-calculated may not be
7811 * precise if multiple special prices were applied over the same dates.
7812 *
7813 * @since 1.13
7814 */
7815 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7816 $promotion['discount'] = [
7817 'amount' => $s['diffcost'],
7818 'pcent' => (int)($s['val_pcent'] == 2),
7819 ];
7820 }
7821 //
7822 $mem[$k]['promotion'] = $promotion;
7823 }
7824
7825 // define the occupancy override
7826 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
7827 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
7828 }
7829
7830 // affected days list
7831 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
7832 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
7833 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
7834 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
7835 }
7836 }
7837 }
7838
7839 // push special price ID
7840 if (!in_array($s['id'], $mem[$k]['spids'])) {
7841 array_push($mem[$k]['spids'], $s['id']);
7842 }
7843
7844 // push difference generated only if to be applied progressively
7845 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
7846 /**
7847 * Push the difference generated by this special price for later transliteration of final price,
7848 * only if the special price is calculated progressively and not on the final price.
7849 *
7850 * @since 1.13.5
7851 */
7852 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7853 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7854 /**
7855 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7856 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7857 * all differences applied by the previous special pricing rules.
7858 *
7859 * @since 1.13.5
7860 */
7861 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7862 array_push($mem[$k]['trans_factors'], $factor);
7863 array_push($mem[$k]['trans_affdays'], $affdays);
7864 }
7865
7866 // push values in memory array
7867 array_push($mem[$k]['sum'], $newprice);
7868 $mem[$k]['daysused'] += $affdays;
7869 array_push($roomschange, $a['idroom']);
7870 }
7871 }
7872 if ($applyseasons) {
7873 foreach ($mem as $k => $v) {
7874 if ($v['daysused'] > 0 && $v['sum']) {
7875 $newprice = 0;
7876 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
7877 $restdays = $arr[$k]['days'] - $v['daysused'];
7878 $addrest = $restdays * $dailyprice;
7879 $newprice += $addrest;
7880
7881 // calculate new final cost
7882 $redo_rounding = null;
7883 foreach ($v['sum'] as $sum_index => $add) {
7884 /**
7885 * The application of the various special pricing rules is made in a progressive and cumulative way
7886 * by always starting from the room base cost or its average daily cost. However, promotions may need
7887 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7888 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7889 * onto the differences generated by all the regular and progressively applied special pricing rules.
7890 *
7891 * @since 1.13.5
7892 */
7893 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7894 /**
7895 * This progressive price difference must be applied on the room final cost, so we need to
7896 * apply the transliteration over the other differences applied by other special prices.
7897 */
7898 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7899 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7900 // this is the % promotion we are looking for applying it on the final cost
7901 $factor = $v['trans_factors'][$transliterate_key];
7902 if (is_object($factor) && $factor->pcent) {
7903 $final_factor = 0;
7904 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7905 $final_factor += $prog_diff * $factor->amount / 100;
7906 }
7907 // update rounding
7908 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7909
7910 /**
7911 * Leverage the final factor depending on how many nights this affected.
7912 *
7913 * @since 1.15.0 (J) - 1.5.0 (WP)
7914 */
7915 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
7916 $avg_final_factor = $final_factor / $arr[$k]['days'];
7917 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7918 }
7919
7920 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7921 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7922 }
7923 }
7924 }
7925
7926 // apply new price progressively
7927 $newprice += $add;
7928 }
7929
7930 // apply rounding from factor
7931 if (!empty($redo_rounding)) {
7932 $newprice = round($newprice, 0, constant($redo_rounding));
7933 }
7934
7935 // set promotion (if any)
7936 if (isset($v['promotion'])) {
7937 $arr[$k]['promotion'] = $v['promotion'];
7938 }
7939
7940 // set occupancy overrides (if any)
7941 if (isset($v['occupancy_ovr'])) {
7942 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
7943 }
7944
7945 // set new final cost and update nights affected
7946 $arr[$k]['cost'] = $newprice;
7947 $arr[$k]['affdays'] = $v['daysused'];
7948 if (array_key_exists('spids', $v) && $v['spids']) {
7949 $arr[$k]['spids'] = $v['spids'];
7950 }
7951 }
7952 }
7953 }
7954 }
7955
7956 // week days with no season
7957 $roomschange = array_unique($roomschange);
7958 $totspecials = 0;
7959 if (!$seasons_wdays) {
7960 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7961
7962 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7963 // avoid making a query
7964 $specials = $cached_seasons[$cache_signature_wdays];
7965 } else {
7966 // get the week-days-with-no-season records by running the query
7967 $dbo->setQuery($q);
7968 $specials = $dbo->loadAssocList();
7969 }
7970
7971 // count records
7972 $totspecials = $specials ? count($specials) : 0;
7973
7974 if ($cache_signature_wdays) {
7975 // cache week-days-with-no-season records
7976 $cached_seasons[$cache_signature_wdays] = $specials;
7977 }
7978 }
7979 if ($totspecials > 0 || $seasons_wdays) {
7980 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7981 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7982 $applyseasons = false;
7983 /**
7984 * We no longer unset the previous memory of the seasons with dates filters
7985 * because we need the responses to be merged. We do it only if not set.
7986 * We only keep the property 'spids' but the others should be unset.
7987 *
7988 * @since 1.11
7989 */
7990 if (!isset($mem)) {
7991 $mem = [];
7992 foreach ($arr as $k => $a) {
7993 $mem[$k]['daysused'] = 0;
7994 $mem[$k]['sum'] = [];
7995 $mem[$k]['spids'] = [];
7996 }
7997 } else {
7998 foreach ($arr as $k => $a) {
7999 $mem[$k]['daysused'] = 0;
8000 $mem[$k]['sum'] = [];
8001 }
8002 }
8003 //
8004 foreach ($specials as $s) {
8005 // 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
8006 if (!empty($s['from']) || !empty($s['to'])) {
8007 continue;
8008 }
8009
8010 // check if this is a promotion registered for skipping
8011 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8012 continue;
8013 }
8014
8015 // Special Price tied to the year
8016 if (!empty($s['year']) && $s['year'] > 0) {
8017 if ($one['year'] != $s['year']) {
8018 continue;
8019 }
8020 }
8021
8022 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8023 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8024 // week days
8025 $filterwdays = !empty($s['wdays']) ? true : false;
8026 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8027 if (is_array($wdays) && $wdays) {
8028 foreach ($wdays as $kw => $wd) {
8029 if (strlen($wd) == 0) {
8030 unset($wdays[$kw]);
8031 }
8032 }
8033 }
8034
8035 foreach ($arr as $k => $a) {
8036 // only rooms with no price modifications from seasons
8037
8038 // applied only to some types of price
8039 if ($allprices && !empty($allprices[0])) {
8040 // VikBooking 1.6: Price Calendar sets the idprice to -1
8041 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8042 continue;
8043 }
8044 }
8045
8046 /**
8047 * We should not exclude the rooms that already had a modification of the price through a season
8048 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8049 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8050 *
8051 * @since 1.11
8052 */
8053 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8054 continue;
8055 }
8056
8057 if (isset($a['days']) && is_float($a['days'])) {
8058 // prevent any possible warning with array_key_exists()
8059 $a['days'] = (int)$a['days'];
8060 }
8061
8062 $affdays = 0;
8063 $season_fromdayts = $fromdayts;
8064 $is_dst = date('I', $season_fromdayts);
8065 for ($i = 0; $i < $a['days']; $i++) {
8066 $todayts = $season_fromdayts + ($i * 86400);
8067 $is_now_dst = date('I', $todayts);
8068 if ($is_dst != $is_now_dst) {
8069 // Daylight Saving Time has changed, check how
8070 if ((bool)$is_dst === true) {
8071 $todayts += 3600;
8072 $season_fromdayts += 3600;
8073 } else {
8074 $todayts -= 3600;
8075 $season_fromdayts -= 3600;
8076 }
8077 $is_dst = $is_now_dst;
8078 }
8079 // week days
8080 if ($filterwdays == true) {
8081 $checkwday = getdate($todayts);
8082 if (in_array($checkwday['wday'], $wdays)) {
8083 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8084 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8085 $affdays++;
8086 }
8087 }
8088 //
8089 }
8090
8091 if (!($affdays > 0)) {
8092 // no nights affected
8093 continue;
8094 }
8095
8096 // apply the rule
8097 $applyseasons = true;
8098 $dailyprice = $a['cost'] / $a['days'];
8099
8100 if (intval($s['val_pcent']) == 2) {
8101 // percentage value
8102 $pctval = $s['diffcost'];
8103 if (strlen((string)$s['losoverride'])) {
8104 // values overrides
8105 $arrvaloverrides = [];
8106 $valovrparts = explode('_', $s['losoverride']);
8107 foreach ($valovrparts as $valovr) {
8108 if (!empty($valovr)) {
8109 $ovrinfo = explode(':', $valovr);
8110 if (strstr($ovrinfo[0], '-i') != false) {
8111 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8112 if ((int)$ovrinfo[0] < $a['days']) {
8113 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8114 }
8115 }
8116 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8117 }
8118 }
8119 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8120 $pctval = $arrvaloverrides[$a['days']];
8121 }
8122 }
8123 if (intval($s['type']) == 1) {
8124 // charge
8125 $cpercent = 100 + $pctval;
8126 } else {
8127 // discount
8128 $cpercent = 100 - $pctval;
8129 }
8130 $dailysum = ($dailyprice * $cpercent / 100);
8131 $newprice = $dailysum * $affdays;
8132 } else {
8133 // absolute value
8134 $absval = $s['diffcost'];
8135 if (strlen((string)$s['losoverride'])) {
8136 // values overrides
8137 $arrvaloverrides = [];
8138 $valovrparts = explode('_', $s['losoverride']);
8139 foreach ($valovrparts as $valovr) {
8140 if (!empty($valovr)) {
8141 $ovrinfo = explode(':', $valovr);
8142 if (strstr($ovrinfo[0], '-i') != false) {
8143 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8144 if ((int)$ovrinfo[0] < $a['days']) {
8145 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8146 }
8147 }
8148 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8149 }
8150 }
8151 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8152 $absval = $arrvaloverrides[$a['days']];
8153 }
8154 }
8155 if (intval($s['type']) == 1) {
8156 // charge
8157 $dailysum = ($dailyprice + $absval);
8158 $newprice = $dailysum * $affdays;
8159 } else {
8160 // discount
8161 $dailysum = ($dailyprice - $absval);
8162 $newprice = $dailysum * $affdays;
8163 }
8164 }
8165
8166 // apply rounding
8167 if (!empty($s['roundmode'])) {
8168 $newprice = round($newprice, 0, constant($s['roundmode']));
8169 } else {
8170 $newprice = round($newprice, 2);
8171 }
8172
8173 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8174 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8175 }
8176 if (!in_array($s['id'], $mem[$k]['spids'])) {
8177 $mem[$k]['spids'][] = $s['id'];
8178 }
8179 $mem[$k]['sum'][] = $newprice;
8180 $mem[$k]['daysused'] += $affdays;
8181 }
8182 }
8183 if ($applyseasons) {
8184 foreach ($mem as $k => $v) {
8185 if ($v['daysused'] > 0 && $v['sum']) {
8186 $newprice = 0;
8187 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8188 $restdays = $arr[$k]['days'] - $v['daysused'];
8189 $addrest = $restdays * $dailyprice;
8190 $newprice += $addrest;
8191 foreach ($v['sum'] as $add) {
8192 $newprice += $add;
8193 }
8194 $arr[$k]['cost'] = $newprice;
8195 $arr[$k]['affdays'] = $v['daysused'];
8196 if (array_key_exists('spids', $v) && $v['spids']) {
8197 $arr[$k]['spids'] = $v['spids'];
8198 }
8199 }
8200 }
8201 }
8202 }
8203 // end week days with no season
8204
8205 return $arr;
8206 }
8207
8208 public static function getRoomRplansClosingDates($idroom)
8209 {
8210 $dbo = JFactory::getDbo();
8211
8212 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8213 $dbo->setQuery($q);
8214 $price_records = $dbo->loadAssocList();
8215 if (!$price_records) {
8216 return [];
8217 }
8218
8219 $closingd = [];
8220 foreach ($price_records as $prec) {
8221 if (empty($prec['closingd'])) {
8222 continue;
8223 }
8224 $price_closing = json_decode($prec['closingd'], true);
8225 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8226 continue;
8227 }
8228 // check expired dates and clean up
8229 $today_midnight = mktime(0, 0, 0);
8230 $cleaned = false;
8231 foreach ($price_closing[$idroom] as $k => $v) {
8232 if (strtotime($v) < $today_midnight) {
8233 $cleaned = true;
8234 unset($price_closing[$idroom][$k]);
8235 }
8236 }
8237 if (!$price_closing[$idroom]) {
8238 unset($price_closing[$idroom]);
8239 } elseif ($cleaned === true) {
8240 // reset array keys for smaller JSON size
8241 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8242 }
8243 if ($cleaned === true) {
8244 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8245 $dbo->setQuery($q);
8246 $dbo->execute();
8247 }
8248 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8249 continue;
8250 }
8251 $closingd[$prec['id']] = $price_closing[$idroom];
8252 }
8253
8254 return $closingd;
8255 }
8256
8257 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8258 {
8259 $dbo = JFactory::getDbo();
8260
8261 $closingd = [];
8262
8263 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8264 $dbo->setQuery($q);
8265 $price_records = $dbo->loadAssocList();
8266 if ($price_records && $roomids) {
8267 $info_start = getdate($checkints);
8268 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8269 $all_nights = [];
8270 for ($i = 0; $i < (int)$numnights; $i++) {
8271 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8272 $all_nights[] = date('Y-m-d', $next_midnight);
8273 }
8274 foreach ($price_records as $prec) {
8275 if (empty($prec['closingd'])) {
8276 continue;
8277 }
8278 $price_closing = json_decode($prec['closingd'], true);
8279 if (!$price_closing) {
8280 continue;
8281 }
8282 foreach ($price_closing as $idroom => $rclosedd) {
8283 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8284 continue;
8285 }
8286 if (!array_key_exists($idroom, $closingd)) {
8287 $closingd[$idroom] = [];
8288 }
8289 foreach ($all_nights as $night) {
8290 if (in_array($night, $rclosedd)) {
8291 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8292 $closingd[$idroom][$prec['id']][] = $night;
8293 } else {
8294 $closingd[$idroom][$prec['id']] = [$night];
8295 }
8296 }
8297 }
8298 }
8299 }
8300 }
8301
8302 return $closingd;
8303 }
8304
8305 /**
8306 * Tells whether at least one payment method must be selected to check-out.
8307 *
8308 * @param array $rooms_involved optional list of room IDs being booked.
8309 *
8310 * @return bool true if at least one payment method is applicable.
8311 *
8312 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
8313 */
8314 public static function areTherePayments($rooms_involved = [])
8315 {
8316 $dbo = JFactory::getDbo();
8317
8318 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
8319 $dbo->setQuery($q);
8320 $dbo->execute();
8321
8322 if (!$dbo->getNumRows()) {
8323 // no published payment options found
8324 return false;
8325 }
8326
8327 $pay_options = $dbo->loadAssocList();
8328
8329 if (!count($rooms_involved)) {
8330 // if no filters given, return true
8331 return true;
8332 }
8333
8334 // validate room associations
8335 foreach ($pay_options as $pay_k => $pay_v) {
8336 if (empty($pay_v['idrooms'])) {
8337 continue;
8338 }
8339 $rooms_supported = json_decode($pay_v['idrooms']);
8340 if (!is_array($rooms_supported) || !count($rooms_supported)) {
8341 continue;
8342 }
8343 // make sure all rooms being booked are supported
8344 foreach ($rooms_involved as $room_id_booked) {
8345 if (!in_array($room_id_booked, $rooms_supported)) {
8346 // we need to unset this payment option as soon as we find one non-matching room ID
8347 unset($pay_options[$pay_k]);
8348 break;
8349 }
8350 }
8351 }
8352
8353 return (bool)count($pay_options);
8354 }
8355
8356 public static function getPayment($idp, $vbo_tn = null)
8357 {
8358 if (empty($idp)) {
8359 return false;
8360 }
8361
8362 if (strstr((string)$idp, '=') !== false) {
8363 $parts = explode('=', $idp);
8364 $idp = $parts[0];
8365 }
8366
8367 $dbo = JFactory::getDbo();
8368
8369 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
8370 $dbo->setQuery($q);
8371 $payment = $dbo->loadAssocList();
8372 if (!$payment) {
8373 return false;
8374 }
8375
8376 if (is_object($vbo_tn)) {
8377 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
8378 }
8379
8380 return $payment[0];
8381 }
8382
8383 public static function getCronKey()
8384 {
8385 return VBOFactory::getConfig()->get('cronkey', '');
8386 }
8387
8388 /**
8389 * Returns the next invoice number to be used for generating a new invoice/e-invoice.
8390 *
8391 * @return int
8392 *
8393 * @see do NOT ever implement caching on this method, as it needs to always query the DB.
8394 */
8395 public static function getNextInvoiceNumber()
8396 {
8397 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
8398 }
8399
8400 public static function getInvoiceNumberSuffix()
8401 {
8402 return VBOFactory::getConfig()->get('invoicesuffix', '');
8403 }
8404
8405 public static function getInvoiceCompanyInfo()
8406 {
8407 return VBOFactory::getConfig()->get('invcompanyinfo', '');
8408 }
8409
8410 /**
8411 * Gets the number for the next booking receipt generation,
8412 * updates the last receipt number used for the later iterations,
8413 * stores a new receipt record to keep track of the receipts issued.
8414 *
8415 * @param int $bid the Booking ID for which we are/want to generating the receipt.
8416 * @param [int] $newnum the last number used for generating the receipt.
8417 *
8418 * @return int
8419 */
8420 public static function getNextReceiptNumber ($bid, $newnum = false) {
8421 $dbo = JFactory::getDbo();
8422 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
8423 $dbo->setQuery($q);
8424 $dbo->execute();
8425 if ($dbo->getNumRows() > 0) {
8426 $s = $dbo->loadResult();
8427 //check if this booking has already a receipt, and return that number
8428 $prev_receipt = array();
8429 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
8430 $dbo->setQuery($q);
8431 $dbo->execute();
8432 if ($dbo->getNumRows() > 0) {
8433 $prev_receipt = $dbo->loadAssoc();
8434 }
8435 //update value (receipt generated)
8436 if ($newnum !== false && $newnum > 0) {
8437 $s = (int)$newnum;
8438 if (!(count($prev_receipt) > 0)) {
8439 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
8440 $dbo->setQuery($q);
8441 $dbo->execute();
8442 //insert the new receipt record
8443 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
8444 $dbo->setQuery($q);
8445 $dbo->execute();
8446 } else {
8447 //update receipt record
8448 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
8449 $dbo->setQuery($q);
8450 $dbo->execute();
8451 }
8452 }
8453 //
8454 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
8455 }
8456 //first execution of the method should create the configuration record
8457 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
8458 $dbo->setQuery($q);
8459 $dbo->execute();
8460 return 1;
8461 }
8462
8463 public static function getReceiptNotes ($newnotes = false) {
8464 $dbo = JFactory::getDbo();
8465 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
8466 $dbo->setQuery($q);
8467 $dbo->execute();
8468 if ($dbo->getNumRows() > 0) {
8469 $s = $dbo->loadResult();
8470 //update value
8471 if ($newnotes !== false) {
8472 $s = $newnotes;
8473 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
8474 $dbo->setQuery($q);
8475 $dbo->execute();
8476 }
8477 //
8478 return $s;
8479 }
8480 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
8481 $dbo->setQuery($q);
8482 $dbo->execute();
8483 return "";
8484 }
8485
8486 public static function loadColorTagsRules()
8487 {
8488 $color_tag_rules = [
8489 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
8490 'pend1' => 'VBWAITINGFORPAYMENT',
8491 'conf1' => 'VBDBTEXTROOMCLOSED',
8492 'conf2' => 'VBOCOLORTAGRULECONFTWO',
8493 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
8494 'inv1' => 'VBOCOLORTAGRULEINVONE',
8495 'rcp1' => 'VBOCOLORTAGRULERCPONE',
8496 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
8497 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
8498 'inv2' => 'VBOCOLORTAGRULEINVTWO',
8499 ];
8500
8501 /**
8502 * Trigger event to let third-party plugins override the default booking color tag rules.
8503 *
8504 * @since 1.16.4 (J) - 1.6.4 (WP)
8505 */
8506 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
8507
8508 return $color_tag_rules;
8509 }
8510
8511 public static function loadDefaultColorTags()
8512 {
8513 $default_color_tags = [
8514 [
8515 'color' => '#9b9b9b',
8516 'name' => 'VBWAITINGFORPAYMENT',
8517 'rule' => 'pend1'
8518 ],
8519 [
8520 'color' => '#333333',
8521 'name' => 'VBDBTEXTROOMCLOSED',
8522 'rule' => 'conf1'
8523 ],
8524 [
8525 'color' => '#ff8606',
8526 'name' => 'VBOCOLORTAGRULECONFTWO',
8527 'rule' => 'conf2'
8528 ],
8529 [
8530 'color' => '#0418c9',
8531 'name' => 'VBOCOLORTAGRULECONFTHREE',
8532 'rule' => 'conf3'
8533 ],
8534 [
8535 'color' => '#bed953',
8536 'name' => 'VBOCOLORTAGRULEINVONE',
8537 'rule' => 'inv1'
8538 ],
8539 [
8540 'color' => '#67f5b5',
8541 'name' => 'VBOCOLORTAGRULERCPONE',
8542 'rule' => 'rcp1'
8543 ],
8544 [
8545 'color' => '#04d2c2',
8546 'name' => 'VBOCOLORTAGRULECONFFOUR',
8547 'rule' => 'conf4'
8548 ],
8549 [
8550 'color' => '#00b316',
8551 'name' => 'VBOCOLORTAGRULECONFFIVE',
8552 'rule' => 'conf5'
8553 ],
8554 [
8555 'color' => '#00f323',
8556 'name' => 'VBOCOLORTAGRULEINVTWO',
8557 'rule' => 'inv2'
8558 ],
8559 ];
8560
8561 /**
8562 * Trigger event to let third-party plugins override the default color tags.
8563 *
8564 * @since 1.16.4 (J) - 1.6.4 (WP)
8565 */
8566 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
8567
8568 return $default_color_tags;
8569 }
8570
8571 public static function loadBookingsColorTags()
8572 {
8573 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
8574
8575 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
8576 }
8577
8578 public static function getBestColorContrast($hexcolor)
8579 {
8580 $hexcolor = str_replace('#', '', $hexcolor);
8581
8582 if (empty($hexcolor) || strlen($hexcolor) != 6) {
8583 return '#000000';
8584 }
8585
8586 $r = hexdec(substr($hexcolor, 0, 2));
8587 $g = hexdec(substr($hexcolor, 2, 2));
8588 $b = hexdec(substr($hexcolor, 4, 2));
8589
8590 // Counting the perceptive luminance - human eye favors green color
8591 // < 0.5 bright colors
8592 // > 0.5 dark colors
8593
8594 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
8595 }
8596
8597 public static function applyBookingColorTag($booking, $tags = [])
8598 {
8599 if (!is_array($tags) || !$tags) {
8600 $tags = self::loadBookingsColorTags();
8601 }
8602
8603 if (!empty($booking['colortag'])) {
8604 $color_tag_arr = json_decode($booking['colortag'], true);
8605 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
8606 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
8607 return $color_tag_arr;
8608 }
8609 }
8610
8611 $dbo = JFactory::getDbo();
8612
8613 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
8614 $docs_data = [];
8615 $invoice_numb = false;
8616 $receipt_numb = false;
8617
8618 if ($booking['status'] == 'confirmed') {
8619 $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`
8620 FROM `#__vikbooking_orders` AS `b`
8621 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
8622 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
8623 WHERE `b`.`id`=" . (int)$bid . ";";
8624 $dbo->setQuery($q);
8625 $docs_data = $dbo->loadAssoc();
8626 if ($docs_data) {
8627 $invoice_numb = (!empty($docs_data['inv_id']));
8628 $receipt_numb = (!empty($docs_data['rcp_id']));
8629
8630 /**
8631 * We inject the invoice number or receipt number into the color tag rules.
8632 * This is useful for the Tableaux page to show the invoice/receipt number.
8633 *
8634 * @since 1.12 (J) - 1.1.7 (WP)
8635 */
8636 if ($invoice_numb || $receipt_numb) {
8637 foreach ($tags as &$tval) {
8638 if ($invoice_numb) {
8639 $tval['invoice_number'] = $docs_data['inv_number'];
8640 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
8641 }
8642 if ($receipt_numb) {
8643 $tval['receipt_number'] = $docs_data['rcp_number'];
8644 }
8645 }
8646 // unset last reference
8647 unset($tval);
8648 }
8649 }
8650 }
8651
8652 /**
8653 * Some OTA bookings using an OTA-Collect business model may look as not entirely
8654 * paid when they actually are, due to commissions and pass through taxes. We also
8655 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
8656 *
8657 * @since 1.16.3 (J) - 1.6.3 (WP)
8658 */
8659 $ota_collected_booking = false;
8660 $hotel_collect_vcc_unpaid = false;
8661 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
8662 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
8663 $ota_collected_booking = true;
8664 }
8665 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
8666 // VCC detected, check the history for a payment event
8667 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
8668 // list of history types to fetch
8669 $h_payment_types = [];
8670 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
8671 if ($h_payment_group && $h_payment_group['types']) {
8672 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
8673 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
8674 }
8675 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
8676 if (!$payment_ev_datas) {
8677 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
8678 $hotel_collect_vcc_unpaid = true;
8679 }
8680 }
8681 }
8682
8683 foreach ($tags as $tkey => $tval) {
8684 if (empty($tval['rule'])) {
8685 continue;
8686 }
8687
8688 /**
8689 * Trigger event to let third-party plugins validate the custom rule.
8690 *
8691 * @since 1.16.4 (J) - 1.6.4 (WP)
8692 */
8693 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
8694 if (!empty($custom_rule)) {
8695 return (array) $custom_rule[0];
8696 }
8697
8698 switch ($tval['rule']) {
8699 case 'pend1':
8700 // Room is waiting for the payment (locked record)
8701 if ($booking['status'] == 'standby') {
8702 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
8703 $dbo->setQuery($q);
8704 $tmp_lock = $dbo->loadAssoc();
8705 if ($tmp_lock) {
8706 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8707 return $tval;
8708 }
8709 }
8710 break;
8711 case 'conf1':
8712 // Confirmed (Room Closed)
8713 if ($booking['status'] == 'confirmed' && $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED')) {
8714 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8715 return $tval;
8716 }
8717 break;
8718 case 'conf2':
8719 // Confirmed (No Rate 0.00/NULL Total)
8720 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
8721 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8722 return $tval;
8723 }
8724 break;
8725 case 'conf3':
8726 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
8727 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) {
8728 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8729 return $tval;
8730 }
8731 break;
8732 case 'inv1':
8733 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
8734 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
8735 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8736 return $tval;
8737 }
8738 break;
8739 case 'rcp1':
8740 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
8741 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
8742 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8743 return $tval;
8744 }
8745 break;
8746 case 'conf4':
8747 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
8748 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
8749 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8750 return $tval;
8751 }
8752 break;
8753 case 'conf5':
8754 // Confirmed (Total > 0 && Total Paid >= Total)
8755 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
8756 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8757 return $tval;
8758 }
8759 break;
8760 case 'inv2':
8761 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
8762 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
8763 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8764 return $tval;
8765 }
8766 break;
8767 default:
8768 // unsupported rule
8769 break;
8770 }
8771 }
8772
8773 return [];
8774 }
8775
8776 public static function getBookingInfoFromID($bid)
8777 {
8778 $dbo = JFactory::getDbo();
8779
8780 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
8781 $dbo->setQuery($q);
8782 $booking_info = $dbo->loadAssoc();
8783 if ($booking_info) {
8784 return $booking_info;
8785 }
8786
8787 return [];
8788 }
8789
8790 /**
8791 * Load booking details for a given list of IDs.
8792 *
8793 * @param int $roomid the room integer number.
8794 * @param array $room_bids_pool list of booking IDs involved.
8795 *
8796 * @return array
8797 */
8798 public static function loadRoomIndexesBookings($roomid, array $room_bids_pool)
8799 {
8800 $dbo = JFactory::getDbo();
8801
8802 if (empty($roomid) || !$room_bids_pool) {
8803 return [];
8804 }
8805
8806 $room_features_bookings = [];
8807 $roomid = (int)$roomid;
8808
8809 $all_bids = [];
8810 foreach ($room_bids_pool as $day => $bids) {
8811 $all_bids = array_merge($all_bids, $bids);
8812 }
8813 $all_bids = array_map('intval', array_unique($all_bids));
8814 $all_bids_str = implode(', ', $all_bids);
8815
8816 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
8817 $dbo->setQuery($q);
8818 $rbookings = $dbo->loadAssocList();
8819 if (!$rbookings) {
8820 return [];
8821 }
8822
8823 foreach ($rbookings as $k => $v) {
8824 if (empty($v['roomindex'])) {
8825 continue;
8826 }
8827 if (!isset($room_features_bookings[$v['roomindex']])) {
8828 $room_features_bookings[$v['roomindex']] = [];
8829 }
8830 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
8831 }
8832
8833 return $room_features_bookings;
8834 }
8835
8836 public static function getSendEmailWhen() {
8837 $dbo = JFactory::getDbo();
8838 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='emailsendwhen';";
8839 $dbo->setQuery($q);
8840 $dbo->execute();
8841 if ($dbo->getNumRows() > 0) {
8842 $cval = $dbo->loadAssocList();
8843 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8844 } else {
8845 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('emailsendwhen','1');";
8846 $dbo->setQuery($q);
8847 $dbo->execute();
8848 }
8849 return 1;
8850 }
8851
8852 public static function getMinutesAutoRemove() {
8853 $dbo = JFactory::getDbo();
8854 $minar = 0;
8855 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='minautoremove';";
8856 $dbo->setQuery($q);
8857 $dbo->execute();
8858 if ($dbo->getNumRows() > 0) {
8859 $minar = (int)$dbo->loadResult();
8860 }
8861 return $minar;
8862 }
8863
8864 public static function getSMSAPIClass() {
8865 $dbo = JFactory::getDbo();
8866 $cfile = '';
8867 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsapi';";
8868 $dbo->setQuery($q);
8869 $dbo->execute();
8870 if ($dbo->getNumRows() > 0) {
8871 $cval = $dbo->loadAssocList();
8872 $cfile = $cval[0]['setting'];
8873 }
8874 return $cfile;
8875 }
8876
8877 public static function autoSendSMSEnabled() {
8878 $dbo = JFactory::getDbo();
8879 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsautosend';";
8880 $dbo->setQuery($q);
8881 $dbo->execute();
8882 if ($dbo->getNumRows() > 0) {
8883 $cval = $dbo->loadAssocList();
8884 return intval($cval[0]['setting']) > 0 ? true : false;
8885 }
8886 return false;
8887 }
8888
8889 public static function getSendSMSTo() {
8890 $dbo = JFactory::getDbo();
8891 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendto';";
8892 $dbo->setQuery($q);
8893 $dbo->execute();
8894 if ($dbo->getNumRows() > 0) {
8895 $cval = $dbo->loadAssocList();
8896 if (!empty($cval[0]['setting'])) {
8897 $sto = json_decode($cval[0]['setting'], true);
8898 if (is_array($sto)) {
8899 return $sto;
8900 }
8901 }
8902 }
8903 return array();
8904 }
8905
8906 public static function getSendSMSWhen() {
8907 $dbo = JFactory::getDbo();
8908 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendwhen';";
8909 $dbo->setQuery($q);
8910 $dbo->execute();
8911 if ($dbo->getNumRows() > 0) {
8912 $cval = $dbo->loadAssocList();
8913 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8914 }
8915 return 1;
8916 }
8917
8918 public static function getSMSAdminPhone() {
8919 $dbo = JFactory::getDbo();
8920 $pnum = '';
8921 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsadminphone';";
8922 $dbo->setQuery($q);
8923 $dbo->execute();
8924 if ($dbo->getNumRows() > 0) {
8925 $cval = $dbo->loadAssocList();
8926 $pnum = $cval[0]['setting'];
8927 }
8928 return $pnum;
8929 }
8930
8931 public static function getSMSParams($as_array = true) {
8932 $dbo = JFactory::getDbo();
8933 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsparams';";
8934 $dbo->setQuery($q);
8935 $dbo->execute();
8936 if ($dbo->getNumRows() > 0) {
8937 $cval = $dbo->loadAssocList();
8938 if (!empty($cval[0]['setting'])) {
8939 if (!$as_array) {
8940 return $cval[0]['setting'];
8941 }
8942 $sparams = json_decode($cval[0]['setting'], true);
8943 if (is_array($sparams)) {
8944 return $sparams;
8945 }
8946 }
8947 }
8948 return array();
8949 }
8950
8951 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
8952 $dbo = JFactory::getDbo();
8953 switch (strtolower($booking_status)) {
8954 case 'standby':
8955 $status = 'pend';
8956 break;
8957 case 'cancelled':
8958 $status = 'canc';
8959 break;
8960 default:
8961 $status = '';
8962 break;
8963 }
8964 $paramtype = 'sms'.$type.'tpl'.$status;
8965 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
8966 $dbo->setQuery($q);
8967 $dbo->execute();
8968 if ($dbo->getNumRows() < 1) {
8969 if ($status == 'canc') {
8970 //Type cancelled is used by VCM since v1.6.6
8971 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
8972 $dbo->setQuery($q);
8973 $dbo->execute();
8974 }
8975 return '';
8976 }
8977 $ft = $dbo->loadAssocList();
8978 if (is_object($vbo_tn)) {
8979 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
8980 }
8981 return $ft[0]['setting'];
8982 }
8983
8984 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8985 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
8986 }
8987
8988 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8989 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
8990 }
8991
8992 public static function checkPhonePrefixCountry($phone, $country_threecode) {
8993 $dbo = JFactory::getDbo();
8994 $phone = str_replace(" ", '', trim($phone));
8995 $cprefix = '';
8996 if (!empty($country_threecode)) {
8997 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
8998 $dbo->setQuery($q);
8999 $dbo->execute();
9000 if ($dbo->getNumRows() > 0) {
9001 $cprefix = $dbo->loadResult();
9002 $cprefix = str_replace(" ", '', trim($cprefix));
9003 }
9004 }
9005 if (!empty($cprefix)) {
9006 if (substr($phone, 0, 1) != '+') {
9007 if (substr($phone, 0, 2) == '00') {
9008 $phone = '+'.substr($phone, 2);
9009 } else {
9010 $phone = $cprefix.$phone;
9011 }
9012 }
9013 }
9014 return $phone;
9015 }
9016
9017 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9018 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9019
9020 /**
9021 * Parse all conditional text rules.
9022 *
9023 * @since 1.14 (J) - 1.4.0 (WP)
9024 */
9025 self::getConditionalRulesInstance()
9026 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9027 ->parseTokens($tpl);
9028 //
9029
9030 $vbo_df = self::getDateFormat();
9031 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9032 $datesep = self::getDateSeparator();
9033
9034 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9035 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9036 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9037 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9038 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9039 if (!empty($booking['confirmnumber'])) {
9040 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9041 } else {
9042 $tpl = str_replace("{confirmnumb}", '', $tpl);
9043 }
9044 if (!empty($booking['idorderota'])) {
9045 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9046 } else {
9047 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9048 }
9049
9050 $rooms_booked = array();
9051 $rooms_names = array();
9052 $tot_adults = 0;
9053 $tot_children = 0;
9054 $tot_guests = 0;
9055 foreach ($booking_rooms as $broom) {
9056 $rooms_names[] = $broom['room_name'];
9057 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9058 $rooms_booked[$broom['room_name']] += 1;
9059 } else {
9060 $rooms_booked[$broom['room_name']] = 1;
9061 }
9062 $tot_adults += (int)$broom['adults'];
9063 $tot_children += (int)$broom['children'];
9064 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9065 }
9066 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9067 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9068 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9069 $rooms_booked_quant = array();
9070 foreach ($rooms_booked as $rname => $quant) {
9071 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9072 }
9073 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9074 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9075 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9076 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9077 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9078 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9079 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9080 $remaining_bal = $booking['total'] - $booking['totpaid'];
9081 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9082
9083 return $tpl;
9084 }
9085
9086 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9087 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9088
9089 /**
9090 * Parse all conditional text rules.
9091 *
9092 * @since 1.14 (J) - 1.4.0 (WP)
9093 */
9094 self::getConditionalRulesInstance()
9095 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9096 ->parseTokens($tpl);
9097 //
9098
9099 $vbo_df = self::getDateFormat();
9100 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9101 $datesep = self::getDateSeparator();
9102
9103 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9104 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9105 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9106 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9107 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9108 if (!empty($booking['confirmnumber'])) {
9109 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9110 } else {
9111 $tpl = str_replace("{confirmnumb}", '', $tpl);
9112 }
9113 if (!empty($booking['idorderota'])) {
9114 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9115 } else {
9116 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9117 }
9118
9119 $rooms_booked = array();
9120 $rooms_names = array();
9121 $tot_adults = 0;
9122 $tot_children = 0;
9123 $tot_guests = 0;
9124 foreach ($booking_rooms as $broom) {
9125 $rooms_names[] = $broom['room_name'];
9126 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9127 $rooms_booked[$broom['room_name']] += 1;
9128 } else {
9129 $rooms_booked[$broom['room_name']] = 1;
9130 }
9131 $tot_adults += (int)$broom['adults'];
9132 $tot_children += (int)$broom['children'];
9133 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9134 }
9135 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9136 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9137 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9138 $rooms_booked_quant = array();
9139 foreach ($rooms_booked as $rname => $quant) {
9140 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9141 }
9142 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9143 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9144 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9145 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9146 $remaining_bal = $booking['total'] - $booking['totpaid'];
9147 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9148 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9149
9150 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9151 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9152
9153 if (VBOPlatformDetection::isWordPress()) {
9154 /**
9155 * @wponly Rewrite URI for front-end
9156 */
9157 $book_link = str_replace(JUri::root(), '', $book_link);
9158 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9159 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9160 if ($itemid) {
9161 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9162 }
9163 } else {
9164 /**
9165 * @joomlaonly Rewrite URI for front-end
9166 */
9167 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9168 $uri_parts = [
9169 'option' => 'com_vikbooking',
9170 'view' => 'booking',
9171 'sid' => $use_sid,
9172 'ts' => $booking['ts'],
9173 ];
9174 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9175 // inject lang in query string for proper link routing
9176 $uri_parts['lang'] = $booking['lang'];
9177 }
9178 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9179 }
9180
9181 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9182 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9183 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9184
9185 return $tpl;
9186 }
9187
9188 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9189 $dbo = JFactory::getDbo();
9190 if (!class_exists('VboApplication')) {
9191 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9192 }
9193 $vbo_app = new VboApplication;
9194 if (empty($oid)) {
9195 return false;
9196 }
9197 $sms_api = self::getSMSAPIClass();
9198 if (empty($sms_api)) {
9199 return false;
9200 }
9201 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9202 return false;
9203 }
9204 $sms_api_params = self::getSMSParams();
9205 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9206 return false;
9207 }
9208 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9209 return false;
9210 }
9211 $send_sms_to = self::getSendSMSTo();
9212 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9213 return false;
9214 }
9215 $booking = array();
9216 $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.";";
9217 $dbo->setQuery($q);
9218 $dbo->execute();
9219 if ($dbo->getNumRows() > 0) {
9220 $booking = $dbo->loadAssoc();
9221 }
9222 if (!(count($booking) > 0)) {
9223 return false;
9224 }
9225 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9226 return false;
9227 }
9228 $booking_rooms = array();
9229 $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'].";";
9230 $dbo->setQuery($q);
9231 $dbo->execute();
9232 if ($dbo->getNumRows() > 0) {
9233 $booking_rooms = $dbo->loadAssocList();
9234 }
9235 $admin_phone = self::getSMSAdminPhone();
9236 $admin_sendermail = self::getSenderMail();
9237 $admin_email = self::getAdminMail();
9238 $f_result = false;
9239 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9240 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9241 //SMS for the administrator
9242 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9243 if (!empty($sms_text)) {
9244 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9245 //administrator phone can contain multiple numbers separated by comma or semicolon
9246 $admin_phones = array();
9247 if (strpos($admin_phone, ',') !== false) {
9248 $all_phones = explode(',', $admin_phone);
9249 foreach ($all_phones as $aph) {
9250 if (!empty($aph)) {
9251 $admin_phones[] = trim($aph);
9252 }
9253 }
9254 } elseif (strpos($admin_phone, ';') !== false) {
9255 $all_phones = explode(';', $admin_phone);
9256 foreach ($all_phones as $aph) {
9257 if (!empty($aph)) {
9258 $admin_phones[] = trim($aph);
9259 }
9260 }
9261 } else {
9262 $admin_phones[] = $admin_phone;
9263 }
9264 foreach ($admin_phones as $admphone) {
9265 $response_obj = $sms_obj->sendMessage($admphone, $sms_text);
9266 if ( !$sms_obj->validateResponse($response_obj) ) {
9267 //notify the administrator via email with the error of the SMS sending
9268 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9269 } else {
9270 $f_result = true;
9271 }
9272 }
9273 }
9274 }
9275 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9276 //SMS for the Customer
9277 $vbo_tn = self::getTranslator();
9278 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9279 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9280 if (!empty($sms_text)) {
9281 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9282 $response_obj = $sms_obj->sendMessage($booking['phone'], $sms_text);
9283 if ( !$sms_obj->validateResponse($response_obj) ) {
9284 //notify the administrator via email with the error of the SMS sending
9285 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9286 } else {
9287 $f_result = true;
9288 }
9289 }
9290 }
9291 return $f_result;
9292 }
9293
9294 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9295 {
9296 if (!defined('_VIKBOOKINGEXEC')) {
9297 define('_VIKBOOKINGEXEC', '1');
9298 }
9299
9300 ob_start();
9301 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
9302 $content = ob_get_contents();
9303 ob_end_clean();
9304
9305 $default_params = array(
9306 'show_header' => 0,
9307 'header_data' => array(),
9308 'show_footer' => 0,
9309 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9310 'pdf_unit' => 'PDF_UNIT',
9311 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9312 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9313 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9314 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9315 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9316 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9317 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9318 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9319 'header_font_size' => '10',
9320 'body_font_size' => '10',
9321 'footer_font_size' => '8',
9322 'show_lines_taxrate_col' => 0,
9323 );
9324
9325 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
9326 $default_params = array_merge($default_params, $invoice_params);
9327 }
9328
9329 return array($content, $default_params);
9330 }
9331
9332 /**
9333 * Includes within a buffer the template file for the custom (manual) invoice.
9334 *
9335 * @param array $invoice the record of the custom invoice
9336 * @param array $customer the customer record
9337 *
9338 * @return string the HTML content of the template file
9339 *
9340 * @since 1.11.1
9341 */
9342 public static function loadCustomInvoiceTmpl($invoice, $customer) {
9343 if (!defined('_VIKBOOKINGEXEC')) {
9344 define('_VIKBOOKINGEXEC', '1');
9345 }
9346 ob_start();
9347 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
9348 $content = ob_get_contents();
9349 ob_end_clean();
9350 $default_params = array(
9351 'show_header' => 0,
9352 'header_data' => array(),
9353 'show_footer' => 0,
9354 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9355 'pdf_unit' => 'PDF_UNIT',
9356 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9357 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9358 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9359 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9360 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9361 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9362 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9363 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9364 'header_font_size' => '10',
9365 'body_font_size' => '10',
9366 'footer_font_size' => '8'
9367 );
9368 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
9369 $default_params = array_merge($default_params, $invoice_params);
9370 }
9371 return array($content, $default_params);
9372 }
9373
9374 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
9375 {
9376 // clone the original template file source
9377 $parsed = $invoicetpl;
9378
9379 /**
9380 * Parse all conditional text rules.
9381 *
9382 * @since 1.14 (J) - 1.4.0 (WP)
9383 */
9384 self::getConditionalRulesInstance()
9385 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9386 ->parseTokens($parsed);
9387 //
9388
9389 $dbo = JFactory::getDbo();
9390 if (is_null($vbo_tn)) {
9391 $vbo_tn = self::getTranslator();
9392 }
9393 // availability helper
9394 $av_helper = self::getAvailabilityInstance();
9395
9396 $nowdf = self::getDateFormat();
9397 if ($nowdf == "%d/%m/%Y") {
9398 $df = 'd/m/Y';
9399 } elseif ($nowdf == "%m/%d/%Y") {
9400 $df = 'm/d/Y';
9401 } else {
9402 $df = 'Y/m/d';
9403 }
9404 $datesep = self::getDateSeparator();
9405
9406 /**
9407 * Split stay reservation.
9408 *
9409 * @since 1.16.0 (J) - 1.6.0 (WP)
9410 */
9411 $room_stay_dates = [];
9412 if ($booking['split_stay']) {
9413 if ($booking['status'] == 'confirmed') {
9414 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
9415 } else {
9416 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
9417 }
9418 // immediately count the number of nights of stay for each split room
9419 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
9420 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
9421 // overwrite values for compatibility with non-confirmed bookings
9422 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
9423 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
9424 }
9425 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
9426 // overwrite the whole array
9427 $room_stay_dates[$sps_r_k] = $sps_r_v;
9428 }
9429 }
9430
9431 $companylogo = self::getSiteLogo();
9432 $uselogo = '';
9433 if (!empty($companylogo)) {
9434 /**
9435 * Let's try to prevent TCPDF errors.
9436 *
9437 * @since August 2nd 2019
9438 */
9439 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9440 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9441 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9442 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9443 }
9444 }
9445 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
9446 $parsed = str_replace("{company_info}", $company_info, $parsed);
9447 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
9448 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
9449 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
9450 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
9451
9452 // custom fields replacement
9453 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
9454 if (is_array($cmatches[1]) && $cmatches[1]) {
9455 $cfids = array();
9456 foreach ($cmatches[1] as $cfid) {
9457 $cfids[] = $cfid;
9458 }
9459 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
9460 $dbo->setQuery($q);
9461 $cfields = $dbo->loadAssocList();
9462 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
9463 $cfmap = array();
9464 foreach ($cfields as $cf) {
9465 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
9466 }
9467 $cfmapreplace = array();
9468 $partsreceived = explode("\n", $booking['custdata']);
9469 if (count($partsreceived) > 0) {
9470 foreach ($partsreceived as $pst) {
9471 if (!empty($pst)) {
9472 $tmpdata = explode(":", $pst);
9473 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
9474 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
9475 }
9476 }
9477 }
9478 }
9479 foreach ($cmatches[1] as $cfid) {
9480 if (array_key_exists($cfid, $cfmapreplace)) {
9481 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
9482 } else {
9483 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
9484 }
9485 }
9486 }
9487
9488 // invoice price description - Start
9489 $rooms = array();
9490 $tars = array();
9491 $arrpeople = array();
9492 $is_package = !empty($booking['pkg']) ? true : false;
9493 $tot_adults = 0;
9494 $tot_children = 0;
9495 $tot_guests = 0;
9496 foreach ($booking_rooms as $kor => $or) {
9497 $num = $kor + 1;
9498 $rooms[$num] = $or;
9499
9500 // determine the days to consider for the count of the availability
9501 $room_nights = $booking['days'];
9502 $room_checkin = $booking['checkin'];
9503 $room_checkout = $booking['checkout'];
9504 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9505 $room_nights = $room_stay_dates[$kor]['nights'];
9506 $room_checkin = $room_stay_dates[$kor]['checkin'];
9507 $room_checkout = $room_stay_dates[$kor]['checkout'];
9508 }
9509
9510 $arrpeople[$num]['adults'] = $or['adults'];
9511 $arrpeople[$num]['children'] = $or['children'];
9512 $tot_adults += $or['adults'];
9513 $tot_children += $or['children'];
9514 $tot_guests += ($or['adults'] + $or['children']);
9515 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9516 //package or custom cost set from the back-end
9517 continue;
9518 }
9519 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
9520 $dbo->setQuery($q);
9521 $tar = $dbo->loadAssocList();
9522 if ($tar) {
9523 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
9524 //different usage
9525 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
9526 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
9527 //Occupancy Override
9528 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
9529 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
9530 //
9531 if (is_array($diffusageprice)) {
9532 //set a charge or discount to the price(s) for the different usage of the room
9533 foreach ($tar as $kpr => $vpr) {
9534 $tar[$kpr]['diffusage'] = $or['adults'];
9535 if ($diffusageprice['chdisc'] == 1) {
9536 //charge
9537 if ($diffusageprice['valpcent'] == 1) {
9538 //fixed value
9539 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9540 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9541 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
9542 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9543 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
9544 } else {
9545 //percentage value
9546 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9547 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
9548 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
9549 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9550 $tar[$kpr]['cost'] = $aduseval;
9551 }
9552 } else {
9553 //discount
9554 if ($diffusageprice['valpcent'] == 1) {
9555 //fixed value
9556 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9557 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9558 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
9559 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9560 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
9561 } else {
9562 //percentage value
9563 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9564 $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);
9565 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
9566 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9567 $tar[$kpr]['cost'] = $aduseval;
9568 }
9569 }
9570 }
9571 }
9572 }
9573 //
9574 $tars[$num] = $tar[0];
9575 }
9576 }
9577 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
9578 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
9579 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
9580 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
9581 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
9582 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
9583
9584 $isdue = 0;
9585 $tot_taxes = 0;
9586 $tot_city_taxes = 0;
9587 $tot_fees = 0;
9588 $pricestr = array();
9589 $optstr = array();
9590 // start building the tax summary
9591 VBOTaxonomySummary::start();
9592 foreach ($booking_rooms as $kor => $or) {
9593 $num = $kor + 1;
9594
9595 // determine the days to consider for the count of the availability
9596 $room_nights = $booking['days'];
9597 $room_checkin = $booking['checkin'];
9598 $room_checkout = $booking['checkout'];
9599 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9600 $room_nights = $room_stay_dates[$kor]['nights'];
9601 $room_checkin = $room_stay_dates[$kor]['checkin'];
9602 $room_checkout = $room_stay_dates[$kor]['checkout'];
9603 }
9604
9605 $pricestr[$num] = array();
9606 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9607 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
9608 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
9609 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
9610 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
9611 $pricestr[$num]['tot'] = $calctar;
9612 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
9613 $tot_taxes += ($calctar - $cost_minus_tax);
9614 $isdue += $calctar;
9615 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
9616 $pricestr[$num]['tax_rate'] = $tax_rate;
9617 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
9618 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9619 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
9620 $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'] : "");
9621 $pricestr[$num]['tot'] = $calctar;
9622 $tars[$num]['calctar'] = $calctar;
9623 $isdue += $calctar;
9624 if ($calctar == $display_rate) {
9625 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
9626 $tot_taxes += ($display_rate - $cost_minus_tax);
9627 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
9628 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
9629 $pricestr[$num]['tax_rate'] = $tax_rate;
9630 } else {
9631 $tot_taxes += ($calctar - $display_rate);
9632 $pricestr[$num]['tax'] = ($calctar - $display_rate);
9633 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
9634 $pricestr[$num]['tax_rate'] = $tax_rate;
9635 }
9636 }
9637 $optstr[$num] = array();
9638 $opt_ind = 0;
9639 if (!empty($or['optionals'])) {
9640 $stepo = explode(";", $or['optionals']);
9641 foreach ($stepo as $roptkey => $oo) {
9642 if (!empty($oo)) {
9643 $stept = explode(":", $oo);
9644 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
9645 $dbo->setQuery($q);
9646 $actopt = $dbo->loadAssocList();
9647 if ($actopt) {
9648 if (is_object($vbo_tn)) {
9649 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
9650 }
9651 $optstr[$num][$opt_ind] = array();
9652 $chvar = '';
9653 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
9654 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
9655 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
9656 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
9657 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
9658 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
9659 $agestept = explode('-', $stept[1]);
9660 $stept[1] = $agestept[0];
9661 $chvar = $agestept[1];
9662 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
9663 //percentage value of the adults tariff
9664 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9665 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9666 } else {
9667 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9668 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9669 }
9670 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
9671 //VBO 1.10 - percentage value of room base cost
9672 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9673 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9674 } else {
9675 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
9676 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9677 }
9678 }
9679 $actopt[0]['chageintv'] = $chvar;
9680 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
9681 $actopt[0]['quan'] = $stept[1];
9682 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
9683 } else {
9684 $actopt[0]['quan'] = $stept[1];
9685 // VBO 1.11 - options percentage cost of the room total fee
9686 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9687 $deftar_basecosts = $or['cust_cost'];
9688 } else {
9689 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9690 }
9691 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
9692 //
9693 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
9694 }
9695 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
9696 $realcost = $actopt[0]['maxprice'];
9697 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
9698 $realcost = $actopt[0]['maxprice'] * $stept[1];
9699 }
9700 }
9701 if ($actopt[0]['perperson'] == 1) {
9702 $realcost = $realcost * $or['adults'];
9703 }
9704 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
9705 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
9706 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
9707 $optstr[$num][$opt_ind]['tax'] = 0;
9708 if ($actopt[0]['is_citytax'] == 1) {
9709 $tot_city_taxes += $tmpopr;
9710 } elseif ($actopt[0]['is_fee'] == 1) {
9711 $tot_fees += $tmpopr;
9712 }
9713 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
9714 if ($tmpopr == $realcost) {
9715 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
9716 $tot_taxes += ($realcost - $opt_minus_tax);
9717 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
9718 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
9719 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9720 } else {
9721 $tot_taxes += ($tmpopr - $realcost);
9722 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
9723 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
9724 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9725 }
9726 //
9727 $opt_ind++;
9728 $isdue += $tmpopr;
9729 }
9730 }
9731 }
9732 }
9733
9734 // custom extra costs
9735 if (!empty($or['extracosts'])) {
9736 $cur_extra_costs = json_decode($or['extracosts'], true);
9737 foreach ($cur_extra_costs as $eck => $ecv) {
9738 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9739 $isdue += $ecplustax;
9740 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
9741 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
9742 $optstr[$num][$opt_ind]['tax'] = 0;
9743 if ($ecplustax == $ecv['cost']) {
9744 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9745 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
9746 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
9747 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
9748 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9749 } else {
9750 $tot_taxes += ($ecplustax - $ecv['cost']);
9751 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
9752 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
9753 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9754 }
9755 $opt_ind++;
9756 }
9757 }
9758 }
9759
9760 $usedcoupon = false;
9761 if (strlen($booking['coupon']) > 0) {
9762 $orig_isdue = $isdue;
9763 $expcoupon = explode(";", $booking['coupon']);
9764 $usedcoupon = $expcoupon;
9765 $isdue = $isdue - (float)$expcoupon[1];
9766 if ($isdue != $orig_isdue) {
9767 //lower taxes proportionally
9768 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9769 }
9770 }
9771 if ($booking['refund'] > 0) {
9772 $orig_isdue = $isdue;
9773 $isdue -= $booking['refund'];
9774 if ($isdue != $orig_isdue) {
9775 //lower taxes proportionally
9776 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9777 }
9778 }
9779 $rows_written = 0;
9780 $inv_rows = '';
9781 foreach ($pricestr as $num => $price_descr) {
9782 // add room split stay information, if any
9783 $split_stay_str = '';
9784 $kor = $num - 1;
9785 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
9786 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
9787 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
9788 }
9789
9790 $inv_rows .= '<tr>'."\n";
9791 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
9792 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($price_descr['tot'] - $price_descr['tax'])).'</td>'."\n";
9793 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tax']).'</td>'."\n";
9794 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tot']).'</td>'."\n";
9795 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9796 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
9797 }
9798 $inv_rows .= '</tr>'."\n";
9799 $rows_written++;
9800 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
9801 foreach ($optstr[$num] as $optk => $optv) {
9802 $inv_rows .= '<tr>'."\n";
9803 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
9804 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($optv['tot'] - $optv['tax'])).'</td>'."\n";
9805 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tax']).'</td>'."\n";
9806 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tot']).'</td>'."\n";
9807 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9808 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
9809 }
9810 $inv_rows .= '</tr>'."\n";
9811 $rows_written++;
9812 }
9813 }
9814 }
9815
9816 // if discount print row
9817 if ($usedcoupon !== false) {
9818 $inv_rows .= '<tr>'."\n";
9819 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9820 $inv_rows .= '</tr>'."\n";
9821 $inv_rows .= '<tr>'."\n";
9822 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
9823 $inv_rows .= '<td></td>'."\n";
9824 $inv_rows .= '<td></td>'."\n";
9825 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($usedcoupon[1]).'</td>'."\n";
9826 $inv_rows .= '</tr>'."\n";
9827 $rows_written += 2;
9828 }
9829 // if refunded amount, print row
9830 if ($booking['refund'] > 0) {
9831 $inv_rows .= '<tr>'."\n";
9832 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9833 $inv_rows .= '</tr>'."\n";
9834 $inv_rows .= '<tr>'."\n";
9835 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
9836 $inv_rows .= '<td></td>'."\n";
9837 $inv_rows .= '<td></td>'."\n";
9838 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($booking['refund']).'</td>'."\n";
9839 $inv_rows .= '</tr>'."\n";
9840 $rows_written += 2;
9841 }
9842 //
9843 $min_records = 8;
9844 if ($rows_written < $min_records) {
9845 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
9846 $inv_rows .= '<tr>'."\n";
9847 $inv_rows .= '<td></td>'."\n";
9848 $inv_rows .= '<td></td>'."\n";
9849 $inv_rows .= '<td></td>'."\n";
9850 $inv_rows .= '</tr>'."\n";
9851 }
9852 }
9853 //invoice price description - End
9854 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
9855 $parsed = str_replace("{invoice_totalnet}", $booking['currencyname'].' '.self::numberFormat(($isdue - $tot_taxes)), $parsed);
9856 $parsed = str_replace("{invoice_totaltax}", $booking['currencyname'].' '.self::numberFormat($tot_taxes), $parsed);
9857 $parsed = str_replace("{invoice_grandtotal}", $booking['currencyname'].' '.self::numberFormat($isdue), $parsed);
9858 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
9859
9860 // invoice tax summary
9861 $tax_summary = VBOTaxonomySummary::get();
9862 $tax_names = VBOTaxonomySummary::getNames();
9863 $taxsum_rows = "";
9864 foreach ($tax_summary as $tax_rate => $tax_amount) {
9865 if (!$tax_rate && !$tax_amount) {
9866 continue;
9867 }
9868 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
9869 $taxsum_rows .= '<tr>' . "\n";
9870 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
9871 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
9872 $taxsum_rows .= '<td>' . $booking['currencyname'] . ' ' . self::numberFormat($tax_amount) . '</td>' . "\n";
9873 $taxsum_rows .= '</tr>' . "\n";
9874 }
9875 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
9876
9877 return $parsed;
9878 }
9879
9880 /**
9881 * Specific method for parsing the template file for the custom (manual) invoices.
9882 *
9883 * @param string $invoicetpl the plain custom invoice template file before parsing
9884 * @param array $invoice the invoice record
9885 * @param array $customer the customer record
9886 *
9887 * @return string the HTML content of the parsed custom invoice template
9888 *
9889 * @since 1.11.1
9890 */
9891 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
9892 $nowdf = self::getDateFormat(true);
9893 if ($nowdf == "%d/%m/%Y") {
9894 $df = 'd/m/Y';
9895 } elseif ($nowdf == "%m/%d/%Y") {
9896 $df = 'm/d/Y';
9897 } else {
9898 $df = 'Y/m/d';
9899 }
9900 $datesep = self::getDateSeparator(true);
9901 $companylogo = self::getSiteLogo();
9902 $uselogo = '';
9903 if (!empty($companylogo)) {
9904 /**
9905 * Let's try to prevent TCPDF errors by checking if the file exists.
9906 *
9907 * @since August 2nd 2019
9908 */
9909 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9910 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9911 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9912 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9913 }
9914 }
9915 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
9916 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
9917 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
9918 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
9919 // customer information
9920 $custinfo = '';
9921 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
9922 $custinfo .= $customer['email'] . "\n";
9923 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
9924 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
9925 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
9926 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
9927 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
9928 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
9929 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
9930 // invoice notes
9931 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
9932
9933 return $invoicetpl;
9934 }
9935
9936 /**
9937 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
9938 *
9939 * @param array $booking the booking information.
9940 * @param int $invoice_num the invoice number.
9941 * @param string $invoice_suff the invoice suffix.
9942 * @param string $invoice_date the invoice date.
9943 * @param string $company_info optional company information.
9944 * @param bool $translate whether the invoice contents should be translated in the booking language.
9945 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
9946 *
9947 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
9948 *
9949 * @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.
9950 */
9951 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
9952 {
9953 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
9954 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
9955 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
9956
9957 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
9958 return false;
9959 }
9960
9961 /**
9962 * Trigger event to allow third party plugins to override some values for the invoice generation.
9963 *
9964 * @since 1.16.3 (J) - 1.6.3 (WP)
9965 */
9966 $event_args = [
9967 'invoice_num' => $invoice_num,
9968 'invoice_suff' => $invoice_suff,
9969 'invoice_date' => $invoice_date,
9970 'company_info' => $company_info,
9971 'translate' => $translate,
9972 ];
9973 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
9974 if (is_array($event_args) && $event_args) {
9975 // extract the new values to be used
9976 extract($event_args);
9977 }
9978
9979 $dbo = JFactory::getDbo();
9980 $vbo_tn = self::getTranslator();
9981
9982 // inject the currency name value
9983 $currencyname = self::getCurrencyName();
9984 $booking['currencyname'] = $currencyname;
9985
9986 // make sure we've got a customer ID assigned to the reservation record
9987 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
9988 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
9989 if ($booking_customer) {
9990 $booking['idcustomer'] = $booking_customer['id'];
9991 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
9992 $booking['customer_pin'] = $booking_customer['pin'];
9993 $booking['country_name'] = $booking_customer['country_name'] ?? '';
9994 }
9995 }
9996
9997 $nowdf = self::getDateFormat(true);
9998 if ($nowdf == "%d/%m/%Y") {
9999 $df = 'd/m/Y';
10000 } elseif ($nowdf == "%m/%d/%Y") {
10001 $df = 'm/d/Y';
10002 } else {
10003 $df = 'Y/m/d';
10004 }
10005 $datesep = self::getDateSeparator(true);
10006
10007 if (empty($invoice_date)) {
10008 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
10009 $used_date = $booking['ts'];
10010 } else {
10011 /**
10012 * We could be re-generating an invoice for a booking that already had an invoice.
10013 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10014 * It is now possible to keep the same invoice date as the previous one, so check
10015 * what value contains $invoice_date to see if it's different from today's date.
10016 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10017 * to also check the length of the string $invoice_date before using that date.
10018 *
10019 * @since 1.10 - August 2018
10020 */
10021 $base_ts = time();
10022 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10023 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10024 }
10025 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10026 $used_date = $base_ts;
10027 }
10028
10029 // load room reservation records
10030 $q = $dbo->getQuery(true)
10031 ->select($dbo->qn('or') . '.*')
10032 ->select($dbo->qn('r.name', 'room_name'))
10033 ->select($dbo->qn('r.fromadult'))
10034 ->select($dbo->qn('r.toadult'))
10035 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10036 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10037 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10038 ->order($dbo->qn('or.id') . ' ASC');
10039
10040 $dbo->setQuery($q);
10041 $booking_rooms = $dbo->loadAssocList();
10042
10043 if (!$booking_rooms) {
10044 return false;
10045 }
10046
10047 // check if an invoice for this reservation exists
10048 $invoice_id = 0;
10049 $dbo->setQuery(
10050 $dbo->getQuery(true)
10051 ->select('*')
10052 ->from($dbo->qn('#__vikbooking_invoices'))
10053 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10054 ->order($dbo->qn('id') . ' DESC')
10055 );
10056 $invoice_data = $dbo->loadAssoc();
10057 if ($invoice_data) {
10058 $invoice_id = $invoice_data['id'];
10059 }
10060
10061 // if requested, prepare data to refresh the PDF invoice
10062 if ($refresh_pdf === true) {
10063 if (!$invoice_id || !$invoice_data) {
10064 // cannot refresh the PDF if an invoice was never created before
10065 return false;
10066 }
10067
10068 // load the same exact previous information to refresh the invoice
10069 $invoice_num = $invoice_data['number'];
10070 $invoice_suff = $invoice_suff;
10071 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10072 $used_date = $invoice_data['for_date'];
10073
10074 // parse the previous (current) invoice number
10075 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10076 if ($matches) {
10077 // overwrite values
10078 $invoice_num = $matches[1] ?: $invoice_num;
10079 $invoice_suff = $matches[2] ?: $invoice_suff;
10080 }
10081 }
10082
10083 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10084 if ($translate === true) {
10085 if (!empty($booking['lang'])) {
10086 $lang = JFactory::getLanguage();
10087 if ($lang->getTag() != $booking['lang']) {
10088 if (VBOPlatformDetection::isWordPress()) {
10089 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10090 } else {
10091 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10092 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10093 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10094 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10095 }
10096 }
10097 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10098 // force the translation to start because contents should be translated
10099 $vbo_tn::$force_tolang = $booking['lang'];
10100 }
10101 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10102 }
10103 }
10104
10105 // the PDF file name
10106 $pdffname = $booking['id'] . '_' . $booking['sid'] . '.pdf';
10107
10108 /**
10109 * If an analogic invoice is not available, make sure to create the record
10110 * before letting the e-invoicing drivers run to avoid a loop.
10111 *
10112 * @since 1.16.7 (J) - 1.6.7 (WP)
10113 */
10114 $retval = null;
10115 if (!$refresh_pdf && !$invoice_id) {
10116 // prepare record object
10117 $invoice_record = new stdClass;
10118 $invoice_record->number = $invoice_num . $invoice_suff;
10119 $invoice_record->file_name = $pdffname;
10120 $invoice_record->idorder = (int)$booking['id'];
10121 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10122 $invoice_record->created_on = time();
10123 $invoice_record->for_date = (int)$used_date;
10124
10125 // insert record
10126 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10127 return false;
10128 }
10129
10130 $retval = $invoice_record->id ?? false;
10131
10132 if (!$retval) {
10133 return false;
10134 }
10135 }
10136
10137 /**
10138 * The generation of the analogic invoices can trigger the drivers for the
10139 * generation of the e-Invoices if they are set to automatically run.
10140 * However, the e-Invoicing classes may be calling this method, so we need
10141 * to make sure the eInvoicing class is not running before proceeding.
10142 * This method could be called within a loop, and so the second iterations
10143 * may already have loaded the eInvocing class. For this we use a static variable.
10144 *
10145 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10146 * though additional information may be available only after the transmission. On top
10147 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10148 */
10149 static $einvocing_can_run = false;
10150
10151 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10152 // allow second iterations calling this method to run
10153 $einvocing_can_run = true;
10154 }
10155
10156 // always load all drivers, db and plugin based ones
10157 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10158
10159 foreach ($drivers as $driver_obj) {
10160 // set the flag of the external call
10161 $driver_obj->externalCall = __METHOD__;
10162
10163 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10164 $driver_obj->externalData['einvnum'] = $invoice_num;
10165
10166 // let the driver eventually elaborate the booking information before invoicing
10167 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10168
10169 if ($einvocing_can_run === true && !$refresh_pdf) {
10170 // generate the e-Invoice
10171 $driver_err = '';
10172 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10173 $driver_err = $driver_obj->getError();
10174 VikError::raiseWarning('', $driver_err);
10175 }
10176
10177 // Booking History - store log for the e-Invoice result
10178 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10179 }
10180 }
10181
10182 /**
10183 * Start the generation of the analogic (courtesy) PDF invoice.
10184 */
10185
10186 // load dependencies
10187 if (!class_exists('TCPDF')) {
10188 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10189 }
10190 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10191
10192 /**
10193 * Trigger event to allow third party plugins to return a specific font name.
10194 *
10195 * @since 1.16.0 (J) - 1.6.0 (WP)
10196 */
10197 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10198 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10199 $usepdffont = $custom_pdf_font[0];
10200 }
10201
10202 // set array variable to the template file
10203 $booking_info = self::getBookingInfoFromID($booking['id']);
10204
10205 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10206
10207 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10208
10209 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10210
10211 if (is_file($pathpdf)) {
10212 @unlink($pathpdf);
10213 }
10214
10215 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10216
10217 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10218 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10219
10220 // header for each page of the pdf
10221 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
10222 $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]);
10223 }
10224
10225 // change some currencies to their unicode (decimal) value
10226 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10227 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10228 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10229 }
10230
10231 // header and footer fonts
10232 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10233 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10234
10235 // margins
10236 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10237 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10238 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10239
10240 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10241 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10242 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10243
10244 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10245 $pdf->SetPrintHeader(false);
10246 }
10247 if ($pdfparams['show_footer'] == 0) {
10248 $pdf->SetPrintFooter(false);
10249 }
10250
10251 $pdf->AddPage();
10252 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10253 $pdf->lastPage();
10254 $pdf->Output($pathpdf, 'F');
10255
10256 if (!is_file($pathpdf)) {
10257 return false;
10258 }
10259
10260 if (VBOPlatformDetection::isWordPress()) {
10261 /**
10262 * @wponly - trigger files mirroring
10263 */
10264 VikBookingLoader::import('update.manager');
10265 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10266 }
10267
10268 // Booking History
10269 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
10270
10271 if ($retval) {
10272 // a new invoice ID was inserted above
10273 return $retval;
10274 }
10275
10276 // prepare record object to be updated (if insert needed, it's done at first)
10277 $invoice_record = new stdClass;
10278 $invoice_record->id = (int)$invoice_id;
10279 $invoice_record->number = $invoice_num . $invoice_suff;
10280 $invoice_record->file_name = $pdffname;
10281 $invoice_record->idorder = (int)$booking['id'];
10282 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10283 $invoice_record->created_on = time();
10284 $invoice_record->for_date = (int)$used_date;
10285
10286 // update record
10287 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
10288
10289 // return the PDF invoice ID
10290 return $invoice_id;
10291 }
10292
10293 /**
10294 * Generates an analogic invoice in PDF format for a custom list of services.
10295 * No bookings are assigned to this custom invoice. The method parses the same
10296 * invoice template as for the regular process with real booking IDs.
10297 * The invoice number must be stored before calling this method.
10298 *
10299 * @param int $invoice_id the ID of the custom invoice record
10300 *
10301 * @return bool
10302 *
10303 * @since 1.11.1 (J) - 1.1.1 (WP)
10304 */
10305 public static function generateCustomInvoice($invoice_id)
10306 {
10307 $dbo = JFactory::getDbo();
10308
10309 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
10310 $dbo->setQuery($q);
10311 $invoice = $dbo->loadAssoc();
10312 if (!$invoice) {
10313 return false;
10314 }
10315
10316 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
10317 $rawcont = is_array($rawcont) ? $rawcont : array();
10318 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
10319 $invoice['rawcont'] = $rawcont;
10320 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
10321 if (!$rawcont || !$rows || !$customer) {
10322 // at least one invoice raw is mandatory as well as the customer
10323 return false;
10324 }
10325
10326 /**
10327 * Start the generation of the electronic invoice, if any driver was enabled.
10328 *
10329 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10330 * though additional information would be available only after the transmission.
10331 */
10332 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10333
10334 if ($drivers) {
10335 // make sure the invoice number is just a number
10336 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
10337
10338 // invoke all drivers that should run automatically
10339 foreach ($drivers as $driver_obj) {
10340 // set the flag of the external call
10341 $driver_obj->externalCall = __METHOD__;
10342
10343 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
10344 $driver_obj->externalData['einvnum'] = $invoice['number'];
10345 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
10346 $driver_obj->externalData['einvcustom'] = $invoice;
10347
10348 // prepare data array for the generation of the e-Invoice
10349 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
10350
10351 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
10352 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
10353 if ($preveinv !== false) {
10354 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
10355 }
10356
10357 // generate the e-Invoice
10358 $driver_err = '';
10359 if (!$driver_obj->generateEInvoice($einvdata)) {
10360 $driver_err = $driver_obj->getError();
10361 VikError::raiseWarning('', $driver_err);
10362 }
10363 }
10364 }
10365
10366 /**
10367 * Start the generation of the analogic (courtesy) PDF invoice.
10368 */
10369
10370 // load dependencies
10371 if (!class_exists('TCPDF')) {
10372 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10373 }
10374 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10375
10376 /**
10377 * Trigger event to allow third party plugins to return a specific font name.
10378 *
10379 * @since 1.16.0 (J) - 1.6.0 (WP)
10380 */
10381 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10382 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10383 $usepdffont = $custom_pdf_font[0];
10384 }
10385
10386 // load invoice template file
10387 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
10388
10389 // parse invoice template file
10390 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
10391
10392 $pdffname = $invoice['file_name'];
10393 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10394
10395 if (is_file($pathpdf)) {
10396 @unlink($pathpdf);
10397 }
10398
10399 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10400
10401 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10402 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
10403
10404 // header for each page of the pdf
10405 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10406 $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]);
10407 }
10408
10409 // change some currencies to their unicode (decimal) value
10410 $currencyname = self::getCurrencyName();
10411 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10412 if (array_key_exists($currencyname, $unichr_map)) {
10413 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
10414 }
10415
10416 // header and footer fonts
10417 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10418 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10419
10420 // margins
10421 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10422 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10423 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10424
10425 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10426 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10427 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10428
10429 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10430 $pdf->SetPrintHeader(false);
10431 }
10432 if ($pdfparams['show_footer'] == 0) {
10433 $pdf->SetPrintFooter(false);
10434 }
10435
10436 $pdf->AddPage();
10437 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10438 $pdf->lastPage();
10439 $pdf->Output($pathpdf, 'F');
10440
10441 if (!is_file($pathpdf)) {
10442 return false;
10443 }
10444
10445 if (VBOPlatformDetection::isWordPress()) {
10446 /**
10447 * @wponly - trigger files mirroring
10448 */
10449 VikBookingLoader::import('update.manager');
10450 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10451 }
10452
10453 // return the result of the generation of the PDF invoice
10454 return true;
10455 }
10456
10457 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
10458 {
10459 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
10460 return false;
10461 }
10462
10463 $dbo = JFactory::getDbo();
10464
10465 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
10466 $dbo->setQuery($q);
10467 $invoice_data = $dbo->loadAssoc();
10468
10469 if (!$invoice_data) {
10470 return false;
10471 }
10472
10473 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
10474 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
10475 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
10476 if (!file_exists($invoice_file_path)) {
10477 return false;
10478 }
10479
10480 $vbo_app = self::getVboApplication();
10481 $admin_sendermail = self::getSenderMail();
10482
10483 $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);
10484
10485 // update record
10486 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
10487 $dbo->setQuery($q);
10488 $dbo->execute();
10489
10490 return true;
10491 }
10492
10493 public static function loadCheckinDocTmpl($booking_info = array(), $booking_rooms = array(), $customer = array()) {
10494 if (!defined('_VIKBOOKINGEXEC')) {
10495 define('_VIKBOOKINGEXEC', '1');
10496 }
10497 ob_start();
10498 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "checkins" . DIRECTORY_SEPARATOR . "checkin_tmpl.php";
10499 $content = ob_get_contents();
10500 ob_end_clean();
10501 $default_params = array(
10502 'show_header' => 0,
10503 'header_data' => array(),
10504 'show_footer' => 0,
10505 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10506 'pdf_unit' => 'PDF_UNIT',
10507 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10508 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10509 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10510 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10511 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10512 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10513 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10514 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10515 'header_font_size' => '10',
10516 'body_font_size' => '10',
10517 'footer_font_size' => '8'
10518 );
10519 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
10520 $default_params = array_merge($default_params, $checkin_params);
10521 }
10522 return array($content, $default_params);
10523 }
10524
10525 public static function parseCheckinDocTemplate($checkintpl, $booking, $booking_rooms, $customer) {
10526 $parsed = $checkintpl;
10527 $dbo = JFactory::getDbo();
10528 $app = JFactory::getApplication();
10529 $nowdf = self::getDateFormat();
10530 if ($nowdf == "%d/%m/%Y") {
10531 $df='d/m/Y';
10532 } elseif ($nowdf == "%m/%d/%Y") {
10533 $df='m/d/Y';
10534 } else {
10535 $df='Y/m/d';
10536 }
10537 $datesep = self::getDateSeparator();
10538 $companylogo = self::getSiteLogo();
10539 $uselogo = '';
10540 if (!empty($companylogo)) {
10541 /**
10542 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
10543 *
10544 * @since March 16th 2021
10545 */
10546 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10547 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10548 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
10549 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10550 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10551 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
10552 }
10553 }
10554 $company_name = self::getFrontTitle();
10555 $company_info = self::getInvoiceCompanyInfo();
10556 $parsed = str_replace("{company_name}", $company_name, $parsed);
10557 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10558 $parsed = str_replace("{company_info}", $company_info, $parsed);
10559 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10560 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10561 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10562 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10563 $tot_guests = 0;
10564 $tot_adults = 0;
10565 $tot_children = 0;
10566 foreach ($booking_rooms as $kor => $or) {
10567 $tot_guests += ($or['adults'] + $or['children']);
10568 $tot_adults += $or['adults'];
10569 $tot_children += $or['children'];
10570 }
10571 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10572 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10573 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10574 if (count($customer) && isset($customer['comments'])) {
10575 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
10576 }
10577 $termsconds = self::getTermsConditions();
10578 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
10579 //custom fields replace
10580 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10581 if (is_array($cmatches[1]) && $cmatches[1]) {
10582 $cfids = array();
10583 foreach ($cmatches[1] as $cfid) {
10584 $cfids[] = $cfid;
10585 }
10586 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10587 $dbo->setQuery($q);
10588 $cfields = $dbo->loadAssocList();
10589 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10590 $cfmap = array();
10591 foreach ($cfields as $cf) {
10592 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10593 }
10594 $cfmapreplace = array();
10595 $partsreceived = explode("\n", $booking['custdata']);
10596 if (count($partsreceived) > 0) {
10597 foreach ($partsreceived as $pst) {
10598 if (!empty($pst)) {
10599 $tmpdata = explode(":", $pst);
10600 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10601 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10602 }
10603 }
10604 }
10605 }
10606 foreach ($cmatches[1] as $cfid) {
10607 if (array_key_exists($cfid, $cfmapreplace)) {
10608 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10609 } else {
10610 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10611 }
10612 }
10613 }
10614 //end custom fields replace
10615
10616 return $parsed;
10617 }
10618
10619 /**
10620 * Returns an array of key-value pairs to be used for building the Guest Details
10621 * in the Check-in process. The keys will be compared to the fields of the table
10622 * #__customers to see if some values already exist. The values can use lang defs
10623 * of both front-end or back-end. To be called as list(fields, attributes).
10624 *
10625 * @param boolean $precheckin true if requested for front-end pre check-in.
10626 * @param string $type optional type of custom pax fields to force.
10627 *
10628 * @return array key-value pairs for showing and collecting guest details.
10629 *
10630 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
10631 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
10632 */
10633 public static function getPaxFields($precheckin = false, $type = null)
10634 {
10635 if (!$precheckin) {
10636 // back-end check-in ("registration") key-value pairs
10637
10638 // check the type of pax fields collection data
10639 $collection_type = !empty($type) ? $type : VBOFactory::getConfig()->getString('checkindata', 'basic');
10640
10641 // return the requested pax fields collection list
10642 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
10643
10644 if (is_array($custom_pax_fields) && count($custom_pax_fields)) {
10645 // requested driver returned a list of fields
10646 return $custom_pax_fields;
10647 }
10648
10649 // in case the driver is unsupported, fallback to "basic" (default) driver
10650 return VBOCheckinPax::getFields('basic');
10651 }
10652
10653 // front-end key-value pairs for pre check-in
10654 $precheckin_pax_fields = [
10655 [
10656 'first_name' => JText::translate('VBCCFIRSTNAME'),
10657 'last_name' => JText::translate('VBCCLASTNAME'),
10658 'date_birth' => JText::translate('ORDER_DBIRTH'),
10659 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
10660 'country' => JText::translate('ORDER_STATE'),
10661 'city' => JText::translate('ORDER_CITY'),
10662 'zip' => JText::translate('ORDER_ZIP'),
10663 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
10664 'gender' => JText::translate('VBOCUSTGENDER'),
10665 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
10666 'docnum' => JText::translate('VBOCUSTDOCNUM'),
10667 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
10668 ],
10669 [
10670 'first_name' => 'text',
10671 'last_name' => 'text',
10672 'date_birth' => 'calendar',
10673 'place_birth' => 'text',
10674 'country' => 'country',
10675 'city' => 'text',
10676 'zip' => 'text',
10677 'nationality' => 'country',
10678 'gender' => [
10679 'M' => JText::translate('VBOCUSTGENDERM'),
10680 'F' => JText::translate('VBOCUSTGENDERF'),
10681 ],
10682 'doctype' => 'text',
10683 'docnum' => 'text',
10684 'documents' => 'file',
10685 ],
10686 ];
10687
10688 // make a safe copy of the default precheckin fields
10689 $def_precheckin_pax_fields = $precheckin_pax_fields;
10690
10691 /**
10692 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
10693 *
10694 * @since 1.16.3 (J) - 1.6.3 (WP)
10695 */
10696 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
10697
10698 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
10699 // use the filtered pax fields
10700 $def_precheckin_pax_fields = $precheckin_pax_fields;
10701 }
10702
10703 // return the front-end key-value pairs for pre check-in
10704 return $def_precheckin_pax_fields;
10705 }
10706
10707 /**
10708 * Returns the associative list of countries from the DB.
10709 *
10710 * @param bool $tn whether to translate the country name.
10711 * @param bool $no_id whether to unset the ID of the country.
10712 *
10713 * @return array associative or empty array.
10714 *
10715 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
10716 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
10717 */
10718 public static function getCountriesArray($tn = true, $no_id = true)
10719 {
10720 $dbo = JFactory::getDbo();
10721 $all_countries = [];
10722
10723 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
10724 $dbo->setQuery($q);
10725 $dbo->execute();
10726 if (!$dbo->getNumRows()) {
10727 return [];
10728 }
10729 $countries = $dbo->loadAssocList();
10730
10731 if ($tn === true) {
10732 $vbo_tn = self::getTranslator();
10733 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
10734 // re-apply sorting by country name
10735 $sorting = [];
10736 foreach ($countries as $country) {
10737 $sorting[$country['country_name']] = $country;
10738 }
10739 ksort($sorting);
10740 $sorted = [];
10741 foreach ($sorting as $country) {
10742 $sorted[] = $country;
10743 }
10744 $countries = $sorted;
10745 unset($sorting, $sorted);
10746 }
10747
10748 foreach ($countries as $v) {
10749 if ($no_id) {
10750 // keep the original structure by unsetting the ID only needed for translation
10751 unset($v['id']);
10752 }
10753 $all_countries[$v['country_3_code']] = $v;
10754 }
10755
10756 return $all_countries;
10757 }
10758
10759 public static function getCountriesSelect($name, $all_countries = array(), $current_value = '', $empty_value = ' ')
10760 {
10761 if (!count($all_countries)) {
10762 $all_countries = self::getCountriesArray();
10763 }
10764
10765 $countries = '<select name="'.$name.'">'."\n";
10766 if (strlen($empty_value)) {
10767 $countries .= '<option value="">'.$empty_value.'</option>'."\n";
10768 }
10769 foreach ($all_countries as $v) {
10770 $countries .= '<option value="'.$v['country_3_code'].'"'.($v['country_3_code'] == $current_value ? ' selected="selected"' : '').'>'.$v['country_name'].'</option>'."\n";
10771 }
10772 $countries .= '</select>';
10773
10774 return $countries;
10775 }
10776
10777 public static function getThumbSize($skipsession = false)
10778 {
10779 static $thumb_size = null;
10780
10781 if ($thumb_size) {
10782 return $thumb_size;
10783 }
10784
10785 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
10786
10787 return $thumb_size;
10788 }
10789
10790 /**
10791 * Checks whether an iCal file for the reservation should be
10792 * attached to the confirmation email for customer and/or admin.
10793 *
10794 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
10795 *
10796 * @since 1.12.0 (J) - 1.2.0 (WP)
10797 */
10798 public static function attachIcal()
10799 {
10800 return VBOFactory::getConfig()->getInt('attachical', 1);
10801 }
10802
10803 /**
10804 * How the calculation of the orphan dates should take place.
10805 *
10806 * @return string "next" for only checking the bookings ahead, "prevnext" if
10807 * also the previous bookings should be checked.
10808 *
10809 * @since 1.13 (J) - 1.3.0 (WP)
10810 */
10811 public static function orphansCalculation()
10812 {
10813 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
10814 }
10815
10816 /**
10817 * Returns the name of the template file to use for the front-end View "search".
10818 *
10819 * @return string the name of the template file. New and upgraded user will both use the classic file.
10820 *
10821 * @since 1.13 (J) - 1.3.0 (WP)
10822 */
10823 public static function searchResultsTmpl()
10824 {
10825 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
10826 }
10827
10828 /**
10829 * Returns a string without any new-line characters
10830 * to be used for JavaScript values without facing
10831 * errors like 'unterminated string literal'.
10832 * By passing nl2br($str) as argument, we can keep
10833 * the wanted new-line HTML tags for PRE tags.
10834 * We use implode() with just one argument to
10835 * not use an empty string as glue for the string.
10836 *
10837 * @param $str string
10838 *
10839 * @return string
10840 */
10841 public static function strTrimLiteral($str) {
10842 $str = str_replace(array("\r\n", "\r"), "\n", $str);
10843 $lines = explode("\n", $str);
10844 $new_lines = array();
10845 foreach ($lines as $i => $line) {
10846 if (strlen($line)) {
10847 $new_lines[] = trim($line);
10848 }
10849 }
10850 return implode($new_lines);
10851 }
10852
10853 /**
10854 * Returns an instance of the VboApplication class.
10855 *
10856 * @return VboApplication
10857 */
10858 public static function getVboApplication()
10859 {
10860 // require library
10861 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
10862
10863 // return a new instance
10864 return new VboApplication();
10865 }
10866
10867 /**
10868 * Returns the translation of the given 0-indexed week day.
10869 *
10870 * @param int $wd the 0-indexed day of the week.
10871 * @param bool $short whether to get the short version of the weekday.
10872 *
10873 * @return string the name of the week day.
10874 *
10875 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10876 */
10877 public static function sayWeekDay($wd, $short = false)
10878 {
10879 switch ($wd) {
10880 case '6' :
10881 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
10882 break;
10883 case '5' :
10884 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
10885 break;
10886 case '4' :
10887 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
10888 break;
10889 case '3' :
10890 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
10891 break;
10892 case '2' :
10893 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
10894 break;
10895 case '1' :
10896 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
10897 break;
10898 default :
10899 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
10900 break;
10901 }
10902 return $ret;
10903 }
10904
10905 /**
10906 * Returns the translation of the given 1-indexed month of the year.
10907 *
10908 * @param int $idm the 1-indexed month of the year.
10909 * @param bool $short whether to get the short version of the month.
10910 *
10911 * @return string the name of the month.
10912 *
10913 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10914 */
10915 public static function sayMonth($idm, $short = false)
10916 {
10917 switch ($idm) {
10918 case '12' :
10919 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
10920 break;
10921 case '11' :
10922 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
10923 break;
10924 case '10' :
10925 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
10926 break;
10927 case '9' :
10928 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
10929 break;
10930 case '8' :
10931 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
10932 break;
10933 case '7' :
10934 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
10935 break;
10936 case '6' :
10937 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
10938 break;
10939 case '5' :
10940 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
10941 break;
10942 case '4' :
10943 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
10944 break;
10945 case '3' :
10946 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
10947 break;
10948 case '2' :
10949 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
10950 break;
10951 default :
10952 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
10953 break;
10954 }
10955 return $ret;
10956 }
10957
10958 public static function sayDayMonth($d) {
10959 switch ($d) {
10960 case '31' :
10961 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
10962 break;
10963 case '30' :
10964 $ret = JText::translate('VBDAYMONTHTHIRTY');
10965 break;
10966 case '29' :
10967 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
10968 break;
10969 case '28' :
10970 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
10971 break;
10972 case '27' :
10973 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
10974 break;
10975 case '26' :
10976 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
10977 break;
10978 case '25' :
10979 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
10980 break;
10981 case '24' :
10982 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
10983 break;
10984 case '23' :
10985 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
10986 break;
10987 case '22' :
10988 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
10989 break;
10990 case '21' :
10991 $ret = JText::translate('VBDAYMONTHTWENTYONE');
10992 break;
10993 case '20' :
10994 $ret = JText::translate('VBDAYMONTHTWENTY');
10995 break;
10996 case '19' :
10997 $ret = JText::translate('VBDAYMONTHNINETEEN');
10998 break;
10999 case '18' :
11000 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
11001 break;
11002 case '17' :
11003 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
11004 break;
11005 case '16' :
11006 $ret = JText::translate('VBDAYMONTHSIXTEEN');
11007 break;
11008 case '15' :
11009 $ret = JText::translate('VBDAYMONTHFIFTEEN');
11010 break;
11011 case '14' :
11012 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11013 break;
11014 case '13' :
11015 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11016 break;
11017 case '12' :
11018 $ret = JText::translate('VBDAYMONTHTWELVE');
11019 break;
11020 case '11' :
11021 $ret = JText::translate('VBDAYMONTHELEVEN');
11022 break;
11023 case '10' :
11024 $ret = JText::translate('VBDAYMONTHTEN');
11025 break;
11026 case '9' :
11027 $ret = JText::translate('VBDAYMONTHNINE');
11028 break;
11029 case '8' :
11030 $ret = JText::translate('VBDAYMONTHEIGHT');
11031 break;
11032 case '7' :
11033 $ret = JText::translate('VBDAYMONTHSEVEN');
11034 break;
11035 case '6' :
11036 $ret = JText::translate('VBDAYMONTHSIX');
11037 break;
11038 case '5' :
11039 $ret = JText::translate('VBDAYMONTHFIVE');
11040 break;
11041 case '4' :
11042 $ret = JText::translate('VBDAYMONTHFOUR');
11043 break;
11044 case '3' :
11045 $ret = JText::translate('VBDAYMONTHTHREE');
11046 break;
11047 case '2' :
11048 $ret = JText::translate('VBDAYMONTHTWO');
11049 break;
11050 default :
11051 $ret = JText::translate('VBDAYMONTHONE');
11052 break;
11053 }
11054 return $ret;
11055 }
11056
11057 public static function totElements($arr) {
11058 $n = 0;
11059 if (is_array($arr)) {
11060 foreach ($arr as $a) {
11061 if (!empty($a)) {
11062 $n++;
11063 }
11064 }
11065 return $n;
11066 }
11067 return false;
11068 }
11069
11070 /**
11071 * Returns a list of documents that were uploaded
11072 * for the specified customer.
11073 *
11074 * @param integer $id The customer ID.
11075 *
11076 * @return array A list of documents.
11077 *
11078 * @since 1.3.0
11079 */
11080 public static function getCustomerDocuments($id)
11081 {
11082 $dbo = JFactory::getDbo();
11083
11084 $q = $dbo->getQuery(true)
11085 ->select($dbo->qn('docsfolder'))
11086 ->from($dbo->qn('#__vikbooking_customers'))
11087 ->where($dbo->qn('id') . ' = ' . (int) $id);
11088
11089 $dbo->setQuery($q, 0, 1);
11090 $dbo->execute();
11091
11092 if (!$dbo->getNumRows())
11093 {
11094 // customer not found
11095 return array();
11096 }
11097
11098 // retrieve customer documents directory name
11099 $dirname = $dbo->loadResult();
11100
11101 if (empty($dirname))
11102 {
11103 // no available directory
11104 return array();
11105 }
11106
11107 // build documents folder path
11108 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11109
11110 if (!is_dir($dirname))
11111 {
11112 // the customer directory doesn't exist
11113 return array();
11114 }
11115
11116 // read all files from customer directory
11117 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11118
11119 $files = array();
11120
11121 foreach ($glob as $path)
11122 {
11123 // skip "index.html"
11124 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11125 {
11126 // extract name and extension from file path
11127 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11128 {
11129 $name = $match[1];
11130 $ext = $match[2];
11131 }
11132 else
11133 {
11134 $name = basename($path);
11135 $ext = '';
11136 }
11137
11138 $file = new stdClass;
11139 $file->path = $path;
11140 $file->name = $name;
11141 $file->ext = $ext;
11142 $file->basename = $file->name . '.' . $file->ext;
11143 $file->size = filesize($path);
11144 $file->date = filemtime($path);
11145 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11146
11147 $files[] = $file;
11148 }
11149 }
11150
11151 // sort files by creation date
11152 usort($files, function($a, $b)
11153 {
11154 return $b->date - $a->date;
11155 });
11156
11157 return $files;
11158 }
11159
11160 public static function displayPaymentParameters($pfile, $pparams = '') {
11161 $html = '<p>---------</p>';
11162
11163 /**
11164 * @wponly The payment gateway is now loaded
11165 * using the apposite dispatcher.
11166 *
11167 * @since 1.0.5
11168 */
11169 JLoader::import('adapter.payment.dispatcher');
11170
11171 try
11172 {
11173 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11174 }
11175 catch (Exception $e)
11176 {
11177 // payment not found
11178 $html = $e->getMessage();
11179
11180 if ($code = $e->getCode())
11181 {
11182 $html = '<b>' . $code . '</b> : ' . $html;
11183 }
11184
11185 return $html;
11186 }
11187 //
11188
11189 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11190 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11191
11192 // get admin parameters
11193 $pconfig = $payment->getAdminParameters();
11194
11195 if (count($pconfig) > 0) {
11196 $html = '';
11197 foreach ($pconfig as $value => $cont) {
11198 if (empty($value)) {
11199 continue;
11200 }
11201 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11202 $label = $labelparts[0];
11203 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11204 if (!empty($cont['help'])) {
11205 $labelhelp = $cont['help'];
11206 }
11207 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11208 $html .= '<div class="vbo-param-container">';
11209 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11210 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11211 }
11212 $html .= '<div class="vbo-param-setting">';
11213 switch ($cont['type']) {
11214 case 'custom':
11215 $html .= $cont['html'];
11216 break;
11217 case 'select':
11218 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11219 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11220 if (isset($cont['multiple']) && $cont['multiple']) {
11221 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11222 } else {
11223 $html .= '<select name="vikpaymentparams['.$value.']">';
11224 }
11225 foreach ($options as $optkey => $poption) {
11226 $checkval = $is_assoc ? $optkey : $poption;
11227 $selected = false;
11228 if (isset($arrparams[$value])) {
11229 if (is_array($arrparams[$value])) {
11230 $selected = in_array($checkval, $arrparams[$value]);
11231 } else {
11232 $selected = ($checkval == $arrparams[$value]);
11233 }
11234 } elseif (isset($default_paramv)) {
11235 if (is_array($default_paramv)) {
11236 $selected = in_array($checkval, $default_paramv);
11237 } else {
11238 $selected = ($checkval == $default_paramv);
11239 }
11240 }
11241 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11242 }
11243 $html .= '</select>';
11244 break;
11245 case 'password':
11246 $html .= '<div class="btn-wrapper input-append">';
11247 $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"/>';
11248 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11249 $html .= '</div>';
11250 break;
11251 case 'number':
11252 $number_attr = array();
11253 if (isset($cont['min'])) {
11254 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11255 }
11256 if (isset($cont['max'])) {
11257 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11258 }
11259 if (isset($cont['step'])) {
11260 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11261 }
11262 $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) . '/>';
11263 break;
11264 case 'textarea':
11265 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11266 break;
11267 case 'hidden':
11268 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
11269 break;
11270 case 'checkbox':
11271 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11272 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
11273 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
11274 break;
11275 default:
11276 $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"/>';
11277 break;
11278 }
11279 if (strlen($labelhelp) > 0) {
11280 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11281 }
11282 $html .= '</div>';
11283 $html .= '</div>';
11284 }
11285 // JS helper function to toggle the password fields
11286 $html .= "\n" . '<script>' . "\n";
11287 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
11288 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11289 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11290 $html .= ' var inp_type = inp.attr("type");' . "\n";
11291 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11292 $html .= '}' . "\n";
11293 $html .= "\n" . '</script>' . "\n";
11294 }
11295
11296 return $html;
11297 }
11298
11299 public static function displaySMSParameters($pfile, $params = null)
11300 {
11301 $html = '---------';
11302
11303 $params = is_string($params) ? json_decode($params, true) : $params;
11304 $params = is_array($params) ? $params : [];
11305
11306 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
11307 return $html;
11308 }
11309
11310 // attempt to load the class file
11311 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
11312
11313 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
11314 return $html;
11315 }
11316
11317 // load the gateway parameters
11318 $config = VikSmsApi::getAdminParameters();
11319
11320 if (!is_array($config) || !count($config)) {
11321 return $html;
11322 }
11323
11324 // flags for JS helpers
11325 $js_helpers = array();
11326
11327 $html = '';
11328 foreach ($config as $value => $cont) {
11329 if (empty($value)) {
11330 continue;
11331 }
11332 $inp_attr = '';
11333 if (isset($cont['attributes'])) {
11334 foreach ($cont['attributes'] as $inpk => $inpv) {
11335 $inp_attr .= $inpk.'="'.$inpv.'" ';
11336 }
11337 $inp_attr = ' ' . rtrim($inp_attr);
11338 }
11339 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11340 $label = $labelparts[0];
11341 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11342 if (!empty($cont['help'])) {
11343 $labelhelp = $cont['help'];
11344 }
11345 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11346 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
11347 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11348 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11349 }
11350 $html .= '<div class="vbo-param-setting">';
11351 switch ($cont['type']) {
11352 case 'custom':
11353 $html .= $cont['html'];
11354 break;
11355 case 'select':
11356 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11357 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11358 if (isset($cont['multiple']) && $cont['multiple']) {
11359 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11360 } else {
11361 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
11362 }
11363 foreach ($options as $optkey => $poption) {
11364 $checkval = $is_assoc ? $optkey : $poption;
11365 $selected = false;
11366 if (isset($params[$value])) {
11367 if (is_array($params[$value])) {
11368 $selected = in_array($checkval, $params[$value]);
11369 } else {
11370 $selected = ($checkval == $params[$value]);
11371 }
11372 } elseif (isset($default_paramv)) {
11373 if (is_array($default_paramv)) {
11374 $selected = in_array($checkval, $default_paramv);
11375 } else {
11376 $selected = ($checkval == $default_paramv);
11377 }
11378 }
11379 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11380 }
11381 $html .= '</select>';
11382 break;
11383 case 'password':
11384 $html .= '<div class="btn-wrapper input-append">';
11385 $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 . '/>';
11386 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11387 $html .= '</div>';
11388 // set flag for JS helper
11389 $js_helpers[] = $cont['type'];
11390 break;
11391 case 'number':
11392 $number_attr = array();
11393 if (isset($cont['min'])) {
11394 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11395 }
11396 if (isset($cont['max'])) {
11397 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11398 }
11399 if (isset($cont['step'])) {
11400 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11401 }
11402 $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 . '/>';
11403 break;
11404 case 'textarea':
11405 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11406 break;
11407 case 'visual_html':
11408 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11409 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
11410 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
11411 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
11412 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11413 break;
11414 case 'hidden':
11415 $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 . '/>';
11416 break;
11417 case 'checkbox':
11418 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11419 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
11420 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11421 break;
11422 default:
11423 $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 . '/>';
11424 break;
11425 }
11426 if (strlen($labelhelp) > 0) {
11427 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11428 }
11429 $html .= '</div>';
11430 $html .= '</div>';
11431 }
11432
11433 // JS helper functions
11434 if (in_array('password', $js_helpers)) {
11435 // toggle the password fields
11436 $html .= "\n" . '<script>' . "\n";
11437 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
11438 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11439 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11440 $html .= ' var inp_type = inp.attr("type");' . "\n";
11441 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11442 $html .= '}' . "\n";
11443 $html .= "\n" . '</script>' . "\n";
11444 }
11445
11446 return $html;
11447 }
11448
11449 /**
11450 * Renders the params of the requested cron job file-class.
11451 *
11452 * @param string $file the name of the cron job driver file.
11453 * @param array $params the parameters array.
11454 *
11455 * @return string the necessary HTML content to render.
11456 *
11457 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
11458 */
11459 public static function displayCronParameters($file, $params = [])
11460 {
11461 try
11462 {
11463 // attempt to create a new instance
11464 $job = VBOFactory::getCronFactory()->createInstance($file);
11465 }
11466 catch (Exception $e)
11467 {
11468 // something went wrong, display error message
11469 return '<p>' . $e->getMessage() . '<p>';
11470 }
11471
11472 // get admin parameters
11473 $config = $job->getForm();
11474
11475 if (!is_array($config) || !count($config))
11476 {
11477 return '<p>---------</p>';
11478 }
11479
11480 // flags for JS helpers
11481 $js_helpers = array();
11482
11483 $html = '';
11484 foreach ($config as $value => $cont) {
11485 if (empty($value)) {
11486 continue;
11487 }
11488
11489 $inp_attr = '';
11490 if (isset($cont['attributes']) && is_array($cont['attributes'])) {
11491 foreach ($cont['attributes'] as $inpk => $inpv) {
11492 $inp_attr .= $inpk . '="' . $inpv . '" ';
11493 }
11494 $inp_attr = ' ' . rtrim($inp_attr);
11495 }
11496
11497 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11498 $label = $labelparts[0];
11499 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11500 if (!empty($cont['help'])) {
11501 $labelhelp = $cont['help'];
11502 }
11503
11504 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11505 $nested_style = (isset($cont['nested']) && $cont['nested']);
11506 $hidden_wrapper = false;
11507 if (isset($cont['conditional']) && isset($config[$cont['conditional']])) {
11508 $check_cond = isset($config[$cont['conditional']]['default']) ? $config[$cont['conditional']]['default'] : null;
11509 $check_cond = isset($params[$cont['conditional']]) ? $params[$cont['conditional']] : $check_cond;
11510 if (!is_null($check_cond) && (!$check_cond || !strcasecmp($check_cond, 'off'))) {
11511 // hide current field because the field to who this is dependant is "off" or disabled (i.e. 0)
11512 $hidden_wrapper = true;
11513 }
11514 }
11515
11516 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], ['textarea', 'visual_html']) ? ' vbo-param-container-full' : '') . ($nested_style ? ' vbo-param-nested' : '') . '"' . ($hidden_wrapper ? ' style="display: none;"' : '') . '>';
11517 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11518 $html .= '<div class="vbo-param-label">' . $label . '</div>';
11519 }
11520 $html .= '<div class="vbo-param-setting">';
11521
11522 switch ($cont['type']) {
11523 case 'custom':
11524 $html .= $cont['html'];
11525 break;
11526 case 'select':
11527 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11528 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11529 if (isset($cont['multiple']) && $cont['multiple']) {
11530 $html .= '<select name="vikcronparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11531 } else {
11532 $html .= '<select name="vikcronparams['.$value.']"' . $inp_attr . '>';
11533 }
11534 foreach ($options as $optkey => $poption) {
11535 $checkval = $is_assoc ? $optkey : $poption;
11536 $selected = false;
11537 if (isset($params[$value])) {
11538 if (is_array($params[$value])) {
11539 $selected = in_array($checkval, $params[$value]);
11540 } else {
11541 $selected = ($checkval == $params[$value]);
11542 }
11543 } elseif (isset($default_paramv)) {
11544 if (is_array($default_paramv)) {
11545 $selected = in_array($checkval, $default_paramv);
11546 } else {
11547 $selected = ($checkval == $default_paramv);
11548 }
11549 }
11550 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11551 }
11552 $html .= '</select>';
11553 break;
11554 case 'password':
11555 $html .= '<div class="btn-wrapper input-append">';
11556 $html .= '<input type="password" name="vikcronparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>';
11557 $html .= '<button type="button" class="btn btn-primary" onclick="vikCronParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11558 $html .= '</div>';
11559 // set flag for JS helper
11560 $js_helpers[] = $cont['type'];
11561 break;
11562 case 'number':
11563 $number_attr = [];
11564 if (isset($cont['min'])) {
11565 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11566 }
11567 if (isset($cont['max'])) {
11568 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11569 }
11570 if (isset($cont['step'])) {
11571 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11572 }
11573 $html .= '<input type="number" name="vikcronparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" ' . implode(' ', $number_attr) . $inp_attr . '/>';
11574 break;
11575 case 'textarea':
11576 $html .= '<textarea name="vikcronparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11577 break;
11578 case 'visual_html':
11579 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11580 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : [];
11581 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : [];
11582 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : [];
11583 $html .= self::getVboApplication()->renderVisualEditor('vikcronparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11584 break;
11585 case 'codemirror':
11586 $editor = JEditor::getInstance('codemirror');
11587 $e_options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11588 $e_name = 'vikcronparams[' . $value . ']';
11589 $e_value = isset($params[$value]) ? $params[$value] : $default_paramv;
11590 $e_width = isset($e_options['width']) ? $e_options['width'] : '100%';
11591 $e_height = isset($e_options['height']) ? $e_options['height'] : 300;
11592 $e_col = isset($e_options['col']) ? $e_options['col'] : 70;
11593 $e_row = isset($e_options['row']) ? $e_options['row'] : 20;
11594 $e_buttons = isset($e_options['buttons']) ? (bool)$e_options['buttons'] : true;
11595 $e_id = isset($e_options['id']) ? $e_options['id'] : 'vikcronparams_' . $value;
11596 $e_params = isset($e_options['params']) && is_array($e_options['params']) ? $e_options['params'] : [];
11597 if (interface_exists('Throwable')) {
11598 /**
11599 * With PHP >= 7 supporting throwable exceptions for Fatal Errors
11600 * we try to avoid issues with third party plugins that make use
11601 * of the WP native function get_current_screen().
11602 *
11603 * @wponly
11604 */
11605 try {
11606 $html .= $editor->display($e_name, $e_value, $e_width, $e_height, $e_col, $e_row, $e_buttons, $e_id, $e_asset = null, $e_autor = null, $e_params);
11607 } catch (Throwable $t) {
11608 $html .= $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine() . '<br/>';
11609 $html .= '<textarea name="vikcronparams[' . $value . ']"' . $inp_attr . '>' . (isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)) . '</textarea>';
11610 }
11611 } else {
11612 $html .= $editor->display($e_name, $e_value, $e_width, $e_height, $e_col, $e_row, $e_buttons, $e_id, $e_asset = null, $e_autor = null, $e_params);
11613 }
11614 break;
11615 case 'hidden':
11616 $html .= '<input type="hidden" name="vikcronparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"' . $inp_attr . '/>';
11617 break;
11618 case 'checkbox':
11619 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11620 $html .= '<input type="hidden" name="vikcronparams['.$value.']" value="0" />';
11621 $html .= self::getVboApplication()->printYesNoButtons('vikcronparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11622 break;
11623 default:
11624 $html .= '<input type="text" name="vikcronparams['.$value.']" value="'.(isset($params[$value]) ? JHtml::fetch('esc_attr', $params[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'" size="20"' . $inp_attr . '/>';
11625 break;
11626 }
11627
11628 if (strlen($labelhelp)) {
11629 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11630 }
11631
11632 $html .= '</div>';
11633 $html .= '</div>';
11634 }
11635
11636 // JS helper functions
11637 if (in_array('password', $js_helpers)) {
11638 // toggle the password fields
11639 $html .= "\n" . '<script>' . "\n";
11640 $html .= 'function vikCronParamTogglePwd(elem) {' . "\n";
11641 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11642 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11643 $html .= ' var inp_type = inp.attr("type");' . "\n";
11644 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11645 $html .= '}' . "\n";
11646 $html .= "\n" . '</script>' . "\n";
11647 }
11648
11649 return $html;
11650 }
11651
11652 public static function invokeChannelManager($skiporder = true, $order = array()) {
11653 $task = VikRequest::getString('task', '', 'request');
11654 $view = VikRequest::getString('view', '', 'request');
11655 $tmpl = VikRequest::getString('tmpl', '', 'request');
11656 $noimpression = array('vieworder', 'booking');
11657 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')) {
11658 //VCM Channel Impression
11659 if (!class_exists('VikChannelManagerConfig')) {
11660 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11661 }
11662 if (!class_exists('VikChannelManager')) {
11663 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11664 }
11665 VikChannelManager::invokeChannelImpression();
11666 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11667 //VCM Channel Conversion-Impression
11668 if (!class_exists('VikChannelManagerConfig')) {
11669 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11670 }
11671 if (!class_exists('VikChannelManager')) {
11672 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11673 }
11674 VikChannelManager::invokeChannelConversionImpression($order);
11675 }
11676 }
11677
11678 public static function validEmail($email) {
11679 $isValid = true;
11680 $atIndex = strrpos($email, "@");
11681 if (is_bool($atIndex) && !$atIndex) {
11682 $isValid = false;
11683 } else {
11684 $domain = substr($email, $atIndex +1);
11685 $local = substr($email, 0, $atIndex);
11686 $localLen = strlen($local);
11687 $domainLen = strlen($domain);
11688 if ($localLen < 1 || $localLen > 64) {
11689 // local part length exceeded
11690 $isValid = false;
11691 } else
11692 if ($domainLen < 1 || $domainLen > 255) {
11693 // domain part length exceeded
11694 $isValid = false;
11695 } else
11696 if ($local[0] == '.' || $local[$localLen -1] == '.') {
11697 // local part starts or ends with '.'
11698 $isValid = false;
11699 } else
11700 if (preg_match('/\\.\\./', $local)) {
11701 // local part has two consecutive dots
11702 $isValid = false;
11703 } else
11704 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
11705 // character not valid in domain part
11706 $isValid = false;
11707 } else
11708 if (preg_match('/\\.\\./', $domain)) {
11709 // domain part has two consecutive dots
11710 $isValid = false;
11711 } else
11712 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
11713 // character not valid in local part unless
11714 // local part is quoted
11715 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
11716 $isValid = false;
11717 }
11718 }
11719 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
11720 // domain not found in DNS
11721 $isValid = false;
11722 }
11723 }
11724 return $isValid;
11725 }
11726
11727 public static function caniWrite($path) {
11728 if ($path[strlen($path) - 1] == '/') {
11729 // ricorsivo return a temporary file path
11730 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
11731 }
11732 if (is_dir($path)) {
11733 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
11734 }
11735 // check tmp file for read/write capabilities
11736 $rm = file_exists($path);
11737 $f = @fopen($path, 'a');
11738 if ($f === false) {
11739 return false;
11740 }
11741 fclose($f);
11742 if (!$rm) {
11743 unlink($path);
11744 }
11745 return true;
11746 }
11747
11748 /**
11749 * Alias method of JFile::upload to unify any
11750 * upload function into one.
11751 *
11752 * @param string $src The name of the php (temporary) uploaded file.
11753 * @param string $dest The path (including filename) to move the uploaded file to.
11754 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
11755 *
11756 * @return boolean True on success.
11757 *
11758 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
11759 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
11760 */
11761 public static function uploadFile($src, $dest, $copy_only = false) {
11762 // always attempt to include the File class
11763 jimport('joomla.filesystem.file');
11764
11765 // upload the file
11766 if (!$copy_only) {
11767 $result = JFile::upload($src, $dest);
11768 } else {
11769 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
11770 $result = JFile::copy($src, $dest);
11771 }
11772
11773 /**
11774 * @wponly in order to not lose uploaded files after installing an update,
11775 * we need to move any uploaded file onto a recovery folder.
11776 */
11777 if ($result) {
11778 VikBookingLoader::import('update.manager');
11779 VikBookingUpdateManager::triggerUploadBackup($dest);
11780 }
11781 //
11782
11783 // return upload result
11784 return $result;
11785 }
11786
11787 /**
11788 * Helper method used to upload the given file (retrieved from $_FILES)
11789 * into the specified destination.
11790 *
11791 * @param array $file An associative array with the file details.
11792 * @param string $dest The destination path.
11793 * @param string $filters A string (or a regex) containing the allowed extensions.
11794 *
11795 * @return array The uploading result.
11796 *
11797 * @throws RuntimeException
11798 *
11799 * @since 1.3.0
11800 */
11801 public static function uploadFileFromRequest($file, $dest, $filters = '*')
11802 {
11803 jimport('joomla.filesystem.file');
11804
11805 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
11806
11807 if (empty($file['name']))
11808 {
11809 throw new RuntimeException('Missing file', 400);
11810 }
11811
11812 /**
11813 * Make sure the upload of the file didn't raise any errors.
11814 *
11815 * @link https://www.php.net/manual/en/features.file-upload.errors.php
11816 */
11817 if ((int) $file['error'])
11818 {
11819 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
11820 {
11821 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
11822 }
11823 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
11824 {
11825 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
11826 }
11827 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
11828 {
11829 $error = 'The uploaded file was only partially uploaded.';
11830 }
11831 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
11832 {
11833 $error = 'No file was uploaded.';
11834 }
11835 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
11836 {
11837 $error = 'Missing a temporary folder.';
11838 }
11839 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
11840 {
11841 $error = 'Failed to write file to disk.';
11842 }
11843 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
11844 {
11845 $error = 'A PHP extension stopped the file upload.';
11846 }
11847 else
11848 {
11849 $error = 'Unknown error.';
11850 }
11851
11852 throw new RuntimeException($error, 500);
11853 }
11854
11855 $src = $file['tmp_name'];
11856
11857 // extract file name and extension
11858 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
11859 {
11860 $filename = $match[1];
11861 $fileext = $match[2];
11862 }
11863 else
11864 {
11865 // probably no extension provided
11866 $filename = basename($file['name']);
11867 $fileext = '';
11868 }
11869
11870 $j = '';
11871
11872 if (file_exists($dest . $filename . $fileext))
11873 {
11874 $j = 2;
11875
11876 while (file_exists($dest . $filename . '-' . $j . $fileext))
11877 {
11878 $j++;
11879 }
11880
11881 $j = '-' . $j;
11882 }
11883
11884 $finaldest = $dest . $filename . $j . $fileext;
11885
11886 if ($filters !== '*')
11887 {
11888 $ext = $file['type'];
11889
11890 // check if we have a regex
11891 if (preg_match("/^[#\/]/", $filters) && preg_match("/[#\/][a-z]*$/", $filters))
11892 {
11893 if (!preg_match($filters, $ext))
11894 {
11895 // extension not supported
11896 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11897 }
11898 }
11899 else
11900 {
11901 // get all supported types
11902 $types = array_map('strtolower', array_filter(explode(',', $filters)));
11903
11904 if (!in_array($ext, $types))
11905 {
11906 // extension not supported
11907 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11908 }
11909 }
11910 }
11911
11912 // try to upload the file
11913 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
11914 {
11915 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
11916 }
11917
11918 $file = new stdClass;
11919 $file->name = $filename . $j;
11920 $file->ext = ltrim($fileext, '.');
11921 $file->filename = basename($finaldest);
11922 $file->path = $finaldest;
11923
11924 return $file;
11925 }
11926
11927 /**
11928 * Gets the instance of a specific report class.
11929 *
11930 * @param string $report the name of the report to load.
11931 *
11932 * @return mixed false or report object instance.
11933 *
11934 * @since 1.3.0
11935 */
11936 public static function getReportInstance($report)
11937 {
11938 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
11939
11940 return VikBookingReport::getInstanceOf($report);
11941 }
11942
11943 /**
11944 * Checks whether the guest reviews are enabled and VCM is installed.
11945 *
11946 * @return boolean true if enabled, false otherwise.
11947 *
11948 * @since 1.3.0
11949 */
11950 public static function allowGuestReviews()
11951 {
11952 $dbo = JFactory::getDbo();
11953 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
11954 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
11955 $dbo->setQuery($q);
11956 $dbo->execute();
11957 if ($dbo->getNumRows()) {
11958 $s = $dbo->loadResult();
11959 return ((int)$s === 1 && $vcm_installed);
11960 }
11961 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
11962 $dbo->setQuery($q);
11963 $dbo->execute();
11964 return $vcm_installed;
11965 }
11966
11967 /**
11968 * Gets the minimum chars for the guest review message.
11969 *
11970 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
11971 *
11972 * @since 1.3.0
11973 */
11974 public static function guestReviewMinChars()
11975 {
11976 $dbo = JFactory::getDbo();
11977 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
11978 $dbo->setQuery($q);
11979 $dbo->execute();
11980 if ($dbo->getNumRows() > 0) {
11981 return (int)$dbo->loadResult();
11982 }
11983 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
11984 $dbo->setQuery($q);
11985 $dbo->execute();
11986 return 15;
11987 }
11988
11989 /**
11990 * The approval type for new guest reviews.
11991 *
11992 * @return string auto or manual.
11993 *
11994 * @since 1.3.0
11995 */
11996 public static function guestReviewsApproval()
11997 {
11998 $dbo = JFactory::getDbo();
11999 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
12000 $dbo->setQuery($q);
12001 $dbo->execute();
12002 if ($dbo->getNumRows() > 0) {
12003 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
12004 }
12005 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
12006 $dbo->setQuery($q);
12007 $dbo->execute();
12008 return 'auto';
12009 }
12010
12011 /**
12012 * The type of reviews guests should leave.
12013 *
12014 * @return string global or service.
12015 *
12016 * @since 1.3.0
12017 */
12018 public static function guestReviewsType()
12019 {
12020 $dbo = JFactory::getDbo();
12021 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12022 $dbo->setQuery($q);
12023 $dbo->execute();
12024 if ($dbo->getNumRows() > 0) {
12025 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12026 }
12027 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12028 $dbo->setQuery($q);
12029 $dbo->execute();
12030 return 'service';
12031 }
12032
12033 /**
12034 * The services to be reviewed by the guests.
12035 *
12036 * @return array list, empty or not, of the services to be reviewed.
12037 *
12038 * @since 1.3.0
12039 */
12040 public static function guestReviewsServices()
12041 {
12042 $dbo = JFactory::getDbo();
12043 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12044 $dbo->setQuery($q);
12045 $dbo->execute();
12046 if ($dbo->getNumRows()) {
12047 return $dbo->loadAssocList();
12048 }
12049 // insert the default services
12050 $default_services = array(
12051 JText::translate('VBOGREVVALUE'),
12052 JText::translate('VBOGREVLOCATION'),
12053 JText::translate('VBOGREVSTAFF'),
12054 JText::translate('VBOGREVCLEAN'),
12055 JText::translate('VBOGREVCOMFORT'),
12056 JText::translate('VBOGREVFACILITIES'),
12057 );
12058 foreach ($default_services as $def_service) {
12059 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12060 $dbo->setQuery($q);
12061 $dbo->execute();
12062 }
12063 return array();
12064 }
12065
12066 /**
12067 * Checks whether the guest reviews should be downloaded (max 1 per day).
12068 * VCM must be installed in order to download the new reviews.
12069 *
12070 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12071 *
12072 * @since 1.3.0
12073 */
12074 public static function shouldDownloadReviews()
12075 {
12076 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12077 return -1;
12078 }
12079
12080 $today = date('Y-m-d');
12081
12082 $dbo = JFactory::getDbo();
12083 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12084 $dbo->setQuery($q);
12085 $dbo->execute();
12086 if ($dbo->getNumRows()) {
12087 $last_download = $dbo->loadResult();
12088 // update last download day
12089 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12090 $dbo->setQuery($q);
12091 $dbo->execute();
12092 //
12093 return ($last_download != $today ? 1 : 0);
12094 }
12095 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12096 $dbo->setQuery($q);
12097 $dbo->execute();
12098 return 1;
12099 }
12100
12101 /**
12102 * Tells whether a booking can be reviewed.
12103 *
12104 * @param array $booking booking array details.
12105 *
12106 * @return boolean true if a review can be left, false otherwise.
12107 *
12108 * @since 1.3.0
12109 */
12110 public static function canBookingBeReviewed($booking)
12111 {
12112 // reviews must be enabled and supported
12113 if (!self::allowGuestReviews()) {
12114 return false;
12115 }
12116
12117 // booking status must be confirmed
12118 if ($booking['status'] != 'confirmed') {
12119 return false;
12120 }
12121
12122 // review can be left starting from the check-out day
12123 $checkout_info = getdate($booking['checkout']);
12124 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12125 if (time() < $checkout_midn) {
12126 return false;
12127 }
12128
12129 // make sure a review for this booking does not exist
12130 $noreview = true;
12131 $dbo = JFactory::getDbo();
12132 try {
12133 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12134 $dbo->setQuery($q);
12135 $dbo->execute();
12136 $noreview = ($dbo->getNumRows() < 1);
12137 } catch (Exception $e) {
12138 // if the query fails, we do not allow the review to be left
12139 $noreview = false;
12140 }
12141
12142 return $noreview;
12143 }
12144
12145 /**
12146 * Gets the review for a booking, no matter if it's published or not.
12147 * Review will be returned only if reviews are enabled and supported.
12148 * Only reviews left through the website, no OTA reviews as this method
12149 * should be used in the front-end to display the review to the guest.
12150 *
12151 * @param array $booking booking array details.
12152 *
12153 * @return mixed associative array or false.
12154 *
12155 * @since 1.3.0
12156 */
12157 public static function getBookingReview($booking)
12158 {
12159 // reviews must be enabled and supported
12160 if (!self::allowGuestReviews()) {
12161 return false;
12162 }
12163
12164 if (empty($booking['id'])) {
12165 return false;
12166 }
12167
12168 // make sure a review for this booking exists
12169 $dbo = JFactory::getDbo();
12170 $review = array();
12171 try {
12172 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12173 $dbo->setQuery($q);
12174 $dbo->execute();
12175 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12176 } catch (Exception $e) {
12177 // query has failed
12178 $review = array();
12179 }
12180
12181 if (count($review)) {
12182 // make sure the review was left from the website
12183 $review['uniquekey'] = (int)$review['uniquekey'];
12184 }
12185
12186 if (count($review) && !empty($review['uniquekey'])) {
12187 // this is an OTA review
12188 return false;
12189 }
12190
12191 if (count($review) && !empty($review['content'])) {
12192 // decode content
12193 $review['content'] = json_decode($review['content'], true);
12194 }
12195
12196 return count($review) ? $review : false;
12197 }
12198
12199 /**
12200 * Attempts to find a translation for a raw customer data label.
12201 * Bookings downloaded from the OTAs will save a raw string of information
12202 * composed of pairs of label-value separated by new line feeds.
12203 * We try to translate the given label into the current language.
12204 *
12205 * @param string $label the raw string label to translate.
12206 *
12207 * @return string either the original or the translated label.
12208 *
12209 * @since 1.3.5
12210 * @since 1.4.0 back-end support added.
12211 */
12212 public static function tnCustomerRawDataLabel($label)
12213 {
12214 // this is a map of the known labels
12215 $known_lbls = array(
12216 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12217 'LASTNAME' => 'ORDER_LNAME',
12218 'COUNTRY' => 'ORDER_STATE',
12219 'EMAIL' => 'VBMAIL',
12220 'TELEPHONE' => 'VBPHONE',
12221 'PHONE' => 'VBPHONE',
12222 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12223 'MEAL_PLAN' => 'VBOMEALPLAN',
12224 'CITY' => 'VBCITY',
12225 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12226 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12227 );
12228
12229 if (self::isAdmin()) {
12230 // override the map of known translation strings
12231 $known_lbls = array(
12232 'NAME' => 'ORDER_NAME',
12233 'LASTNAME' => 'ORDER_LNAME',
12234 'COUNTRY' => 'ORDER_STATE',
12235 'ADDRESS' => 'ORDER_ADDRESS',
12236 'CITY' => 'ORDER_CITY',
12237 'LOCATION' => 'ORDER_CITY',
12238 'EMAIL' => 'ORDER_EMAIL',
12239 'TELEPHONE' => 'ORDER_PHONE',
12240 'PHONE' => 'ORDER_PHONE',
12241 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12242 );
12243 }
12244
12245 // we get rid of any empty space by keeping the underscores
12246 $converted = str_replace(' ', '', strtoupper($label));
12247
12248 if (isset($known_lbls[$converted])) {
12249 // this language definition has been mapped
12250 return JText::translate($known_lbls[$converted]);
12251 }
12252
12253 // we try to guess the translation string by prepending VBO
12254 $guessed = JText::translate('VBO' . $converted);
12255 if ($guessed != 'VBO' . $converted) {
12256 // the label was translated correctly
12257 return $guessed;
12258 }
12259
12260 // this label could not be translated, so we return the plain string
12261 return $label;
12262 }
12263
12264 /**
12265 * We check whether some bookings are available for import from third party plugins.
12266 *
12267 * @return mixed array with list of plugins supported, false otherwise.
12268 *
12269 * @wponly this method is only useful for WordPress.
12270 *
12271 * @since 1.3.5
12272 */
12273 public static function canImportBookingsFromThirdPartyPlugins()
12274 {
12275 $dbo = JFactory::getDbo();
12276
12277 $plugins = array();
12278
12279 /**
12280 * As requested from hundreds of our clients, for the moment we check only the custom
12281 * post types of type "mphb_booking" to see if some bookings are available for import.
12282 */
12283 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12284 $dbo->setQuery($q);
12285 $dbo->execute();
12286
12287 if (!$dbo->getNumRows()) {
12288 return false;
12289 }
12290
12291 // push this third party plugin
12292 $plugins['mphb'] = 'MotoPress Hotel Booking';
12293
12294 return count($plugins) ? $plugins : false;
12295 }
12296
12297 /**
12298 * This method returns a list of the known languages sorted by the
12299 * administrator custom preferences. Useful for the phone input fields.
12300 *
12301 * @param boolean $code_assoc whether to get an associative array with the lang name.
12302 *
12303 * @return array the sorted list of preferred countries.
12304 *
12305 * @since 1.3.11
12306 */
12307 public static function preferredCountriesOrdering($code_assoc = false)
12308 {
12309 $preferred_countries = array();
12310
12311 // try to get the preferred countries from db
12312 $dbo = JFactory::getDbo();
12313 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12314 $dbo->setQuery($q);
12315 $setting = $dbo->loadResult();
12316 if (!$setting) {
12317 // create empty configuration record
12318 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12319 $dbo->setQuery($q);
12320 $dbo->execute();
12321 } else {
12322 $preferred_countries = json_decode($setting);
12323 }
12324
12325 // get the default known languages
12326 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12327
12328 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12329 // sort the default known languages by country code alphabetically
12330 ksort($sorted_known_langs);
12331 foreach ($sorted_known_langs as $k => $v) {
12332 $langsep = strpos($k, '_') !== false ? '_' : '-';
12333 $langparts = explode($langsep, $k);
12334 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
12335 }
12336 // update the database record
12337 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
12338 $dbo->setQuery($q);
12339 $dbo->execute();
12340 }
12341
12342 if ($code_assoc) {
12343 // this is useful for displaying the preferred countries codes together with the language name
12344 $map = array();
12345 foreach ($preferred_countries as $ccode) {
12346 // look for the current country code in the keys of the known language tags
12347 $match_found = false;
12348 foreach ($sorted_known_langs as $langtag => $langinfo) {
12349 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
12350 $langparts = explode($langsep, $langtag);
12351 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
12352 // match found
12353 $match_found = true;
12354 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12355 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
12356 // match found
12357 $match_found = true;
12358 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12359 }
12360 }
12361 if (!$match_found) {
12362 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
12363 $map[$ccode] = strtoupper($ccode);
12364 }
12365 }
12366 if (count($map)) {
12367 // set the associatve array to be returned
12368 $preferred_countries = $map;
12369 }
12370 }
12371
12372 return $preferred_countries;
12373 }
12374
12375 /**
12376 * Gets the instance of the admin widgets helper class.
12377 *
12378 * @return VikBookingHelperAdminWidgets
12379 *
12380 * @since 1.4.0
12381 */
12382 public static function getAdminWidgetsInstance()
12383 {
12384 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
12385
12386 return VikBookingHelperAdminWidgets::getInstance();
12387 }
12388
12389 /**
12390 * Gets the instance of the conditional rules helper class.
12391 *
12392 * @param bool $require_only whether to return the object.
12393 *
12394 * @return mixed VikBookingHelperConditionalRules or true.
12395 *
12396 * @since 1.4.0
12397 */
12398 public static function getConditionalRulesInstance($require_only = false)
12399 {
12400 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
12401
12402 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
12403 }
12404
12405 /**
12406 * Gets the instance of the geocoding helper class.
12407 *
12408 * @param bool $require_only whether to return the object.
12409 *
12410 * @return mixed VikBookingHelperGeocoding or true.
12411 *
12412 * @since 1.4.0
12413 */
12414 public static function getGeocodingInstance($require_only = false)
12415 {
12416 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
12417
12418 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
12419 }
12420
12421 /**
12422 * Helper method to cope with the removal of the same method
12423 * in the JApplication class introduced with Joomla 4. Using
12424 * isClient() would break the compatibility with J < 3.7 so
12425 * we can rely on this helper method to avoid Fatal Errors.
12426 *
12427 * @return boolean
12428 *
12429 * @since October 2020
12430 */
12431 public static function isAdmin()
12432 {
12433 $app = JFactory::getApplication();
12434 if (method_exists($app, 'isClient')) {
12435 return $app->isClient('administrator');
12436 }
12437
12438 return $app->isAdmin();
12439 }
12440
12441 /**
12442 * Helper method to cope with the removal of the same method
12443 * in the JApplication class introduced with Joomla 4. Using
12444 * isClient() would break the compatibility with J < 3.7 so
12445 * we can rely on this helper method to avoid Fatal Errors.
12446 *
12447 * @return boolean
12448 *
12449 * @since October 2020
12450 */
12451 public static function isSite()
12452 {
12453 $app = JFactory::getApplication();
12454 if (method_exists($app, 'isClient')) {
12455 return $app->isClient('site');
12456 }
12457
12458 return $app->isSite();
12459 }
12460
12461 /**
12462 * Gets the Google Maps API Key.
12463 *
12464 * @return string
12465 *
12466 * @since 1.14 (J) - 1.4.0 (WP)
12467 */
12468 public static function getGoogleMapsKey()
12469 {
12470 $dbo = JFactory::getDbo();
12471 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
12472 $dbo->setQuery($q);
12473 $dbo->execute();
12474 if ($dbo->getNumRows()) {
12475 return $dbo->loadResult();
12476 }
12477 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
12478 $dbo->setQuery($q);
12479 $dbo->execute();
12480 return '';
12481 }
12482
12483 /**
12484 * Checks whether the interactive map booking is enabled.
12485 *
12486 * @return bool
12487 *
12488 * @since 1.14 (J) - 1.4.0 (WP)
12489 */
12490 public static function interactiveMapEnabled()
12491 {
12492 $dbo = JFactory::getDbo();
12493 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
12494 $dbo->setQuery($q);
12495 $dbo->execute();
12496 if ($dbo->getNumRows()) {
12497 return ((int)$dbo->loadResult() > 0);
12498 }
12499
12500 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
12501 $dbo->setQuery($q);
12502 $dbo->execute();
12503
12504 return false;
12505 }
12506
12507 /**
12508 * Gets the preferred colors saved in the configuration, if any.
12509 *
12510 * @return array
12511 *
12512 * @since 1.14 (J) - 1.4.0 (WP)
12513 */
12514 public static function getPreferredColors()
12515 {
12516 $dbo = JFactory::getDbo();
12517 $pref_colors = array(
12518 'textcolor' => '',
12519 'bgcolor' => '',
12520 'fontcolor' => '',
12521 'bgcolorhov' => '',
12522 'fontcolorhov' => '',
12523 );
12524
12525 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
12526 $dbo->setQuery($q);
12527 $dbo->execute();
12528 if ($dbo->getNumRows()) {
12529 $colors = json_decode($dbo->loadResult(), true);
12530 if (!is_array($colors) || !isset($colors['textcolor'])) {
12531 return $pref_colors;
12532 }
12533 return $colors;
12534 }
12535 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
12536 $dbo->setQuery($q);
12537 $dbo->execute();
12538 return $pref_colors;
12539 }
12540
12541 /**
12542 * Adds to the document inline styles for the preferred colors, if any.
12543 *
12544 * @return void
12545 *
12546 * @since 1.14 (J) - 1.4.0 (WP)
12547 */
12548 public static function loadPreferredColorStyles()
12549 {
12550 $view = VikRequest::getString('view', '', 'request');
12551 $pref_colors = self::getPreferredColors();
12552
12553 $css_classes = array();
12554
12555 if (!empty($pref_colors['textcolor'])) {
12556 // titles and headings
12557 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
12558 // stepbar, oconfirm
12559 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; }');
12560 // buttons secondary color
12561 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
12562 if (!empty($pref_colors['fontcolor'])) {
12563 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
12564 }
12565 // datepicker
12566 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
12567 color: ' . $pref_colors['textcolor'] . ' !important;
12568 border-color: ' . $pref_colors['textcolor'] . ' !important;
12569 }');
12570 array_push($css_classes, '.ui-datepicker .ui-datepicker-today a {
12571 color: ' . $pref_colors['textcolor'] . ' !important;
12572 }');
12573 // operators tableaux
12574 if ($view == 'tableaux') {
12575 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
12576 }
12577 }
12578
12579 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
12580 // elements with backgrounds
12581 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12582 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12583 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12584 // buttons with backgrounds
12585 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12586 // stepbar
12587 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12588 // datepicker
12589 array_push($css_classes, '.ui-datepicker table td:hover {
12590 border-color: ' . $pref_colors['bgcolor'] . ' !important;
12591 }');
12592 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day {
12593 background: ' . $pref_colors['bgcolor'] . ' !important;
12594 color: ' . $pref_colors['fontcolor'] . ' !important;
12595 }');
12596 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
12597 color: ' . $pref_colors['fontcolor'] . ' !important;
12598 }');
12599 // operators tableaux
12600 if ($view == 'tableaux') {
12601 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; }');
12602 }
12603 }
12604
12605 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
12606 // buttons with backgrounds during hover state
12607 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12608 // operators tableaux
12609 if ($view == 'tableaux') {
12610 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12611 }
12612 }
12613
12614 if (!count($css_classes)) {
12615 return;
12616 }
12617
12618 // add in-line style declaration
12619 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
12620 }
12621
12622 /**
12623 * Given the full endpoint URL for the AJAX request, it
12624 * returns an appropriate URI for the current platform.
12625 *
12626 * @param mixed $query The query string or a routed URL.
12627 * @param boolean $xhtml Replace & by &amp; for XML compliance.
12628 *
12629 * @return string The AJAX end-point URI.
12630 *
12631 * @since 1.14 (J) - 1.4.0 (WP)
12632 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
12633 */
12634 public static function ajaxUrl($query = '', $xhtml = false)
12635 {
12636 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
12637 }
12638
12639 /**
12640 * Tells whether no tax rates have been defined so far.
12641 *
12642 * @return bool True if no tax rates defined, false otherwise.
12643 *
12644 * @since 1.14 (J) - 1.4.0 (WP)
12645 */
12646 public static function noTaxRates()
12647 {
12648 $dbo = JFactory::getDbo();
12649
12650 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
12651 $dbo->setQuery($q);
12652 $dbo->execute();
12653
12654 return ($dbo->getNumRows() < 1);
12655 }
12656
12657 /**
12658 * Booking Type feature is strictly connected to VCM and OTAs. Updated
12659 * versions of VBO will always support it as long as VCM is installed.
12660 * Needed by VCM to understand whether certain SQL queries can be performed.
12661 *
12662 * @return bool true if VCM exists, false otherwise.
12663 *
12664 * @since 1.14 (J) - 1.4.0 (WP)
12665 */
12666 public static function isBookingTypeSupported()
12667 {
12668 return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12669 }
12670
12671 /**
12672 * Returns an instance of the Rates Flow helper class of VCM, if available.
12673 *
12674 * @return mixed VCMRatesFlow if VCM available and updated, or null.
12675 *
12676 * @since 1.15.0 (J) - 1.5.0 (WP)
12677 */
12678 public static function getRatesFlowInstance()
12679 {
12680 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
12681 if (!is_file($vcm_fpath)) {
12682 return null;
12683 }
12684
12685 // load dependencies
12686 require_once $vcm_fpath;
12687
12688 // return the instance of the class
12689 return VCMRatesFlow::getInstance();
12690 }
12691
12692 /**
12693 * Returns the current appearance preference.
12694 *
12695 * @return string light, auto or dark.
12696 *
12697 * @since 1.15.0 (J) - 1.5.0 (WP)
12698 */
12699 public static function getAppearancePref()
12700 {
12701 static $preference = null;
12702
12703 if ($preference !== null) {
12704 return $preference;
12705 }
12706
12707 // auto is the default appearance preference
12708 $default_pref = 'auto';
12709
12710 // accepted preferences
12711 $valid_pref = [
12712 'auto',
12713 'light',
12714 'dark',
12715 ];
12716
12717 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
12718 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
12719
12720 return $preference;
12721 }
12722
12723 /**
12724 * According to the appearance preferences, the apposite
12725 * CSS assets are loaded within the document.
12726 *
12727 * @param bool $get_info true to not load any assets.
12728 *
12729 * @return mixed string light, auto, dark, info array or false.
12730 *
12731 * @since 1.15.0 (J) - 1.5.0 (WP)
12732 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
12733 */
12734 public static function loadAppearancePreferenceAssets($get_info = false)
12735 {
12736 // get document object
12737 $document = JFactory::getDocument();
12738
12739 // load current color scheme preference
12740 $current_pref = self::getAppearancePref();
12741
12742 // define caching values
12743 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
12744
12745 // define file attributes
12746 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
12747
12748 // apposite file path and URI for back-end
12749 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
12750 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
12751
12752 // check if this is for the front-end ("auto" file only)
12753 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
12754 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
12755 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
12756 // check if front-end appearance is disabled
12757 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
12758 // do nothing for the front-end by default
12759 return false;
12760 }
12761 }
12762
12763 if (VBOPlatformDetection::isJoomla()) {
12764 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
12765 $css_uri = str_replace('resources/', '', $css_uri);
12766 }
12767
12768 if (!is_file($css_path)) {
12769 // this preference does not require a specific stylesheet
12770 return false;
12771 }
12772
12773 if ($get_info) {
12774 // do not actually load any assets
12775 return [
12776 'id' => $file_attr['id'],
12777 'href' => $css_uri,
12778 ];
12779 }
12780
12781 // load the apposite CSS file
12782 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
12783
12784 return $current_pref;
12785 }
12786
12787 /**
12788 * VBO trigger for VCM subscription status and updates. If called within the admin section
12789 * returns the expiration reminder array to display a modal window, and sets a warning
12790 * message. If called within the front-end, may trigger an email alert.
12791 *
12792 * @return mixed null, array or boolean depending on client position.
12793 *
12794 * @since 1.15.0 (J) - 1.5.0 (WP)
12795 */
12796 public static function getVCMSubscriptionStatus()
12797 {
12798 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
12799 if (!is_file($vcm_lib_path)) {
12800 // do nothing
12801 return null;
12802 }
12803
12804 // determine client position
12805 $is_admin = self::isAdmin();
12806
12807 try {
12808 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
12809 // VCM is outdated
12810 return null;
12811 }
12812
12813 // always attempt to load the admin lang handler of VCM
12814 $lang = JFactory::getLanguage();
12815
12816 // load the VCM admin language file
12817 $vcm_admin_lang_path = '';
12818 if (VBOPlatformDetection::isJoomla()) {
12819 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
12820 /**
12821 * @joomlaonly if not defined VCM Software Version constant, include the defines files.
12822 */
12823 if (!defined('VIKCHANNELMANAGER_SOFTWARE_VERSION')) {
12824 include_once VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'adapter' . DIRECTORY_SEPARATOR . 'defines.php';
12825 }
12826 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
12827 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
12828 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
12829 /**
12830 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
12831 */
12832 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
12833 }
12834 // load translation file
12835 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
12836 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
12837 /**
12838 * @wponly load language admin handler as well for WP.
12839 * We do this only because of WordPress, but in a way also compatible with Joomla as
12840 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
12841 */
12842 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
12843 }
12844
12845 if ($is_admin) {
12846 // back-end section of the site: check status
12847 if (VikChannelManager::loadExpirationDetails() === false) {
12848 // download and update the expiration details
12849 VikChannelManager::downloadExpirationDetails();
12850 }
12851 $expiration_reminder = null;
12852 if (VikChannelManager::isSubscriptionExpired()) {
12853 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
12854 } else {
12855 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
12856 // we also check if an update is available
12857 if (VikChannelManager::checkUpdates()) {
12858 // an update is more important
12859 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
12860 exit;
12861 }
12862 }
12863 // return reminder status
12864 return $expiration_reminder;
12865 }
12866
12867 // front-end section of the site: trigger alert, if any (bool)
12868 return VikChannelManager::checkSubscriptionReminder();
12869
12870 } catch (Exception $e) {
12871 // do nothing
12872 }
12873
12874 return null;
12875 }
12876
12877 /**
12878 * Given a list of website rate plans, returns the sorted list.
12879 * Naming score is the highest sorting method, pricing scoring is also supported.
12880 *
12881 * @param array $rate_plans the list of rate plan arrays to sort.
12882 * @param bool $assoc whether to keep the original array keys.
12883 *
12884 * @return array the sorted list of rate plan arrays.
12885 *
12886 * @since 1.15.0 (J) - 1.5.0 (WP)
12887 */
12888 public static function sortRatePlans($rate_plans, $assoc = false)
12889 {
12890 if (!is_array($rate_plans) || count($rate_plans) < 2) {
12891 return $rate_plans;
12892 }
12893
12894 foreach ($rate_plans as $rp_ind => $rate_plan) {
12895 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
12896 // do not proceed in case of a weird array structure
12897 return $rate_plans;
12898 }
12899 // count the sorting-score of this rate plan
12900 $sort_score = 0;
12901 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
12902 // "standard rate" rate plans should go first
12903 $sort_score += 3;
12904 } else {
12905 $sort_score--;
12906 }
12907 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
12908 // this doesn't seem to be a "non refundable rate" so it should go first
12909 $sort_score += 2;
12910 }
12911
12912 // set sorting score for this rate plan ID
12913 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
12914 }
12915
12916 // apply custom sorting and keep index association
12917 uasort($rate_plans, function($a, $b) {
12918 // sort by name-scoring DESC
12919 $sort_diff = $b['sort_score'] - $a['sort_score'];
12920 if (isset($b['cost'])) {
12921 // apply also sorting by price ASC
12922 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
12923 }
12924 return $sort_diff;
12925 });
12926
12927 return !$assoc ? array_values($rate_plans) : $rate_plans;
12928 }
12929
12930 /**
12931 * Returns an instance of the Availability helper class.
12932 *
12933 * @return VikBookingAvailability
12934 *
12935 * @since 1.15.0 (J) - 1.5.0 (WP)
12936 */
12937 public static function getAvailabilityInstance()
12938 {
12939 // require library
12940 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
12941
12942 // return the instance of the class
12943 return VikBookingAvailability::getInstance();
12944 }
12945
12946 /**
12947 * Helper method to quickly guess the internal dependency to import.
12948 *
12949 * @param string $path_id either a full path to a file or its identifier.
12950 *
12951 * @return bool true if the requested library was imported or false.
12952 *
12953 * @since 1.15.0 (J) - 1.5.0 (WP)
12954 */
12955 public static function import($path_id)
12956 {
12957 if (empty($path_id)) {
12958 return false;
12959 }
12960
12961 // always append .php extension
12962 $path_id = basename($path_id, '.php') . '.php';
12963
12964 if (is_file($path_id)) {
12965 // full path given
12966 require_once $path_id;
12967
12968 return true;
12969 }
12970
12971 // try guessing where the file id is located
12972 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12973 // library was found
12974 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12975
12976 return true;
12977 }
12978
12979 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12980 // library was found
12981 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12982
12983 return true;
12984 }
12985
12986 return false;
12987 }
12988 }
12989
12990 if (!class_exists('VikResizer'))
12991 {
12992 class VikResizer
12993 {
12994 public function __construct()
12995 {
12996 // objects of this class can also be instantiated without calling the methods statically.
12997 }
12998
12999 /**
13000 * Resizes an image proportionally. For PNG files it can optionally
13001 * trim the image to exclude the transparency, and add some padding to it.
13002 * All PNG files keep the alpha background in the resized version.
13003 *
13004 * @param string $fileimg path to original image file
13005 * @param string $dest path to destination image file
13006 * @param int $towidth
13007 * @param int $toheight
13008 * @param bool $trim_png remove empty background from image
13009 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13010 *
13011 * @return boolean
13012 */
13013 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
13014 {
13015 if (!is_file($fileimg)) {
13016 return false;
13017 }
13018 if (empty($towidth) && empty($toheight)) {
13019 return copy($fileimg, $dest);
13020 }
13021
13022 $result = true;
13023
13024 list ($owid, $ohei, $type) = getimagesize($fileimg);
13025
13026 if ($owid > $towidth || $ohei > $toheight) {
13027 $xscale = $owid / $towidth;
13028 $yscale = $ohei / $toheight;
13029 if ($yscale > $xscale) {
13030 $new_width = round($owid * (1 / $yscale));
13031 $new_height = round($ohei * (1 / $yscale));
13032 } else {
13033 $new_width = round($owid * (1 / $xscale));
13034 $new_height = round($ohei * (1 / $xscale));
13035 }
13036
13037 $imageresized = imagecreatetruecolor($new_width, $new_height);
13038
13039 switch ($type) {
13040 case '1' :
13041 $imagetmp = imagecreatefromgif ($fileimg);
13042 break;
13043 case '2' :
13044 $imagetmp = imagecreatefromjpeg($fileimg);
13045 break;
13046 case '18' :
13047 $imagetmp = imagecreatefromwebp($fileimg);
13048 break;
13049 default :
13050 //keep alpha for PNG files
13051 $background = imagecolorallocate($imageresized, 0, 0, 0);
13052 imagecolortransparent($imageresized, $background);
13053 imagealphablending($imageresized, false);
13054 imagesavealpha($imageresized, true);
13055 //
13056 $imagetmp = imagecreatefrompng($fileimg);
13057 break;
13058 }
13059
13060 if (!$imagetmp) {
13061 return false;
13062 }
13063
13064 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13065
13066 switch ($type) {
13067 case '1' :
13068 imagegif($imageresized, $dest);
13069 break;
13070 case '2' :
13071 imagejpeg($imageresized, $dest);
13072 break;
13073 case '18' :
13074 imagewebp($imageresized, $dest);
13075 break;
13076 default :
13077 if ($trim_png) {
13078 self::imageTrim($imageresized, $background, $trim_pad);
13079 }
13080 imagepng($imageresized, $dest);
13081 break;
13082 }
13083
13084 imagedestroy($imageresized);
13085 } else {
13086 $result = copy($fileimg, $dest);
13087 }
13088
13089 if (VBOPlatformDetection::isWordPress()) {
13090 /**
13091 * @wponly in order to not lose resized files after installing an update,
13092 * we need to move any uploaded file onto a recovery folder.
13093 */
13094 VikBookingLoader::import('update.manager');
13095 VikBookingUpdateManager::triggerUploadBackup($dest);
13096 }
13097
13098 return $result;
13099 }
13100
13101 /**
13102 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13103 * trim the image to exclude the transparency, and add some padding to it.
13104 * All PNG files keep the alpha background in the resized version.
13105 *
13106 * @param resource $im Image link resource (reference)
13107 * @param int $bg imagecolorallocate color identifier
13108 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13109 *
13110 * @return void
13111 */
13112 public static function imagetrim(&$im, $bg, $pad = null)
13113 {
13114 // Calculate padding for each side.
13115 if (isset($pad)) {
13116 $pp = explode(' ', $pad);
13117 if (isset($pp[3])) {
13118 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13119 } elseif (isset($pp[2])) {
13120 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13121 } elseif (isset($pp[1])) {
13122 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13123 } else {
13124 $p = array_fill(0, 4, (int) $pp[0]);
13125 }
13126 } else {
13127 $p = array_fill(0, 4, 0);
13128 }
13129
13130 // Get the image width and height.
13131 $imw = imagesx($im);
13132 $imh = imagesy($im);
13133
13134 // Set the X variables.
13135 $xmin = $imw;
13136 $xmax = 0;
13137
13138 // Start scanning for the edges.
13139 for ($iy=0; $iy<$imh; $iy++) {
13140 $first = true;
13141 for ($ix=0; $ix<$imw; $ix++) {
13142 $ndx = imagecolorat($im, $ix, $iy);
13143 if ($ndx != $bg) {
13144 if ($xmin > $ix) {
13145 $xmin = $ix;
13146 }
13147 if ($xmax < $ix) {
13148 $xmax = $ix;
13149 }
13150 if (!isset($ymin)) {
13151 $ymin = $iy;
13152 }
13153 $ymax = $iy;
13154 if ($first) {
13155 $ix = $xmax;
13156 $first = false;
13157 }
13158 }
13159 }
13160 }
13161
13162 // The new width and height of the image. (not including padding)
13163 $imw = 1+$xmax-$xmin; // Image width in pixels
13164 $imh = 1+$ymax-$ymin; // Image height in pixels
13165
13166 // Make another image to place the trimmed version in.
13167 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13168
13169 // Make the background of the new image the same as the background of the old one.
13170 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13171 imagefill($im2, 0, 0, $bg2);
13172
13173 // Copy it over to the new image.
13174 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13175
13176 // To finish up, we replace the old image which is referenced.
13177 $im = $im2;
13178 }
13179
13180 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13181 {
13182 if (!file_exists($fileimg)) {
13183 return false;
13184 }
13185 if (empty($towidth) && empty($toheight)) {
13186 copy($fileimg, $dest);
13187 return true;
13188 }
13189
13190 $exp = explode(",", $rgb);
13191 if (count($exp) == 3) {
13192 $r = trim($exp[0]);
13193 $g = trim($exp[1]);
13194 $b = trim($exp[2]);
13195 } else {
13196 $r = 0;
13197 $g = 0;
13198 $b = 0;
13199 }
13200
13201 list ($owid, $ohei, $type) = getimagesize($fileimg);
13202
13203 if ($owid > $towidth || $ohei > $toheight) {
13204 $xscale = $owid / $towidth;
13205 $yscale = $ohei / $toheight;
13206 if ($yscale > $xscale) {
13207 $new_width = round($owid * (1 / $yscale));
13208 $new_height = round($ohei * (1 / $yscale));
13209 $ydest = 0;
13210 $diff = $towidth - $new_width;
13211 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13212 } else {
13213 $new_width = round($owid * (1 / $xscale));
13214 $new_height = round($ohei * (1 / $xscale));
13215 $xdest = 0;
13216 $diff = $toheight - $new_height;
13217 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13218 }
13219
13220 $imageresized = imagecreatetruecolor($towidth, $toheight);
13221
13222 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13223 imagefill($imageresized, 0, 0, $bgColor);
13224
13225 switch ($type) {
13226 case '1' :
13227 $imagetmp = imagecreatefromgif ($fileimg);
13228 break;
13229 case '2' :
13230 $imagetmp = imagecreatefromjpeg($fileimg);
13231 break;
13232 default :
13233 $imagetmp = imagecreatefrompng($fileimg);
13234 break;
13235 }
13236
13237 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13238
13239 switch ($type) {
13240 case '1' :
13241 imagegif ($imageresized, $dest);
13242 break;
13243 case '2' :
13244 imagejpeg($imageresized, $dest);
13245 break;
13246 default :
13247 imagepng($imageresized, $dest);
13248 break;
13249 }
13250
13251 imagedestroy($imageresized);
13252
13253 return true;
13254 } else {
13255 copy($fileimg, $dest);
13256 }
13257 return true;
13258 }
13259
13260 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13261 {
13262 if (!file_exists($fileimg)) {
13263 return false;
13264 }
13265 if (empty($towidth) && empty($toheight)) {
13266 copy($fileimg, $dest);
13267 return true;
13268 }
13269
13270 list ($owid, $ohei, $type) = getimagesize($fileimg);
13271
13272 if ($owid <= $ohei) {
13273 $new_width = $towidth;
13274 $new_height = ($towidth / $owid) * $ohei;
13275 } else {
13276 $new_height = $toheight;
13277 $new_width = ($new_height / $ohei) * $owid;
13278 }
13279
13280 switch ($type) {
13281 case '1' :
13282 $img_src = imagecreatefromgif ($fileimg);
13283 $img_dest = imagecreate($new_width, $new_height);
13284 break;
13285 case '2' :
13286 $img_src = imagecreatefromjpeg($fileimg);
13287 $img_dest = imagecreatetruecolor($new_width, $new_height);
13288 break;
13289 default :
13290 $img_src = imagecreatefrompng($fileimg);
13291 $img_dest = imagecreatetruecolor($new_width, $new_height);
13292 break;
13293 }
13294
13295 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13296
13297 switch ($type) {
13298 case '1' :
13299 $cropped = imagecreate($towidth, $toheight);
13300 break;
13301 case '2' :
13302 $cropped = imagecreatetruecolor($towidth, $toheight);
13303 break;
13304 default :
13305 $cropped = imagecreatetruecolor($towidth, $toheight);
13306 break;
13307 }
13308
13309 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
13310
13311 switch ($type) {
13312 case '1' :
13313 imagegif ($cropped, $dest);
13314 break;
13315 case '2' :
13316 imagejpeg($cropped, $dest);
13317 break;
13318 default :
13319 imagepng($cropped, $dest);
13320 break;
13321 }
13322
13323 imagedestroy($img_dest);
13324 imagedestroy($cropped);
13325
13326 return true;
13327 }
13328 }
13329 }
13330