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

13,312 lines 449.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 if (!class_exists('VikBookingIcons')) {
14 // require the Icons class
15 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "icons.php");
16 }
17
18 if (!function_exists('showSelectVb')) {
19 function showSelectVb($err, $err_code_info = array()) {
20 include(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'error_form.php');
21 }
22 }
23
24 class VikBooking
25 {
26 public static function addJoomlaUser($name, $username, $email, $password) {
27 //new method
28 jimport('joomla.application.component.helper');
29 $params = JComponentHelper::getParams('com_users');
30 $user = new JUser;
31 $data = array();
32 //Get the default new user group, Registered if not specified.
33 $system = $params->get('new_usertype', 2);
34 $data['groups'] = array();
35 $data['groups'][] = $system;
36 $data['name'] = $name;
37 $data['username'] = $username;
38 $data['email'] = self::getVboApplication()->emailToPunycode($email);
39 $data['password'] = $password;
40 $data['password2'] = $password;
41 $data['sendEmail'] = 0; //should the user receive system mails?
42 //$data['block'] = 0;
43 if (!$user->bind($data)) {
44 VikError::raiseWarning('', JText::translate($user->getError()));
45 return false;
46 }
47 if (!$user->save()) {
48 VikError::raiseWarning('', JText::translate($user->getError()));
49 return false;
50 }
51 return $user->id;
52 }
53
54 public static function userIsLogged() {
55 $user = JFactory::getUser();
56
57 return !$user->guest;
58 }
59
60 public static function prepareViewContent()
61 {
62 /**
63 * @wponly JApplication::getMenu() cannot be adapted to WP
64 */
65 }
66
67 public static function isFontAwesomeEnabled()
68 {
69 return VBOFactory::getConfig()->getBool('usefa', true);
70 }
71
72 public static function loadFontAwesome($force_load = false)
73 {
74 if (!self::isFontAwesomeEnabled() && !$force_load) {
75 return false;
76 }
77
78 /**
79 * We let the class VikBookingIcons load the proper FontAwesome libraries.
80 *
81 * @since 1.11
82 */
83 VikBookingIcons::loadAssets();
84
85 return true;
86 }
87
88 /**
89 * Checks if modifications or cancellations via front-end are allowed.
90 * 0 = everything is Disabled.
91 * 1 = Disabled, with request message (default).
92 * 2 = Modification Enabled, Cancellation Disabled.
93 * 3 = Cancellation Enabled, Modification Disabled.
94 * 4 = everything is Enabled.
95 *
96 * @return int
97 */
98 public static function getReservationModCanc()
99 {
100 return VBOFactory::getConfig()->getInt('resmodcanc', 1);
101 }
102
103 public static function getReservationModCancMin()
104 {
105 return VBOFactory::getConfig()->getInt('resmodcancmin', 1);
106 }
107
108 public static function getDefaultDistinctiveFeatures()
109 {
110 return [
111 'VBODEFAULTDISTFEATUREONE' => '',
112 // Below is the default feature for 'Room Code'. One default feature is sufficient
113 // 'VBODEFAULTDISTFEATURETWO' => '',
114 ];
115 }
116
117 /**
118 * Given the room's parameters and index, tries to take the first distinctive feature.
119 *
120 * @param mixed $rparams string to be decoded, or decoded array/object params
121 * @param int $rindex room index to look for, starting from 1.
122 *
123 * @return mixed false on failure, array otherwise [feature name, feature value]
124 *
125 * @since 1.14 (J) - 1.4.0 (WP)
126 */
127 public static function getRoomUnitDistinctiveFeature($rparams, $rindex) {
128 $rindex = (int)$rindex;
129 if ($rindex < 1 || empty($rparams)) {
130 return false;
131 }
132
133 if (is_string($rparams)) {
134 // decode params
135 $rparams = self::getRoomParam('features', $rparams);
136 }
137
138 if (is_object($rparams)) {
139 // typecast to array
140 $rparams = (array)$rparams;
141 }
142
143 if (!is_array($rparams) || !count($rparams)) {
144 return false;
145 }
146
147 if (isset($rparams['features'])) {
148 $rparams = $rparams['features'];
149 }
150
151 $feature = array();
152 foreach ($rparams as $param_index => $rfeatures) {
153 if ((int)$param_index != $rindex || !is_array($rfeatures) || !count($rfeatures)) {
154 continue;
155 }
156 foreach ($rfeatures as $featname => $featval) {
157 if (empty($featval)) {
158 continue;
159 }
160 // use the first distinctive feature
161 $tn_featname = JText::translate($featname);
162 if ($tn_featname == $featname) {
163 // no translation was applied
164 if (VBOPlatformDetection::isWordPress()) {
165 // try to apply a translation through Gettext even if we have to pass a variable
166 $tn_featname = __($featname);
167 } else {
168 // convert the string to a hypothetical INI constant
169 $ini_constant = str_replace(' ', '_', strtoupper($featname));
170 $tn_featname = JText::translate($ini_constant);
171 $tn_featname = $tn_featname == $ini_constant ? $featname : $tn_featname;
172 }
173 }
174 // store values and break loop
175 $feature = array($tn_featname, $featval);
176 break;
177 }
178 }
179
180 return count($feature) ? $feature : false;
181 }
182
183 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 public static function vcmBcomReportingSupported() {
1209 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1210 return false;
1211 }
1212 $dbo = JFactory::getDbo();
1213 $q = "SELECT `id` FROM `#__vikchannelmanager_channel` WHERE `uniquekey`='4';";
1214 $dbo->setQuery($q);
1215 $dbo->execute();
1216 return ($dbo->getNumRows() > 0);
1217 }
1218
1219 /**
1220 * Gets a list of all channels supporting the promotions.
1221 *
1222 * @param string $key the key of the handler.
1223 *
1224 * @return mixed false if VCM is not installed, empty array otherwise.
1225 *
1226 * @requires Vik Channel Manager 1.7.1
1227 *
1228 * @since 1.3.0
1229 */
1230 public static function getPromotionHandlers($key = null)
1231 {
1232 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1233 // VCM not installed
1234 return false;
1235 }
1236
1237 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionHandlers')) {
1238 // VCM is outdated yet installed: an empty array is sufficient to not display warning messages
1239 return [];
1240 }
1241
1242 return VikChannelManager::getPromotionHandlers($key);
1243 }
1244
1245 /**
1246 * Gets the factors for suggesting the application of the promotions.
1247 *
1248 * @param mixed $data some optional instructions to be passed as argument.
1249 *
1250 * @return mixed false if VCM is not installed, associative array otherwise.
1251 *
1252 * @requires Vik Channel Manager 1.7.1
1253 *
1254 * @since 1.3.0
1255 */
1256 public static function getPromotionFactors($data = null)
1257 {
1258 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
1259 // VCM not installed
1260 return false;
1261 }
1262
1263 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'getPromotionFactors')) {
1264 // VCM is outdated
1265 return false;
1266 }
1267
1268 return VikChannelManager::getPromotionFactors($data);
1269 }
1270
1271 public static function getTheme()
1272 {
1273 return VBOFactory::getConfig()->get('theme', '');
1274 }
1275
1276 public static function getFooterOrdMail($vbo_tn = null)
1277 {
1278 $dbo = JFactory::getDbo();
1279
1280 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='footerordmail';";
1281 $dbo->setQuery($q);
1282 $ft = $dbo->loadAssocList();
1283
1284 if (!is_object($vbo_tn)) {
1285 $vbo_tn = self::getTranslator();
1286 }
1287
1288 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
1289
1290 return $ft ? $ft[0]['setting'] : '';
1291 }
1292
1293 public static function requireLogin()
1294 {
1295 return VBOFactory::getConfig()->getBool('requirelogin', false);
1296 }
1297
1298 public static function autoRoomUnit()
1299 {
1300 return VBOFactory::getConfig()->getBool('autoroomunit', false);
1301 }
1302
1303 public static function todayBookings()
1304 {
1305 static $todayBookings = null;
1306
1307 if ($todayBookings === null) {
1308 $todayBookings = VBOFactory::getConfig()->getBool('todaybookings', false);
1309 }
1310
1311 return $todayBookings;
1312 }
1313
1314 public static function couponsEnabled()
1315 {
1316 return VBOFactory::getConfig()->getBool('enablecoupons', false);
1317 }
1318
1319 public static function customersPinEnabled()
1320 {
1321 return VBOFactory::getConfig()->getBool('enablepin', false);
1322 }
1323
1324 /**
1325 * Detects the type of visitor from the user agent.
1326 * Known types are: computer, smartphone, tablet.
1327 *
1328 * @param boolean $returnua whether the type of visitor should be returned. If false
1329 * boolean is returned in case of mobile device detected.
1330 * @param boolean $loadassets whether the system should load an apposite CSS file.
1331 *
1332 * @return mixed string for the type of visitor or boolean if mobile detected.
1333 *
1334 * @since 1.0.13 - Revision September 2018
1335 */
1336 public static function detectUserAgent($returnua = false, $loadassets = true) {
1337 $session = JFactory::getSession();
1338 $sval = $session->get('vbuseragent', '');
1339 $mobiles = array('tablet', 'smartphone');
1340 if (!empty($sval)) {
1341 if ($loadassets) {
1342 self::userAgentStyleSheet($sval);
1343 }
1344 return $returnua ? $sval : in_array($sval, $mobiles);
1345 }
1346
1347 // detect visitor (device) type
1348 $visitoris = 'computer';
1349 try {
1350 if (!class_exists('MobileDetector')) {
1351 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "mobile_detector.php";
1352 }
1353 $detector = new MobileDetector;
1354 $visitoris = $detector->isMobile() ? ($detector->isTablet() ? 'tablet' : 'smartphone') : 'computer';
1355 } catch (Exception $e) {
1356 // do nothing
1357 }
1358
1359 $session->set('vbuseragent', $visitoris);
1360 if ($loadassets) {
1361 self::userAgentStyleSheet($visitoris);
1362 }
1363
1364 return $returnua ? $visitoris : in_array($visitoris, $mobiles);
1365 }
1366
1367 public static function userAgentStyleSheet($ua)
1368 {
1369 /**
1370 * @wponly in order to not interfere with AJAX requests, we do nothing if doing AJAX.
1371 */
1372 if (function_exists('wp_doing_ajax') && wp_doing_ajax()) {
1373 return;
1374 }
1375 //
1376
1377 $document = JFactory::getDocument();
1378 /**
1379 * @wponly the following CSS files are located in /site/resources/ for WP, not just on /site
1380 */
1381 if ($ua == 'smartphone') {
1382 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_smartphones.css');
1383 } elseif ($ua == 'tablet') {
1384 $document->addStyleSheet(VBO_SITE_URI.'resources/vikbooking_tablets.css');
1385 }
1386 return true;
1387 }
1388
1389 public static function loadJquery()
1390 {
1391 return VBOFactory::getConfig()->getBool('loadjquery', true);
1392 }
1393
1394 public static function loadBootstrap()
1395 {
1396 return VBOFactory::getConfig()->getBool('bootstrap', true);
1397 }
1398
1399 public static function allowMultiLanguage()
1400 {
1401 return VBOFactory::getConfig()->getBool('multilang', true);
1402 }
1403
1404 public static function getTranslator()
1405 {
1406 if (!class_exists('VikBookingTranslator')) {
1407 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'translator.php');
1408 }
1409
1410 return new VikBookingTranslator();
1411 }
1412
1413 /**
1414 * New method's name spelled correctly. The object will be cached by default.
1415 *
1416 * @return VikBookingCustomersPin
1417 *
1418 * @since 1.15.5 (J) - 1.5.11 (WP) cache the instance of the object to better support new cookies set.
1419 */
1420 public static function getCPinInstance()
1421 {
1422 static $cpin_instance = null;
1423
1424 if (!$cpin_instance) {
1425 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'cpin.php';
1426
1427 $cpin_instance = new VikBookingCustomersPin();
1428 }
1429
1430 return $cpin_instance;
1431 }
1432
1433 /**
1434 * Old method's name spelled wrongly for BC.
1435 *
1436 * @return VikBookingCustomersPin
1437 */
1438 public static function getCPinIstance()
1439 {
1440 return self::getCPinInstance();
1441 }
1442
1443 /**
1444 * Returns an instance of the VikBookingTracker Class.
1445 * It is also possible to call this method to just require the library.
1446 * This is useful for the back-end to access some static methods
1447 * without tracking any data.
1448 *
1449 * @param boolean $require_only whether to return the object.
1450 *
1451 * @return VikBookingTracker
1452 *
1453 * @since 1.11
1454 */
1455 public static function getTracker($require_only = false) {
1456 if (!class_exists('VikBookingTracker')) {
1457 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tracker.php");
1458 }
1459 return $require_only ? true : VikBookingTracker::getInstance();
1460 }
1461
1462 /**
1463 * Returns an instance of the VikBookingOperator Class.
1464 *
1465 * @return VikBookingOperator
1466 *
1467 * @since 1.11
1468 */
1469 public static function getOperatorInstance() {
1470 if (!class_exists('VikBookingOperator')) {
1471 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "operator.php");
1472 }
1473 return VikBookingOperator::getInstance();
1474 }
1475
1476 /**
1477 * Returns an instance of the VikBookingFestivities Class.
1478 *
1479 * @return VikBookingFestivities
1480 *
1481 * @since 1.12
1482 */
1483 public static function getFestivitiesInstance() {
1484 if (!class_exists('VikBookingFestivities')) {
1485 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'festivities.php');
1486 }
1487 return VikBookingFestivities::getInstance();
1488 }
1489
1490 /**
1491 * Returns an instance of the VikBookingCriticalDates Class.
1492 *
1493 * @return VikBookingCriticalDates
1494 *
1495 * @since 1.13.5
1496 */
1497 public static function getCriticalDatesInstance() {
1498 if (!class_exists('VikBookingCriticalDates')) {
1499 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'critical_dates.php');
1500 }
1501 return VikBookingCriticalDates::getInstance();
1502 }
1503
1504 /**
1505 * Checks whether the chat is enabled.
1506 *
1507 * @return int -1 if VCM is not installed, 0 if disabled, 1 otherwise
1508 *
1509 * @since 1.12
1510 */
1511 public static function chatEnabled() {
1512 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1513 return -1;
1514 }
1515 $dbo = JFactory::getDbo();
1516 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatenabled';";
1517 $dbo->setQuery($q);
1518 $dbo->execute();
1519 if (!$dbo->getNumRows()) {
1520 // enable the chat by default if VCM is installed
1521 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatenabled', '1');";
1522 $dbo->setQuery($q);
1523 $dbo->execute();
1524 return 1;
1525 }
1526 $s = $dbo->loadResult();
1527 return intval($s);
1528 }
1529
1530 /**
1531 * Loads the chat parameters from the configuration.
1532 *
1533 * @return object stdClass object from decoded JSON string
1534 *
1535 * @since 1.12
1536 */
1537 public static function getChatParams() {
1538 if (!is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
1539 return new stdClass;
1540 }
1541 $dbo = JFactory::getDbo();
1542 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='chatparams';";
1543 $dbo->setQuery($q);
1544 $dbo->execute();
1545 if (!$dbo->getNumRows()) {
1546 // compose default and basic chat params for the first time
1547 $basic_params = new stdClass;
1548 $basic_params->res_status = array('confirmed', 'standby', 'cancelled');
1549 $basic_params->av_type = 'checkin';
1550 $basic_params->av_days = 0;
1551
1552 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('chatparams', ".$dbo->quote(json_encode($basic_params)).");";
1553 $dbo->setQuery($q);
1554 $dbo->execute();
1555
1556 return $basic_params;
1557 }
1558 return json_decode($dbo->loadResult());
1559 }
1560
1561 /**
1562 * Checks whether the pre-checkin is enabled.
1563 *
1564 * @return int 0 if disabled, 1 otherwise
1565 *
1566 * @since 1.12
1567 */
1568 public static function precheckinEnabled() {
1569 $dbo = JFactory::getDbo();
1570 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinenabled';";
1571 $dbo->setQuery($q);
1572 $dbo->execute();
1573 if (!$dbo->getNumRows()) {
1574 // enable the pre-checkin by default
1575 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinenabled', '1');";
1576 $dbo->setQuery($q);
1577 $dbo->execute();
1578 return 1;
1579 }
1580 $s = $dbo->loadResult();
1581 return intval($s);
1582 }
1583
1584 /**
1585 * Returns the minimum number of days in advance to
1586 * enable the pre-checkin via front-end.
1587 *
1588 * @return int the min number of days in advance for pre-checkin.
1589 *
1590 * @since 1.12
1591 */
1592 public static function precheckinMinOffset() {
1593 $dbo = JFactory::getDbo();
1594 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='precheckinminoffset';";
1595 $dbo->setQuery($q);
1596 $dbo->execute();
1597 if (!$dbo->getNumRows()) {
1598 // set the limit to 1 day before arrival by default
1599 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('precheckinminoffset', '1');";
1600 $dbo->setQuery($q);
1601 $dbo->execute();
1602 return 1;
1603 }
1604 $s = $dbo->loadResult();
1605 return intval($s);
1606 }
1607
1608 /**
1609 * Whether upselling extra services is enabled.
1610 *
1611 * @return boolean true if enabled, false otherwise.
1612 *
1613 * @since 1.13
1614 */
1615 public static function upsellingEnabled() {
1616 $dbo = JFactory::getDbo();
1617 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='upselling';";
1618 $dbo->setQuery($q);
1619 $dbo->execute();
1620 if (!$dbo->getNumRows()) {
1621 // enable the upselling feature by default
1622 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES('upselling', '1');";
1623 $dbo->setQuery($q);
1624 $dbo->execute();
1625 return true;
1626 }
1627 return (intval($dbo->loadResult()) > 0);
1628 }
1629
1630 /**
1631 * Collects a list of options/extra that can be upsold for the
1632 * the rooms booked.
1633 *
1634 * @param array $upsell_data list of stdClass objects for each room booked.
1635 * @param array $info some booking details (id, checkin, checkout).
1636 * @param object $vbo_tn the translation object.
1637 *
1638 * @return array list of upsellable options for each room booked.
1639 *
1640 * @since 1.3.0
1641 */
1642 public static function loadUpsellingData($upsell_data, $info, $vbo_tn) {
1643 $dbo = JFactory::getDbo();
1644 // get all options for all rooms booked
1645 $all_room_ids = array();
1646 foreach ($upsell_data as $v) {
1647 if (!in_array($v->id, $all_room_ids)) {
1648 array_push($all_room_ids, $v->id);
1649 }
1650 }
1651 $q = "SELECT `id`, `idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $all_room_ids) . ");";
1652 $dbo->setQuery($q);
1653 $dbo->execute();
1654 if (!$dbo->getNumRows()) {
1655 // no rooms found
1656 return array();
1657 }
1658 $records = $dbo->loadAssocList();
1659 $all_options = array();
1660 $rooms_options = array();
1661 foreach ($records as $v) {
1662 $allopts = explode(';', $v['idopt']);
1663 $room_opt = array();
1664 foreach ($allopts as $o) {
1665 if (empty($o)) {
1666 continue;
1667 }
1668 if (!in_array((int)$o, $all_options)) {
1669 array_push($all_options, (int)$o);
1670 }
1671 array_push($room_opt, (int)$o);
1672 }
1673 $rooms_options[$v['id']] = $room_opt;
1674 }
1675 if (!count($all_options)) {
1676 // no options found
1677 return array();
1678 }
1679 // load all options that could be used by the booked rooms no matter what was already booked
1680 $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;";
1681 $dbo->setQuery($q);
1682 $dbo->execute();
1683 if (!$dbo->getNumRows()) {
1684 // no upsell-able options found
1685 return array();
1686 }
1687 $records = $dbo->loadAssocList();
1688 // filter options by available date and translate records
1689 self::filterOptionalsByDate($records, $info['checkin'], $info['checkout']);
1690 $vbo_tn->translateContents($records, '#__vikbooking_optionals');
1691 $records = !is_array($records) ? array() : $records;
1692 //
1693 $tot_upsellable = 0;
1694 foreach ($upsell_data as $k => $rdata) {
1695 if (!isset($upsell_data[$k]->upsellable)) {
1696 $upsell_data[$k]->upsellable = array();
1697 }
1698 foreach ($records as $opt) {
1699 if (!empty($opt['ageintervals'])) {
1700 // upsellable options should not contain age intervals for children
1701 continue;
1702 }
1703 if (!in_array($opt['id'], $rooms_options[$rdata->id])) {
1704 // this option is not assigned to this room
1705 continue;
1706 }
1707 // check if the option is suited for this room party
1708 $clone_opt = array($opt);
1709 self::filterOptionalsByParty($clone_opt, $rdata->adults, $rdata->children);
1710 if (!is_array($clone_opt) || !count($clone_opt)) {
1711 // this option is not suited for this room party
1712 continue;
1713 }
1714 //
1715 if (in_array($opt['id'], $rdata->options)) {
1716 // this option has already been booked
1717 continue;
1718 }
1719 // push this option and increase counter
1720 array_push($upsell_data[$k]->upsellable, $opt);
1721 $tot_upsellable++;
1722 }
1723 }
1724
1725 // if no upsellable options were found, we return an empty array
1726 return $tot_upsellable > 0 ? $upsell_data : array();
1727 }
1728
1729 /**
1730 * Returns the minimum days in advance for booking, by considering by default
1731 * also the property closing dates. If the property is currently closed, then
1732 * the minimum number of days in advance will be increased.
1733 *
1734 * @param bool $no_closing_dates whether to skip checking closing dates.
1735 *
1736 * @return int the number of days in advance for booking.
1737 *
1738 * @since 1.14 (J) - 1.4.0 (WP) the signature was changed from:
1739 * getMinDaysAdvance($skipsession = false) to:
1740 * getMinDaysAdvance($no_closing_dates = false)
1741 * This was made to return the proper number of days in advance
1742 * in case the property is currently closed.
1743 */
1744 public static function getMinDaysAdvance($no_closing_dates = false) {
1745 // cache value in static var
1746 static $getMinDaysAdvance = null;
1747
1748 if ($getMinDaysAdvance) {
1749 return $getMinDaysAdvance;
1750 }
1751 //
1752
1753 $dbo = JFactory::getDbo();
1754
1755 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='mindaysadvance';";
1756 $dbo->setQuery($q);
1757 $dbo->execute();
1758 $mind = $dbo->getNumRows() ? (int)$dbo->loadResult() : 0;
1759
1760 // update cached var
1761 $getMinDaysAdvance = $mind;
1762
1763 if ($no_closing_dates) {
1764 // do not check the closing dates
1765 return $mind;
1766 }
1767
1768 // check if the property is currently closed
1769 $cur_closed_dates = self::getClosingDates();
1770 if (is_array($cur_closed_dates) && count($cur_closed_dates)) {
1771 $today_midnight = mktime(0, 0, 0);
1772 $closed_until = null;
1773 foreach ($cur_closed_dates as $kcd => $vcd) {
1774 if ($today_midnight >= $vcd['from'] && $today_midnight <= $vcd['to']) {
1775 // closing period found
1776 $closed_until = $vcd['to'];
1777 break;
1778 }
1779 }
1780 if ($closed_until !== null) {
1781 // count the number of days until property is closed
1782 $mind = 0;
1783 $today_info = getdate($today_midnight);
1784 while ($today_info[0] <= $closed_until) {
1785 $mind++;
1786 $today_info = getdate(mktime(0, 0, 0, $today_info['mon'], ($today_info['mday'] + 1), $today_info['year']));
1787 }
1788 }
1789 }
1790
1791 // update cached var
1792 $getMinDaysAdvance = $mind;
1793
1794 return $mind;
1795 }
1796
1797 public static function getDefaultNightsCalendar($skipsession = false) {
1798 if ($skipsession) {
1799 $dbo = JFactory::getDbo();
1800 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1801 $dbo->setQuery($q);
1802 $dbo->execute();
1803 $s = $dbo->loadAssocList();
1804 return (int)$s[0]['setting'];
1805 } else {
1806 $session = JFactory::getSession();
1807 $sval = $session->get('vbdefaultNightsCalendar', '');
1808 if (!empty($sval)) {
1809 return (int)$sval;
1810 } else {
1811 $dbo = JFactory::getDbo();
1812 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='autodefcalnights';";
1813 $dbo->setQuery($q);
1814 $dbo->execute();
1815 $s = $dbo->loadAssocList();
1816 $session->set('vbdefaultNightsCalendar', $s[0]['setting']);
1817 return (int)$s[0]['setting'];
1818 }
1819 }
1820 }
1821
1822 public static function getSearchNumRooms($skipsession = true)
1823 {
1824 $dbo = JFactory::getDbo();
1825 if ($skipsession) {
1826 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1827 $dbo->setQuery($q);
1828 $dbo->execute();
1829 $s = $dbo->loadAssocList();
1830 return (int)$s[0]['setting'];
1831 }
1832 $session = JFactory::getSession();
1833 $sval = $session->get('vbsearchNumRooms', '');
1834 if (!empty($sval)) {
1835 return (int)$sval;
1836 }
1837 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numrooms';";
1838 $dbo->setQuery($q);
1839 $dbo->execute();
1840 $s = $dbo->loadAssocList();
1841 $session->set('vbsearchNumRooms', $s[0]['setting']);
1842 return (int)$s[0]['setting'];
1843 }
1844
1845 public static function getSearchNumAdults($skipsession = true)
1846 {
1847 $dbo = JFactory::getDbo();
1848 if ($skipsession) {
1849 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1850 $dbo->setQuery($q);
1851 $dbo->execute();
1852 $s = $dbo->loadAssocList();
1853 return $s[0]['setting'];
1854 }
1855 $session = JFactory::getSession();
1856 $sval = $session->get('vbsearchNumAdults', '');
1857 if (!empty($sval)) {
1858 return $sval;
1859 }
1860 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numadults';";
1861 $dbo->setQuery($q);
1862 $dbo->execute();
1863 $s = $dbo->loadAssocList();
1864 $session->set('vbsearchNumAdults', $s[0]['setting']);
1865 return $s[0]['setting'];
1866 }
1867
1868 public static function getSearchNumChildren($skipsession = true)
1869 {
1870 $dbo = JFactory::getDbo();
1871 if ($skipsession) {
1872 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1873 $dbo->setQuery($q);
1874 $dbo->execute();
1875 $s = $dbo->loadAssocList();
1876 return $s[0]['setting'];
1877 }
1878 $session = JFactory::getSession();
1879 $sval = $session->get('vbsearchNumChildren', '');
1880 if (!empty($sval)) {
1881 return $sval;
1882 }
1883 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numchildren';";
1884 $dbo->setQuery($q);
1885 $dbo->execute();
1886 $s = $dbo->loadAssocList();
1887 $session->set('vbsearchNumChildren', $s[0]['setting']);
1888 return $s[0]['setting'];
1889 }
1890
1891 public static function getSmartSearchType($skipsession = true)
1892 {
1893 $dbo = JFactory::getDbo();
1894 if ($skipsession) {
1895 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1896 $dbo->setQuery($q);
1897 $dbo->execute();
1898 $s = $dbo->loadAssocList();
1899 return $s[0]['setting'];
1900 }
1901 $session = JFactory::getSession();
1902 $sval = $session->get('vbsmartSearchType', '');
1903 if (!empty($sval)) {
1904 return $sval;
1905 }
1906 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smartsearch';";
1907 $dbo->setQuery($q);
1908 $dbo->execute();
1909 $s = $dbo->loadAssocList();
1910 $session->set('vbsmartSearchType', $s[0]['setting']);
1911 return $s[0]['setting'];
1912 }
1913
1914 /**
1915 * Returns the maximum advance booking notice (offset) as a period.
1916 * It either returns the global configuration setting, or the value
1917 * defined at room-level, if any and if requested.
1918 *
1919 * @param int $idroom optional room ID for room-level setting.
1920 *
1921 * @return string the maximum booking notice period (i.e. +1Y).
1922 *
1923 * @since 1.16.3 (J) - 1.6.3 (WP) replaced previous argument $skipsession with
1924 * $idroom to support room-level settings.
1925 */
1926 public static function getMaxDateFuture($idroom = 0)
1927 {
1928 static $max_adv_book_pool = [];
1929
1930 $pool_id = !empty($idroom) && is_numeric($idroom) ? (int)$idroom : 'global';
1931
1932 if (isset($max_adv_book_pool[$pool_id])) {
1933 return $max_adv_book_pool[$pool_id];
1934 }
1935
1936 $config = VBOFactory::getConfig();
1937
1938 if ($pool_id === 'global') {
1939 $max_adv_book_pool[$pool_id] = $config->get('maxdate', '');
1940 } else {
1941 $room_level_max_notice = $config->get("room_{$pool_id}_max_adv_notice");
1942 if (!empty($room_level_max_notice)) {
1943 // setting is defined at room-level
1944 $max_adv_book_pool[$pool_id] = $room_level_max_notice;
1945 } else {
1946 // recurse for global setting
1947 $max_adv_book_pool[$pool_id] = self::getMaxDateFuture();
1948 }
1949 }
1950
1951 return $max_adv_book_pool[$pool_id];
1952 }
1953
1954 /**
1955 * Validates the maximum advance booking notice against the requested check-in
1956 * timestamp. The method supports the validation at both room and property levels.
1957 *
1958 * @param int $checkints the check-in date timestamp.
1959 * @param int $idroom the optional room ID for room-level validation.
1960 *
1961 * @return string error date string in case of validation failure.
1962 *
1963 * @since 1.16.3 (J) - 1.6.3 (WP) introduced $idroom argument for room-level.
1964 */
1965 public static function validateMaxDateBookings($checkints, $idroom = 0)
1966 {
1967 $datelim = self::getMaxDateFuture($idroom);
1968 $datelim = empty($datelim) ? '+2y' : $datelim;
1969 $numlim = (int)substr($datelim, 1, (strlen($datelim) - 2));
1970 $quantlim = substr($datelim, -1, 1);
1971
1972 $now = getdate();
1973 if ($quantlim == 'w') {
1974 $until_ts = strtotime("+$numlim weeks") + 86399;
1975 } else {
1976 $use_mon = $quantlim == 'm' ? ($now['mon'] + $numlim) : $now['mon'];
1977 $use_day = $quantlim == 'd' ? ($now['mday'] + $numlim) : $now['mday'];
1978 $use_year = $quantlim == 'y' ? ($now['year'] + $numlim) : $now['year'];
1979 $until_ts = mktime(23, 59, 59, $use_mon, $use_day, $use_year);
1980 }
1981
1982 if ($until_ts > $now[0] && $checkints > $until_ts) {
1983 $vbo_df = self::getDateFormat();
1984 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
1985
1986 // error, return the maximum date in the future allowed
1987 return date($df, $until_ts);
1988 }
1989
1990 // validation passed
1991 return '';
1992 }
1993
1994 public static function validateMinDaysAdvance($checkints) {
1995 $mindadv = self::getMinDaysAdvance(true);
1996 if ($mindadv > 0) {
1997 $tsinfo = getdate($checkints);
1998 $limit_ts = mktime($tsinfo['hours'], $tsinfo['minutes'], $tsinfo['seconds'], date('n'), ((int)date('j') + $mindadv), date('Y'));
1999 if ($checkints < $limit_ts) {
2000 return $mindadv;
2001 }
2002 }
2003
2004 return '';
2005 }
2006
2007 /**
2008 * The only supported calendar type has been changed to jQuery UI.
2009 *
2010 * @since 1.13
2011 */
2012 public static function calendarType($skipsession = false)
2013 {
2014 return 'jqueryui';
2015 }
2016
2017 public static function getSiteLogo()
2018 {
2019 static $siteLogo = null;
2020
2021 if ($siteLogo !== null) {
2022 return $siteLogo;
2023 }
2024
2025 $siteLogo = VBOFactory::getConfig()->get('sitelogo', '');
2026
2027 return $siteLogo;
2028 }
2029
2030 public static function getBackendLogo()
2031 {
2032 static $backLogo = null;
2033
2034 if ($backLogo !== null) {
2035 return $backLogo;
2036 }
2037
2038 $backLogo = VBOFactory::getConfig()->get('backlogo', '');
2039
2040 return $backLogo;
2041 }
2042
2043 public static function numCalendars()
2044 {
2045 static $numCalendars = null;
2046
2047 if ($numCalendars !== null) {
2048 return $numCalendars;
2049 }
2050
2051 $numCalendars = VBOFactory::getConfig()->getUInt('numcalendars', 0);
2052
2053 return $numCalendars;
2054 }
2055
2056 public static function getFirstWeekDay()
2057 {
2058 static $firstWeekDay = null;
2059
2060 if ($firstWeekDay !== null) {
2061 return $firstWeekDay;
2062 }
2063
2064 $firstWeekDay = VBOFactory::getConfig()->getUInt('firstwday', 0);
2065
2066 return $firstWeekDay;
2067 }
2068
2069 public static function showPartlyReserved()
2070 {
2071 return VBOFactory::getConfig()->getBool('showpartlyreserved', false);
2072 }
2073
2074 public static function showStatusCheckinoutOnly()
2075 {
2076 return VBOFactory::getConfig()->getBool('showcheckinoutonly', false);
2077 }
2078
2079 public static function getDisclaimer($vbo_tn = null)
2080 {
2081 $dbo = JFactory::getDbo();
2082
2083 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disclaimer';";
2084 $dbo->setQuery($q);
2085 $ft = $dbo->loadAssocList();
2086 if (!$ft) {
2087 return '';
2088 }
2089
2090 if (!is_object($vbo_tn)) {
2091 $vbo_tn = self::getTranslator();
2092 }
2093 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2094
2095 return $ft[0]['setting'];
2096 }
2097
2098 public static function showFooter()
2099 {
2100 return VBOFactory::getConfig()->getBool('showfooter', false);
2101 }
2102
2103 public static function getPriceName($idp, $vbo_tn = null)
2104 {
2105 $dbo = JFactory::getDbo();
2106
2107 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2108 $dbo->setQuery($q, 0, 1);
2109 $n = $dbo->loadAssocList();
2110 if ($n) {
2111 if (is_object($vbo_tn)) {
2112 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2113 }
2114 return $n[0]['name'];
2115 }
2116
2117 return "";
2118 }
2119
2120 public static function getPriceAttr($idp, $vbo_tn = null)
2121 {
2122 $dbo = JFactory::getDbo();
2123
2124 $q = "SELECT `id`,`attr` FROM `#__vikbooking_prices` WHERE `id`=" . (int)$idp;
2125 $dbo->setQuery($q, 0, 1);
2126 $n = $dbo->loadAssocList();
2127 if ($n) {
2128 if (is_object($vbo_tn)) {
2129 $vbo_tn->translateContents($n, '#__vikbooking_prices');
2130 }
2131 return $n[0]['attr'];
2132 }
2133
2134 return "";
2135 }
2136
2137 /**
2138 * Fetches the requested rate plan information.
2139 *
2140 * @param int $idp the rate plan ID.
2141 * @param object $vbo_tn the Vik Booking translator object.
2142 *
2143 * @return array empty array or associative record.
2144 */
2145 public static function getPriceInfo($idp, $vbo_tn = null)
2146 {
2147 // cache values
2148 static $priceInfos = [];
2149
2150 $idp = (int)$idp;
2151
2152 if ($priceInfos && isset($priceInfos[$idp])) {
2153 return $priceInfos[$idp];
2154 }
2155
2156 $dbo = JFactory::getDbo();
2157
2158 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `id`={$idp}";
2159 $dbo->setQuery($q, 0, 1);
2160 $rplan = $dbo->loadAssocList();
2161 if (!$rplan) {
2162 return [];
2163 }
2164
2165 if (is_object($vbo_tn)) {
2166 $vbo_tn->translateContents($rplan, '#__vikbooking_prices');
2167 }
2168
2169 // set cached value
2170 $priceInfos[$idp] = $rplan[0];
2171
2172 return $rplan[0];
2173 }
2174
2175 public static function getAliq($idal)
2176 {
2177 $dbo = JFactory::getDbo();
2178
2179 $q = "SELECT `aliq` FROM `#__vikbooking_iva` WHERE `id`=" . (int)$idal . ";";
2180 $dbo->setQuery($q);
2181 $aliq = $dbo->loadResult();
2182 if ($aliq) {
2183 return $aliq;
2184 }
2185
2186 return 0;
2187 }
2188
2189 public static function getTimeOpenStore($skipsession = false)
2190 {
2191 // cache value in static var
2192 static $getTimeOpenStore = null;
2193
2194 if ($getTimeOpenStore) {
2195 return $getTimeOpenStore;
2196 }
2197 //
2198
2199 $dbo = JFactory::getDbo();
2200 $session = JFactory::getSession();
2201
2202 if ($skipsession) {
2203 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2204 if (empty($setting) && $setting != "0") {
2205 return false;
2206 } else {
2207 $x = explode("-", $setting);
2208 if (!empty($x[1]) && $x[1] != "0") {
2209 $getTimeOpenStore = $x;
2210 return $x;
2211 }
2212 }
2213 } else {
2214 $sval = $session->get('vbgetTimeOpenStore', '');
2215 if (!empty($sval)) {
2216 return $sval;
2217 } else {
2218 $setting = VBOFactory::getConfig()->get('timeopenstore', '');
2219 if (empty($setting) && $setting != "0") {
2220 return false;
2221 } else {
2222 $x = explode("-", $setting);
2223 if (!empty($x[1]) && $x[1] != "0") {
2224 $session->set('vbgetTimeOpenStore', $x);
2225 $getTimeOpenStore = $x;
2226 return $x;
2227 }
2228 }
2229 }
2230 }
2231 return false;
2232 }
2233
2234 public static function getHoursMinutes($secs) {
2235 if ($secs >= 3600) {
2236 $op = $secs / 3600;
2237 $hours = floor($op);
2238 $less = $hours * 3600;
2239 $newsec = $secs - $less;
2240 $optwo = $newsec / 60;
2241 $minutes = floor($optwo);
2242 } else {
2243 $hours = "0";
2244 $optwo = $secs / 60;
2245 $minutes = floor($optwo);
2246 }
2247 $x[] = $hours;
2248 $x[] = $minutes;
2249 return $x;
2250 }
2251
2252 public static function getClosingDates()
2253 {
2254 // cache value in static var
2255 static $getClosingDates = null;
2256
2257 if (is_array($getClosingDates)) {
2258 return $getClosingDates;
2259 }
2260
2261 $dbo = JFactory::getDbo();
2262
2263 $allcd = [];
2264
2265 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='closingdates';";
2266 $dbo->setQuery($q);
2267 $s = $dbo->loadAssocList();
2268 if ($s && !empty($s[0]['setting'])) {
2269 $allcd = json_decode($s[0]['setting'], true);
2270 $allcd = is_array($allcd) ? $allcd : array();
2271 $base_ts = mktime(0, 0, 0, date("n"), date("j"), date("Y"));
2272 foreach ($allcd as $k => $v) {
2273 if ($v['to'] < $base_ts) {
2274 unset($allcd[$k]);
2275 }
2276 }
2277 $allcd = array_values($allcd);
2278 }
2279
2280 $getClosingDates = $allcd;
2281
2282 return $allcd;
2283 }
2284
2285 public static function parseJsClosingDates() {
2286 $cd = self::getClosingDates();
2287 if (count($cd) > 0) {
2288 $cdjs = array();
2289 foreach ($cd as $k => $v) {
2290 $cdjs[] = array(date('Y-m-d', $v['from']), date('Y-m-d', $v['to']));
2291 }
2292 return $cdjs;
2293 }
2294 return array();
2295 }
2296
2297 public static function validateClosingDates($checkints, $checkoutts, $df = null)
2298 {
2299 $cd = self::getClosingDates();
2300 if (!count($cd)) {
2301 return '';
2302 }
2303 $df = empty($df) ? 'Y-m-d' : $df;
2304 $margin_seconds = 22 * 60 * 60;
2305 foreach ($cd as $k => $v) {
2306 $inner_closed = ($checkints >= $v['from'] && $checkints <= ($v['to'] + $margin_seconds));
2307 $outer_closed = ($checkoutts >= $v['from'] && $checkoutts <= ($v['to'] + $margin_seconds));
2308 $middle_closed = ($checkints <= $v['from'] && $checkoutts >= ($v['to'] + $margin_seconds));
2309 if ($inner_closed || $outer_closed || $middle_closed) {
2310 return date($df, $v['from']) . ' - ' . date($df, $v['to']);
2311 }
2312 }
2313 return '';
2314 }
2315
2316 /**
2317 * Whether the categories dropdown filter menu should be displayed.
2318 *
2319 * @param boolean $skipsession [optional] re-read the configuration setting.
2320 *
2321 * @return boolean
2322 */
2323 public static function showCategoriesFront($skipsession = true)
2324 {
2325 $dbo = JFactory::getDbo();
2326 if ($skipsession) {
2327 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2328 $dbo->setQuery($q);
2329 $dbo->execute();
2330 if ($dbo->getNumRows() > 0) {
2331 $s = $dbo->loadAssocList();
2332 return (intval($s[0]['setting']) == 1);
2333 }
2334 return false;
2335 }
2336 $session = JFactory::getSession();
2337 $sval = $session->get('vbshowCategoriesFront', '');
2338 if (strlen($sval)) {
2339 return (intval($sval) == 1 ? true : false);
2340 }
2341 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showcategories';";
2342 $dbo->setQuery($q);
2343 $dbo->execute();
2344 if ($dbo->getNumRows() > 0) {
2345 $s = $dbo->loadAssocList();
2346 $session->set('vbshowCategoriesFront', $s[0]['setting']);
2347 return (intval($s[0]['setting']) == 1);
2348 }
2349 return false;
2350 }
2351
2352 /**
2353 * Whether the number of children dropdown menu should be displayed.
2354 * Defaults to skip the session values and to re-read the configuration setting.
2355 *
2356 * @param boolean $skipsession [optional] re-read the configuration setting.
2357 *
2358 * @return boolean
2359 */
2360 public static function showChildrenFront($skipsession = true)
2361 {
2362 $dbo = JFactory::getDbo();
2363 if ($skipsession) {
2364 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2365 $dbo->setQuery($q);
2366 $dbo->execute();
2367 if ($dbo->getNumRows() > 0) {
2368 $s = $dbo->loadAssocList();
2369 return (intval($s[0]['setting']) == 1);
2370 }
2371 return false;
2372 }
2373 $session = JFactory::getSession();
2374 $sval = $session->get('vbshowChildrenFront', '');
2375 if (strlen($sval)) {
2376 return (intval($sval) == 1 ? true : false);
2377 }
2378 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='showchildren';";
2379 $dbo->setQuery($q);
2380 $dbo->execute();
2381 if ($dbo->getNumRows() > 0) {
2382 $s = $dbo->loadAssocList();
2383 $session->set('vbshowChildrenFront', $s[0]['setting']);
2384 return (intval($s[0]['setting']) == 1);
2385 }
2386 return false;
2387 }
2388
2389 public static function allowBooking() {
2390 $dbo = JFactory::getDbo();
2391 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='allowbooking';";
2392 $dbo->setQuery($q);
2393 $dbo->execute();
2394 if ($dbo->getNumRows() > 0) {
2395 $s = $dbo->loadAssocList();
2396 return (intval($s[0]['setting']) == 1 ? true : false);
2397 } else {
2398 return false;
2399 }
2400 }
2401
2402 public static function getDisabledBookingMsg($vbo_tn = null) {
2403 $dbo = JFactory::getDbo();
2404 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='disabledbookingmsg';";
2405 $dbo->setQuery($q);
2406 $dbo->execute();
2407 $s = $dbo->loadAssocList();
2408 if (!is_object($vbo_tn)) {
2409 $vbo_tn = self::getTranslator();
2410 }
2411 $vbo_tn->translateContents($s, '#__vikbooking_texts');
2412 return $s[0]['setting'];
2413 }
2414
2415 /**
2416 * Returns the text for the guests allowed policy.
2417 *
2418 * @param mixed $vbo_tn null or VikBookingTranslator instance to translate the text.
2419 *
2420 * @return string either the raw or the translated policy text.
2421 *
2422 * @since 1.16.5 (WP) - 1.6.5 (WP)
2423 */
2424 public static function getGuestsAllowedPolicy($vbo_tn = null)
2425 {
2426 $dbo = JFactory::getDbo();
2427
2428 $q = $dbo->getQuery(true)
2429 ->select([$dbo->qn('id'), $dbo->qn('setting')])
2430 ->from($dbo->qn('#__vikbooking_texts'))
2431 ->where($dbo->qn('param') . ' = ' . $dbo->q('guests_allowed_policy'));
2432
2433 $dbo->setQuery($q);
2434 $text_record = $dbo->loadAssocList();
2435
2436 if (!$text_record) {
2437 // missing record, insert it with empty values
2438 if (self::isAdmin()) {
2439 $record = new stdClass;
2440 $record->param = 'guests_allowed_policy';
2441 $record->exp = JText::translate('VBO_GUESTS_POLICY');
2442 $record->setting = '';
2443 $dbo->insertObject('#__vikbooking_texts', $record, 'id');
2444 }
2445
2446 return '';
2447 }
2448
2449 if (is_object($vbo_tn)) {
2450 // translate the record only if requested
2451 $vbo_tn->translateContents($text_record, '#__vikbooking_texts');
2452 }
2453
2454 return $text_record[0]['setting'];
2455 }
2456
2457 public static function getDateFormat($skipsession = true)
2458 {
2459 // cache value in static var
2460 static $getDateFormat = null;
2461
2462 if ($getDateFormat) {
2463 return $getDateFormat;
2464 }
2465
2466 $dbo = JFactory::getDbo();
2467
2468 if ($skipsession) {
2469 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2470 $dbo->setQuery($q, 0, 1);
2471 $getDateFormat = $dbo->loadResult();
2472
2473 return $getDateFormat;
2474 }
2475
2476 $session = JFactory::getSession();
2477 $sval = $session->get('vbgetDateFormat', '');
2478 if (!empty($sval)) {
2479 return $sval;
2480 }
2481
2482 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat'";
2483 $dbo->setQuery($q, 0, 1);
2484 $getDateFormat = $dbo->loadResult();
2485
2486 $session->set('vbgetDateFormat', $getDateFormat);
2487
2488 return $getDateFormat;
2489 }
2490
2491 public static function getDateSeparator($skipsession = true)
2492 {
2493 // cache value in static var
2494 static $getDateSeparator = null;
2495
2496 if ($getDateSeparator) {
2497 return $getDateSeparator;
2498 }
2499
2500 $dbo = JFactory::getDbo();
2501
2502 if ($skipsession) {
2503 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2504 $dbo->setQuery($q, 0, 1);
2505 $val = $dbo->loadResult();
2506
2507 $getDateSeparator = empty($val) ? "/" : $val;
2508
2509 return $getDateSeparator;
2510 }
2511
2512 $session = JFactory::getSession();
2513 $sval = $session->get('vbgetDateSep', '');
2514 if (!empty($sval)) {
2515 return $sval;
2516 }
2517
2518 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='datesep'";
2519 $dbo->setQuery($q, 0, 1);
2520 $val = $dbo->loadResult();
2521
2522 $getDateSeparator = empty($val) ? "/" : $val;
2523
2524 $session->set('vbgetDateSep', $getDateSeparator);
2525
2526 return $getDateSeparator;
2527 }
2528
2529 public static function getHoursMoreRb($skipsession = false)
2530 {
2531 // cache value in static var
2532 static $getHoursMoreRb = null;
2533
2534 if ($getHoursMoreRb !== null) {
2535 return $getHoursMoreRb;
2536 }
2537
2538 $dbo = JFactory::getDbo();
2539
2540 if ($skipsession) {
2541 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2542 $dbo->setQuery($q, 0, 1);
2543 $getHoursMoreRb = (int)$dbo->loadResult();
2544
2545 return $getHoursMoreRb;
2546 }
2547
2548 $session = JFactory::getSession();
2549 $sval = $session->get('getHoursMoreRb', '');
2550 if (strlen($sval)) {
2551 return $sval;
2552 }
2553
2554 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmorebookingback'";
2555 $dbo->setQuery($q, 0, 1);
2556 $getHoursMoreRb = (int)$dbo->loadResult();
2557
2558 $session->set('getHoursMoreRb', $getHoursMoreRb);
2559
2560 return $getHoursMoreRb;
2561 }
2562
2563 public static function getHoursRoomAvail()
2564 {
2565 $dbo = JFactory::getDbo();
2566
2567 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='hoursmoreroomavail'";
2568 $dbo->setQuery($q, 0, 1);
2569
2570 return (int)$dbo->loadResult();
2571 }
2572
2573 public static function getFrontTitle($vbo_tn = null) {
2574 // cache value in static var
2575 static $getFrontTitle = null;
2576
2577 if ($getFrontTitle) {
2578 return $getFrontTitle;
2579 }
2580 //
2581
2582 $dbo = JFactory::getDbo();
2583 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2584 $dbo->setQuery($q);
2585 $dbo->execute();
2586 $ft = $dbo->loadAssocList();
2587 if (!is_object($vbo_tn)) {
2588 $vbo_tn = self::getTranslator();
2589 }
2590 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2591
2592 $getFrontTitle = $ft[0]['setting'];
2593
2594 return $ft[0]['setting'];
2595 }
2596
2597 public static function getFrontTitleTag()
2598 {
2599 return VBOFactory::getConfig()->get('fronttitletag', '');
2600 }
2601
2602 public static function getFrontTitleTagClass()
2603 {
2604 return VBOFactory::getConfig()->get('fronttitletagclass', '');
2605 }
2606
2607 public static function getCurrencyName()
2608 {
2609 static $currencyName = null;
2610
2611 if ($currencyName !== null) {
2612 return $currencyName;
2613 }
2614
2615 $currencyName = VBOFactory::getConfig()->get('currencyname', '');
2616
2617 return $currencyName;
2618 }
2619
2620 public static function getCurrencySymb($skipsession = false) {
2621 // cache value in static var
2622 static $getCurrencySymb = null;
2623
2624 if ($getCurrencySymb) {
2625 return $getCurrencySymb;
2626 }
2627
2628 $dbo = JFactory::getDbo();
2629
2630 if ($skipsession) {
2631 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2632 $dbo->setQuery($q, 0, 1);
2633 $csymb = $dbo->loadResult();
2634
2635 $getCurrencySymb = $csymb;
2636
2637 return $csymb;
2638 }
2639
2640 $session = JFactory::getSession();
2641 $sval = $session->get('vbgetCurrencySymb', '');
2642 if (!empty($sval)) {
2643 return $sval;
2644 }
2645
2646 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencysymb'";
2647 $dbo->setQuery($q, 0, 1);
2648 $csymb = $dbo->loadResult();
2649
2650 // update session value, cache it and return it
2651 $session->set('vbgetCurrencySymb', $csymb);
2652 $getCurrencySymb = $csymb;
2653
2654 return $csymb;
2655 }
2656
2657 public static function getNumberFormatData($skipsession = false) {
2658 // cache value in static var
2659 static $getNumberFormatData = null;
2660
2661 if ($getNumberFormatData) {
2662 return $getNumberFormatData;
2663 }
2664 //
2665
2666 $dbo = JFactory::getDbo();
2667
2668 if ($skipsession) {
2669 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2670 $dbo->setQuery($q, 0, 1);
2671 $dbo->execute();
2672 $numfdata = $dbo->loadResult();
2673
2674 // cache value and return it
2675 $getNumberFormatData = $numfdata;
2676
2677 return $numfdata;
2678 }
2679
2680 $session = JFactory::getSession();
2681 $sval = $session->get('getNumberFormatData', '');
2682 if (!empty($sval)) {
2683 return $sval;
2684 }
2685
2686 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='numberformat'";
2687 $dbo->setQuery($q, 0, 1);
2688 $dbo->execute();
2689 $numfdata = $dbo->loadResult();
2690
2691 // update session value, cache it and return it
2692 $session->set('getNumberFormatData', $numfdata);
2693 $getNumberFormatData = $numfdata;
2694
2695 return $numfdata;
2696 }
2697
2698 /**
2699 * It is possible to hide the decimals if they are like "N.00".
2700 *
2701 * @return int 0 if disabled, 1 if enabled (default).
2702 *
2703 * @since 1.14 (J) - 1.4.0 (WP)
2704 */
2705 public static function hideEmptyDecimals()
2706 {
2707 // cache value in static var
2708 static $hideEmptyDecimals = null;
2709
2710 if ($hideEmptyDecimals !== null) {
2711 return $hideEmptyDecimals;
2712 }
2713 //
2714
2715 $dbo = JFactory::getDbo();
2716
2717 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='noemptydecimals';";
2718 $dbo->setQuery($q);
2719 $hideEmptyDecimals = $dbo->loadResult();
2720 if ($hideEmptyDecimals !== null) {
2721 $hideEmptyDecimals = (int)$hideEmptyDecimals;
2722 return $hideEmptyDecimals;
2723 }
2724
2725 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('noemptydecimals', '1');";
2726 $dbo->setQuery($q);
2727 $dbo->execute();
2728
2729 $hideEmptyDecimals = 1;
2730
2731 return $hideEmptyDecimals;
2732 }
2733
2734 public static function numberFormat($num, $skipsession = false)
2735 {
2736 $formatvals = self::getNumberFormatData($skipsession);
2737 $formatparts = explode(':', $formatvals);
2738
2739 if ((int)$formatparts[0] > 0 && (floatval($num) - intval($num)) == 0 && self::hideEmptyDecimals()) {
2740 // number has got no decimals
2741 $formatparts[0] = 0;
2742 }
2743
2744 return number_format((float)$num, (int)$formatparts[0], $formatparts[1], $formatparts[2]);
2745 }
2746
2747 /**
2748 * Given a date timestamp, returns the formatted date.
2749 *
2750 * @param int|string $ts the date representation in Unix timestamp.
2751 * @param bool $wtime if true, a formatted date-time will be returned.
2752 *
2753 * @return string the formatted date according to settings.
2754 *
2755 * @since 1.16.0 (J) - 1.6.0 (WP)
2756 */
2757 public static function formatDateTs($ts, $wtime = false)
2758 {
2759 $nowdf = self::getDateFormat();
2760 if ($nowdf == "%d/%m/%Y") {
2761 $df = 'd/m/Y';
2762 } elseif ($nowdf == "%m/%d/%Y") {
2763 $df = 'm/d/Y';
2764 } else {
2765 $df = 'Y/m/d';
2766 }
2767 $datesep = self::getDateSeparator();
2768
2769 $time_f = $wtime ? ' H:i' : '';
2770
2771 return date(str_replace("/", $datesep, $df) . $time_f, $ts);
2772 }
2773
2774 /**
2775 * Given a country name, 3-char or 2-char code, this method attempts
2776 * to guess the best language to assign to the booking according to
2777 * what languages are installed on the website. This way, cron jobs
2778 * and any other email notification will be correctly and automatically
2779 * sent to the guest in the proper language without any manual action.
2780 * To comply with VCM (>= 1.8.3), also VBO supports this feature.
2781 *
2782 * @param string $country the country name, 3-char or 2-char code.
2783 * @param string $locale optional locale lang tag of the guest.
2784 *
2785 * @return mixed the best lang-tag string to use or null.
2786 *
2787 * @since 1.16.0 (J) - 1.6.0 (WP)
2788 */
2789 public static function guessBookingLangFromCountry($country, $locale = null)
2790 {
2791 if (empty($country)) {
2792 return null;
2793 }
2794
2795 // get all the available languages
2796 $known_langs = self::getVboApplication()->getKnownLanguages();
2797 if (!is_array($known_langs) || !count($known_langs)) {
2798 return null;
2799 }
2800
2801 // check if the booking included a supported "locale" for the guest
2802 if (!empty($locale)) {
2803 // make the locale compatible with the language tags format
2804 $locale = str_replace('_', '-', $locale);
2805 foreach ($known_langs as $ltag => $ldet) {
2806 if (stripos($ltag, $locale) !== false || stripos($locale, $ltag) !== false) {
2807 // we support this language tag, so we just use it
2808 return $ltag;
2809 }
2810 }
2811 }
2812
2813 // build similarities with country-languages
2814 $similarities = array(
2815 'AU' => 'en',
2816 'GB' => 'en',
2817 'IE' => 'en',
2818 'NZ' => 'en',
2819 'US' => 'en',
2820 'CA' => array(
2821 'en',
2822 'fr',
2823 ),
2824 'CL' => 'es',
2825 'AR' => 'es',
2826 'PE' => 'es',
2827 'MX' => 'es',
2828 'CR' => 'es',
2829 'CO' => 'es',
2830 'EC' => 'es',
2831 'BO' => 'es',
2832 'CU' => 'es',
2833 'VE' => 'es',
2834 'BE' => 'fr',
2835 'LU' => 'fr',
2836 'CH' => array(
2837 'de',
2838 'it',
2839 'fr',
2840 ),
2841 'AT' => 'de',
2842 'GR' => 'el',
2843 'GL' => 'dk',
2844 );
2845
2846 // fetch values from db
2847 $dbo = JFactory::getDbo();
2848 $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);
2849 $dbo->setQuery($q, 0, 1);
2850 $dbo->execute();
2851 if (!$dbo->getNumRows()) {
2852 return null;
2853 }
2854 $country_record = $dbo->loadAssoc();
2855
2856 // assign country name/code versions
2857 $country_name = $country_record['country_name'];
2858 $country_3char = strtoupper($country_record['country_3_code']);
2859 $country_2char = strtoupper($country_record['country_2_code']);
2860
2861 // build an associative array of language tags and related match-score
2862 $langtags_score = array();
2863 foreach ($known_langs as $ltag => $ldet) {
2864 // default language tag score is 0 for no matches
2865 $langtags_score[$ltag] = 0;
2866 // get language and country codes
2867 $lang_country_codes = explode('-', str_replace('_', '-', strtoupper($ltag)));
2868
2869 // check matches with the installed language details
2870 if ($lang_country_codes[0] == $country_2char || $lang_country_codes[0] == $country_3char) {
2871 // increase language tag score
2872 $langtags_score[$ltag]++;
2873 }
2874 if (!empty($lang_country_codes[1]) && ($lang_country_codes[1] == $country_2char || $lang_country_codes[1] == $country_3char)) {
2875 // increase language tag score
2876 $langtags_score[$ltag]++;
2877 }
2878 if (!empty($ldet['locale'])) {
2879 // sanitize locale for matching the 2-char code safely
2880 $ldet['locale'] = str_replace(array('standard', 'euro', 'iso', 'utf'), '', strtolower($ldet['locale']));
2881 if (stripos($ldet['locale'], $country_2char) !== false || stripos($ldet['locale'], $country_name) !== false) {
2882 // increase language tag score
2883 $langtags_score[$ltag]++;
2884 }
2885 }
2886 if (!empty($ldet['name']) && stripos($ldet['name'], $country_name) !== false) {
2887 // increase language tag score
2888 $langtags_score[$ltag]++;
2889 }
2890 if (!empty($ldet['nativeName']) && stripos($ldet['nativeName'], $country_name) !== false) {
2891 // increase language tag score
2892 $langtags_score[$ltag]++;
2893 }
2894
2895 // check language similarities between countries
2896 if (isset($similarities[$country_2char])) {
2897 $spoken_tags = !is_array($similarities[$country_2char]) ? array($similarities[$country_2char]) : $similarities[$country_2char];
2898 // check if language tag(s) is available for this spoken language
2899 foreach ($spoken_tags as $spoken_tag) {
2900 if ($lang_country_codes[0] == strtoupper($spoken_tag)) {
2901 // increase language tag score
2902 $langtags_score[$ltag]++;
2903 }
2904 }
2905 }
2906 }
2907
2908 // make sure at least one language tag has got some points
2909 if (max($langtags_score) === 0) {
2910 // no languages installed to honor this country
2911 return null;
2912 }
2913
2914 // sort language tag scores
2915 arsort($langtags_score);
2916
2917 // reset array pointer to the first (highest) element
2918 reset($langtags_score);
2919
2920 // return the language tag with the highest score
2921 return key($langtags_score);
2922 }
2923
2924 public static function getCurrencyCodePp() {
2925 $dbo = JFactory::getDbo();
2926 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='currencycodepp';";
2927 $dbo->setQuery($q);
2928 $dbo->execute();
2929 $ft = $dbo->loadAssocList();
2930 return $ft[0]['setting'];
2931 }
2932
2933 public static function getIntroMain($vbo_tn = null) {
2934 $dbo = JFactory::getDbo();
2935 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='intromain';";
2936 $dbo->setQuery($q);
2937 $dbo->execute();
2938 $ft = $dbo->loadAssocList();
2939 if (!is_object($vbo_tn)) {
2940 $vbo_tn = self::getTranslator();
2941 }
2942 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2943 return $ft[0]['setting'];
2944 }
2945
2946 public static function getClosingMain($vbo_tn = null) {
2947 $dbo = JFactory::getDbo();
2948 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='closingmain';";
2949 $dbo->setQuery($q);
2950 $dbo->execute();
2951 $ft = $dbo->loadAssocList();
2952 if (!is_object($vbo_tn)) {
2953 $vbo_tn = self::getTranslator();
2954 }
2955 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2956 return $ft[0]['setting'];
2957 }
2958
2959 public static function getFullFrontTitle($vbo_tn = null) {
2960 $dbo = JFactory::getDbo();
2961 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='fronttitle';";
2962 $dbo->setQuery($q);
2963 $dbo->execute();
2964 $ft = $dbo->loadAssocList();
2965 if (!is_object($vbo_tn)) {
2966 $vbo_tn = self::getTranslator();
2967 }
2968 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
2969 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletag';";
2970 $dbo->setQuery($q);
2971 $dbo->execute();
2972 $fttag = $dbo->loadAssocList();
2973 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='fronttitletagclass';";
2974 $dbo->setQuery($q);
2975 $dbo->execute();
2976 $fttagclass = $dbo->loadAssocList();
2977 if (empty($ft[0]['setting'])) {
2978 return "";
2979 } else {
2980 if (empty($fttag[0]['setting'])) {
2981 return $ft[0]['setting'] . "<br/>\n";
2982 } else {
2983 $tag = str_replace("<", "", $fttag[0]['setting']);
2984 $tag = str_replace(">", "", $tag);
2985 $tag = str_replace("/", "", $tag);
2986 $tag = trim($tag);
2987 return "<" . $tag . "" . (!empty($fttagclass) ? " class=\"" . $fttagclass[0]['setting'] . "\"" : "") . ">" . $ft[0]['setting'] . "</" . $tag . ">";
2988 }
2989 }
2990 }
2991
2992 public static function dateIsValid($date) {
2993 $df = self::getDateFormat();
2994 $datesep = self::getDateSeparator();
2995 if (strlen($date) != 10) {
2996 return false;
2997 }
2998 $cur_dsep = "/";
2999 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3000 $cur_dsep = $datesep;
3001 }
3002 $x = explode($cur_dsep, $date);
3003 if ($df == "%d/%m/%Y") {
3004 if (strlen($x[0]) != 2 || $x[0] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[2]) != 4) {
3005 return false;
3006 }
3007 } elseif ($df == "%m/%d/%Y") {
3008 if (strlen($x[1]) != 2 || $x[1] > 31 || strlen($x[0]) != 2 || $x[0] > 12 || strlen($x[2]) != 4) {
3009 return false;
3010 }
3011 } else {
3012 if (strlen($x[2]) != 2 || $x[2] > 31 || strlen($x[1]) != 2 || $x[1] > 12 || strlen($x[0]) != 4) {
3013 return false;
3014 }
3015 }
3016 return true;
3017 }
3018
3019 public static function sayDateFormat() {
3020 $dbo = JFactory::getDbo();
3021 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='dateformat';";
3022 $dbo->setQuery($q);
3023 $dbo->execute();
3024 $s = $dbo->loadAssocList();
3025 if ($s[0]['setting'] == "%d/%m/%Y") {
3026 return JText::translate('VBCONFIGONETWELVE');
3027 } elseif ($s[0]['setting'] == "%m/%d/%Y") {
3028 return JText::translate('VBCONFIGONEMDY');
3029 } else {
3030 return JText::translate('VBCONFIGONETENTHREE');
3031 }
3032 }
3033
3034 /**
3035 * Calculates the Unix timestamp from the given date and
3036 * time. Avoids DST issues thanks to mktime. With prior releases,
3037 * DST issues may occur due to the sum of seconds.
3038 *
3039 * @param string $date the date string formatted with the current settings
3040 * @param int $h hours from 0 to 23 for check-in/check-out
3041 * @param int $m minutes from 0 to 59 for check-in/check-out
3042 * @param int $s seconds from 0 to 59 for check-in/check-out
3043 *
3044 * @return int the Unix timestamp of the date
3045 *
3046 * @since 1.0.14
3047 * @since 1.15.0 (J) - 1.5.0 (WP) added capability to always support Y-m-d format for VCM.
3048 * signature modified for $h and $m that now take 0 as default value.
3049 */
3050 public static function getDateTimestamp($date, $h = 0, $m = 0, $s = 0) {
3051 $df = self::getDateFormat();
3052 $datesep = self::getDateSeparator();
3053 $cur_dsep = "/";
3054 if ($datesep != $cur_dsep && strpos($date, $datesep) !== false) {
3055 $cur_dsep = $datesep;
3056 }
3057 if (preg_match("/^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])$/", $date)) {
3058 // date is in Y-m-d format (with no time)
3059 $cur_dsep = '-';
3060 $df = "%Y/%m/%d";
3061 }
3062 $x = explode($cur_dsep, $date);
3063 if (!(count($x) > 2)) {
3064 return 0;
3065 }
3066 if ($df == "%d/%m/%Y") {
3067 $month = (int)$x[1];
3068 $mday = (int)$x[0];
3069 $year = (int)$x[2];
3070 } elseif ($df == "%m/%d/%Y") {
3071 $month = (int)$x[0];
3072 $mday = (int)$x[1];
3073 $year = (int)$x[2];
3074 } else {
3075 $month = (int)$x[1];
3076 $mday = (int)$x[2];
3077 $year = (int)$x[0];
3078 }
3079 $h = empty($h) ? 0 : (int)$h;
3080 $m = empty($m) ? 0 : (int)$m;
3081 $s = $s > 0 && $s <= 59 ? $s : 0;
3082
3083 return mktime($h, $m, $s, $month, $mday, $year);
3084 }
3085
3086 public static function ivaInclusa($skipsession = false) {
3087 // cache value in static var
3088 static $getTaxIncluded = null;
3089
3090 if ($getTaxIncluded !== null) {
3091 return (bool)$getTaxIncluded;
3092 }
3093
3094 $dbo = JFactory::getDbo();
3095
3096 if ($skipsession) {
3097 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3098 $dbo->setQuery($q, 0, 1);
3099 $dbo->execute();
3100 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3101
3102 // cache value and return it
3103 $getTaxIncluded = $vat_incl_data;
3104
3105 return (bool)$vat_incl_data;
3106 }
3107
3108 $session = JFactory::getSession();
3109 $sval = $session->get('getTaxIncluded', '');
3110 if (strlen($sval)) {
3111 return (bool)$sval;
3112 }
3113
3114 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ivainclusa'";
3115 $dbo->setQuery($q, 0, 1);
3116 $dbo->execute();
3117 $vat_incl_data = $dbo->getNumRows() ? (int)$dbo->loadResult() : 1;
3118
3119 // update session value, cache it and return it
3120 $session->set('getTaxIncluded', $vat_incl_data);
3121 $getTaxIncluded = $vat_incl_data;
3122
3123 return (bool)$vat_incl_data;
3124 }
3125
3126 public static function showTaxOnSummaryOnly($skipsession = false) {
3127 if ($skipsession) {
3128 $dbo = JFactory::getDbo();
3129 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3130 $dbo->setQuery($q);
3131 $dbo->execute();
3132 $s = $dbo->loadAssocList();
3133 return (intval($s[0]['setting']) == 1 ? true : false);
3134 } else {
3135 $session = JFactory::getSession();
3136 $sval = $session->get('vbshowTaxOnSummaryOnly', '');
3137 if (strlen($sval) > 0) {
3138 return (intval($sval) == 1 ? true : false);
3139 } else {
3140 $dbo = JFactory::getDbo();
3141 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='taxsummary';";
3142 $dbo->setQuery($q);
3143 $dbo->execute();
3144 $s = $dbo->loadAssocList();
3145 $session->set('vbshowTaxOnSummaryOnly', $s[0]['setting']);
3146 return (intval($s[0]['setting']) == 1 ? true : false);
3147 }
3148 }
3149 }
3150
3151 public static function tokenForm() {
3152 $dbo = JFactory::getDbo();
3153 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='tokenform';";
3154 $dbo->setQuery($q);
3155 $dbo->execute();
3156 $s = $dbo->loadAssocList();
3157 return (intval($s[0]['setting']) == 1 ? true : false);
3158 }
3159
3160 public static function getPaypalAcc() {
3161 $dbo = JFactory::getDbo();
3162 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='ccpaypal';";
3163 $dbo->setQuery($q);
3164 $dbo->execute();
3165 $s = $dbo->loadAssocList();
3166 return $s[0]['setting'];
3167 }
3168
3169 public static function getAccPerCent() {
3170 $dbo = JFactory::getDbo();
3171 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='payaccpercent';";
3172 $dbo->setQuery($q);
3173 $dbo->execute();
3174 $s = $dbo->loadAssocList();
3175 return $s[0]['setting'];
3176 }
3177
3178 public static function getTypeDeposit($skipsession = false) {
3179 if ($skipsession) {
3180 $dbo = JFactory::getDbo();
3181 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3182 $dbo->setQuery($q);
3183 $dbo->execute();
3184 $s = $dbo->loadAssocList();
3185 return $s[0]['setting'];
3186 } else {
3187 $session = JFactory::getSession();
3188 $sval = $session->get('getTypeDeposit', '');
3189 if (strlen($sval) > 0) {
3190 return $sval;
3191 } else {
3192 $dbo = JFactory::getDbo();
3193 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='typedeposit';";
3194 $dbo->setQuery($q);
3195 $dbo->execute();
3196 $s = $dbo->loadAssocList();
3197 $session->set('getTypeDeposit', $s[0]['setting']);
3198 return $s[0]['setting'];
3199 }
3200 }
3201 }
3202
3203 public static function multiplePayments()
3204 {
3205 return VBOFactory::getConfig()->getBool('multipay', false);
3206 }
3207
3208 public static function getAdminMail()
3209 {
3210 return VBOFactory::getConfig()->getString('adminemail', '');
3211 }
3212
3213 public static function getSenderMail()
3214 {
3215 $sender = VBOFactory::getConfig()->getString('senderemail', '');
3216
3217 return empty($sender) ? self::getAdminMail() : $sender;
3218 }
3219
3220 public static function getPaymentName($vbo_tn = null) {
3221 $dbo = JFactory::getDbo();
3222 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='paymentname';";
3223 $dbo->setQuery($q);
3224 $dbo->execute();
3225 $s = $dbo->loadAssocList();
3226 if (!is_object($vbo_tn)) {
3227 $vbo_tn = self::getTranslator();
3228 }
3229 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3230 return $s[0]['setting'];
3231 }
3232
3233 public static function getTermsConditions($vbo_tn = null) {
3234 //VBO 1.10
3235 $dbo = JFactory::getDbo();
3236 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='termsconds';";
3237 $dbo->setQuery($q);
3238 $dbo->execute();
3239 if ($dbo->getNumRows() > 0) {
3240 $s = $dbo->loadAssocList();
3241 if (!is_object($vbo_tn)) {
3242 $vbo_tn = self::getTranslator();
3243 }
3244 $vbo_tn->translateContents($s, '#__vikbooking_texts');
3245 } else {
3246 //the record has never been saved. Compose it with the default lang definition
3247 $timeopst = self::getTimeOpenStore(true);
3248 if (is_array($timeopst)) {
3249 $openat = self::getHoursMinutes($timeopst[0]);
3250 $closeat = self::getHoursMinutes($timeopst[1]);
3251 } else {
3252 $openat = array(12, 0);
3253 $closeat = array(10, 0);
3254 }
3255 $checkin_str = ($openat[0] < 10 ? '0'.$openat[0] : $openat[0]).':'.($openat[1] < 10 ? '0'.$openat[1] : $openat[1]);
3256 $checkout_str = ($closeat[0] < 10 ? '0'.$closeat[0] : $closeat[0]).':'.($closeat[1] < 10 ? '0'.$closeat[1] : $closeat[1]);
3257 $s = array(0 => array('setting' => nl2br(JText::sprintf('VBOTERMSCONDSDEFTEXT', $checkin_str, $checkout_str))));
3258 }
3259
3260 return $s[0]['setting'];
3261 }
3262
3263 public static function getMinutesLock($conv = false)
3264 {
3265 static $minutesLock = null;
3266
3267 if ($minutesLock === null) {
3268 $minutesLock = VBOFactory::getConfig()->getInt('minuteslock', 0);
3269 }
3270
3271 if (!$minutesLock) {
3272 return 0;
3273 }
3274
3275 if ($conv) {
3276 return (time() + ($minutesLock * 60));
3277 }
3278
3279 return $minutesLock;
3280 }
3281
3282 /**
3283 * Checks if the room units are temporarily locked on the given dates.
3284 *
3285 * @param int $idroom the room ID.
3286 * @param int $units the total number of room units.
3287 * @param int $first the check-in timestamp.
3288 * @param int $second the check-out timestamp.
3289 * @param bool $occupied if true, the system will also consider the booked units.
3290 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3291 *
3292 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3293 *
3294 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3295 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3296 */
3297 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3298 {
3299 $dbo = JFactory::getDbo();
3300
3301 if ($units < 1) {
3302 return false;
3303 }
3304
3305 $actnow = time();
3306
3307 // clean up expired records
3308 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3309 $dbo->setQuery($q);
3310 $dbo->execute();
3311
3312 $secdiff = $second - $first;
3313 $daysdiff = $secdiff / 86400;
3314 if (is_int($daysdiff)) {
3315 if ($daysdiff < 1) {
3316 $daysdiff = 1;
3317 }
3318 } else {
3319 if ($daysdiff < 1) {
3320 $daysdiff = 1;
3321 } else {
3322 $sum = floor($daysdiff) * 86400;
3323 $newdiff = $secdiff - $sum;
3324 $maxhmore = self::getHoursMoreRb() * 3600;
3325 if ($maxhmore >= $newdiff) {
3326 $daysdiff = floor($daysdiff);
3327 } else {
3328 $daysdiff = ceil($daysdiff);
3329 }
3330 }
3331 }
3332 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3333 $tmplock_map = [];
3334
3335 // check temporarily locked records
3336 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3337 $dbo->setQuery($check);
3338 $busy = $dbo->loadAssocList();
3339 if ($busy) {
3340 foreach ($groupdays as $kg => $gday) {
3341 $bfound = 0;
3342 foreach ($busy as $bu) {
3343 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3344 $bfound++;
3345 }
3346 }
3347 $tmplock_map[$kg] = $bfound;
3348 if ($bfound >= $units) {
3349 return false;
3350 }
3351 }
3352 }
3353
3354 if ($occupied) {
3355 // check also the busy records and some the occupied units to the temporarily locked ones
3356 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3357 $dbo->setQuery($check);
3358 $busy = $dbo->loadAssocList();
3359 if (!$busy) {
3360 return true;
3361 }
3362
3363 foreach ($groupdays as $kg => $gday) {
3364 $bfound = 0;
3365 foreach ($busy as $bu) {
3366 if (in_array($bu['id'], $skip_busy_ids)) {
3367 // Booking modification
3368 continue;
3369 }
3370 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3371 $bfound++;
3372 }
3373 }
3374 // sum units occupied to the units temporarily locked
3375 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3376 if ($bfound >= $units) {
3377 return false;
3378 }
3379 }
3380 }
3381
3382 return true;
3383 }
3384
3385 public static function getGroupDays($first, $second, $daysdiff) {
3386 $ret = array();
3387 $ret[] = $first;
3388 if ($daysdiff > 1) {
3389 $start = getdate($first);
3390 $end = getdate($second);
3391 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3392 for($i = 1; $i < $daysdiff; $i++) {
3393 $checkday = $start['mday'] + $i;
3394 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3395 if ($dayts != $endcheck) {
3396 $ret[] = $dayts;
3397 }
3398 }
3399 }
3400 $ret[] = $second;
3401 return $ret;
3402 }
3403
3404 /**
3405 * Counts the hours of difference between the current
3406 * server time and the selected check-in date and time.
3407 *
3408 * @param int $checkin_ts
3409 * @param [int] $now_ts
3410 *
3411 * @return int
3412 */
3413 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3414 $hoursdiff = 0;
3415
3416 if (empty($now_ts)) {
3417 $now_ts = time();
3418 }
3419
3420 if ($now_ts >= $checkin_ts) {
3421 return $hoursdiff;
3422 }
3423
3424 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3425
3426 return $hoursdiff;
3427 }
3428
3429 /**
3430 * Loads all the occupied records for the given rooms and offset.
3431 *
3432 * @param array $roomids the optional list of room IDs to filter.
3433 * @param int $from_ts the optional base date to use.
3434 * @param int $max_ts the optional max date to use.
3435 * @param bool $closures whether to ignore, include or exclude closures.
3436 *
3437 * @return array
3438 *
3439 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3440 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3441 */
3442 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3443 {
3444 $base_ts = empty($from_ts) ? time() : $from_ts;
3445 $busy = [];
3446 $clauses = [];
3447
3448 if (is_array($roomids) && count($roomids)) {
3449 // filter busy records only by the requested room ids
3450 $roomids = array_map(function($rid) {
3451 return (int)$rid;
3452 }, $roomids);
3453 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3454 }
3455
3456 // exclude past reservations
3457 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3458
3459 if (!empty($max_ts)) {
3460 // exclude future reservations (recommended for large datasets)
3461 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3462 }
3463
3464 if (is_bool($closures)) {
3465 // include or exclude closures
3466 if ($closures) {
3467 $clauses[] = "`o`.`closure` = 1";
3468 } else {
3469 $clauses[] = "`o`.`closure` != 1";
3470 }
3471 }
3472
3473 $dbo = JFactory::getDbo();
3474
3475 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3476 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3477 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3478 WHERE " . implode(' AND ', $clauses) . ";";
3479 $dbo->setQuery($q);
3480 $allbusy = $dbo->loadAssocList();
3481 if ($allbusy) {
3482 foreach ($allbusy as $br) {
3483 if (!isset($busy[$br['idroom']])) {
3484 $busy[$br['idroom']] = [];
3485 }
3486 $busy[$br['idroom']][] = $br;
3487 }
3488 }
3489
3490 return $busy;
3491 }
3492
3493 /**
3494 * Loads all the busy records by excluding the closures.
3495 * Built for the page dashboard to calculate the actual
3496 * rooms occupancy by excluding the rooms closed.
3497 *
3498 * @param array $roomids the optional list of room IDs to filter.
3499 * @param int $from_ts the optional base date to use.
3500 * @param int $max_ts the optional max date to use.
3501 *
3502 * @return array the list of busy records.
3503 *
3504 * @since 1.12
3505 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3506 */
3507 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3508 {
3509 if (!is_array($roomids) || !count($roomids)) {
3510 return [];
3511 }
3512
3513 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3514 }
3515
3516 public static function loadBookingBusyIds($idorder) {
3517 $busy = array();
3518 if (empty($idorder)) {
3519 return $busy;
3520 }
3521 $dbo = JFactory::getDbo();
3522 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$idorder.";";
3523 $dbo->setQuery($q);
3524 $allbusy = $dbo->loadAssocList();
3525 if ($allbusy) {
3526 foreach ($allbusy as $b) {
3527 array_push($busy, $b['idbusy']);
3528 }
3529 }
3530 return $busy;
3531 }
3532
3533 public static function loadLockedRecords($roomids, $ts = 0) {
3534 $dbo = JFactory::getDbo();
3535 $actnow = empty($ts) ? time() : $ts;
3536 $locked = array();
3537 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3538 $dbo->setQuery($q);
3539 $dbo->execute();
3540 if (!is_array($roomids) || !(count($roomids) > 0)) {
3541 return $locked;
3542 }
3543 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3544 $dbo->setQuery($check);
3545 $all_locked = $dbo->loadAssocList();
3546 if ($all_locked) {
3547 foreach ($all_locked as $kb => $br) {
3548 $locked[$br['idroom']][$kb] = $br;
3549 }
3550 }
3551 return $locked;
3552 }
3553
3554 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3555 $bookings = [];
3556 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3557 return $bookings;
3558 }
3559 $dbo = JFactory::getDbo();
3560 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3561 $dbo->setQuery($q);
3562 $all_booking_ids = $dbo->loadAssocList();
3563 if ($all_booking_ids) {
3564 $oids = array();
3565 foreach ($all_booking_ids as $bid) {
3566 $oids[] = $bid['idorder'];
3567 }
3568 $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` ".
3569 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3570 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3571 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3572 "ORDER BY `o`.`checkout` ASC;";
3573 $dbo->setQuery($q);
3574 $bookings = $dbo->loadAssocList();
3575 if (!$bookings) {
3576 $bookings = [];
3577 }
3578 }
3579 return $bookings;
3580 }
3581
3582 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3583 {
3584 $dbo = JFactory::getDbo();
3585
3586 if ($units < 1) {
3587 return false;
3588 }
3589
3590 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units']);
3591 if (!$room_info) {
3592 return false;
3593 }
3594
3595 $secdiff = $second - $first;
3596 $daysdiff = $secdiff / 86400;
3597 if (is_int($daysdiff)) {
3598 if ($daysdiff < 1) {
3599 $daysdiff = 1;
3600 }
3601 } else {
3602 if ($daysdiff < 1) {
3603 $daysdiff = 1;
3604 } else {
3605 $sum = floor($daysdiff) * 86400;
3606 $newdiff = $secdiff - $sum;
3607 $maxhmore = self::getHoursMoreRb() * 3600;
3608 if ($maxhmore >= $newdiff) {
3609 $daysdiff = floor($daysdiff);
3610 } else {
3611 $daysdiff = ceil($daysdiff);
3612 }
3613 }
3614 }
3615
3616 $q = $dbo->getQuery(true)
3617 ->select($dbo->qn([
3618 'id',
3619 'checkin',
3620 'realback',
3621 ]))
3622 ->from($dbo->qn('#__vikbooking_busy'))
3623 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3624 ->where($dbo->qn('realback') . ' >= ' . $first);
3625
3626 $dbo->setQuery($q);
3627 $busy = $dbo->loadAssocList();
3628 if (!$busy) {
3629 return ($units <= $room_info['units']);
3630 }
3631
3632 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3633 foreach ($groupdays as $gday) {
3634 $bfound = 0;
3635 foreach ($busy as $bu) {
3636 if (in_array($bu['id'], $skip_busy_ids)) {
3637 // booking modification
3638 continue;
3639 }
3640 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3641 $bfound++;
3642 }
3643 }
3644 if ($bfound >= $units) {
3645 return false;
3646 }
3647 }
3648
3649 return ($units <= $room_info['units']);
3650 }
3651
3652 public static function payTotal()
3653 {
3654 return VBOFactory::getConfig()->getBool('paytotal', true);
3655 }
3656
3657 public static function getDepositIfDays()
3658 {
3659 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3660 }
3661
3662 public static function depositAllowedDaysAdv($checkints) {
3663 $days_adv = self::getDepositIfDays();
3664 if (!($days_adv > 0) || !($checkints > 0)) {
3665 return true;
3666 }
3667 $now_info = getdate();
3668 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3669 return $maxts > $checkints ? false : true;
3670 }
3671
3672 public static function depositCustomerChoice() {
3673 $dbo = JFactory::getDbo();
3674 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3675 $dbo->setQuery($q);
3676 $dbo->execute();
3677 $s = $dbo->loadAssocList();
3678 return (intval($s[0]['setting']) == 1 ? true : false);
3679 }
3680
3681 public static function getDepositOverrides($getjson = false) {
3682 $dbo = JFactory::getDbo();
3683 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3684 $dbo->setQuery($q);
3685 $dbo->execute();
3686 if ($dbo->getNumRows() > 0) {
3687 $s = $dbo->loadResult();
3688 return $getjson ? $s : json_decode($s, true);
3689 }
3690 //count of this array will be at least 1 to store the "more" property
3691 $def_arr = array('more' => '');
3692 $def_val = json_encode($def_arr);
3693 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
3694 $dbo->setQuery($q);
3695 $dbo->execute();
3696 return $getjson ? $def_val : $def_arr;
3697 }
3698
3699 public static function calcDepositOverride($amount_deposit, $nights) {
3700 $overrides = self::getDepositOverrides();
3701 $nights = intval($nights);
3702 $andmore = intval($overrides['more']);
3703 if (!(count($overrides) > 1)) {
3704 //no overrides
3705 return $amount_deposit;
3706 }
3707 foreach ($overrides as $k => $v) {
3708 if ((int)$k == $nights && strlen($v) > 0) {
3709 //exact override found
3710 return (float)$v;
3711 }
3712 }
3713 if ($andmore > 0 && $andmore <= $nights) {
3714 foreach ($overrides as $k => $v) {
3715 if ((int)$k == $andmore && strlen($v) > 0) {
3716 //"and more" nights found
3717 return (float)$v;
3718 }
3719 }
3720 }
3721 //nothing was found
3722 return $amount_deposit;
3723 }
3724
3725 public static function noDepositForNonRefund() {
3726 $dbo = JFactory::getDbo();
3727 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
3728 $dbo->setQuery($q);
3729 $dbo->execute();
3730 if ($dbo->getNumRows() > 0) {
3731 $s = $dbo->loadResult();
3732 return ((int)$s == 1);
3733 }
3734 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
3735 $dbo->setQuery($q);
3736 $dbo->execute();
3737 //default to false
3738 return false;
3739 }
3740
3741 /**
3742 * This method returns the room-rate array with the lowest price
3743 * that matches the preferred rate plan parameter (if available).
3744 * The array $website_rates could not be an array, or it could be
3745 * an array with the error string (response from the TACVBO Class).
3746 * The method has been introduced in VBO 1.10 and it's mainly used
3747 * by the module mod_vikbooking_channelrates and its ajax requests.
3748 *
3749 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
3750 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
3751 *
3752 * @return array
3753 */
3754 public static function getBestRoomRate($website_rates, $def_rplan) {
3755 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
3756 return array();
3757 }
3758 $best_room_rate = array();
3759 foreach ($website_rates as $rid => $tars) {
3760 foreach ($tars as $tar) {
3761 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
3762 //the array $website_rates is already sorted by price ASC, so we take the first useful array
3763 $best_room_rate = $tar;
3764 break 2;
3765 }
3766 }
3767 }
3768 if (!(count($best_room_rate) > 0)) {
3769 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
3770 foreach ($website_rates as $rid => $tars) {
3771 foreach ($tars as $tar) {
3772 $best_room_rate = $tar;
3773 break 2;
3774 }
3775 }
3776 }
3777
3778 return $best_room_rate;
3779 }
3780
3781 /**
3782 * This method returns an array with the details
3783 * of all channels in VCM that supports AV requests,
3784 * and that have at least one room type mapped.
3785 * The method invokes the Logos Class to return details
3786 * about the name and logo URL of the channel.
3787 * The method has been introduced in VBO 1.10 and it's mainly used
3788 * by the module mod_vikbooking_channelrates and its ajax requests.
3789 *
3790 * @param array $channels an array of channel IDs to be mapped on the VCM relations
3791 *
3792 * @return array
3793 */
3794 public static function getChannelsMap($channels) {
3795 if (!is_array($channels) || !(count($channels))) {
3796 return array();
3797 }
3798 $vcm_logos = self::getVcmChannelsLogo('', true);
3799 if (!is_object($vcm_logos)) {
3800 return array();
3801 }
3802 $channels_ids = array();
3803 foreach ($channels as $chid) {
3804 $ichid = (int)$chid;
3805 if ($ichid < 1) {
3806 continue;
3807 }
3808 array_push($channels_ids, $ichid);
3809 }
3810 if (!(count($channels_ids))) {
3811 return array();
3812 }
3813 $dbo = JFactory::getDbo();
3814 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
3815 $dbo->setQuery($q);
3816 $dbo->execute();
3817 if ($dbo->getNumRows() < 1) {
3818 return array();
3819 }
3820 $channels_names = $dbo->loadAssocList();
3821 $channels_map = array();
3822 foreach ($channels_names as $ch) {
3823 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
3824 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
3825 $chdata = array(
3826 'id' => $ch['idchannel'],
3827 'name' => ucwords($ch['channel']),
3828 'logo' => $ota_logo_url
3829 );
3830 array_push($channels_map, $chdata);
3831 }
3832 return $channels_map;
3833 }
3834
3835 /**
3836 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
3837 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
3838 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
3839 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
3840 * module/widget and its ajax requests.
3841 *
3842 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
3843 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
3844 *
3845 * @return mixed string (empty or indicating the alteration), or associative array per channel.
3846 *
3847 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
3848 */
3849 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
3850 {
3851 $otas_rates_val = '';
3852 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
3853 return $otas_rates_val;
3854 }
3855 if (!class_exists('VikChannelManager')) {
3856 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
3857 }
3858 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
3859 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
3860 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
3861 // the Bulk Rates Cache contains data for this room type and rate plan
3862 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
3863 // build rates modification string
3864 if ((int)$data_cont['rmod'] > 0) {
3865 // rates were modified for the OTAs, check how
3866 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
3867 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
3868 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
3869 // check alterations per channel, if requested
3870 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
3871 // we compose an associative array with the alteration for each channel (0 = generic rule)
3872 $channel_alterations = array($otas_rates_val);
3873 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
3874 if ((int)$ch_mods['rmod'] > 0) {
3875 // custom rate alteration for this channel
3876 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
3877 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
3878 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
3879 } else {
3880 // no rates alteration for this channel identifier
3881 $ch_alter_string = '+0%';
3882 }
3883 $channel_alterations[$ch_id] = $ch_alter_string;
3884 }
3885 // overwrite return value with associative array
3886 $otas_rates_val = $channel_alterations;
3887 }
3888 }
3889 }
3890 }
3891
3892 return $otas_rates_val;
3893 }
3894
3895 /**
3896 * This method checks if some non-refundable rates were selected
3897 * (`free_cancellation`=0), the only argument is an array of tariffs.
3898 * The property 'idprice' must be defined on each sub-array.
3899 *
3900 * @param $tars array
3901 *
3902 * @return boolean
3903 **/
3904 public static function findNonRefundableRates($tars = [])
3905 {
3906 $id_prices = [];
3907
3908 foreach ($tars as $tar) {
3909 if (isset($tar['idprice'])) {
3910 if (!in_array($tar['idprice'], $id_prices)) {
3911 array_push($id_prices, (int)$tar['idprice']);
3912 }
3913 continue;
3914 }
3915 foreach ($tar as $t) {
3916 if (isset($t['idprice'])) {
3917 if (!in_array($t['idprice'], $id_prices)) {
3918 array_push($id_prices, (int)$t['idprice']);
3919 }
3920 }
3921 }
3922 }
3923
3924 if (!count($id_prices)) {
3925 // no price IDs found (probably a package)
3926 return false;
3927 }
3928
3929 $dbo = JFactory::getDbo();
3930
3931 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
3932 $dbo->setQuery($q);
3933 $dbo->execute();
3934
3935 return (bool)($dbo->getNumRows() > 0);
3936 }
3937
3938 /**
3939 * This method checks if the deposit is allowed depending on
3940 * the selected rate plans (idprice) for the rooms reserved.
3941 * If the configuration setting is enabled, and if some
3942 * non-refundable rates were selected (`free_cancellation`=0),
3943 * the method will return false, because the deposit is not allowed.
3944 * The only argument is an array of tariffs. The property 'idprice'
3945 * must be defined on each sub-array (multi-dimension supported)
3946 * throgh the method findNonRefundableRates();
3947 *
3948 * @param $tars array
3949 *
3950 * @return boolean
3951 **/
3952 public static function allowDepositFromRates($tars) {
3953 if (!self::noDepositForNonRefund()) {
3954 //deposit can be paid also if non-refundable rates
3955 return true;
3956 }
3957 return !self::findNonRefundableRates($tars);
3958 }
3959
3960 public static function showSearchSuggestions() {
3961 $dbo = JFactory::getDbo();
3962 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
3963 $dbo->setQuery($q);
3964 $dbo->execute();
3965 if ($dbo->getNumRows() > 0) {
3966 return (int)$dbo->loadResult();
3967 }
3968 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
3969 $dbo->setQuery($q);
3970 $dbo->execute();
3971 return 1;
3972 }
3973
3974 /**
3975 * Given a code, returns the coupon information array.
3976 *
3977 * @param string $code the coupon code.
3978 *
3979 * @return array empty array or coupon record.
3980 *
3981 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
3982 */
3983 public static function getCouponInfo($code)
3984 {
3985 $dbo = JFactory::getDbo();
3986
3987 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
3988 $dbo->setQuery($q, 0, 1);
3989 $coupon = $dbo->loadAssoc();
3990 if (!$coupon) {
3991 return [];
3992 }
3993
3994 // check for customers assigned
3995 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
3996 $dbo->setQuery($q);
3997 $coupon_customers = $dbo->loadAssocList();
3998 if ($coupon_customers) {
3999 // set customers and automatic properties
4000 $coupon['customers'] = [];
4001 $coupon['automatic'] = $coupon_customers[0]['automatic'];
4002 foreach ($coupon_customers as $coupon_customer) {
4003 // push customer ID
4004 $coupon['customers'][] = $coupon_customer['idcustomer'];
4005 }
4006 }
4007
4008 return $coupon;
4009 }
4010
4011 public static function getRoomInfo($idroom, $columns = [])
4012 {
4013 static $room_infos = [];
4014
4015 if (isset($room_infos[$idroom])) {
4016 return $room_infos[$idroom];
4017 }
4018
4019 $dbo = JFactory::getDbo();
4020
4021 if ($columns) {
4022 $columns = array_map([$dbo, 'qn'], $columns);
4023 }
4024
4025 $q = $dbo->getQuery(true)
4026 ->select($columns ? $columns : '*')
4027 ->from($dbo->qn('#__vikbooking_rooms'))
4028 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4029
4030 $dbo->setQuery($q);
4031 $room = $dbo->loadAssoc();
4032
4033 $room_infos[$idroom] = $room ? $room : [];
4034
4035 return $room_infos[$idroom];
4036 }
4037
4038 public static function loadOrdersRoomsData($idorder)
4039 {
4040 $dbo = JFactory::getDbo();
4041
4042 $q = $dbo->getQuery(true)
4043 ->select($dbo->qn('or') . '.*')
4044 ->select($dbo->qn('r.name', 'room_name'))
4045 ->select($dbo->qn('r.params'))
4046 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4047 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4048 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4049 ->order($dbo->qn('or.id') . ' ASC');
4050
4051 $dbo->setQuery($q);
4052
4053 return $dbo->loadAssocList();
4054 }
4055
4056 public static function sayCategory($ids, $vbo_tn = null)
4057 {
4058 $dbo = JFactory::getDbo();
4059 $split = explode(";", $ids);
4060 $say = "";
4061 foreach ($split as $k => $s) {
4062 if (!strlen($s)) {
4063 continue;
4064 }
4065 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4066 $dbo->setQuery($q);
4067 $nam = $dbo->loadAssocList();
4068 if (!$nam) {
4069 continue;
4070 }
4071 if (is_object($vbo_tn)) {
4072 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4073 }
4074 $say .= $nam[0]['name'];
4075 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4076 }
4077 return $say;
4078 }
4079
4080 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4081 {
4082 $dbo = JFactory::getDbo();
4083 $split = explode(";", $idc);
4084 $carat = "";
4085 $arr = array();
4086 $where = array();
4087 foreach ($split as $s) {
4088 if (!empty($s)) {
4089 $where[] = $s;
4090 }
4091 }
4092 if ($where) {
4093 $q = "SELECT `c`.* FROM `#__vikbooking_characteristics` AS `c` WHERE `c`.`id` IN (" . implode(",", $where) . ") ORDER BY `c`.`ordering` ASC;";
4094 $dbo->setQuery($q);
4095 $arr = $dbo->loadAssocList();
4096 if ($arr && is_object($vbo_tn)) {
4097 $vbo_tn->translateContents($arr, '#__vikbooking_characteristics');
4098 }
4099 }
4100 if ($arr) {
4101 $carat .= "<div class=\"vbo-room-carats\">\n";
4102 foreach ($arr as $a) {
4103 if (!empty($a['textimg'])) {
4104 //tooltip icon text is not empty
4105 if (!empty($a['icon'])) {
4106 //an icon has been uploaded: display the image
4107 $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";
4108 } else {
4109 if (strpos($a['textimg'], '</i>') !== false) {
4110 //the tooltip icon text is a font-icon, we can use the name as tooltip
4111 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['name']."\">".$a['textimg']."</span></span>\n";
4112 } else {
4113 //display just the text
4114 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4115 }
4116 }
4117 } else {
4118 $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");
4119 }
4120 }
4121 $carat .= "</div>\n";
4122 }
4123 return $carat;
4124 }
4125
4126 public static function getRoomOptionals($idopts, $vbo_tn = null)
4127 {
4128 $dbo = JFactory::getDbo();
4129 $split = explode(";", $idopts);
4130 $fetch = array();
4131 foreach ($split as $s) {
4132 if (!empty($s)) {
4133 $fetch[] = $s;
4134 }
4135 }
4136 if ($fetch) {
4137 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4138 $dbo->setQuery($q);
4139 $arr = $dbo->loadAssocList();
4140 if ($arr) {
4141 if (is_object($vbo_tn)) {
4142 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4143 }
4144 return $arr;
4145 }
4146 }
4147 return [];
4148 }
4149
4150 public static function getSingleOption($idopt, $vbo_tn = null)
4151 {
4152 $dbo = JFactory::getDbo();
4153 $opt = array();
4154 if (!empty($idopt)) {
4155 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4156 $dbo->setQuery($q);
4157 $opt = $dbo->loadAssoc();
4158 if ($opt && is_object($vbo_tn)) {
4159 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4160 }
4161 }
4162 return $opt;
4163 }
4164
4165 /**
4166 * Unsets the options that are not available in the requested dates.
4167 *
4168 * @param array $optionals the array of options passed by reference
4169 * @param int $checkin the timestamp of the check-in date and time
4170 * @param int $checkout the timestamp of the check-out date and time
4171 *
4172 * @return void
4173 *
4174 * @since 1.11
4175 */
4176 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout)
4177 {
4178 if (!$optionals || empty($checkin) || empty($checkout)) {
4179 return;
4180 }
4181
4182 foreach ($optionals as $k => $v) {
4183 if (!empty($v['alwaysav'])) {
4184 $dates = explode(';', $v['alwaysav']);
4185 if (empty($dates[0]) || empty($dates[1])) {
4186 continue;
4187 }
4188 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4189 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4190 unset($optionals[$k]);
4191 }
4192 }
4193 }
4194 }
4195
4196 /**
4197 * Unsets the options that are not suited for the requested room party.
4198 *
4199 * @param array $optionals the array of options passed by reference.
4200 * @param int $adults the number of adults in the room party.
4201 * @param int $children the number of children in the room party.
4202 *
4203 * @return void
4204 *
4205 * @since 1.13.5
4206 */
4207 public static function filterOptionalsByParty(&$optionals, $adults, $children)
4208 {
4209 if (!$optionals || (empty($adults) && empty($children))) {
4210 return;
4211 }
4212
4213 foreach ($optionals as $k => $v) {
4214 if (empty($v['oparams'])) {
4215 continue;
4216 }
4217 $v['oparams'] = json_decode($v['oparams'], true);
4218 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4219 continue;
4220 }
4221 if (!empty($v['oparams']['minguestsnum'])) {
4222 // filter by minimum adults/guests
4223 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4224 // minimum number of adults not sufficient, unset option
4225 unset($optionals[$k]);
4226 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4227 // minimum number of total guests not sufficient, unset option
4228 unset($optionals[$k]);
4229 }
4230 }
4231 if (!empty($v['oparams']['maxguestsnum'])) {
4232 // filter by maximum adults/guests
4233 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4234 // maximum number of adults exceeded, unset option
4235 unset($optionals[$k]);
4236 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4237 // maximum number of total guests exceeded, unset option
4238 unset($optionals[$k]);
4239 }
4240 }
4241 }
4242 }
4243
4244 /**
4245 * Builds up the information related to mandatory taxes and fees for a list of rooms.
4246 *
4247 * @param array $id_rooms list of room IDs.
4248 * @param int $num_adults number of adults.
4249 * @param int $num_nights number of nights of stay.
4250 * @param int $checkin optional check-in timestamp.
4251 * @param int $checkout optional check-out timestamp.
4252 *
4253 * @return array associative information of city taxes, fees and options.
4254 *
4255 * @since 1.16.7 (J) - 1.6.7 (WP) added arguments $checkin and $checkout to allow filtering by dates.
4256 */
4257 public static function getMandatoryTaxesFees(array $id_rooms, $num_adults, $num_nights, $checkin = null, $checkout = null)
4258 {
4259 $dbo = JFactory::getDbo();
4260
4261 $taxes = 0;
4262 $fees = 0;
4263
4264 $options_data = [];
4265 $id_options = [];
4266
4267 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(", ", array_map('intval', $id_rooms)) . ");";
4268 $dbo->setQuery($q);
4269 $assocs = $dbo->loadAssocList();
4270 if ($assocs) {
4271 foreach ($assocs as $opts) {
4272 if (!empty($opts['idopt'])) {
4273 $r_ido = explode(';', rtrim($opts['idopt']));
4274 foreach ($r_ido as $ido) {
4275 if (!empty($ido) && !in_array($ido, $id_options)) {
4276 $id_options[] = (int)$ido;
4277 }
4278 }
4279 }
4280 }
4281 }
4282
4283 if ($id_options) {
4284 $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);";
4285 $dbo->setQuery($q);
4286 $alltaxesfees = $dbo->loadAssocList();
4287
4288 if ($checkin && $checkout) {
4289 // filter the records by stay dates to support, for example, seasonal city taxes
4290 self::filterOptionalsByDate($alltaxesfees, $checkin, $checkout);
4291 }
4292
4293 foreach ($alltaxesfees as $tf) {
4294 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4295 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4296 $realcost = $tf['maxprice'];
4297 }
4298 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4299 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4300 if ($tf['is_citytax'] == 1) {
4301 $taxes += $realcost;
4302 } elseif ($tf['is_fee'] == 1) {
4303 $fees += $realcost;
4304 }
4305 $optsett = explode('-', $tf['forceval']);
4306 $options_data[] = $tf['id'].':'.$optsett[0];
4307 }
4308 }
4309
4310 return [
4311 'city_taxes' => $taxes,
4312 'fees' => $fees,
4313 'options' => $options_data
4314 ];
4315 }
4316
4317 /**
4318 * From a list of option/extras records, it returns an array with the options/extras
4319 * that do not support age intervals, and options for the children age intervals.
4320 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4321 *
4322 * @param array $optionals the full list of options/extras records.
4323 * @param int $adults the number of adults in the room requested.
4324 * @param int $children the number of children in the room requested.
4325 *
4326 * @return array the filtered regular and children age intervals options.
4327 */
4328 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4329 {
4330 // container for age intervals
4331 $ageintervals = [];
4332
4333 // regular options should start as an array
4334 if (!is_array($optionals)) {
4335 $optionals = [];
4336 }
4337
4338 // check for the first valid age intervals option
4339 foreach ($optionals as $kopt => $opt) {
4340 if (!empty($opt['ageintervals'])) {
4341 $intervals = explode(';;', $opt['ageintervals']);
4342 foreach ($intervals as $intv) {
4343 if (empty($intv)) {
4344 continue;
4345 }
4346 $parts = explode('_', $intv);
4347 if (count($parts) >= 3) {
4348 // age intervals option found, get the first and only one
4349 $ageintervals = $optionals[$kopt];
4350 break 2;
4351 }
4352 }
4353 }
4354 }
4355
4356 if ($ageintervals) {
4357 /**
4358 * We allow price adjustments for a minimum total guests by overriding the costs
4359 * for each age interval for specific child numbers when the party covers some children.
4360 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4361 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4362 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4363 *
4364 * @since 1.13.5
4365 */
4366 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4367 $oparams = !is_array($oparams) ? array() : $oparams;
4368 $adults = (int)$adults;
4369 $children = (int)$children;
4370 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4371 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4372 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4373 if ($children_to_pay < $children) {
4374 // compose string for free age intervals
4375 $intervals = explode(';;', $ageintervals['ageintervals']);
4376 foreach ($intervals as $k => $v) {
4377 $parts = explode('_', $v);
4378 // set cost to 0 (key = 2)
4379 $parts[2] = '0';
4380 $intervals[$k] = implode('_', $parts);
4381 }
4382 $free_ageintervals = implode(';;', $intervals);
4383 for ($i = 1; $i <= $children; $i++) {
4384 if (($children - $i + 1) <= $children_to_pay) {
4385 // costs for this Nth last child should be regular as this child should pay
4386 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4387 } else {
4388 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4389 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4390 }
4391 }
4392 }
4393 }
4394
4395 // remove age intervals from regular options
4396 foreach ($optionals as $kopt => $opt) {
4397 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4398 // unset the option of type age intervals from the regular options
4399 unset($optionals[$kopt]);
4400 }
4401 }
4402 }
4403
4404 // return the filtered list of regular options and age intervals options
4405 return [$optionals, $ageintervals];
4406 }
4407
4408 /**
4409 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4410 * This is because some children in the party may not need to pay anything due to the min guests.
4411 *
4412 * @param array $optional the option full record to parse.
4413 * @param int $adults the number of adults in the room requested.
4414 * @param int $children the number of children in the room requested.
4415 *
4416 * @return array associative array with default age intervals and override strings.
4417 *
4418 * @since 1.13.5
4419 */
4420 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4421 {
4422 if (!is_array($optional)) {
4423 $optional = array();
4424 }
4425
4426 $overrides = array(
4427 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4428 );
4429
4430 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4431 $oparams = !is_array($oparams) ? array() : $oparams;
4432 $adults = (int)$adults;
4433 $children = (int)$children;
4434
4435 if (!count($oparams)) {
4436 return $overrides;
4437 }
4438
4439 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4440 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4441 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4442 if ($children_to_pay < $children) {
4443 // compose string for free age intervals
4444 $intervals = explode(';;', $optional['ageintervals']);
4445 foreach ($intervals as $k => $v) {
4446 $parts = explode('_', $v);
4447 // set cost to 0 (key = 2)
4448 $parts[2] = '0';
4449 $intervals[$k] = implode('_', $parts);
4450 }
4451 $free_ageintervals = implode(';;', $intervals);
4452 for ($i = 1; $i <= $children; $i++) {
4453 if (($children - $i + 1) <= $children_to_pay) {
4454 // costs for this Nth last child should be regular as this child should pay
4455 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4456 } else {
4457 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4458 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4459 }
4460 }
4461 }
4462 }
4463
4464 return $overrides;
4465 }
4466
4467 /**
4468 * Returns the number/index of the children being parsed given the full list of room options string,
4469 * the current key in the array of the split room options string, the ID of the ageintervals option,
4470 * and the total number of children. This is useful for later applying the cost overrides for the
4471 * children ages through the method getOptionIntervalChildOverrides().
4472 *
4473 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4474 * @param int $optid the ID of the option of type age intervals to check.
4475 * @param int $roptkey the current position in the loop of the room options string.
4476 * @param int $children the number of children in the room requested.
4477 *
4478 * @return int the position/number of the children being parsed, -1 if not found.
4479 *
4480 * @since 1.13.5
4481 */
4482 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4483 {
4484 // default to -1 for not found
4485 $child_num = -1;
4486
4487 $valid_opt_counter = 0;
4488
4489 $roptions = explode(";", $roptstr);
4490 foreach ($roptions as $k => $opt) {
4491 if (empty($opt)) {
4492 continue;
4493 }
4494 $optvals = explode(":", $opt);
4495 /**
4496 * In some cases, like the "saveorder" task, the room option string may contain
4497 * the room number beside the option ID, separated with an underscore.
4498 * If an underscore is present, we need to split it to find the option ID.
4499 */
4500 if (strpos($optvals[0], '_') !== false) {
4501 // underscore found in room number portion, extract the option ID
4502 $rn_parts = explode('_', $optvals[0]);
4503 // 0th element is the room number in the party, 1st elem is the option ID
4504 $optvals[0] = $rn_parts[1];
4505 }
4506 //
4507 if ((int)$optvals[0] != (int)$optid) {
4508 // we are not interested into this option ID
4509 continue;
4510 }
4511 // increase counter for this option ID
4512 $valid_opt_counter++;
4513
4514 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4515 // children position found, we need to return it as a 0th base
4516 $child_num = ($valid_opt_counter - 1);
4517 break;
4518 }
4519 }
4520
4521 return $child_num;
4522 }
4523
4524 public static function getOptionIntervalsCosts($intvstr) {
4525 $optcosts = array();
4526 $intervals = explode(';;', $intvstr);
4527 foreach ($intervals as $kintv => $intv) {
4528 if (empty($intv)) continue;
4529 $parts = explode('_', $intv);
4530 if (count($parts) >= 3) {
4531 $optcosts[$kintv] = (float)$parts[2];
4532 }
4533 }
4534 return $optcosts;
4535 }
4536
4537 public static function getOptionIntervalsAges($intvstr) {
4538 $optages = array();
4539 $intervals = explode(';;', $intvstr);
4540 foreach ($intervals as $kintv => $intv) {
4541 if (empty($intv)) continue;
4542 $parts = explode('_', $intv);
4543 if (count($parts) >= 3) {
4544 $optages[$kintv] = $parts[0].' - '.$parts[1];
4545 }
4546 }
4547 return $optages;
4548 }
4549
4550 public static function getOptionIntervalsPercentage($intvstr) {
4551 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4552 $optcostspcent = array();
4553 $intervals = explode(';;', $intvstr);
4554 foreach ($intervals as $kintv => $intv) {
4555 if (empty($intv)) continue;
4556 $parts = explode('_', $intv);
4557 if (count($parts) >= 3) {
4558 //fixed amount
4559 $setval = 0;
4560 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4561 //percentage value of the room base cost (VBO 1.10)
4562 $setval = 2;
4563 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4564 //percentage value of the adults tariff
4565 $setval = 1;
4566 }
4567 $optcostspcent[$kintv] = $setval;
4568 }
4569 }
4570 return $optcostspcent;
4571 }
4572
4573 public static function dayValidTs($days, $first, $second)
4574 {
4575 $secdiff = $second - $first;
4576 $daysdiff = $secdiff / 86400;
4577 if (is_int($daysdiff)) {
4578 if ($daysdiff < 1) {
4579 $daysdiff = 1;
4580 }
4581 } else {
4582 if ($daysdiff < 1) {
4583 $daysdiff = 1;
4584 } else {
4585 $sum = floor($daysdiff) * 86400;
4586 $newdiff = $secdiff - $sum;
4587 $maxhmore = self::getHoursMoreRb() * 3600;
4588 if ($maxhmore >= $newdiff) {
4589 $daysdiff = floor($daysdiff);
4590 } else {
4591 $daysdiff = ceil($daysdiff);
4592 }
4593 }
4594 }
4595
4596 return ($daysdiff == $days);
4597 }
4598
4599 /**
4600 * Calculates the stay fee inclusive of taxes.
4601 *
4602 * @param float $cost the cost to evaluate.
4603 * @param int $idprice the rate plan ID associated.
4604 *
4605 * @return float the given cost after tax, if taxes had to be applied.
4606 */
4607 public static function sayCostPlusIva($cost, $idprice)
4608 {
4609 $ivainclusa = self::ivaInclusa();
4610 if ($ivainclusa) {
4611 return $cost;
4612 }
4613
4614 static $tax_rplans_map = [];
4615
4616 if (array_key_exists($idprice, $tax_rplans_map)) {
4617 $tax_rate = $tax_rplans_map[$idprice];
4618 } else {
4619 $dbo = JFactory::getDbo();
4620 $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 . ";";
4621 $dbo->setQuery($q);
4622 $tax_rate = $dbo->loadAssoc();
4623
4624 // cache value
4625 $tax_rplans_map[$idprice] = $tax_rate;
4626 }
4627
4628 if (!$tax_rate || empty($tax_rate['aliq'])) {
4629 return $cost;
4630 }
4631
4632 $subt = 100 + $tax_rate['aliq'];
4633 $op = ($cost * $subt / 100);
4634
4635 /**
4636 * Tax Cap implementation for prices tax excluded (most common).
4637 *
4638 * @since 1.12
4639 */
4640 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4641 $op = ($cost + $tax_rate['taxcap']);
4642 }
4643
4644 // apply rounding to avoid issues with multiple tax rates when tax excluded
4645 $formatvals = self::getNumberFormatData();
4646 $formatparts = explode(':', $formatvals);
4647 $rounded_op = round($op, (int)$formatparts[0]);
4648 if ($rounded_op > $op) {
4649 return $rounded_op;
4650 }
4651
4652 /**
4653 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4654 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4655 *
4656 * @since 1.11.1
4657 */
4658 return round($op, (int)$formatparts[0]);
4659 }
4660
4661 /**
4662 * Calculates the stay fee exclusive of taxes.
4663 *
4664 * @param float $cost the cost to evaluate.
4665 * @param int $idprice the rate plan ID associated.
4666 *
4667 * @return float the given cost before tax.
4668 */
4669 public static function sayCostMinusIva($cost, $idprice)
4670 {
4671 $ivainclusa = self::ivaInclusa();
4672 if (!$ivainclusa) {
4673 return $cost;
4674 }
4675
4676 static $tax_rplans_map = [];
4677
4678 if (array_key_exists($idprice, $tax_rplans_map)) {
4679 $tax_rate = $tax_rplans_map[$idprice];
4680 } else {
4681 $dbo = JFactory::getDbo();
4682 $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 . ";";
4683 $dbo->setQuery($q);
4684 $tax_rate = $dbo->loadAssoc();
4685
4686 // cache value
4687 $tax_rplans_map[$idprice] = $tax_rate;
4688 }
4689
4690 if (!$tax_rate || empty($tax_rate['aliq'])) {
4691 return $cost;
4692 }
4693
4694 $subt = 100 + $tax_rate['aliq'];
4695 $op = ($cost * 100 / $subt);
4696
4697 /**
4698 * Tax Cap implementation also when prices tax included.
4699 *
4700 * @since 1.12
4701 */
4702 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4703 $op = ($cost - $tax_rate['taxcap']);
4704 }
4705
4706 // apply rounding to avoid issues with multiple tax rates when tax included
4707 $formatvals = self::getNumberFormatData();
4708 $formatparts = explode(':', $formatvals);
4709 $rounded_op = round($op, (int)$formatparts[0]);
4710 if ($rounded_op < $op) {
4711 return $rounded_op;
4712 }
4713
4714 /**
4715 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4716 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4717 *
4718 * @since 1.11.1
4719 */
4720 return round($op, (int)$formatparts[0]);
4721 }
4722
4723 /**
4724 * Given an option/extra cost, determines whether taxes should be applied over it.
4725 * Can also be used to calculate taxes on the extra costs per room in the bookings
4726 *
4727 * @param float $cost the cost to evaluate, option or extra service fee.
4728 * @param int $idiva the ID of the tax rate to use to apply taxes.
4729 * @param bool $force if true, taxes will always be applied.
4730 *
4731 * @return float the given option/extra cost plus tax, if taxes had to be applied.
4732 *
4733 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4734 */
4735 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
4736 {
4737 $ivainclusa = self::ivaInclusa();
4738 if ($ivainclusa && $force !== true) {
4739 return $cost;
4740 }
4741
4742 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4743 if (!$tax_rate) {
4744 return $cost;
4745 }
4746
4747 $subt = 100 + $tax_rate['aliq'];
4748 $op = ($cost * $subt / 100);
4749
4750 /**
4751 * Tax Cap implementation for prices tax excluded (most common).
4752 *
4753 * @since 1.12
4754 */
4755 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4756 $op = ($cost + $tax_rate['taxcap']);
4757 }
4758
4759 // apply rounding to avoid issues with multiple tax rates when tax excluded
4760 $formatvals = self::getNumberFormatData();
4761 $formatparts = explode(':', $formatvals);
4762 $rounded_op = round($op, (int)$formatparts[0]);
4763 if ($rounded_op > $op) {
4764 return $rounded_op;
4765 }
4766
4767 return $op;
4768 }
4769
4770 /**
4771 * Calculates the net amount for an option/extra service.
4772 *
4773 * @param float $cost the cost to evaluate, option or extra service fee.
4774 * @param int $idiva the ID of the tax rate to use to apply taxes.
4775 *
4776 * @return float the given option/extra cost before tax, if taxes were inclusive.
4777 */
4778 public static function sayOptionalsMinusIva($cost, $idiva)
4779 {
4780 if (empty($idiva)) {
4781 return $cost;
4782 }
4783
4784 $ivainclusa = self::ivaInclusa();
4785 if (!$ivainclusa) {
4786 return $cost;
4787 }
4788
4789 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4790 if (!$tax_rate) {
4791 return $cost;
4792 }
4793
4794 $subt = 100 + $tax_rate['aliq'];
4795 $op = ($cost * 100 / $subt);
4796
4797 /**
4798 * Tax Cap implementation also when prices tax included.
4799 *
4800 * @since 1.12
4801 */
4802 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4803 $op = ($cost - $tax_rate['taxcap']);
4804 }
4805
4806 // apply rounding to avoid issues with multiple tax rates when tax included
4807 $formatvals = self::getNumberFormatData();
4808 $formatparts = explode(':', $formatvals);
4809 $rounded_op = round($op, (int)$formatparts[0]);
4810 if ($rounded_op < $op) {
4811 return $rounded_op;
4812 }
4813
4814 return $op;
4815 }
4816
4817 /**
4818 * Given a cost, determines whether taxes should be applied over it.
4819 *
4820 * @param float $cost the cost to evaluate, could be a room custom price.
4821 * @param int $idiva the ID of the tax rate to use to apply taxes.
4822 * @param bool $force if true, taxes will always be applied.
4823 *
4824 * @return float the given cost plus tax, if taxes had to be applied.
4825 *
4826 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4827 */
4828 public static function sayPackagePlusIva($cost, $idiva, $force = false)
4829 {
4830 $ivainclusa = self::ivaInclusa();
4831 if ($ivainclusa && $force !== true) {
4832 return $cost;
4833 }
4834
4835 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4836 if (!$tax_rate) {
4837 return $cost;
4838 }
4839
4840 $subt = 100 + $tax_rate['aliq'];
4841 $op = ($cost * $subt / 100);
4842
4843 /**
4844 * Tax Cap implementation for prices tax excluded (most common).
4845 *
4846 * @since 1.12
4847 */
4848 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4849 $op = ($cost + $tax_rate['taxcap']);
4850 }
4851
4852 // apply rounding to avoid issues with multiple tax rates when tax excluded
4853 $formatvals = self::getNumberFormatData();
4854 $formatparts = explode(':', $formatvals);
4855 $rounded_op = round($op, (int)$formatparts[0]);
4856 if ($rounded_op > $op) {
4857 return $rounded_op;
4858 }
4859
4860 return $op;
4861 }
4862
4863 /**
4864 * Calculates the net amount for a package.
4865 *
4866 * @param float $cost the cost to evaluate, option or extra service fee.
4867 * @param int $idiva the ID of the tax rate to use to apply taxes.
4868 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
4869 *
4870 * @return float the given cost before tax.
4871 */
4872 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
4873 {
4874 $ivainclusa = self::ivaInclusa();
4875 if (!$ivainclusa && $force_invoice_excltax !== true) {
4876 return $cost;
4877 }
4878
4879 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4880 if (!$tax_rate) {
4881 return $cost;
4882 }
4883
4884 $subt = 100 + $tax_rate['aliq'];
4885 $op = ($cost * 100 / $subt);
4886
4887 /**
4888 * Tax Cap implementation also when prices tax included.
4889 *
4890 * @since 1.12
4891 */
4892 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4893 $op = ($cost - $tax_rate['taxcap']);
4894 }
4895
4896 // apply rounding to avoid issues with multiple tax rates when tax included
4897 $formatvals = self::getNumberFormatData();
4898 $formatparts = explode(':', $formatvals);
4899 $rounded_op = round($op, (int)$formatparts[0]);
4900 if ($rounded_op < $op) {
4901 return $rounded_op;
4902 }
4903
4904 return $op;
4905 }
4906
4907 public static function getSecretLink()
4908 {
4909 $dbo = JFactory::getDbo();
4910
4911 $sid = mt_rand();
4912
4913 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
4914 $dbo->setQuery($q);
4915 $all = $dbo->loadAssocList();
4916 if ($all) {
4917 $list = [];
4918 foreach ($all as $s) {
4919 $list[] = $s['sid'];
4920 }
4921 if (in_array($sid, $list)) {
4922 while(in_array($sid, $list)) {
4923 $sid++;
4924 }
4925 }
4926 }
4927
4928 return $sid;
4929 }
4930
4931 /**
4932 * Generates a confirmation number for the given booking.
4933 * Modified the way random string-numbers are obtained to
4934 * avoid any possible brute-force attempt.
4935 *
4936 * @since 1.15.1 (J) - 1.5.4 (WP)
4937 */
4938 public static function generateConfirmNumber($oid, $update = true)
4939 {
4940 $dbo = JFactory::getDbo();
4941
4942 $confirmnumb = date('y') . $oid;
4943 for ($i = 0; $i < 8; $i++) {
4944 $confirmnumb .= rand(0, 9);
4945 }
4946
4947 if ($update) {
4948 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
4949 $dbo->setQuery($q);
4950 $dbo->execute();
4951 }
4952
4953 return $confirmnumb;
4954 }
4955
4956 public static function buildCustData($arr, $sep) {
4957 $cdata = "";
4958 foreach ($arr as $k => $e) {
4959 if (strlen($e)) {
4960 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
4961 }
4962 }
4963 return $cdata;
4964 }
4965
4966 /**
4967 * This method parses the Joomla menu object to see if a menu item of a
4968 * specific type is available, to get its ID. Useful when links should be
4969 * displayed in pages where there is no Itemid set (booking details pages).
4970 *
4971 * @param array $viewtypes list of accepted menu items
4972 * @param string $lang the optional language to use.
4973 *
4974 * @return int
4975 *
4976 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
4977 */
4978 public static function findProperItemIdType($viewtypes, $lang = null)
4979 {
4980 if (VBOPlatformDetection::isWordPress()) {
4981 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
4982
4983 $itemid = $model->best($viewtypes, $lang);
4984
4985 if ($itemid) {
4986 return $itemid;
4987 }
4988
4989 return 0;
4990 }
4991
4992 $bestitemid = 0;
4993
4994 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
4995
4996 $app = JFactory::getApplication();
4997
4998 $menu = $app->getMenu('site');
4999
5000 if (!$menu) {
5001 return 0;
5002 }
5003
5004 $menu_items = $menu->getMenu();
5005
5006 if (!$menu_items) {
5007 return 0;
5008 }
5009
5010 foreach ($menu_items as $itemid => $item) {
5011 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
5012 // proper menu item type found
5013 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
5014
5015 if (isset($item->language) && $item->language == $current_lang) {
5016 // we found the exact menu item type for the given language
5017 return $itemid;
5018 }
5019 }
5020 }
5021
5022 return $bestitemid;
5023 }
5024
5025 /**
5026 * Rewrites an internal URI that needs to be used outside of the website.
5027 * This means that the routed URI MUST start with the base path of the site.
5028 *
5029 * @param mixed $query The query string or an associative array of data.
5030 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5031 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5032 *
5033 * @return string The complete routed URI.
5034 *
5035 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5036 * routing from back-end (if available on the CMS version).
5037 */
5038 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5039 {
5040 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5041 }
5042
5043 /**
5044 * Generates an iCal file to be attached to the email message for the
5045 * customer or the administrator with some basic booking details.
5046 *
5047 * @param string $recip either admin or customer.
5048 * @param array $booking the booking array or some keys.
5049 *
5050 * @return mixed string in case of success, false otherwise.
5051 *
5052 * @since 1.12.0
5053 */
5054 public static function getEmailIcal($recip, $booking) {
5055 // load configuration setting
5056 $attachical = self::attachIcal();
5057
5058 if ($attachical === 0) {
5059 // do not attach any iCal file
5060 return false;
5061 }
5062
5063 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5064 // skip the iCal for the admin
5065 return false;
5066 }
5067
5068 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5069 // skip the iCal for the customer
5070 return false;
5071 }
5072
5073 if (strpos($recip, 'admin') !== false) {
5074 // prepare event description and summary for the admin
5075 $description = $booking['custdata'];
5076 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5077 $fname = $booking['ts'] . '.ics';
5078 } else {
5079 // event description and summary for the customer
5080 $description = '';
5081 $summary = self::getFrontTitle();
5082 $fname = 'reservation_reminder.ics';
5083 }
5084
5085 // prepare iCal head
5086 $company_name = self::getFrontTitle();
5087 $ics_str = "BEGIN:VCALENDAR\r\n" .
5088 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5089 "CALSCALE:GREGORIAN\r\n" .
5090 "VERSION:2.0\r\n";
5091 // compose iCal body
5092 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5093 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5094 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5095 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5096 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5097 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5098 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5099 $ics_str .= 'END:VEVENT'."\r\n";
5100 // close iCal file content
5101 $ics_str .= "END:VCALENDAR";
5102
5103 /**
5104 * Trigger event to allow third party plugins to overwrite the iCal file.
5105 *
5106 * @since 1.16.0 (J) - 1.6.0 (WP)
5107 */
5108 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5109
5110 if (empty($ics_str)) {
5111 return false;
5112 }
5113
5114 // store the event onto a .ics file. We use the resources folder in back-end.
5115 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5116 $fp = fopen($fpath, 'w+');
5117 $bytes = fwrite($fp, $ics_str);
5118 fclose($fp);
5119
5120 return $bytes ? $fpath : false;
5121 }
5122
5123 public static function loadEmailTemplate($booking_info = array()) {
5124 define('_VIKBOOKINGEXEC', '1');
5125 ob_start();
5126 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5127 $content = ob_get_contents();
5128 ob_end_clean();
5129 return $content;
5130 }
5131
5132 /**
5133 * Parses the raw HTML content of the booking email template.
5134 *
5135 * @param string $tmpl the raw content of the template.
5136 * @param mixed $bid int for the booking ID or booking array.
5137 * @param array $rooms list of rooms booked and translated.
5138 * @param array $rates list of translated rates for the booked rooms.
5139 * @param array $options list of translated options booked.
5140 * @param float [$total] the booking total amount (in case it has changed).
5141 * @param string [$link] the booking link can be passed for the no-deposit.
5142 *
5143 * @return string the HTML content of the parsed email template.
5144 *
5145 * @since 1.13 with different arguments.
5146 */
5147 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5148 {
5149 $app = JFactory::getApplication();
5150 $dbo = JFactory::getDbo();
5151 $vbo_tn = self::getTranslator();
5152
5153 // availability helper
5154 $av_helper = self::getAvailabilityInstance();
5155
5156 // get necessary values
5157 if (is_array($bid)) {
5158 // we got the full booking record
5159 $order_info = $bid;
5160 $bid = $order_info['id'];
5161 } else {
5162 $order_info = array();
5163 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5164 $dbo->setQuery($q);
5165 $order_info = $dbo->loadAssoc();
5166 if (!$order_info) {
5167 throw new Exception('Booking not found', 404);
5168 }
5169 }
5170
5171 $tars_info = array();
5172 $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'] . ";";
5173 $dbo->setQuery($q);
5174 $tars_info = $dbo->loadAssocList();
5175 if (!$tars_info) {
5176 throw new Exception('No rooms found', 404);
5177 }
5178
5179 /**
5180 * Split stay reservation.
5181 *
5182 * @since 1.16.0 (J) - 1.6.0 (WP)
5183 */
5184 $room_stay_dates = [];
5185 if ($order_info['split_stay']) {
5186 if ($order_info['status'] == 'confirmed') {
5187 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5188 } else {
5189 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5190 }
5191 // immediately count the number of nights of stay for each split room
5192 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5193 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5194 // overwrite values for compatibility with non-confirmed bookings
5195 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5196 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5197 }
5198 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5199 // overwrite the whole array
5200 $room_stay_dates[$sps_r_k] = $sps_r_v;
5201 }
5202 }
5203
5204 // check if the language in use is the same as the one used during the checkout
5205 $lang = JFactory::getLanguage();
5206 if (!empty($order_info['lang'])) {
5207 if ($lang->getTag() != $order_info['lang']) {
5208 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5209 if (VBOPlatformDetection::isJoomla()) {
5210 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5211 }
5212 }
5213 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5214 // force the translation to start because contents should be translated
5215 $vbo_tn::$force_tolang = $order_info['lang'];
5216 }
5217 }
5218
5219 // values for replacements
5220 $company_name = self::getFrontTitle();
5221 $currencyname = self::getCurrencyName();
5222 $sitelogo = self::getSiteLogo();
5223 $footermess = self::getFooterOrdMail($vbo_tn);
5224 $dateformat = self::getDateFormat();
5225 $datesep = self::getDateSeparator();
5226 if ($dateformat == "%d/%m/%Y") {
5227 $df = 'd/m/Y';
5228 } elseif ($dateformat == "%m/%d/%Y") {
5229 $df = 'm/d/Y';
5230 } else {
5231 $df = 'Y/m/d';
5232 }
5233 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5234 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5235 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5236 $customer_info = nl2br($order_info['custdata']);
5237 $company_logo = '';
5238 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5239 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5240 }
5241 if ($order_info['status'] == 'cancelled') {
5242 $confirmnumber = '';
5243 $status_str = JText::translate('VBCANCELLED');
5244 } elseif ($order_info['status'] == 'standby') {
5245 $confirmnumber = '';
5246 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5247 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5248 // this is an inquiry reservation
5249 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5250 }
5251 } else {
5252 $confirmnumber = $order_info['confirmnumber'];
5253 $status_str = JText::translate('VBCOMPLETED');
5254 }
5255 // booking total amount
5256 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5257 // booking link
5258 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5259 if (is_null($link)) {
5260 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5261 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5262 }
5263
5264 // raw HTML content
5265 $parsed = $tmpl;
5266
5267 /**
5268 * Parse all conditional text rules.
5269 *
5270 * @since 1.14 (J) - 1.4.0 (WP)
5271 */
5272 self::getConditionalRulesInstance()
5273 ->set(array('booking', 'rooms'), array($order_info, $rooms))
5274 ->parseTokens($parsed);
5275 //
5276
5277 // special tokens (tags) replacement
5278 $parsed = str_replace("{logo}", $company_logo, $parsed);
5279 $parsed = str_replace("{company_name}", $company_name, $parsed);
5280 $parsed = str_replace("{order_id}", $order_info['id'], $parsed);
5281 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5282 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5283 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5284 $parsed = str_replace("{order_status}", $status_str, $parsed);
5285 $parsed = str_replace("{order_date}", $create_date, $parsed);
5286
5287 // PIN Code
5288 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5289 $cpin = self::getCPinInstance();
5290 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5291 if (!empty($customer_pin)) {
5292 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5293 }
5294 }
5295
5296 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5297 // Confirmation Number
5298 if (strlen($confirmnumber) > 0) {
5299 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5300 } else {
5301 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5302 }
5303 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5304 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5305
5306 $roomsnum = count($rooms);
5307 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5308 $roomstr = "";
5309 // Rooms Distinctive Features
5310 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5311 //
5312 foreach ($rooms as $num => $r) {
5313 $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/>";
5314 // Rooms Distinctive Features
5315 if (is_array($matches[1]) && count($matches[1])) {
5316 $distinctive_features = array();
5317 $rparams = (array)json_decode($r['params'], true);
5318 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'])) {
5319 $distinctive_features = $rparams['features'][$r['roomindex']];
5320 }
5321 $docheck = (count($distinctive_features) > 0);
5322 foreach ($matches[1] as $reqf) {
5323 $feature_found = false;
5324 if ($docheck) {
5325 foreach ($distinctive_features as $dfk => $dfv) {
5326 if (stripos($dfk, $reqf) !== false) {
5327 $feature_found = $dfk;
5328 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5329 break;
5330 }
5331 }
5332 }
5333 }
5334 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5335 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5336 }
5337 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5338 }
5339 }
5340 }
5341
5342 // custom fields replace
5343 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5344 if (is_array($cmatches[1]) && count($cmatches[1])) {
5345 $cfids = array();
5346 foreach ($cmatches[1] as $cfid ) {
5347 $cfids[] = $cfid;
5348 }
5349 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5350 $dbo->setQuery($q);
5351 $cfields = $dbo->loadAssocList();
5352 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5353 $cfmap = array();
5354 foreach ($cfields as $cf) {
5355 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5356 }
5357 $cfmapreplace = array();
5358 $partsreceived = explode("\n", $order_info['custdata']);
5359 if (count($partsreceived) > 0) {
5360 foreach ($partsreceived as $pst) {
5361 if (!empty($pst)) {
5362 $tmpdata = explode(":", $pst);
5363 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5364 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5365 }
5366 }
5367 }
5368 }
5369 foreach ($cmatches[1] as $cfid ) {
5370 if (array_key_exists($cfid, $cfmapreplace)) {
5371 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5372 } else {
5373 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5374 }
5375 }
5376 }
5377
5378 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5379 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5380 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5381
5382 // order details
5383 $orderdetails = "";
5384 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5385 foreach ($rooms as $num => $r) {
5386 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5387 $use_room_ind = $num - $room_loop_ind;
5388 $split_stay_str = '';
5389 $split_stay_info = [];
5390 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) {
5391 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5392 $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']);
5393 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5394 }
5395
5396 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5397 $faredets = explode(":", $expdet[0]);
5398 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5399 if (!empty($expdet[1])) {
5400 $attrfaredets = explode(":", $expdet[1]);
5401 if (strlen($attrfaredets[1]) > 0) {
5402 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5403 }
5404 }
5405 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5406 $orderdetails .= '<div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($fareprice).'</span></div></div>';
5407 // options
5408 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5409 foreach ($options[$num] as $oo) {
5410 $expopts = explode("\n", $oo);
5411 foreach ($expopts as $optinfo) {
5412 if (!empty($optinfo)) {
5413 $splitopt = explode(":", $optinfo);
5414 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5415 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($optprice).'</span></div></div>';
5416 }
5417 }
5418 }
5419 }
5420 //
5421 if ($roomsnum > 1 && $num < $roomsnum) {
5422 $orderdetails .= '<br/>';
5423 }
5424 }
5425 //
5426 // coupon
5427 if (!empty($order_info['coupon'])) {
5428 $expcoupon = explode(";", $order_info['coupon']);
5429 $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>';
5430 }
5431 //
5432 // discount payment method
5433 if ($order_info['status'] != 'cancelled') {
5434 $idpayment = $order_info['idpayment'];
5435 if (!empty($idpayment)) {
5436 $exppay = explode('=', $idpayment);
5437 $payment = self::getPayment($exppay[0], $vbo_tn);
5438 if (is_array($payment)) {
5439 if ($payment['charge'] > 0.00 && $payment['ch_disc'] != 1) {
5440 // Discount (not charge)
5441 if ($payment['val_pcent'] == 1) {
5442 // fixed value
5443 $total -= $payment['charge'];
5444 $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>';
5445 } else {
5446 // percent value
5447 $percent_disc = $total * $payment['charge'] / 100;
5448 $total -= $percent_disc;
5449 $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>';
5450 }
5451 }
5452 }
5453 }
5454 }
5455 //
5456 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5457 //
5458 $parsed = str_replace("{order_total}", $currencyname.' '.self::numberFormat($total), $parsed);
5459 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5460 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5461 $parsed = str_replace("{booking_link}", $link, $parsed);
5462
5463 // deposit
5464 $deposit_str = '';
5465 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5466 $percentdeposit = self::getAccPerCent();
5467 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5468 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5469 if (self::getTypeDeposit() == "fixed") {
5470 $deposit_amount = $percentdeposit;
5471 } else {
5472 $deposit_amount = $total * $percentdeposit / 100;
5473 }
5474 if ($deposit_amount > 0) {
5475 $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>';
5476 }
5477 }
5478 }
5479 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5480 //
5481 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5482 $totpaid_str = '';
5483 if ($order_info['refund'] > 0) {
5484 $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>';
5485 }
5486 if ($order_info['status'] != 'cancelled') {
5487 $tot_paid = $order_info['totpaid'];
5488 $diff_topay = (float)$total - (float)$tot_paid;
5489 if ((float)$tot_paid > 0) {
5490 $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>';
5491 // only in case the remaining balance is greater than 1 to avoid commissions issues
5492 if ($diff_topay > 1) {
5493 $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>';
5494 }
5495 }
5496 }
5497 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5498 $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>';
5499 }
5500 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5501
5502 /**
5503 * Language direction (LTR or RTL).
5504 *
5505 * @since 1.16.4 (J) - 1.6.4 (WP)
5506 */
5507 $lang_direction = 'ltr';
5508 if (!empty($order_info['lang'])) {
5509 // attempt to define the language direction and replace the special tag
5510 try {
5511 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5512 } catch (Throwable $e) {
5513 // do nothing, default to ltr
5514 $lang_direction = 'ltr';
5515 }
5516 // static map for Arabic and Hebrew
5517 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5518 }
5519 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5520 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5521 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5522
5523 /**
5524 * Replace static floating styles depending on language direction (LTR or RTL).
5525 * For BC with older email template sources, we need to keep the inline style
5526 * for the floating of certain elements, which looks good only on LTR.
5527 *
5528 * @since 1.16.4 (J) - 1.6.4 (WP)
5529 */
5530 if ($lang_direction === 'rtl') {
5531 // get rid of inline static style declaration because the template uses CSS classes
5532 $parsed = str_replace("float: right;", '', $parsed);
5533 }
5534
5535 return $parsed;
5536 }
5537
5538 /**
5539 * New method for sending booking email messages
5540 * to the guest or to the administrator(s).
5541 *
5542 * @param int $bid the booking ID.
5543 * @param array $for guest, admin or a custom email address.
5544 * @param bool $send whether to send or return the HTML message.
5545 * @param bool $no_config if true, no configuration settings will be used for the status.
5546 * @param string $type optional type to indicate a booking modification.
5547 *
5548 * @return mixed True or False depending on the result or HTML string for the preview.
5549 *
5550 * @since 1.13 (J) - 1.3 (WP)
5551 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5552 * the reservation status is pending and against the configuration.
5553 * @since 1.16.7 (J) - 1.6.7 (WP) added argument $type.
5554 */
5555 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false, $type = '')
5556 {
5557 $dbo = JFactory::getDbo();
5558 $app = JFactory::getApplication();
5559
5560 $vbo_tn = self::getTranslator();
5561 $av_helper = self::getAvailabilityInstance();
5562
5563 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5564 $dbo->setQuery($q);
5565 $booking = $dbo->loadAssoc();
5566 if (!$booking) {
5567 return false;
5568 }
5569
5570 $result = false;
5571
5572 // check if the language in use is the same as the one used during the checkout
5573 $lang = JFactory::getLanguage();
5574 if (!empty($booking['lang'])) {
5575 if ($lang->getTag() != $booking['lang']) {
5576 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
5577 if (VBOPlatformDetection::isJoomla()) {
5578 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
5579 }
5580 }
5581 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
5582 // force the translation to start because contents should be translated
5583 $vbo_tn::$force_tolang = $booking['lang'];
5584 }
5585 }
5586
5587 // load rooms booked
5588 $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;";
5589 $dbo->setQuery($q);
5590 $ordersrooms = $dbo->loadAssocList();
5591 if (!$ordersrooms) {
5592 return false;
5593 }
5594 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
5595
5596 /**
5597 * Split stay reservation.
5598 *
5599 * @since 1.16.0 (J) - 1.6.0 (WP)
5600 */
5601 $room_stay_dates = [];
5602 if ($booking['split_stay']) {
5603 if ($booking['status'] == 'confirmed') {
5604 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
5605 } else {
5606 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
5607 }
5608 // immediately count the number of nights of stay for each split room
5609 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5610 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5611 // overwrite values for compatibility with non-confirmed bookings
5612 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5613 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5614 }
5615 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5616 // overwrite the whole array
5617 $room_stay_dates[$sps_r_k] = $sps_r_v;
5618 }
5619 }
5620
5621 $rooms = array();
5622 $tars = array();
5623 $is_package = !empty($booking['pkg']) ? true : false;
5624 $ftitle = self::getFrontTitle();
5625 $currencyname = self::getCurrencyName();
5626 foreach ($ordersrooms as $kor => $or) {
5627 $num = $kor + 1;
5628 $rooms[$num] = $or;
5629 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5630 // package or custom cost set from the back-end
5631 continue;
5632 }
5633
5634 // determine the days to consider for the count of the availability
5635 $room_nights = $booking['days'];
5636 $room_checkin = $booking['checkin'];
5637 $room_checkout = $booking['checkout'];
5638 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5639 $room_nights = $room_stay_dates[$kor]['nights'];
5640 $room_checkin = $room_stay_dates[$kor]['checkin'];
5641 $room_checkout = $room_stay_dates[$kor]['checkout'];
5642 }
5643
5644 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
5645 $dbo->setQuery($q);
5646 $tar = $dbo->loadAssocList();
5647 if (!$tar) {
5648 // tariff not found
5649 if (self::isAdmin()) {
5650 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
5651 }
5652 continue;
5653 }
5654
5655 // apply seasonal rates
5656 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
5657
5658 // different usage
5659 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
5660 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
5661 // Occupancy Override
5662 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
5663 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
5664 //
5665 if (is_array($diffusageprice)) {
5666 // set a charge or discount to the price(s) for the different usage of the room
5667 foreach ($tar as $kpr => $vpr) {
5668 $tar[$kpr]['diffusage'] = $or['adults'];
5669 if ($diffusageprice['chdisc'] == 1) {
5670 // charge
5671 if ($diffusageprice['valpcent'] == 1) {
5672 // fixed value
5673 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5674 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5675 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
5676 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5677 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
5678 } else {
5679 // percentage value
5680 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5681 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
5682 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
5683 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5684 $tar[$kpr]['cost'] = $aduseval;
5685 }
5686 } else {
5687 // discount
5688 if ($diffusageprice['valpcent'] == 1) {
5689 // fixed value
5690 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5691 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5692 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
5693 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5694 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
5695 } else {
5696 // percentage value
5697 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5698 $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);
5699 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
5700 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5701 $tar[$kpr]['cost'] = $aduseval;
5702 }
5703 }
5704 }
5705 }
5706 }
5707 //
5708 $tars[$num] = $tar[0];
5709 }
5710
5711 $secdiff = $booking['checkout'] - $booking['checkin'];
5712 $daysdiff = $secdiff / 86400;
5713 if (is_int($daysdiff)) {
5714 if ($daysdiff < 1) {
5715 $daysdiff = 1;
5716 }
5717 } else {
5718 if ($daysdiff < 1) {
5719 $daysdiff = 1;
5720 } else {
5721 $sum = floor($daysdiff) * 86400;
5722 $newdiff = $secdiff - $sum;
5723 $maxhmore = self::getHoursMoreRb() * 3600;
5724 if ($maxhmore >= $newdiff) {
5725 $daysdiff = floor($daysdiff);
5726 } else {
5727 $daysdiff = ceil($daysdiff);
5728 }
5729 }
5730 }
5731
5732 $isdue = 0;
5733 $pricestr = array();
5734 $optstr = array();
5735 foreach ($ordersrooms as $kor => $or) {
5736 $num = $kor + 1;
5737
5738 // determine the days to consider for the count of the availability
5739 $room_nights = $booking['days'];
5740 $room_checkin = $booking['checkin'];
5741 $room_checkout = $booking['checkout'];
5742 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5743 $room_nights = $room_stay_dates[$kor]['nights'];
5744 $room_checkin = $room_stay_dates[$kor]['checkin'];
5745 $room_checkout = $room_stay_dates[$kor]['checkout'];
5746 }
5747
5748 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5749 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
5750 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
5751 $isdue += $calctar;
5752 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
5753 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
5754 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5755 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
5756 $tars[$num]['calctar'] = $calctar;
5757 $isdue += $calctar;
5758 $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'] : "");
5759 }
5760
5761 if (!empty($or['optionals'])) {
5762 $stepo = explode(";", $or['optionals']);
5763 foreach ($stepo as $roptkey => $oo) {
5764 if (empty($oo)) {
5765 continue;
5766 }
5767 $stept = explode(":", $oo);
5768 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
5769 $dbo->setQuery($q);
5770 $actopt = $dbo->loadAssocList();
5771 if ($actopt) {
5772 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
5773 $chvar = '';
5774 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
5775 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
5776 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
5777 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
5778 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
5779 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
5780 $agestept = explode('-', $stept[1]);
5781 $stept[1] = $agestept[0];
5782 $chvar = $agestept[1];
5783 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
5784 // percentage value of the adults tariff
5785 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5786 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5787 } else {
5788 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5789 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5790 }
5791 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
5792 // VBO 1.10 - percentage value of room base cost
5793 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5794 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5795 } else {
5796 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
5797 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5798 }
5799 }
5800 $actopt[0]['chageintv'] = $chvar;
5801 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
5802 $actopt[0]['quan'] = $stept[1];
5803 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
5804 } else {
5805 $actopt[0]['quan'] = $stept[1];
5806 // VBO 1.11 - options percentage cost of the room total fee
5807 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5808 $deftar_basecosts = $or['cust_cost'];
5809 } else {
5810 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5811 }
5812 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
5813 //
5814 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
5815 }
5816 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
5817 $realcost = $actopt[0]['maxprice'];
5818 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
5819 $realcost = $actopt[0]['maxprice'] * $stept[1];
5820 }
5821 }
5822 if ($actopt[0]['perperson'] == 1) {
5823 $realcost = $realcost * $or['adults'];
5824 }
5825 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
5826 $isdue += $tmpopr;
5827 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
5828 }
5829 }
5830 }
5831
5832 // custom extra costs
5833 if (!empty($or['extracosts'])) {
5834 $cur_extra_costs = json_decode($or['extracosts'], true);
5835 foreach ($cur_extra_costs as $eck => $ecv) {
5836 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
5837 $isdue += $ecplustax;
5838 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
5839 }
5840 }
5841 }
5842
5843 // force the original total amount if rates have changed
5844 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
5845 $isdue = $booking['total'];
5846 }
5847
5848 // mail subject
5849 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
5850 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
5851
5852 // inject the recipient of the message for the template
5853 $booking['for'] = $for;
5854
5855 // load template file that will get $booking as variable
5856 $tmpl = self::loadEmailTemplate($booking);
5857
5858 // parse email template
5859 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
5860 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
5861
5862 if ($send !== true) {
5863 // return the content of the email message parsed
5864 return $hmess;
5865 }
5866
5867 // when the message can be sent
5868 $sendwhen = self::getSendEmailWhen();
5869
5870 if ($no_config) {
5871 // force the configuration setting to send the message no matter what's the booking status
5872 $sendwhen = 1;
5873 }
5874
5875 // send the message
5876 foreach ($for as $who) {
5877 $use_subject = $subject;
5878 $recipients = array();
5879 $attachments = self::addEmailAttachment(null);
5880 $attach_ical = false;
5881 $force_replyto = null;
5882 if (strpos($who, '@') !== false) {
5883 // send email to custom email address
5884 array_push($recipients, trim($who));
5885 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
5886 // send email to the customer
5887 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5888 continue;
5889 }
5890 array_push($recipients, $booking['custmail']);
5891 /**
5892 * Check whether an iCal should be attached for the customer.
5893 *
5894 * @since 1.12.0
5895 */
5896 $attach_ical = self::getEmailIcal('customer', $booking);
5897 } elseif (stripos($who, 'admin') !== false) {
5898 // send email to the administrator(s)
5899 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5900 continue;
5901 }
5902 $use_subject = $subject . ' #' . $booking['id'];
5903 if (!strcasecmp($type, 'modified')) {
5904 $use_subject = JText::sprintf('VBOMODDEDORDER', $booking['id']);
5905 }
5906 $adminemail = self::getAdminMail();
5907 $extra_admin_recipients = self::addAdminEmailRecipient(null);
5908 if (empty($adminemail) && empty($extra_admin_recipients)) {
5909 // Prevent Joomla Exceptions that would stop the script execution
5910 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
5911 continue;
5912 }
5913 if (strpos($adminemail, ',') !== false) {
5914 // multiple addresses
5915 $adminemails = explode(',', $adminemail);
5916 foreach ($adminemails as $am) {
5917 if (strpos($am, '@') !== false) {
5918 array_push($recipients, trim($am));
5919 }
5920 }
5921 } else {
5922 // single address
5923 array_push($recipients, trim($adminemail));
5924 }
5925
5926 // merge extra recipients
5927 $recipients = array_merge($recipients, $extra_admin_recipients);
5928
5929 // admin should reply to the customer
5930 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
5931
5932 /**
5933 * Check whether an iCal should be attached for the admin.
5934 *
5935 * @since 1.2.0
5936 */
5937 $attach_ical = self::getEmailIcal('admin', array(
5938 'ts' => $booking['ts'],
5939 'custdata' => $booking['custdata'],
5940 'checkin' => $booking['checkin'],
5941 'checkout' => $booking['checkout'],
5942 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
5943 ));
5944 }
5945
5946 // send the message, recipients should always be an array to support multiple admin addresses
5947
5948 // get sender e-mail
5949 $adsendermail = VBOFactory::getConfig()->get('senderemail');
5950
5951 // init mail data
5952 $mail = new VBOMailWrapper([
5953 'sender' => [$adsendermail, $ftitle],
5954 'recipient' => $recipients,
5955 'bcc' => self::addAdminEmailRecipient(null, true),
5956 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
5957 'subject' => $use_subject,
5958 'content' => $hmess,
5959 'attachments' => $attachments,
5960 ]);
5961
5962 if ($attach_ical !== false && $booking['status'] != 'standby') {
5963 // attach iCal file
5964 $mail->addAttachment($attach_ical);
5965 }
5966
5967 /**
5968 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
5969 *
5970 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
5971 *
5972 * @since 1.15.4 (J) - 1.5.10 (WP)
5973 */
5974 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
5975
5976 // send e-mail
5977 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
5978
5979 // unlink iCal file
5980 if ($attach_ical !== false) {
5981 @unlink($attach_ical);
5982 }
5983 }
5984
5985 return $result;
5986 }
5987
5988 /**
5989 * This method allows to add one or more recipient email
5990 * addresses for the next queue of email sending for the admin.
5991 * This method can be used in the template file for the customer
5992 * email to register an additional email address, maybe when a
5993 * specific room-type is booked.
5994 * The methods sending the email messages are supposed to call this
5995 * method by passing no arguments to obtain the extra addresses set.
5996 *
5997 * @param mixed $email null, string or array of email address(es).
5998 * @param bool $bcc if true, addresses will be used as bcc.
5999 * @param bool $reset if true, the queues will be emptied.
6000 *
6001 * @return array the current extra recipients or bcc addresses set.
6002 *
6003 * @since 1.13 (J) - 1.3.0 (WP)
6004 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
6005 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6006 */
6007 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
6008 {
6009 static $extra_recipients = [];
6010 static $extra_bcc = [];
6011
6012 if ($reset) {
6013 $extra_recipients = [];
6014 $extra_bcc = [];
6015 }
6016
6017 if (!empty($email)) {
6018 if (is_scalar($email)) {
6019 if ($bcc) {
6020 array_push($extra_bcc, $email);
6021 } else {
6022 array_push($extra_recipients, $email);
6023 }
6024 } else {
6025 if ($bcc) {
6026 $extra_bcc = array_merge($extra_bcc, $email);
6027 } else {
6028 $extra_recipients = array_merge($extra_recipients, $email);
6029 }
6030 }
6031 }
6032
6033 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6034 }
6035
6036 /**
6037 * This method serves to add one or more attachments to the
6038 * next queue of email sending for the admin.
6039 * The methods sending the email messages are supposed to call this
6040 * method by passing a null argument to obtain the attachments set.
6041 *
6042 * @param mixed $file null or string with path to file to attach.
6043 * @param bool $reset if true, the queue will be emptied.
6044 *
6045 * @return array the current attachments set.
6046 *
6047 * @since 1.14
6048 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6049 */
6050 public static function addEmailAttachment($file, $reset = false)
6051 {
6052 static $extra_attachments = [];
6053
6054 if ($reset) {
6055 $extra_attachments = [];
6056 }
6057
6058 if (!empty($file)) {
6059 if (is_scalar($file)) {
6060 array_push($extra_attachments, $file);
6061 } else {
6062 $extra_attachments = array_merge($extra_attachments, $file);
6063 }
6064 }
6065
6066 return array_unique($extra_attachments);
6067 }
6068
6069 /**
6070 * This method is called whenever some rooms get booked.
6071 * It checks whether the rooms involved have shared calendars.
6072 * If some are found, then also such rooms will be occupied.
6073 *
6074 * @param int $bid the booking ID.
6075 * @param array $roomids the list of rooms booked.
6076 * @param int $checkin checkin timestamp.
6077 * @param int $checkout checkout timestamp.
6078 *
6079 * @return boolean true if some other cals were occupied, false otherwise.
6080 *
6081 * @since 1.13 (J) - 1.3.0 (WP)
6082 *
6083 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6084 */
6085 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6086 {
6087 $dbo = JFactory::getDbo();
6088 $bid = (int)$bid;
6089
6090 // availability helper
6091 $av_helper = self::getAvailabilityInstance();
6092
6093 // check for split stay rooms reservation
6094 $known_split_stay = false;
6095 $is_split_stay = 0;
6096
6097 if (empty($checkin) || empty($checkout)) {
6098 // get checkin and checkout timestamps from booking
6099 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6100 $dbo->setQuery($q);
6101 $dbo->execute();
6102 if (!$dbo->getNumRows()) {
6103 // booking not found
6104 return false;
6105 }
6106 $bdata = $dbo->loadAssoc();
6107 $checkin = $bdata['checkin'];
6108 $checkout = $bdata['checkout'];
6109 $is_split_stay = $bdata['split_stay'];
6110 // turn flag on
6111 $known_split_stay = true;
6112 }
6113
6114 if (!$known_split_stay) {
6115 // we need to query the db to access this information
6116 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6117 $dbo->setQuery($q, 0, 1);
6118 $dbo->execute();
6119 if ($dbo->getNumRows()) {
6120 $is_split_stay = (int)$dbo->loadResult();
6121 }
6122 // turn flag on
6123 $known_split_stay = true;
6124 }
6125
6126 // check split stay room records
6127 $room_stay_dates = [];
6128 if ($is_split_stay) {
6129 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6130 // immediately count the number of nights of stay for each split room
6131 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6132 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6133 // overwrite values for compatibility with non-confirmed bookings
6134 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6135 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6136 }
6137 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6138 // overwrite the whole array
6139 $room_stay_dates[$sps_r_k] = $sps_r_v;
6140 }
6141 }
6142
6143 // get the rooms booked
6144 if (!count($roomids)) {
6145 // get the IDs of all rooms booked
6146 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6147 $dbo->setQuery($q);
6148 $orr = $dbo->loadAssocList();
6149 if ($orr) {
6150 foreach ($orr as $or) {
6151 array_push($roomids, $or['idroom']);
6152 }
6153 }
6154 }
6155 if (!count($roomids) || empty($bid)) {
6156 // unable to proceed
6157 return false;
6158 }
6159 $roomids = array_unique($roomids);
6160
6161 // build room stay dates map in case of split stay
6162 $room_split_stay_map = [];
6163 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6164 // we have unique room IDs in a split stay reservation for more than one room
6165 foreach ($room_stay_dates as $room_stay_date) {
6166 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6167 }
6168 }
6169
6170 // get rooms involved
6171 $involved = [];
6172 $involved_stay_map = [];
6173 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6174 $dbo->setQuery($q);
6175 $rooms_found = $dbo->loadAssocList();
6176 if (!$rooms_found) {
6177 // no rooms involved that need their calendars updated
6178 return false;
6179 }
6180 foreach ($rooms_found as $rf) {
6181 if (!in_array($rf['mainroom'], $roomids)) {
6182 // push room ID
6183 array_push($involved, $rf['mainroom']);
6184 // check for specific stay dates in connected room
6185 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6186 // split stay information available
6187 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6188 }
6189 }
6190 if (!in_array($rf['childroom'], $roomids)) {
6191 // push room ID
6192 array_push($involved, $rf['childroom']);
6193 // check for specific stay dates in connected room
6194 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6195 // split stay information available
6196 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6197 }
6198 }
6199 }
6200 $involved = array_unique($involved);
6201 if (!count($involved)) {
6202 // no rooms involved
6203 return false;
6204 }
6205
6206 // turnover seconds
6207 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6208
6209 // occupy the calendars for the involved rooms found
6210 $bids_generated = [];
6211 foreach ($involved as $rid) {
6212 // determine the values to use
6213 $room_checkin = (int)$checkin;
6214 $room_checkout = (int)$checkout;
6215 $room_realback = ($checkout + $turnover_secs);
6216 if (isset($involved_stay_map[$rid])) {
6217 // split stay dates available
6218 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6219 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6220 $room_realback = ($room_checkout + $turnover_secs);
6221 }
6222
6223 // occupy record in room with shared calendar
6224 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6225 $dbo->setQuery($q);
6226 $dbo->execute();
6227 array_push($bids_generated, $dbo->insertid());
6228 }
6229
6230 // store busy relations created
6231 foreach ($bids_generated as $busyid) {
6232 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6233 $dbo->setQuery($q);
6234 $dbo->execute();
6235 }
6236
6237 return true;
6238 }
6239
6240 /**
6241 * This method is needed whenever a booking gets modified by
6242 * adding or removing rooms. This way we reset (remove) all
6243 * busy records that were previously stored due to a shared
6244 * calendar. The correct relations should then be re-created
6245 * by calling updateSharedCalendars().
6246 *
6247 * @param int $bid the booking ID.
6248 *
6249 * @return boolean true if some records were cleaned.
6250 *
6251 * @see updateSharedCalendars() should be called after.
6252 *
6253 * @since 1.3.0
6254 */
6255 public static function cleanSharedCalendarsBusy($bid) {
6256 $dbo = JFactory::getDbo();
6257 $bid = (int)$bid;
6258 // get all the occupied records due to shared calendars for this booking
6259 $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;";
6260 $dbo->setQuery($q);
6261 $allbusy = $dbo->loadAssocList();
6262 if (!$allbusy) {
6263 return false;
6264 }
6265
6266 $busy_ids = array();
6267 foreach ($allbusy as $b) {
6268 // push busy ID to be removed later
6269 array_push($busy_ids, $b['id']);
6270 // delete the current busy ID-booking ID relation for this shared calendar
6271 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6272 $dbo->setQuery($q);
6273 $dbo->execute();
6274 }
6275 if (count($busy_ids)) {
6276 // delete all busy records due to shared calendars
6277 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6278 $dbo->setQuery($q);
6279 $dbo->execute();
6280 }
6281
6282 return true;
6283 }
6284
6285 public static function sendJutility() {
6286 //deprecated in VBO 1.10
6287 $dbo = JFactory::getDbo();
6288 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6289 $dbo->setQuery($q);
6290 $dbo->execute();
6291 $s = $dbo->loadAssocList();
6292 return (intval($s[0]['setting']) == 1 ? true : false);
6293 }
6294
6295 public static function getCategoryName($idcat, $vbo_tn = null) {
6296 $dbo = JFactory::getDbo();
6297 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6298 $dbo->setQuery($q);
6299 $dbo->execute();
6300 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6301 if (is_object($vbo_tn) && count($p) > 0) {
6302 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6303 }
6304 return count($p) > 0 ? $p[0]['name'] : '';
6305 }
6306
6307 public static function loadAdultsDiff($idroom, $adults)
6308 {
6309 static $obp_map = [];
6310
6311 $map_sign = "{$idroom}:{$adults}";
6312
6313 if (array_key_exists($map_sign, $obp_map)) {
6314 return $obp_map[$map_sign];
6315 }
6316
6317 $dbo = JFactory::getDbo();
6318
6319 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6320 $dbo->setQuery($q, 0, 1);
6321 $obp_diff = $dbo->loadAssoc();
6322 if ($obp_diff) {
6323 // cache value and return it
6324 $obp_map[$map_sign] = $obp_diff;
6325
6326 return $obp_diff;
6327 }
6328
6329 // cache value and return it
6330 $obp_map[$map_sign] = null;
6331
6332 return null;
6333 }
6334
6335 public static function loadRoomAdultsDiff($idroom)
6336 {
6337 $dbo = JFactory::getDbo();
6338
6339 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6340 $dbo->setQuery($q);
6341 $diff = $dbo->loadAssocList();
6342 if ($diff) {
6343 $roomdiff = [];
6344 foreach ($diff as $v) {
6345 $roomdiff[$v['adults']] = $v;
6346 }
6347
6348 return $roomdiff;
6349 }
6350
6351 return [];
6352 }
6353
6354 public static function occupancyOverrideExists($tar, $adults) {
6355 foreach ($tar as $k => $v) {
6356 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6357 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6358 return $v['occupancy_ovr'][$adults];
6359 }
6360 }
6361 }
6362 return false;
6363 }
6364
6365 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6366 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6367 $charges = array();
6368 if (!($children > 0) || !(count($ages) > 0)) {
6369 return $charges;
6370 }
6371 $dbo = JFactory::getDbo();
6372 $id_options = array();
6373 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6374 $dbo->setQuery($q);
6375 $dbo->execute();
6376 if ($dbo->getNumRows() > 0) {
6377 $assocs = $dbo->loadAssocList();
6378 foreach ($assocs as $opts) {
6379 if (!empty($opts['idopt'])) {
6380 $r_ido = explode(';', rtrim($opts['idopt']));
6381 foreach ($r_ido as $ido) {
6382 if (!empty($ido) && !in_array($ido, $id_options)) {
6383 $id_options[] = $ido;
6384 }
6385 }
6386 }
6387 }
6388 }
6389 if (count($id_options) > 0) {
6390 $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;";
6391 $dbo->setQuery($q);
6392 $dbo->execute();
6393 if ($dbo->getNumRows() > 0) {
6394 $ageintervals = $dbo->loadAssocList();
6395 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6396 $age_range = array();
6397 foreach ($split_ages as $kg => $spage) {
6398 if (empty($spage)) {
6399 continue;
6400 }
6401 $parts = explode('_', $spage);
6402 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6403 $ind = count($age_range);
6404 $age_range[$ind]['from'] = intval($parts[0]);
6405 $age_range[$ind]['to'] = intval($parts[1]);
6406 //taxes are calculated later in VCM
6407 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6408 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6409 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6410 }
6411 }
6412 if (count($age_range) > 0) {
6413 $tot_charge = 0;
6414 $affected = array();
6415 $option_str = '';
6416 foreach ($ages as $age) {
6417 if (strlen($age) == 0) {
6418 continue;
6419 }
6420 foreach ($age_range as $range) {
6421 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6422 $tot_charge += $range['cost'];
6423 $affected[] = $age;
6424 $option_str .= $range['option_str'].';';
6425 break;
6426 }
6427 }
6428 }
6429 if ($tot_charge > 0) {
6430 $charges['total'] = $tot_charge;
6431 $charges['affected'] = $affected;
6432 $charges['options'] = $option_str;
6433 }
6434 }
6435 }
6436 }
6437
6438 return $charges;
6439 }
6440
6441 public static function sortRoomPrices($arr) {
6442 $newarr = array();
6443 foreach ($arr as $k => $v) {
6444 $newarr[$k] = $v['cost'];
6445 }
6446 asort($newarr);
6447 $sorted = array();
6448 foreach ($newarr as $k => $v) {
6449 $sorted[$k] = $arr[$k];
6450 }
6451 return $sorted;
6452 }
6453
6454 public static function sortResults($arr) {
6455 $newarr = array();
6456 foreach ($arr as $k => $v) {
6457 $newarr[$k] = $v[0]['cost'];
6458 }
6459 asort($newarr);
6460 $sorted = array();
6461 foreach ($newarr as $k => $v) {
6462 $sorted[$k] = $arr[$k];
6463 }
6464 return $sorted;
6465 }
6466
6467 public static function sortMultipleResults($arr) {
6468 foreach ($arr as $k => $v) {
6469 $newarr = array();
6470 foreach ($v as $subk => $subv) {
6471 $newarr[$subk] = $subv[0]['cost'];
6472 }
6473 asort($newarr);
6474 $sorted = array();
6475 foreach ($newarr as $nk => $v) {
6476 $sorted[$nk] = $arr[$k][$nk];
6477 }
6478 $arr[$k] = $sorted;
6479 }
6480 return $arr;
6481 }
6482
6483 /**
6484 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6485 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6486 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6487 *
6488 * @param array $promos optional list of promotion IDs to register in the execution flow.
6489 *
6490 * @return array list of cached promotion IDs (if any).
6491 *
6492 * @since 1.16.4 (J) - 1.6.4 (WP)
6493 */
6494 public static function registerPromotionIds(array $promos = null)
6495 {
6496 static $flagged_promos = [];
6497
6498 if ($promos) {
6499 // set cached value
6500 $flagged_promos = $promos;
6501 }
6502
6503 return $flagged_promos;
6504 }
6505
6506 /**
6507 * Getter and setter for seasons cache allowed.
6508 *
6509 * @param bool $enabled true or false for setter, null for getter.
6510 *
6511 * @return bool|null
6512 *
6513 * @since 1.16.5 (J) - 1.6.5 (WP)
6514 */
6515 public static function setSeasonsCache($enabled = null)
6516 {
6517 static $seasons_cache = null;
6518
6519 if ($enabled !== null) {
6520 // set value
6521 $seasons_cache = (bool)$enabled;
6522 }
6523
6524 return $seasons_cache;
6525 }
6526
6527 /**
6528 * Applies the seasonal rates over a list of room rate records.
6529 *
6530 * @param array $arr list of room rate records.
6531 * @param int $from unix timestamp for start date.
6532 * @param int $to unix timestamp for end date.
6533 *
6534 * @return array list of manipulated room rate records.
6535 */
6536 public static function applySeasonalPrices(array $arr, $from, $to)
6537 {
6538 $dbo = JFactory::getDbo();
6539 $vbo_tn = self::getTranslator();
6540
6541 $roomschange = [];
6542 $one = getdate($from);
6543
6544 // leap years
6545 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6546 $isleap = true;
6547 } else {
6548 $isleap = false;
6549 }
6550
6551 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6552 $tomidnightone = intval($one['hours']) * 3600;
6553 $tomidnightone += intval($one['minutes']) * 60;
6554 $sfrom = $from - $baseone - $tomidnightone;
6555 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6556 $two = getdate($to);
6557 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6558 $tomidnighttwo = intval($two['hours']) * 3600;
6559 $tomidnighttwo += intval($two['minutes']) * 60;
6560 $sto = $to - $basetwo - $tomidnighttwo;
6561
6562 // leap years, check what dates to manipulate
6563 if ($isleap) {
6564 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
6565 if ($one[0] > $leapts) {
6566 /**
6567 * Timestamp must be greater than the leap-day of Feb 29th.
6568 * It used to be checked for >= $leapts.
6569 *
6570 * @since July 3rd 2019
6571 */
6572 $sfrom -= 86400;
6573 }
6574 if ($two[0] > $leapts && $one['year'] == $two['year']) {
6575 // lower checkin date when in leap year but not for checkout
6576 $sto -= 86400;
6577 }
6578 }
6579
6580 // check for DST changes to adjust the query values to fetch
6581 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6582 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6583 // we are parsing the day when the DST changed (probably a Sunday)
6584 if (!date('I', $from)) {
6585 // DST was just turned off
6586 $sfrom -= 3600;
6587 }
6588 }
6589 }
6590
6591 // count nights requested
6592 $booking_nights = 1;
6593 foreach ($arr as $k => $a) {
6594 if (!isset($a[0])) {
6595 continue;
6596 }
6597 if (isset($a[0]['booking_nights'])) {
6598 // this value may be set when displaying pricing calendars
6599 $booking_nights = $a[0]['booking_nights'];
6600 break;
6601 }
6602 if (isset($a[0]['days'])) {
6603 $booking_nights = $a[0]['days'];
6604 break;
6605 }
6606 }
6607
6608 /**
6609 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
6610 *
6611 * @since 1.16.4 (J) - 1.6.4 (WP)
6612 */
6613 $skip_promo_ids = self::registerPromotionIds();
6614
6615 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6616 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6617 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6618 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6619 ($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`)") .
6620 ($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`)") .
6621 ($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`)") .
6622 ($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`)") .
6623 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6624 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6625
6626 $dbo->setQuery($q);
6627 $seasons = $dbo->loadAssocList();
6628
6629 if ($seasons) {
6630 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
6631 $applyseasons = false;
6632 $mem = [];
6633 foreach ($arr as $k => $a) {
6634 $mem[$k]['daysused'] = 0;
6635 $mem[$k]['sum'] = [];
6636 /**
6637 * The keys below are all needed to apply the promotions on the room's final cost.
6638 *
6639 * @since 1.13.5
6640 */
6641 $mem[$k]['diffs'] = [];
6642 $mem[$k]['trans_keys'] = [];
6643 $mem[$k]['trans_factors'] = [];
6644 $mem[$k]['trans_affdays'] = [];
6645 }
6646 foreach ($seasons as $s) {
6647 // check if this is a promotion registered for skipping
6648 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
6649 continue;
6650 }
6651
6652 // Special Price tied to the year
6653 if (!empty($s['year']) && $s['year'] > 0) {
6654 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
6655 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
6656 //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
6657 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
6658 continue;
6659 }
6660 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
6661 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
6662 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
6663 if ($calc_ends < ($from - $tomidnightone)) {
6664 continue;
6665 }
6666 }
6667 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
6668 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
6669 if ($s['from'] > $s['to']) {
6670 continue;
6671 }
6672 if (($basetwo + $s['from'] + 86399) > $to) {
6673 /**
6674 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
6675 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
6676 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
6677 *
6678 * @since 1.14.3 (J) - 1.4.3 (WP)
6679 */
6680 continue;
6681 }
6682 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
6683 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
6684 /**
6685 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
6686 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
6687 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
6688 *
6689 * @since 1.14.3 (J) - 1.4.3 (WP)
6690 */
6691 continue;
6692 }
6693 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
6694 // 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)
6695 if (($baseone + $s['from']) > $to) {
6696 continue;
6697 }
6698 }
6699 }
6700 //
6701 $allrooms = explode(",", $s['idrooms']);
6702 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
6703 $inits = $baseone + $s['from'];
6704 if ($s['from'] < $s['to']) {
6705 $ends = $basetwo + $s['to'];
6706 // check if the inits must be set to the year after
6707 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
6708 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
6709 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6710 $inits = $tmpbase + $s['from'];
6711 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6712 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
6713 $ends = $baseone + $s['to'];
6714 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6715 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
6716 $ends = $baseone + $s['to'];
6717 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
6718 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
6719 $inits = $basetwo + $s['from'];
6720 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
6721 // booking dates across two years for a season tied to a specific year
6722 if ($one['year'] == $s['year']) {
6723 $inits = $baseone + $s['from'];
6724 } else {
6725 $inits = $basetwo + $s['from'];
6726 }
6727 if ($two['year'] == $s['year']) {
6728 $ends = $basetwo + $s['to'];
6729 } else {
6730 $ends = $baseone + $s['to'];
6731 }
6732 }
6733 } else {
6734 // between 2 years
6735 if ($baseone < $basetwo) {
6736 // ex. 29/12/2012 - 14/01/2013
6737 $ends = $basetwo + $s['to'];
6738 } else {
6739 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
6740 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
6741 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6742 $ends = $tmpbase + $s['to'];
6743 } else {
6744 // ex. 03/01 - 09/01
6745 $ends = $basetwo + $s['to'];
6746 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
6747 $inits = $tmpbase + $s['from'];
6748 }
6749 }
6750 }
6751
6752 // leap years
6753 if ($isleap == true) {
6754 $infoseason = getdate($inits);
6755 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
6756 // leap years, check what dates to manipulate
6757 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
6758 /**
6759 * Timestamp must be greater than the leap-day of Feb 29th.
6760 * It used to be checked for >= $leapts.
6761 *
6762 * @since July 3rd 2019
6763 */
6764 $inits += 86400;
6765 if ($s['from'] < $s['to']) {
6766 // increase season end date only if still on the same leap year
6767 $ends += 86400;
6768 }
6769 }
6770 }
6771
6772 // promotions
6773 $promotion = [];
6774 if ($s['promo'] == 1) {
6775 $daysadv = (($inits - time()) / 86400);
6776 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
6777 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
6778 continue;
6779 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
6780 $secstocheckin = ($from - time());
6781 if ($s['promolastmin'] < $secstocheckin) {
6782 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
6783 continue;
6784 }
6785 }
6786 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
6787 /**
6788 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
6789 *
6790 * @since 1.13.5
6791 */
6792 continue;
6793 }
6794 $promotion['todaydaysadv'] = $daysadv;
6795 $promotion['promodaysadv'] = $s['promodaysadv'];
6796 $promotion['promotxt'] = $s['promotxt'];
6797 }
6798
6799 // occupancy override
6800 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
6801
6802 // week days
6803 $filterwdays = !empty($s['wdays']) ? true : false;
6804 $wdays = $filterwdays ? explode(';', $s['wdays']) : '';
6805 if (is_array($wdays) && $wdays) {
6806 foreach ($wdays as $kw => $wd) {
6807 if (strlen($wd) == 0) {
6808 unset($wdays[$kw]);
6809 }
6810 }
6811 }
6812
6813 // checkin must be after the beginning of the season
6814 $checkininclok = true;
6815 if ($s['checkinincl'] == 1) {
6816 $checkininclok = false;
6817 if ($s['from'] < $s['to']) {
6818 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
6819 $checkininclok = true;
6820 }
6821 } else {
6822 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
6823 $checkininclok = true;
6824 }
6825 }
6826 }
6827 if ($checkininclok !== true) {
6828 continue;
6829 }
6830
6831 foreach ($arr as $k => $a) {
6832 // applied only to some types of price
6833 if ($allprices && !empty($allprices[0])) {
6834 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
6835 continue;
6836 }
6837 }
6838 // applied only to some room types
6839 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
6840 continue;
6841 }
6842
6843 // count affected nights of stay
6844 $affdays = 0;
6845 $season_fromdayts = $fromdayts;
6846 $is_dst = date('I', $season_fromdayts);
6847 for ($i = 0; $i < $a[0]['days']; $i++) {
6848 $todayts = $season_fromdayts + ($i * 86400);
6849 $is_now_dst = date('I', $todayts);
6850 if ($is_dst != $is_now_dst) {
6851 // Daylight Saving Time has changed, check how
6852 if ((bool)$is_dst === true) {
6853 $todayts += 3600;
6854 $season_fromdayts += 3600;
6855 } else {
6856 $todayts -= 3600;
6857 $season_fromdayts -= 3600;
6858 }
6859 $is_dst = $is_now_dst;
6860 }
6861 if ($todayts >= $inits && $todayts <= $ends) {
6862 // week days
6863 if ($filterwdays == true) {
6864 $checkwday = getdate($todayts);
6865 if (in_array($checkwday['wday'], $wdays)) {
6866 $affdays++;
6867 }
6868 } else {
6869 $affdays++;
6870 }
6871 //
6872 }
6873 }
6874 if (!($affdays > 0)) {
6875 // no nights affected
6876 continue;
6877 }
6878
6879 // apply the rule
6880 $applyseasons = true;
6881 $dailyprice = $a[0]['cost'] / $a[0]['days'];
6882
6883 // modification factor object
6884 $factor = new stdClass;
6885
6886 // calculate new price progressively
6887 if (intval($s['val_pcent']) == 2) {
6888 // percentage value
6889 $factor->pcent = 1;
6890 $pctval = $s['diffcost'];
6891 if (strlen((string)$s['losoverride'])) {
6892 // values overrides
6893 $arrvaloverrides = [];
6894 $valovrparts = explode('_', $s['losoverride']);
6895 foreach ($valovrparts as $valovr) {
6896 if (!empty($valovr)) {
6897 $ovrinfo = explode(':', $valovr);
6898 if (strstr($ovrinfo[0], '-i') != false) {
6899 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6900 if ((int)$ovrinfo[0] < $a[0]['days']) {
6901 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6902 }
6903 }
6904 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6905 }
6906 }
6907 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6908 $pctval = $arrvaloverrides[$a[0]['days']];
6909 }
6910 }
6911 if (intval($s['type']) == 1) {
6912 // charge
6913 $factor->type = '+';
6914 $cpercent = 100 + $pctval;
6915 } else {
6916 // discount
6917 $factor->type = '-';
6918 $cpercent = 100 - $pctval;
6919 }
6920 $factor->amount = $pctval;
6921 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
6922 } else {
6923 // absolute value
6924 $factor->pcent = 0;
6925 $absval = $s['diffcost'];
6926 if (strlen((string)$s['losoverride'])) {
6927 // values overrides
6928 $arrvaloverrides = [];
6929 $valovrparts = explode('_', $s['losoverride']);
6930 foreach ($valovrparts as $valovr) {
6931 if (!empty($valovr)) {
6932 $ovrinfo = explode(':', $valovr);
6933 if (strstr($ovrinfo[0], '-i') != false) {
6934 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6935 if ((int)$ovrinfo[0] < $a[0]['days']) {
6936 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6937 }
6938 }
6939 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6940 }
6941 }
6942 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6943 $absval = $arrvaloverrides[$a[0]['days']];
6944 }
6945 }
6946 if (intval($s['type']) == 1) {
6947 // charge
6948 $factor->type = '+';
6949 $newprice = ($dailyprice + $absval) * $affdays;
6950 } else {
6951 // discount
6952 $factor->type = '-';
6953 $newprice = ($dailyprice - $absval) * $affdays;
6954 }
6955 $factor->amount = $absval;
6956 }
6957
6958 // apply rounding
6959 $factor->roundmode = $s['roundmode'];
6960 if (!empty($s['roundmode'])) {
6961 $newprice = round($newprice, 0, constant($s['roundmode']));
6962 } else {
6963 $newprice = round($newprice, 2);
6964 }
6965
6966 // define the promotion (only if no value overrides set the amount to 0)
6967 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
6968 /**
6969 * Include the discount information (if any). The cost re-calculated may not be
6970 * precise if multiple special prices were applied over the same dates.
6971 *
6972 * @since 1.13
6973 */
6974 if ($s['type'] == 2 && $s['diffcost'] > 0) {
6975 $promotion['discount'] = [
6976 'amount' => $s['diffcost'],
6977 'pcent' => (int)($s['val_pcent'] == 2),
6978 ];
6979 }
6980 //
6981 $mem[$k]['promotion'] = $promotion;
6982 }
6983
6984 // define the occupancy override
6985 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
6986 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
6987 }
6988
6989 // push difference generated only if to be applied progressively
6990 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
6991 /**
6992 * Push the difference generated by this special price for later transliteration of final price,
6993 * only if the special price is calculated progressively and not on the final price.
6994 *
6995 * @since 1.13.5
6996 */
6997 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
6998 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
6999 /**
7000 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7001 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7002 * all differences applied by the previous special pricing rules.
7003 *
7004 * @since 1.13.5
7005 */
7006 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7007 array_push($mem[$k]['trans_factors'], $factor);
7008 array_push($mem[$k]['trans_affdays'], $affdays);
7009 }
7010
7011 // push values in memory array
7012 array_push($mem[$k]['sum'], $newprice);
7013 $mem[$k]['daysused'] += $affdays;
7014 array_push($roomschange, $a[0]['idroom']);
7015 }
7016 }
7017 if ($applyseasons) {
7018 foreach ($mem as $k => $v) {
7019 if ($v['daysused'] > 0 && $v['sum']) {
7020 $newprice = 0;
7021 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7022 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7023 $addrest = $restdays * $dailyprice;
7024 $newprice += $addrest;
7025
7026 // calculate new final cost
7027 $redo_rounding = null;
7028 foreach ($v['sum'] as $sum_index => $add) {
7029 /**
7030 * The application of the various special pricing rules is made in a progressive and cumulative way
7031 * by always starting from the room base cost or its average daily cost. However, promotions may need
7032 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7033 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7034 * onto the differences generated by all the regular and progressively applied special pricing rules.
7035 *
7036 * @since 1.13.5
7037 */
7038 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7039 /**
7040 * This progressive price difference must be applied on the room final cost, so we need to
7041 * apply the transliteration over the other differences applied by other special prices.
7042 */
7043 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7044 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7045 // this is the % promotion we are looking for applying it on the final cost
7046 $factor = $v['trans_factors'][$transliterate_key];
7047 if (is_object($factor) && $factor->pcent) {
7048 $final_factor = 0;
7049 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7050 $final_factor += $prog_diff * $factor->amount / 100;
7051 }
7052 // update rounding
7053 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7054
7055 /**
7056 * Leverage the final factor depending on how many nights this affected.
7057 *
7058 * @since 1.15.0 (J) - 1.5.0 (WP)
7059 */
7060 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7061 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7062 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7063 }
7064
7065 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7066 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7067 }
7068 }
7069 }
7070
7071 // apply new price progressively
7072 $newprice += $add;
7073 }
7074
7075 // apply rounding from factor
7076 if (!empty($redo_rounding)) {
7077 $newprice = round($newprice, 0, constant($redo_rounding));
7078 }
7079
7080 // set promotion (if any)
7081 if (isset($v['promotion'])) {
7082 $arr[$k][0]['promotion'] = $v['promotion'];
7083 }
7084
7085 // set occupancy overrides (if any)
7086 if (isset($v['occupancy_ovr'])) {
7087 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7088 }
7089
7090 // set new final cost and update nights affected
7091 $arr[$k][0]['cost'] = $newprice;
7092 $arr[$k][0]['affdays'] = $v['daysused'];
7093 }
7094 }
7095 }
7096 }
7097
7098 // week days with no season
7099 $roomschange = array_unique($roomschange);
7100 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7101 $dbo->setQuery($q);
7102 $specials = $dbo->loadAssocList();
7103 if ($specials) {
7104 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7105 $applyseasons = false;
7106 unset($mem);
7107 $mem = [];
7108 foreach ($arr as $k => $a) {
7109 $mem[$k]['daysused'] = 0;
7110 $mem[$k]['sum'] = [];
7111 }
7112 foreach ($specials as $s) {
7113 // check if this is a promotion registered for skipping
7114 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7115 continue;
7116 }
7117
7118 // Special Price tied to the year
7119 if (!empty($s['year']) && $s['year'] > 0) {
7120 if ($one['year'] != $s['year']) {
7121 continue;
7122 }
7123 }
7124
7125 $allrooms = explode(",", $s['idrooms']);
7126 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7127 // week days
7128 $filterwdays = !empty($s['wdays']) ? true : false;
7129 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7130 if (is_array($wdays) && $wdays) {
7131 foreach ($wdays as $kw => $wd) {
7132 if (strlen($wd) == 0) {
7133 unset($wdays[$kw]);
7134 }
7135 }
7136 }
7137
7138 foreach ($arr as $k => $a) {
7139 // only rooms with no price modifications from seasons
7140
7141 // applied only to some types of price
7142 if ($allprices && !empty($allprices[0])) {
7143 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7144 continue;
7145 }
7146 }
7147
7148 /**
7149 * We should not exclude the rooms that already had a modification of the price through a season
7150 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7151 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7152 *
7153 * @since 1.11
7154 */
7155 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7156 continue;
7157 }
7158
7159 $affdays = 0;
7160 $season_fromdayts = $fromdayts;
7161 $is_dst = date('I', $season_fromdayts);
7162 for ($i = 0; $i < $a[0]['days']; $i++) {
7163 $todayts = $season_fromdayts + ($i * 86400);
7164 $is_now_dst = date('I', $todayts);
7165 if ($is_dst != $is_now_dst) {
7166 // Daylight Saving Time has changed, check how
7167 if ((bool)$is_dst === true) {
7168 $todayts += 3600;
7169 $season_fromdayts += 3600;
7170 } else {
7171 $todayts -= 3600;
7172 $season_fromdayts -= 3600;
7173 }
7174 $is_dst = $is_now_dst;
7175 }
7176 // week days
7177 if ($filterwdays == true) {
7178 $checkwday = getdate($todayts);
7179 if (in_array($checkwday['wday'], $wdays)) {
7180 $affdays++;
7181 }
7182 }
7183 }
7184 if (!($affdays > 0)) {
7185 // no nights affected
7186 continue;
7187 }
7188
7189 // apply the rule
7190 $applyseasons = true;
7191 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7192
7193 if (intval($s['val_pcent']) == 2) {
7194 // percentage value
7195 $pctval = $s['diffcost'];
7196 if (strlen((string)$s['losoverride'])) {
7197 // values overrides
7198 $arrvaloverrides = [];
7199 $valovrparts = explode('_', $s['losoverride']);
7200 foreach ($valovrparts as $valovr) {
7201 if (!empty($valovr)) {
7202 $ovrinfo = explode(':', $valovr);
7203 if (strstr($ovrinfo[0], '-i') != false) {
7204 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7205 if ((int)$ovrinfo[0] < $a[0]['days']) {
7206 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7207 }
7208 }
7209 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7210 }
7211 }
7212 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7213 $pctval = $arrvaloverrides[$a[0]['days']];
7214 }
7215 }
7216 if (intval($s['type']) == 1) {
7217 // charge
7218 $cpercent = 100 + $pctval;
7219 } else {
7220 // discount
7221 $cpercent = 100 - $pctval;
7222 }
7223 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7224 } else {
7225 // absolute value
7226 $absval = $s['diffcost'];
7227 if (strlen((string)$s['losoverride'])) {
7228 // values overrides
7229 $arrvaloverrides = [];
7230 $valovrparts = explode('_', $s['losoverride']);
7231 foreach ($valovrparts as $valovr) {
7232 if (!empty($valovr)) {
7233 $ovrinfo = explode(':', $valovr);
7234 if (strstr($ovrinfo[0], '-i') != false) {
7235 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7236 if ((int)$ovrinfo[0] < $a[0]['days']) {
7237 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7238 }
7239 }
7240 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7241 }
7242 }
7243 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7244 $absval = $arrvaloverrides[$a[0]['days']];
7245 }
7246 }
7247 if (intval($s['type']) == 1) {
7248 // charge
7249 $newprice = ($dailyprice + $absval) * $affdays;
7250 } else {
7251 // discount
7252 $newprice = ($dailyprice - $absval) * $affdays;
7253 }
7254 }
7255
7256 // apply rounding
7257 if (!empty($s['roundmode'])) {
7258 $newprice = round($newprice, 0, constant($s['roundmode']));
7259 } else {
7260 $newprice = round($newprice, 2);
7261 }
7262
7263 // push values in memory array
7264 array_push($mem[$k]['sum'], $newprice);
7265 $mem[$k]['daysused'] += $affdays;
7266 }
7267 }
7268 if ($applyseasons) {
7269 foreach ($mem as $k => $v) {
7270 if ($v['daysused'] > 0 && $v['sum']) {
7271 $newprice = 0;
7272 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7273 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7274 $addrest = $restdays * $dailyprice;
7275 $newprice += $addrest;
7276 foreach ($v['sum'] as $add) {
7277 $newprice += $add;
7278 }
7279 $arr[$k][0]['cost'] = $newprice;
7280 $arr[$k][0]['affdays'] = $v['daysused'];
7281 }
7282 }
7283 }
7284 }
7285 // end week days with no season
7286
7287 return $arr;
7288 }
7289
7290 /**
7291 * Applies the special prices over an array of tariffs.
7292 * The function is also used by VCM (>= 1.6.5) with specific arguments.
7293 *
7294 * @param array $arr array of tariffs taken from the DB
7295 * @param int $from start timestamp
7296 * @param int $to end timestamp
7297 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
7298 * @param array $seasons_dates (VBO 1.10) array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7299 * @param array $seasons_wdays (VBO 1.10) array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7300 *
7301 * @return array
7302 */
7303 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], array $seasons_wdays = [])
7304 {
7305 static $cached_seasons = [];
7306
7307 $cache_signature = '';
7308 $cache_signature_wdays = '';
7309 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates && !$seasons_wdays) {
7310 // enable season records caching
7311 $cache_signature = "{$from}_{$to}";
7312 $cache_signature_wdays = "wdays";
7313 }
7314
7315 $dbo = JFactory::getDbo();
7316 $vbo_tn = self::getTranslator();
7317
7318 $roomschange = [];
7319 $one = getdate($from);
7320
7321 // leap years
7322 if ($one['year'] % 4 == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7323 $isleap = true;
7324 } else {
7325 $isleap = false;
7326 }
7327
7328 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7329 $tomidnightone = intval($one['hours']) * 3600;
7330 $tomidnightone += intval($one['minutes']) * 60;
7331 $sfrom = $from - $baseone - $tomidnightone;
7332 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7333 $two = getdate($to);
7334 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7335 $tomidnighttwo = intval($two['hours']) * 3600;
7336 $tomidnighttwo += intval($two['minutes']) * 60;
7337 $sto = $to - $basetwo - $tomidnighttwo;
7338
7339 // leap years, check what dates to manipulate
7340 if ($isleap) {
7341 $leapts = mktime(0, 0, 0, 2, 29, $one['year']);
7342 if ($one[0] > $leapts) {
7343 /**
7344 * Timestamp must be greater than the leap-day of Feb 29th.
7345 * It used to be checked for >= $leapts.
7346 *
7347 * @since July 3rd 2019
7348 */
7349 $sfrom -= 86400;
7350 }
7351 if ($two[0] > $leapts && $one['year'] == $two['year']) {
7352 // lower checkin date when in leap year but not for checkout
7353 $sto -= 86400;
7354 }
7355 }
7356
7357 // check for DST changes to adjust the query values to fetch
7358 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7359 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7360 // we are parsing the day when the DST changed (probably a Sunday)
7361 if (!date('I', $from)) {
7362 // DST was just turned off
7363 $sfrom -= 3600;
7364 }
7365 }
7366 }
7367
7368 // count nights requested
7369 $booking_nights = 1;
7370 foreach ($arr as $k => $a) {
7371 if (isset($a['booking_nights'])) {
7372 // this value may be set when displaying pricing calendars
7373 $booking_nights = $a['booking_nights'];
7374 break;
7375 }
7376 if (isset($a['days'])) {
7377 $booking_nights = $a['days'];
7378 break;
7379 }
7380 }
7381
7382 /**
7383 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7384 *
7385 * @since 1.16.4 (J) - 1.6.4 (WP)
7386 */
7387 $skip_promo_ids = self::registerPromotionIds();
7388
7389 $totseasons = 0;
7390 if (!$parsed_season && !$seasons_dates) {
7391 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7392 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7393 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7394 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7395 ($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`)") .
7396 ($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`)") .
7397 ($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`)") .
7398 ($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`)") .
7399 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7400 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7401
7402 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
7403 // avoid making a query
7404 $seasons = $cached_seasons[$cache_signature];
7405 } else {
7406 // get the season records by running the query
7407 $dbo->setQuery($q);
7408 $seasons = $dbo->loadAssocList();
7409 }
7410
7411 // count total seasons
7412 $totseasons = $seasons ? count($seasons) : 0;
7413
7414 if ($cache_signature) {
7415 // cache season records
7416 $cached_seasons[$cache_signature] = $seasons;
7417 }
7418 }
7419
7420 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
7421 if ($totseasons > 0) {
7422 $seasons = $seasons;
7423 } elseif ($parsed_season) {
7424 $seasons = [$parsed_season];
7425 } else {
7426 $seasons = $seasons_dates;
7427 }
7428 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7429 $applyseasons = false;
7430 $mem = [];
7431 foreach ($arr as $k => $a) {
7432 $mem[$k]['daysused'] = 0;
7433 $mem[$k]['sum'] = [];
7434 $mem[$k]['spids'] = [];
7435 /**
7436 * The keys below are all needed to apply the promotions on the room's final cost.
7437 *
7438 * @since 1.13.5
7439 */
7440 $mem[$k]['diffs'] = [];
7441 $mem[$k]['trans_keys'] = [];
7442 $mem[$k]['trans_factors'] = [];
7443 $mem[$k]['trans_affdays'] = [];
7444 }
7445 $affdayslistless = [];
7446 foreach ($seasons as $s) {
7447 // 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
7448 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
7449 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
7450 continue;
7451 }
7452
7453 /**
7454 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
7455 * We need to skip such fake seasons as they do not need any parsing.
7456 *
7457 * @since 1.13
7458 */
7459 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
7460 continue;
7461 }
7462
7463 // check if this is a promotion registered for skipping
7464 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7465 continue;
7466 }
7467
7468 // Special Price tied to the year
7469 if (!empty($s['year']) && $s['year'] > 0) {
7470 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7471 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7472 //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
7473 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7474 continue;
7475 }
7476 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7477 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7478 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7479 if ($calc_ends < ($from - $tomidnightone)) {
7480 continue;
7481 }
7482 }
7483 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7484 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7485 if ($s['from'] > $s['to']) {
7486 continue;
7487 }
7488 if (($basetwo + $s['from'] + 86399) > $to) {
7489 /**
7490 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7491 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7492 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7493 *
7494 * @since 1.14.3 (J) - 1.4.3 (WP)
7495 */
7496 continue;
7497 }
7498 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7499 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7500 /**
7501 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7502 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7503 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7504 *
7505 * @since 1.14.3 (J) - 1.4.3 (WP)
7506 */
7507 continue;
7508 }
7509 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7510 // 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)
7511 if (($baseone + $s['from']) > $to) {
7512 continue;
7513 }
7514 }
7515 }
7516 //
7517 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7518 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7519 $inits = $baseone + $s['from'];
7520 if ($s['from'] < $s['to']) {
7521 $ends = $basetwo + $s['to'];
7522 // check if the inits must be set to the year after
7523 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7524 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7525 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7526 $inits = $tmpbase + $s['from'];
7527 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7528 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7529 $ends = $baseone + $s['to'];
7530 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7531 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7532 $ends = $baseone + $s['to'];
7533 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7534 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7535 $inits = $basetwo + $s['from'];
7536 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7537 // booking dates across two years for a season tied to a specific year
7538 if ($one['year'] == $s['year']) {
7539 $inits = $baseone + $s['from'];
7540 } else {
7541 $inits = $basetwo + $s['from'];
7542 }
7543 if ($two['year'] == $s['year']) {
7544 $ends = $basetwo + $s['to'];
7545 } else {
7546 $ends = $baseone + $s['to'];
7547 }
7548 }
7549 } else {
7550 // between 2 years
7551 if ($baseone < $basetwo) {
7552 // ex. 29/12/2012 - 14/01/2013
7553 $ends = $basetwo + $s['to'];
7554 } else {
7555 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7556 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7557 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7558 $ends = $tmpbase + $s['to'];
7559 } else {
7560 // ex. 03/01 - 09/01
7561 $ends = $basetwo + $s['to'];
7562 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7563 $inits = $tmpbase + $s['from'];
7564 }
7565 }
7566 }
7567 // leap years
7568 if ($isleap == true) {
7569 $infoseason = getdate($inits);
7570 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7571 // leap years, check what dates to manipulate
7572 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7573 /**
7574 * Timestamp must be greater than the leap-day of Feb 29th.
7575 * It used to be checked for >= $leapts.
7576 *
7577 * @since July 3rd 2019
7578 */
7579 $inits += 86400;
7580 if ($s['from'] < $s['to']) {
7581 // increase season end date only if still on the same leap year
7582 $ends += 86400;
7583 }
7584 }
7585 }
7586
7587 // promotions
7588 $promotion = [];
7589 if ($s['promo'] == 1) {
7590 $daysadv = (($inits - time()) / 86400);
7591 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7592 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7593 continue;
7594 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7595 $secstocheckin = ($from - time());
7596 if ($s['promolastmin'] < $secstocheckin) {
7597 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7598 continue;
7599 }
7600 }
7601 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7602 /**
7603 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7604 *
7605 * @since 1.13.5
7606 */
7607 continue;
7608 }
7609 $promotion['todaydaysadv'] = $daysadv;
7610 $promotion['promodaysadv'] = $s['promodaysadv'];
7611 $promotion['promotxt'] = $s['promotxt'];
7612 }
7613
7614 // occupancy override
7615 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7616
7617 //week days
7618 $filterwdays = !empty($s['wdays']) ? true : false;
7619 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7620 if (is_array($wdays) && $wdays) {
7621 foreach ($wdays as $kw => $wd) {
7622 if (strlen($wd) == 0) {
7623 unset($wdays[$kw]);
7624 }
7625 }
7626 }
7627
7628 // checkin must be after the begin of the season
7629 $checkininclok = true;
7630 if ($s['checkinincl'] == 1) {
7631 $checkininclok = false;
7632 if ($s['from'] < $s['to']) {
7633 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7634 $checkininclok = true;
7635 }
7636 } else {
7637 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7638 $checkininclok = true;
7639 }
7640 }
7641 }
7642 if ($checkininclok !== true) {
7643 continue;
7644 }
7645
7646 foreach ($arr as $k => $a) {
7647 // applied only to some types of price
7648 if ($allprices && !empty($allprices[0])) {
7649 // VikBooking 1.6: Price Calendar sets the idprice to -1
7650 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
7651 continue;
7652 }
7653 }
7654 // applied only to some room types
7655 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
7656 continue;
7657 }
7658
7659 if (isset($a['days']) && is_float($a['days'])) {
7660 // prevent any possible warning with array_key_exists()
7661 $a['days'] = (int)$a['days'];
7662 }
7663
7664 $affdays = 0;
7665 $season_fromdayts = $fromdayts;
7666 $is_dst = date('I', $season_fromdayts);
7667 for ($i = 0; $i < $a['days']; $i++) {
7668 $todayts = $season_fromdayts + ($i * 86400);
7669 $is_now_dst = date('I', $todayts);
7670 if ($is_dst != $is_now_dst) {
7671 // Daylight Saving Time has changed, check how
7672 if ((bool)$is_dst === true) {
7673 $todayts += 3600;
7674 $season_fromdayts += 3600;
7675 } else {
7676 $todayts -= 3600;
7677 $season_fromdayts -= 3600;
7678 }
7679 $is_dst = $is_now_dst;
7680 }
7681 if ($todayts >= $inits && $todayts <= $ends) {
7682 $checkwday = getdate($todayts);
7683 // week days
7684 if ($filterwdays == true) {
7685 if (in_array($checkwday['wday'], $wdays)) {
7686 if (!isset($arr[$k]['affdayslist'])) {
7687 $arr[$k]['affdayslist'] = [];
7688 }
7689 $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;
7690 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7691 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7692 $affdays++;
7693 }
7694 } else {
7695 if (!isset($arr[$k]['affdayslist'])) {
7696 $arr[$k]['affdayslist'] = [];
7697 }
7698 $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;
7699 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7700 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7701 $affdays++;
7702 }
7703 //
7704 }
7705 }
7706
7707 if (!($affdays > 0)) {
7708 // no nights affected
7709 continue;
7710 }
7711
7712 // apply the rule
7713 $applyseasons = true;
7714 $dailyprice = $a['cost'] / $a['days'];
7715
7716 // modification factor object
7717 $factor = new stdClass;
7718
7719 // calculate new price progressively
7720 if (intval($s['val_pcent']) == 2) {
7721 // percentage value
7722 $factor->pcent = 1;
7723 $pctval = $s['diffcost'];
7724 if (strlen((string)$s['losoverride'])) {
7725 // values overrides
7726 $arrvaloverrides = [];
7727 $valovrparts = explode('_', $s['losoverride']);
7728 foreach ($valovrparts as $valovr) {
7729 if (!empty($valovr)) {
7730 $ovrinfo = explode(':', $valovr);
7731 if (strstr($ovrinfo[0], '-i') != false) {
7732 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7733 if ((int)$ovrinfo[0] < $a['days']) {
7734 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7735 }
7736 }
7737 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7738 }
7739 }
7740 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7741 $pctval = $arrvaloverrides[$a['days']];
7742 }
7743 }
7744 if (intval($s['type']) == 1) {
7745 // charge
7746 $factor->type = '+';
7747 $cpercent = 100 + $pctval;
7748 } else {
7749 // discount
7750 $factor->type = '-';
7751 $cpercent = 100 - $pctval;
7752 }
7753 $factor->amount = $pctval;
7754 $dailysum = ($dailyprice * $cpercent / 100);
7755 $newprice = $dailysum * $affdays;
7756 } else {
7757 // absolute value
7758 $factor->pcent = 0;
7759 $absval = $s['diffcost'];
7760 if (strlen((string)$s['losoverride'])) {
7761 // values overrides
7762 $arrvaloverrides = [];
7763 $valovrparts = explode('_', $s['losoverride']);
7764 foreach ($valovrparts as $valovr) {
7765 if (!empty($valovr)) {
7766 $ovrinfo = explode(':', $valovr);
7767 if (strstr($ovrinfo[0], '-i') != false) {
7768 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7769 if ((int)$ovrinfo[0] < $a['days']) {
7770 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7771 }
7772 }
7773 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7774 }
7775 }
7776 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7777 $absval = $arrvaloverrides[$a['days']];
7778 }
7779 }
7780 if (intval($s['type']) == 1) {
7781 // charge
7782 $factor->type = '+';
7783 $dailysum = ($dailyprice + $absval);
7784 $newprice = $dailysum * $affdays;
7785 } else {
7786 // discount
7787 $factor->type = '-';
7788 $dailysum = ($dailyprice - $absval);
7789 $newprice = $dailysum * $affdays;
7790 }
7791 $factor->amount = $absval;
7792 }
7793
7794 // apply rounding
7795 $factor->roundmode = $s['roundmode'];
7796 if (!empty($s['roundmode'])) {
7797 $newprice = round($newprice, 0, constant($s['roundmode']));
7798 } else {
7799 $newprice = round($newprice, 2);
7800 }
7801
7802 // define the promotion (only if no value overrides set the amount to 0)
7803 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
7804 /**
7805 * Include the discount information (if any). The cost re-calculated may not be
7806 * precise if multiple special prices were applied over the same dates.
7807 *
7808 * @since 1.13
7809 */
7810 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7811 $promotion['discount'] = [
7812 'amount' => $s['diffcost'],
7813 'pcent' => (int)($s['val_pcent'] == 2),
7814 ];
7815 }
7816 //
7817 $mem[$k]['promotion'] = $promotion;
7818 }
7819
7820 // define the occupancy override
7821 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
7822 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
7823 }
7824
7825 // affected days list
7826 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
7827 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
7828 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
7829 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
7830 }
7831 }
7832 }
7833
7834 // push special price ID
7835 if (!in_array($s['id'], $mem[$k]['spids'])) {
7836 array_push($mem[$k]['spids'], $s['id']);
7837 }
7838
7839 // push difference generated only if to be applied progressively
7840 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
7841 /**
7842 * Push the difference generated by this special price for later transliteration of final price,
7843 * only if the special price is calculated progressively and not on the final price.
7844 *
7845 * @since 1.13.5
7846 */
7847 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7848 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7849 /**
7850 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7851 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7852 * all differences applied by the previous special pricing rules.
7853 *
7854 * @since 1.13.5
7855 */
7856 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7857 array_push($mem[$k]['trans_factors'], $factor);
7858 array_push($mem[$k]['trans_affdays'], $affdays);
7859 }
7860
7861 // push values in memory array
7862 array_push($mem[$k]['sum'], $newprice);
7863 $mem[$k]['daysused'] += $affdays;
7864 array_push($roomschange, $a['idroom']);
7865 }
7866 }
7867 if ($applyseasons) {
7868 foreach ($mem as $k => $v) {
7869 if ($v['daysused'] > 0 && $v['sum']) {
7870 $newprice = 0;
7871 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
7872 $restdays = $arr[$k]['days'] - $v['daysused'];
7873 $addrest = $restdays * $dailyprice;
7874 $newprice += $addrest;
7875
7876 // calculate new final cost
7877 $redo_rounding = null;
7878 foreach ($v['sum'] as $sum_index => $add) {
7879 /**
7880 * The application of the various special pricing rules is made in a progressive and cumulative way
7881 * by always starting from the room base cost or its average daily cost. However, promotions may need
7882 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7883 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7884 * onto the differences generated by all the regular and progressively applied special pricing rules.
7885 *
7886 * @since 1.13.5
7887 */
7888 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7889 /**
7890 * This progressive price difference must be applied on the room final cost, so we need to
7891 * apply the transliteration over the other differences applied by other special prices.
7892 */
7893 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7894 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7895 // this is the % promotion we are looking for applying it on the final cost
7896 $factor = $v['trans_factors'][$transliterate_key];
7897 if (is_object($factor) && $factor->pcent) {
7898 $final_factor = 0;
7899 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7900 $final_factor += $prog_diff * $factor->amount / 100;
7901 }
7902 // update rounding
7903 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7904
7905 /**
7906 * Leverage the final factor depending on how many nights this affected.
7907 *
7908 * @since 1.15.0 (J) - 1.5.0 (WP)
7909 */
7910 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
7911 $avg_final_factor = $final_factor / $arr[$k]['days'];
7912 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7913 }
7914
7915 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7916 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7917 }
7918 }
7919 }
7920
7921 // apply new price progressively
7922 $newprice += $add;
7923 }
7924
7925 // apply rounding from factor
7926 if (!empty($redo_rounding)) {
7927 $newprice = round($newprice, 0, constant($redo_rounding));
7928 }
7929
7930 // set promotion (if any)
7931 if (isset($v['promotion'])) {
7932 $arr[$k]['promotion'] = $v['promotion'];
7933 }
7934
7935 // set occupancy overrides (if any)
7936 if (isset($v['occupancy_ovr'])) {
7937 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
7938 }
7939
7940 // set new final cost and update nights affected
7941 $arr[$k]['cost'] = $newprice;
7942 $arr[$k]['affdays'] = $v['daysused'];
7943 if (array_key_exists('spids', $v) && $v['spids']) {
7944 $arr[$k]['spids'] = $v['spids'];
7945 }
7946 }
7947 }
7948 }
7949 }
7950
7951 // week days with no season
7952 $roomschange = array_unique($roomschange);
7953 $totspecials = 0;
7954 if (!$seasons_wdays) {
7955 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7956
7957 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7958 // avoid making a query
7959 $specials = $cached_seasons[$cache_signature_wdays];
7960 } else {
7961 // get the week-days-with-no-season records by running the query
7962 $dbo->setQuery($q);
7963 $specials = $dbo->loadAssocList();
7964 }
7965
7966 // count records
7967 $totspecials = $specials ? count($specials) : 0;
7968
7969 if ($cache_signature_wdays) {
7970 // cache week-days-with-no-season records
7971 $cached_seasons[$cache_signature_wdays] = $specials;
7972 }
7973 }
7974 if ($totspecials > 0 || $seasons_wdays) {
7975 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7976 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7977 $applyseasons = false;
7978 /**
7979 * We no longer unset the previous memory of the seasons with dates filters
7980 * because we need the responses to be merged. We do it only if not set.
7981 * We only keep the property 'spids' but the others should be unset.
7982 *
7983 * @since 1.11
7984 */
7985 if (!isset($mem)) {
7986 $mem = [];
7987 foreach ($arr as $k => $a) {
7988 $mem[$k]['daysused'] = 0;
7989 $mem[$k]['sum'] = [];
7990 $mem[$k]['spids'] = [];
7991 }
7992 } else {
7993 foreach ($arr as $k => $a) {
7994 $mem[$k]['daysused'] = 0;
7995 $mem[$k]['sum'] = [];
7996 }
7997 }
7998 //
7999 foreach ($specials as $s) {
8000 // 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
8001 if (!empty($s['from']) || !empty($s['to'])) {
8002 continue;
8003 }
8004
8005 // check if this is a promotion registered for skipping
8006 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
8007 continue;
8008 }
8009
8010 // Special Price tied to the year
8011 if (!empty($s['year']) && $s['year'] > 0) {
8012 if ($one['year'] != $s['year']) {
8013 continue;
8014 }
8015 }
8016
8017 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
8018 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
8019 // week days
8020 $filterwdays = !empty($s['wdays']) ? true : false;
8021 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
8022 if (is_array($wdays) && $wdays) {
8023 foreach ($wdays as $kw => $wd) {
8024 if (strlen($wd) == 0) {
8025 unset($wdays[$kw]);
8026 }
8027 }
8028 }
8029
8030 foreach ($arr as $k => $a) {
8031 // only rooms with no price modifications from seasons
8032
8033 // applied only to some types of price
8034 if ($allprices && !empty($allprices[0])) {
8035 // VikBooking 1.6: Price Calendar sets the idprice to -1
8036 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8037 continue;
8038 }
8039 }
8040
8041 /**
8042 * We should not exclude the rooms that already had a modification of the price through a season
8043 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8044 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8045 *
8046 * @since 1.11
8047 */
8048 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8049 continue;
8050 }
8051
8052 if (isset($a['days']) && is_float($a['days'])) {
8053 // prevent any possible warning with array_key_exists()
8054 $a['days'] = (int)$a['days'];
8055 }
8056
8057 $affdays = 0;
8058 $season_fromdayts = $fromdayts;
8059 $is_dst = date('I', $season_fromdayts);
8060 for ($i = 0; $i < $a['days']; $i++) {
8061 $todayts = $season_fromdayts + ($i * 86400);
8062 $is_now_dst = date('I', $todayts);
8063 if ($is_dst != $is_now_dst) {
8064 // Daylight Saving Time has changed, check how
8065 if ((bool)$is_dst === true) {
8066 $todayts += 3600;
8067 $season_fromdayts += 3600;
8068 } else {
8069 $todayts -= 3600;
8070 $season_fromdayts -= 3600;
8071 }
8072 $is_dst = $is_now_dst;
8073 }
8074 // week days
8075 if ($filterwdays == true) {
8076 $checkwday = getdate($todayts);
8077 if (in_array($checkwday['wday'], $wdays)) {
8078 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8079 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8080 $affdays++;
8081 }
8082 }
8083 //
8084 }
8085
8086 if (!($affdays > 0)) {
8087 // no nights affected
8088 continue;
8089 }
8090
8091 // apply the rule
8092 $applyseasons = true;
8093 $dailyprice = $a['cost'] / $a['days'];
8094
8095 if (intval($s['val_pcent']) == 2) {
8096 // percentage value
8097 $pctval = $s['diffcost'];
8098 if (strlen((string)$s['losoverride'])) {
8099 // values overrides
8100 $arrvaloverrides = [];
8101 $valovrparts = explode('_', $s['losoverride']);
8102 foreach ($valovrparts as $valovr) {
8103 if (!empty($valovr)) {
8104 $ovrinfo = explode(':', $valovr);
8105 if (strstr($ovrinfo[0], '-i') != false) {
8106 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8107 if ((int)$ovrinfo[0] < $a['days']) {
8108 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8109 }
8110 }
8111 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8112 }
8113 }
8114 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8115 $pctval = $arrvaloverrides[$a['days']];
8116 }
8117 }
8118 if (intval($s['type']) == 1) {
8119 // charge
8120 $cpercent = 100 + $pctval;
8121 } else {
8122 // discount
8123 $cpercent = 100 - $pctval;
8124 }
8125 $dailysum = ($dailyprice * $cpercent / 100);
8126 $newprice = $dailysum * $affdays;
8127 } else {
8128 // absolute value
8129 $absval = $s['diffcost'];
8130 if (strlen((string)$s['losoverride'])) {
8131 // values overrides
8132 $arrvaloverrides = [];
8133 $valovrparts = explode('_', $s['losoverride']);
8134 foreach ($valovrparts as $valovr) {
8135 if (!empty($valovr)) {
8136 $ovrinfo = explode(':', $valovr);
8137 if (strstr($ovrinfo[0], '-i') != false) {
8138 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8139 if ((int)$ovrinfo[0] < $a['days']) {
8140 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8141 }
8142 }
8143 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8144 }
8145 }
8146 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8147 $absval = $arrvaloverrides[$a['days']];
8148 }
8149 }
8150 if (intval($s['type']) == 1) {
8151 // charge
8152 $dailysum = ($dailyprice + $absval);
8153 $newprice = $dailysum * $affdays;
8154 } else {
8155 // discount
8156 $dailysum = ($dailyprice - $absval);
8157 $newprice = $dailysum * $affdays;
8158 }
8159 }
8160
8161 // apply rounding
8162 if (!empty($s['roundmode'])) {
8163 $newprice = round($newprice, 0, constant($s['roundmode']));
8164 } else {
8165 $newprice = round($newprice, 2);
8166 }
8167
8168 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8169 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8170 }
8171 if (!in_array($s['id'], $mem[$k]['spids'])) {
8172 $mem[$k]['spids'][] = $s['id'];
8173 }
8174 $mem[$k]['sum'][] = $newprice;
8175 $mem[$k]['daysused'] += $affdays;
8176 }
8177 }
8178 if ($applyseasons) {
8179 foreach ($mem as $k => $v) {
8180 if ($v['daysused'] > 0 && $v['sum']) {
8181 $newprice = 0;
8182 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8183 $restdays = $arr[$k]['days'] - $v['daysused'];
8184 $addrest = $restdays * $dailyprice;
8185 $newprice += $addrest;
8186 foreach ($v['sum'] as $add) {
8187 $newprice += $add;
8188 }
8189 $arr[$k]['cost'] = $newprice;
8190 $arr[$k]['affdays'] = $v['daysused'];
8191 if (array_key_exists('spids', $v) && $v['spids']) {
8192 $arr[$k]['spids'] = $v['spids'];
8193 }
8194 }
8195 }
8196 }
8197 }
8198 // end week days with no season
8199
8200 return $arr;
8201 }
8202
8203 public static function getRoomRplansClosingDates($idroom)
8204 {
8205 $dbo = JFactory::getDbo();
8206
8207 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8208 $dbo->setQuery($q);
8209 $price_records = $dbo->loadAssocList();
8210 if (!$price_records) {
8211 return [];
8212 }
8213
8214 $closingd = [];
8215 foreach ($price_records as $prec) {
8216 if (empty($prec['closingd'])) {
8217 continue;
8218 }
8219 $price_closing = json_decode($prec['closingd'], true);
8220 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8221 continue;
8222 }
8223 // check expired dates and clean up
8224 $today_midnight = mktime(0, 0, 0);
8225 $cleaned = false;
8226 foreach ($price_closing[$idroom] as $k => $v) {
8227 if (strtotime($v) < $today_midnight) {
8228 $cleaned = true;
8229 unset($price_closing[$idroom][$k]);
8230 }
8231 }
8232 if (!$price_closing[$idroom]) {
8233 unset($price_closing[$idroom]);
8234 } elseif ($cleaned === true) {
8235 // reset array keys for smaller JSON size
8236 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8237 }
8238 if ($cleaned === true) {
8239 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8240 $dbo->setQuery($q);
8241 $dbo->execute();
8242 }
8243 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8244 continue;
8245 }
8246 $closingd[$prec['id']] = $price_closing[$idroom];
8247 }
8248
8249 return $closingd;
8250 }
8251
8252 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8253 {
8254 $dbo = JFactory::getDbo();
8255
8256 $closingd = [];
8257
8258 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8259 $dbo->setQuery($q);
8260 $price_records = $dbo->loadAssocList();
8261 if ($price_records && $roomids) {
8262 $info_start = getdate($checkints);
8263 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8264 $all_nights = [];
8265 for ($i = 0; $i < (int)$numnights; $i++) {
8266 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8267 $all_nights[] = date('Y-m-d', $next_midnight);
8268 }
8269 foreach ($price_records as $prec) {
8270 if (empty($prec['closingd'])) {
8271 continue;
8272 }
8273 $price_closing = json_decode($prec['closingd'], true);
8274 if (!$price_closing) {
8275 continue;
8276 }
8277 foreach ($price_closing as $idroom => $rclosedd) {
8278 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8279 continue;
8280 }
8281 if (!array_key_exists($idroom, $closingd)) {
8282 $closingd[$idroom] = [];
8283 }
8284 foreach ($all_nights as $night) {
8285 if (in_array($night, $rclosedd)) {
8286 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8287 $closingd[$idroom][$prec['id']][] = $night;
8288 } else {
8289 $closingd[$idroom][$prec['id']] = [$night];
8290 }
8291 }
8292 }
8293 }
8294 }
8295 }
8296
8297 return $closingd;
8298 }
8299
8300 /**
8301 * Tells whether at least one payment method must be selected to check-out.
8302 *
8303 * @param array $rooms_involved optional list of room IDs being booked.
8304 *
8305 * @return bool true if at least one payment method is applicable.
8306 *
8307 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
8308 */
8309 public static function areTherePayments($rooms_involved = [])
8310 {
8311 $dbo = JFactory::getDbo();
8312
8313 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
8314 $dbo->setQuery($q);
8315 $dbo->execute();
8316
8317 if (!$dbo->getNumRows()) {
8318 // no published payment options found
8319 return false;
8320 }
8321
8322 $pay_options = $dbo->loadAssocList();
8323
8324 if (!count($rooms_involved)) {
8325 // if no filters given, return true
8326 return true;
8327 }
8328
8329 // validate room associations
8330 foreach ($pay_options as $pay_k => $pay_v) {
8331 if (empty($pay_v['idrooms'])) {
8332 continue;
8333 }
8334 $rooms_supported = json_decode($pay_v['idrooms']);
8335 if (!is_array($rooms_supported) || !count($rooms_supported)) {
8336 continue;
8337 }
8338 // make sure all rooms being booked are supported
8339 foreach ($rooms_involved as $room_id_booked) {
8340 if (!in_array($room_id_booked, $rooms_supported)) {
8341 // we need to unset this payment option as soon as we find one non-matching room ID
8342 unset($pay_options[$pay_k]);
8343 break;
8344 }
8345 }
8346 }
8347
8348 return (bool)count($pay_options);
8349 }
8350
8351 public static function getPayment($idp, $vbo_tn = null)
8352 {
8353 if (empty($idp)) {
8354 return false;
8355 }
8356
8357 if (strstr((string)$idp, '=') !== false) {
8358 $parts = explode('=', $idp);
8359 $idp = $parts[0];
8360 }
8361
8362 $dbo = JFactory::getDbo();
8363
8364 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
8365 $dbo->setQuery($q);
8366 $payment = $dbo->loadAssocList();
8367 if (!$payment) {
8368 return false;
8369 }
8370
8371 if (is_object($vbo_tn)) {
8372 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
8373 }
8374
8375 return $payment[0];
8376 }
8377
8378 public static function getCronKey()
8379 {
8380 return VBOFactory::getConfig()->get('cronkey', '');
8381 }
8382
8383 public static function getNextInvoiceNumber()
8384 {
8385 return (VBOFactory::getConfig()->getInt('invoiceinum', 0) + 1);
8386 }
8387
8388 public static function getInvoiceNumberSuffix()
8389 {
8390 return VBOFactory::getConfig()->get('invoicesuffix', '');
8391 }
8392
8393 public static function getInvoiceCompanyInfo()
8394 {
8395 return VBOFactory::getConfig()->get('invcompanyinfo', '');
8396 }
8397
8398 /**
8399 * Gets the number for the next booking receipt generation,
8400 * updates the last receipt number used for the later iterations,
8401 * stores a new receipt record to keep track of the receipts issued.
8402 *
8403 * @param int $bid the Booking ID for which we are/want to generating the receipt.
8404 * @param [int] $newnum the last number used for generating the receipt.
8405 *
8406 * @return int
8407 */
8408 public static function getNextReceiptNumber ($bid, $newnum = false) {
8409 $dbo = JFactory::getDbo();
8410 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
8411 $dbo->setQuery($q);
8412 $dbo->execute();
8413 if ($dbo->getNumRows() > 0) {
8414 $s = $dbo->loadResult();
8415 //check if this booking has already a receipt, and return that number
8416 $prev_receipt = array();
8417 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
8418 $dbo->setQuery($q);
8419 $dbo->execute();
8420 if ($dbo->getNumRows() > 0) {
8421 $prev_receipt = $dbo->loadAssoc();
8422 }
8423 //update value (receipt generated)
8424 if ($newnum !== false && $newnum > 0) {
8425 $s = (int)$newnum;
8426 if (!(count($prev_receipt) > 0)) {
8427 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
8428 $dbo->setQuery($q);
8429 $dbo->execute();
8430 //insert the new receipt record
8431 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
8432 $dbo->setQuery($q);
8433 $dbo->execute();
8434 } else {
8435 //update receipt record
8436 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
8437 $dbo->setQuery($q);
8438 $dbo->execute();
8439 }
8440 }
8441 //
8442 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
8443 }
8444 //first execution of the method should create the configuration record
8445 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
8446 $dbo->setQuery($q);
8447 $dbo->execute();
8448 return 1;
8449 }
8450
8451 public static function getReceiptNotes ($newnotes = false) {
8452 $dbo = JFactory::getDbo();
8453 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
8454 $dbo->setQuery($q);
8455 $dbo->execute();
8456 if ($dbo->getNumRows() > 0) {
8457 $s = $dbo->loadResult();
8458 //update value
8459 if ($newnotes !== false) {
8460 $s = $newnotes;
8461 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
8462 $dbo->setQuery($q);
8463 $dbo->execute();
8464 }
8465 //
8466 return $s;
8467 }
8468 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
8469 $dbo->setQuery($q);
8470 $dbo->execute();
8471 return "";
8472 }
8473
8474 public static function loadColorTagsRules()
8475 {
8476 $color_tag_rules = [
8477 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
8478 'pend1' => 'VBWAITINGFORPAYMENT',
8479 'conf1' => 'VBDBTEXTROOMCLOSED',
8480 'conf2' => 'VBOCOLORTAGRULECONFTWO',
8481 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
8482 'inv1' => 'VBOCOLORTAGRULEINVONE',
8483 'rcp1' => 'VBOCOLORTAGRULERCPONE',
8484 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
8485 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
8486 'inv2' => 'VBOCOLORTAGRULEINVTWO',
8487 ];
8488
8489 /**
8490 * Trigger event to let third-party plugins override the default booking color tag rules.
8491 *
8492 * @since 1.16.4 (J) - 1.6.4 (WP)
8493 */
8494 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
8495
8496 return $color_tag_rules;
8497 }
8498
8499 public static function loadDefaultColorTags()
8500 {
8501 $default_color_tags = [
8502 [
8503 'color' => '#9b9b9b',
8504 'name' => 'VBWAITINGFORPAYMENT',
8505 'rule' => 'pend1'
8506 ],
8507 [
8508 'color' => '#333333',
8509 'name' => 'VBDBTEXTROOMCLOSED',
8510 'rule' => 'conf1'
8511 ],
8512 [
8513 'color' => '#ff8606',
8514 'name' => 'VBOCOLORTAGRULECONFTWO',
8515 'rule' => 'conf2'
8516 ],
8517 [
8518 'color' => '#0418c9',
8519 'name' => 'VBOCOLORTAGRULECONFTHREE',
8520 'rule' => 'conf3'
8521 ],
8522 [
8523 'color' => '#bed953',
8524 'name' => 'VBOCOLORTAGRULEINVONE',
8525 'rule' => 'inv1'
8526 ],
8527 [
8528 'color' => '#67f5b5',
8529 'name' => 'VBOCOLORTAGRULERCPONE',
8530 'rule' => 'rcp1'
8531 ],
8532 [
8533 'color' => '#04d2c2',
8534 'name' => 'VBOCOLORTAGRULECONFFOUR',
8535 'rule' => 'conf4'
8536 ],
8537 [
8538 'color' => '#00b316',
8539 'name' => 'VBOCOLORTAGRULECONFFIVE',
8540 'rule' => 'conf5'
8541 ],
8542 [
8543 'color' => '#00f323',
8544 'name' => 'VBOCOLORTAGRULEINVTWO',
8545 'rule' => 'inv2'
8546 ],
8547 ];
8548
8549 /**
8550 * Trigger event to let third-party plugins override the default color tags.
8551 *
8552 * @since 1.16.4 (J) - 1.6.4 (WP)
8553 */
8554 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
8555
8556 return $default_color_tags;
8557 }
8558
8559 public static function loadBookingsColorTags()
8560 {
8561 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
8562
8563 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
8564 }
8565
8566 public static function getBestColorContrast($hexcolor)
8567 {
8568 $hexcolor = str_replace('#', '', $hexcolor);
8569
8570 if (empty($hexcolor) || strlen($hexcolor) != 6) {
8571 return '#000000';
8572 }
8573
8574 $r = hexdec(substr($hexcolor, 0, 2));
8575 $g = hexdec(substr($hexcolor, 2, 2));
8576 $b = hexdec(substr($hexcolor, 4, 2));
8577
8578 // Counting the perceptive luminance - human eye favors green color
8579 // < 0.5 bright colors
8580 // > 0.5 dark colors
8581
8582 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
8583 }
8584
8585 public static function applyBookingColorTag($booking, $tags = [])
8586 {
8587 if (!is_array($tags) || !$tags) {
8588 $tags = self::loadBookingsColorTags();
8589 }
8590
8591 if (!empty($booking['colortag'])) {
8592 $color_tag_arr = json_decode($booking['colortag'], true);
8593 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
8594 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
8595 return $color_tag_arr;
8596 }
8597 }
8598
8599 $dbo = JFactory::getDbo();
8600
8601 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
8602 $docs_data = [];
8603 $invoice_numb = false;
8604 $receipt_numb = false;
8605
8606 if ($booking['status'] == 'confirmed') {
8607 $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`
8608 FROM `#__vikbooking_orders` AS `b`
8609 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
8610 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
8611 WHERE `b`.`id`=" . (int)$bid . ";";
8612 $dbo->setQuery($q);
8613 $docs_data = $dbo->loadAssoc();
8614 if ($docs_data) {
8615 $invoice_numb = (!empty($docs_data['inv_id']));
8616 $receipt_numb = (!empty($docs_data['rcp_id']));
8617
8618 /**
8619 * We inject the invoice number or receipt number into the color tag rules.
8620 * This is useful for the Tableaux page to show the invoice/receipt number.
8621 *
8622 * @since 1.12 (J) - 1.1.7 (WP)
8623 */
8624 if ($invoice_numb || $receipt_numb) {
8625 foreach ($tags as &$tval) {
8626 if ($invoice_numb) {
8627 $tval['invoice_number'] = $docs_data['inv_number'];
8628 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
8629 }
8630 if ($receipt_numb) {
8631 $tval['receipt_number'] = $docs_data['rcp_number'];
8632 }
8633 }
8634 // unset last reference
8635 unset($tval);
8636 }
8637 }
8638 }
8639
8640 /**
8641 * Some OTA bookings using an OTA-Collect business model may look as not entirely
8642 * paid when they actually are, due to commissions and pass through taxes. We also
8643 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
8644 *
8645 * @since 1.16.3 (J) - 1.6.3 (WP)
8646 */
8647 $ota_collected_booking = false;
8648 $hotel_collect_vcc_unpaid = false;
8649 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
8650 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
8651 $ota_collected_booking = true;
8652 }
8653 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
8654 // VCC detected, check the history for a payment event
8655 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
8656 // list of history types to fetch
8657 $h_payment_types = [];
8658 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
8659 if ($h_payment_group && $h_payment_group['types']) {
8660 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
8661 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
8662 }
8663 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
8664 if (!$payment_ev_datas) {
8665 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
8666 $hotel_collect_vcc_unpaid = true;
8667 }
8668 }
8669 }
8670
8671 foreach ($tags as $tkey => $tval) {
8672 if (empty($tval['rule'])) {
8673 continue;
8674 }
8675
8676 /**
8677 * Trigger event to let third-party plugins validate the custom rule.
8678 *
8679 * @since 1.16.4 (J) - 1.6.4 (WP)
8680 */
8681 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
8682 if (!empty($custom_rule)) {
8683 return (array) $custom_rule[0];
8684 }
8685
8686 switch ($tval['rule']) {
8687 case 'pend1':
8688 // Room is waiting for the payment (locked record)
8689 if ($booking['status'] == 'standby') {
8690 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
8691 $dbo->setQuery($q);
8692 $tmp_lock = $dbo->loadAssoc();
8693 if ($tmp_lock) {
8694 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8695 return $tval;
8696 }
8697 }
8698 break;
8699 case 'conf1':
8700 // Confirmed (Room Closed)
8701 if ($booking['status'] == 'confirmed' && $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED')) {
8702 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8703 return $tval;
8704 }
8705 break;
8706 case 'conf2':
8707 // Confirmed (No Rate 0.00/NULL Total)
8708 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
8709 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8710 return $tval;
8711 }
8712 break;
8713 case 'conf3':
8714 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
8715 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) {
8716 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8717 return $tval;
8718 }
8719 break;
8720 case 'inv1':
8721 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
8722 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
8723 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8724 return $tval;
8725 }
8726 break;
8727 case 'rcp1':
8728 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
8729 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
8730 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8731 return $tval;
8732 }
8733 break;
8734 case 'conf4':
8735 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
8736 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
8737 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8738 return $tval;
8739 }
8740 break;
8741 case 'conf5':
8742 // Confirmed (Total > 0 && Total Paid >= Total)
8743 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
8744 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8745 return $tval;
8746 }
8747 break;
8748 case 'inv2':
8749 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
8750 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
8751 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8752 return $tval;
8753 }
8754 break;
8755 default:
8756 // unsupported rule
8757 break;
8758 }
8759 }
8760
8761 return [];
8762 }
8763
8764 public static function getBookingInfoFromID($bid)
8765 {
8766 $dbo = JFactory::getDbo();
8767
8768 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
8769 $dbo->setQuery($q);
8770 $booking_info = $dbo->loadAssoc();
8771 if ($booking_info) {
8772 return $booking_info;
8773 }
8774
8775 return [];
8776 }
8777
8778 /**
8779 * Load booking details for a given list of IDs.
8780 *
8781 * @param int $roomid the room integer number.
8782 * @param array $room_bids_pool list of booking IDs involved.
8783 *
8784 * @return array
8785 */
8786 public static function loadRoomIndexesBookings($roomid, array $room_bids_pool)
8787 {
8788 $dbo = JFactory::getDbo();
8789
8790 if (empty($roomid) || !$room_bids_pool) {
8791 return [];
8792 }
8793
8794 $room_features_bookings = [];
8795 $roomid = (int)$roomid;
8796
8797 $all_bids = [];
8798 foreach ($room_bids_pool as $day => $bids) {
8799 $all_bids = array_merge($all_bids, $bids);
8800 }
8801 $all_bids = array_map('intval', array_unique($all_bids));
8802 $all_bids_str = implode(', ', $all_bids);
8803
8804 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
8805 $dbo->setQuery($q);
8806 $rbookings = $dbo->loadAssocList();
8807 if (!$rbookings) {
8808 return [];
8809 }
8810
8811 foreach ($rbookings as $k => $v) {
8812 if (empty($v['roomindex'])) {
8813 continue;
8814 }
8815 if (!isset($room_features_bookings[$v['roomindex']])) {
8816 $room_features_bookings[$v['roomindex']] = [];
8817 }
8818 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
8819 }
8820
8821 return $room_features_bookings;
8822 }
8823
8824 public static function getSendEmailWhen() {
8825 $dbo = JFactory::getDbo();
8826 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='emailsendwhen';";
8827 $dbo->setQuery($q);
8828 $dbo->execute();
8829 if ($dbo->getNumRows() > 0) {
8830 $cval = $dbo->loadAssocList();
8831 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8832 } else {
8833 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('emailsendwhen','1');";
8834 $dbo->setQuery($q);
8835 $dbo->execute();
8836 }
8837 return 1;
8838 }
8839
8840 public static function getMinutesAutoRemove() {
8841 $dbo = JFactory::getDbo();
8842 $minar = 0;
8843 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='minautoremove';";
8844 $dbo->setQuery($q);
8845 $dbo->execute();
8846 if ($dbo->getNumRows() > 0) {
8847 $minar = (int)$dbo->loadResult();
8848 }
8849 return $minar;
8850 }
8851
8852 public static function getSMSAPIClass() {
8853 $dbo = JFactory::getDbo();
8854 $cfile = '';
8855 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsapi';";
8856 $dbo->setQuery($q);
8857 $dbo->execute();
8858 if ($dbo->getNumRows() > 0) {
8859 $cval = $dbo->loadAssocList();
8860 $cfile = $cval[0]['setting'];
8861 }
8862 return $cfile;
8863 }
8864
8865 public static function autoSendSMSEnabled() {
8866 $dbo = JFactory::getDbo();
8867 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsautosend';";
8868 $dbo->setQuery($q);
8869 $dbo->execute();
8870 if ($dbo->getNumRows() > 0) {
8871 $cval = $dbo->loadAssocList();
8872 return intval($cval[0]['setting']) > 0 ? true : false;
8873 }
8874 return false;
8875 }
8876
8877 public static function getSendSMSTo() {
8878 $dbo = JFactory::getDbo();
8879 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendto';";
8880 $dbo->setQuery($q);
8881 $dbo->execute();
8882 if ($dbo->getNumRows() > 0) {
8883 $cval = $dbo->loadAssocList();
8884 if (!empty($cval[0]['setting'])) {
8885 $sto = json_decode($cval[0]['setting'], true);
8886 if (is_array($sto)) {
8887 return $sto;
8888 }
8889 }
8890 }
8891 return array();
8892 }
8893
8894 public static function getSendSMSWhen() {
8895 $dbo = JFactory::getDbo();
8896 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendwhen';";
8897 $dbo->setQuery($q);
8898 $dbo->execute();
8899 if ($dbo->getNumRows() > 0) {
8900 $cval = $dbo->loadAssocList();
8901 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8902 }
8903 return 1;
8904 }
8905
8906 public static function getSMSAdminPhone() {
8907 $dbo = JFactory::getDbo();
8908 $pnum = '';
8909 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsadminphone';";
8910 $dbo->setQuery($q);
8911 $dbo->execute();
8912 if ($dbo->getNumRows() > 0) {
8913 $cval = $dbo->loadAssocList();
8914 $pnum = $cval[0]['setting'];
8915 }
8916 return $pnum;
8917 }
8918
8919 public static function getSMSParams($as_array = true) {
8920 $dbo = JFactory::getDbo();
8921 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsparams';";
8922 $dbo->setQuery($q);
8923 $dbo->execute();
8924 if ($dbo->getNumRows() > 0) {
8925 $cval = $dbo->loadAssocList();
8926 if (!empty($cval[0]['setting'])) {
8927 if (!$as_array) {
8928 return $cval[0]['setting'];
8929 }
8930 $sparams = json_decode($cval[0]['setting'], true);
8931 if (is_array($sparams)) {
8932 return $sparams;
8933 }
8934 }
8935 }
8936 return array();
8937 }
8938
8939 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
8940 $dbo = JFactory::getDbo();
8941 switch (strtolower($booking_status)) {
8942 case 'standby':
8943 $status = 'pend';
8944 break;
8945 case 'cancelled':
8946 $status = 'canc';
8947 break;
8948 default:
8949 $status = '';
8950 break;
8951 }
8952 $paramtype = 'sms'.$type.'tpl'.$status;
8953 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
8954 $dbo->setQuery($q);
8955 $dbo->execute();
8956 if ($dbo->getNumRows() < 1) {
8957 if ($status == 'canc') {
8958 //Type cancelled is used by VCM since v1.6.6
8959 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
8960 $dbo->setQuery($q);
8961 $dbo->execute();
8962 }
8963 return '';
8964 }
8965 $ft = $dbo->loadAssocList();
8966 if (is_object($vbo_tn)) {
8967 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
8968 }
8969 return $ft[0]['setting'];
8970 }
8971
8972 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8973 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
8974 }
8975
8976 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8977 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
8978 }
8979
8980 public static function checkPhonePrefixCountry($phone, $country_threecode) {
8981 $dbo = JFactory::getDbo();
8982 $phone = str_replace(" ", '', trim($phone));
8983 $cprefix = '';
8984 if (!empty($country_threecode)) {
8985 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
8986 $dbo->setQuery($q);
8987 $dbo->execute();
8988 if ($dbo->getNumRows() > 0) {
8989 $cprefix = $dbo->loadResult();
8990 $cprefix = str_replace(" ", '', trim($cprefix));
8991 }
8992 }
8993 if (!empty($cprefix)) {
8994 if (substr($phone, 0, 1) != '+') {
8995 if (substr($phone, 0, 2) == '00') {
8996 $phone = '+'.substr($phone, 2);
8997 } else {
8998 $phone = $cprefix.$phone;
8999 }
9000 }
9001 }
9002 return $phone;
9003 }
9004
9005 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
9006 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
9007
9008 /**
9009 * Parse all conditional text rules.
9010 *
9011 * @since 1.14 (J) - 1.4.0 (WP)
9012 */
9013 self::getConditionalRulesInstance()
9014 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9015 ->parseTokens($tpl);
9016 //
9017
9018 $vbo_df = self::getDateFormat();
9019 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9020 $datesep = self::getDateSeparator();
9021
9022 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9023 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9024 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9025 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9026 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9027 if (!empty($booking['confirmnumber'])) {
9028 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9029 } else {
9030 $tpl = str_replace("{confirmnumb}", '', $tpl);
9031 }
9032 if (!empty($booking['idorderota'])) {
9033 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9034 } else {
9035 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9036 }
9037
9038 $rooms_booked = array();
9039 $rooms_names = array();
9040 $tot_adults = 0;
9041 $tot_children = 0;
9042 $tot_guests = 0;
9043 foreach ($booking_rooms as $broom) {
9044 $rooms_names[] = $broom['room_name'];
9045 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9046 $rooms_booked[$broom['room_name']] += 1;
9047 } else {
9048 $rooms_booked[$broom['room_name']] = 1;
9049 }
9050 $tot_adults += (int)$broom['adults'];
9051 $tot_children += (int)$broom['children'];
9052 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9053 }
9054 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9055 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9056 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9057 $rooms_booked_quant = array();
9058 foreach ($rooms_booked as $rname => $quant) {
9059 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9060 }
9061 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9062 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9063 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9064 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9065 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9066 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9067 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9068 $remaining_bal = $booking['total'] - $booking['totpaid'];
9069 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9070
9071 return $tpl;
9072 }
9073
9074 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9075 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9076
9077 /**
9078 * Parse all conditional text rules.
9079 *
9080 * @since 1.14 (J) - 1.4.0 (WP)
9081 */
9082 self::getConditionalRulesInstance()
9083 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9084 ->parseTokens($tpl);
9085 //
9086
9087 $vbo_df = self::getDateFormat();
9088 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9089 $datesep = self::getDateSeparator();
9090
9091 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9092 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9093 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9094 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9095 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9096 if (!empty($booking['confirmnumber'])) {
9097 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9098 } else {
9099 $tpl = str_replace("{confirmnumb}", '', $tpl);
9100 }
9101 if (!empty($booking['idorderota'])) {
9102 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9103 } else {
9104 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9105 }
9106
9107 $rooms_booked = array();
9108 $rooms_names = array();
9109 $tot_adults = 0;
9110 $tot_children = 0;
9111 $tot_guests = 0;
9112 foreach ($booking_rooms as $broom) {
9113 $rooms_names[] = $broom['room_name'];
9114 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9115 $rooms_booked[$broom['room_name']] += 1;
9116 } else {
9117 $rooms_booked[$broom['room_name']] = 1;
9118 }
9119 $tot_adults += (int)$broom['adults'];
9120 $tot_children += (int)$broom['children'];
9121 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9122 }
9123 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9124 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9125 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9126 $rooms_booked_quant = array();
9127 foreach ($rooms_booked as $rname => $quant) {
9128 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9129 }
9130 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9131 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9132 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9133 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9134 $remaining_bal = $booking['total'] - $booking['totpaid'];
9135 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9136 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9137
9138 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9139 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9140
9141 if (VBOPlatformDetection::isWordPress()) {
9142 /**
9143 * @wponly Rewrite URI for front-end
9144 */
9145 $book_link = str_replace(JUri::root(), '', $book_link);
9146 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9147 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9148 if ($itemid) {
9149 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9150 }
9151 } else {
9152 /**
9153 * @joomlaonly Rewrite URI for front-end
9154 */
9155 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9156 $uri_parts = [
9157 'option' => 'com_vikbooking',
9158 'view' => 'booking',
9159 'sid' => $use_sid,
9160 'ts' => $booking['ts'],
9161 ];
9162 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9163 // inject lang in query string for proper link routing
9164 $uri_parts['lang'] = $booking['lang'];
9165 }
9166 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9167 }
9168
9169 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9170 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9171 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9172
9173 return $tpl;
9174 }
9175
9176 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9177 $dbo = JFactory::getDbo();
9178 if (!class_exists('VboApplication')) {
9179 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9180 }
9181 $vbo_app = new VboApplication;
9182 if (empty($oid)) {
9183 return false;
9184 }
9185 $sms_api = self::getSMSAPIClass();
9186 if (empty($sms_api)) {
9187 return false;
9188 }
9189 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9190 return false;
9191 }
9192 $sms_api_params = self::getSMSParams();
9193 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9194 return false;
9195 }
9196 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9197 return false;
9198 }
9199 $send_sms_to = self::getSendSMSTo();
9200 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9201 return false;
9202 }
9203 $booking = array();
9204 $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.";";
9205 $dbo->setQuery($q);
9206 $dbo->execute();
9207 if ($dbo->getNumRows() > 0) {
9208 $booking = $dbo->loadAssoc();
9209 }
9210 if (!(count($booking) > 0)) {
9211 return false;
9212 }
9213 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9214 return false;
9215 }
9216 $booking_rooms = array();
9217 $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'].";";
9218 $dbo->setQuery($q);
9219 $dbo->execute();
9220 if ($dbo->getNumRows() > 0) {
9221 $booking_rooms = $dbo->loadAssocList();
9222 }
9223 $admin_phone = self::getSMSAdminPhone();
9224 $admin_sendermail = self::getSenderMail();
9225 $admin_email = self::getAdminMail();
9226 $f_result = false;
9227 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9228 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9229 //SMS for the administrator
9230 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9231 if (!empty($sms_text)) {
9232 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9233 //administrator phone can contain multiple numbers separated by comma or semicolon
9234 $admin_phones = array();
9235 if (strpos($admin_phone, ',') !== false) {
9236 $all_phones = explode(',', $admin_phone);
9237 foreach ($all_phones as $aph) {
9238 if (!empty($aph)) {
9239 $admin_phones[] = trim($aph);
9240 }
9241 }
9242 } elseif (strpos($admin_phone, ';') !== false) {
9243 $all_phones = explode(';', $admin_phone);
9244 foreach ($all_phones as $aph) {
9245 if (!empty($aph)) {
9246 $admin_phones[] = trim($aph);
9247 }
9248 }
9249 } else {
9250 $admin_phones[] = $admin_phone;
9251 }
9252 foreach ($admin_phones as $admphone) {
9253 $response_obj = $sms_obj->sendMessage($admphone, $sms_text);
9254 if ( !$sms_obj->validateResponse($response_obj) ) {
9255 //notify the administrator via email with the error of the SMS sending
9256 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9257 } else {
9258 $f_result = true;
9259 }
9260 }
9261 }
9262 }
9263 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9264 //SMS for the Customer
9265 $vbo_tn = self::getTranslator();
9266 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9267 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9268 if (!empty($sms_text)) {
9269 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9270 $response_obj = $sms_obj->sendMessage($booking['phone'], $sms_text);
9271 if ( !$sms_obj->validateResponse($response_obj) ) {
9272 //notify the administrator via email with the error of the SMS sending
9273 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9274 } else {
9275 $f_result = true;
9276 }
9277 }
9278 }
9279 return $f_result;
9280 }
9281
9282 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9283 {
9284 if (!defined('_VIKBOOKINGEXEC')) {
9285 define('_VIKBOOKINGEXEC', '1');
9286 }
9287
9288 ob_start();
9289 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
9290 $content = ob_get_contents();
9291 ob_end_clean();
9292
9293 $default_params = array(
9294 'show_header' => 0,
9295 'header_data' => array(),
9296 'show_footer' => 0,
9297 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9298 'pdf_unit' => 'PDF_UNIT',
9299 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9300 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9301 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9302 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9303 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9304 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9305 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9306 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9307 'header_font_size' => '10',
9308 'body_font_size' => '10',
9309 'footer_font_size' => '8',
9310 'show_lines_taxrate_col' => 0,
9311 );
9312
9313 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
9314 $default_params = array_merge($default_params, $invoice_params);
9315 }
9316
9317 return array($content, $default_params);
9318 }
9319
9320 /**
9321 * Includes within a buffer the template file for the custom (manual) invoice.
9322 *
9323 * @param array $invoice the record of the custom invoice
9324 * @param array $customer the customer record
9325 *
9326 * @return string the HTML content of the template file
9327 *
9328 * @since 1.11.1
9329 */
9330 public static function loadCustomInvoiceTmpl($invoice, $customer) {
9331 if (!defined('_VIKBOOKINGEXEC')) {
9332 define('_VIKBOOKINGEXEC', '1');
9333 }
9334 ob_start();
9335 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
9336 $content = ob_get_contents();
9337 ob_end_clean();
9338 $default_params = array(
9339 'show_header' => 0,
9340 'header_data' => array(),
9341 'show_footer' => 0,
9342 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9343 'pdf_unit' => 'PDF_UNIT',
9344 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9345 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9346 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9347 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9348 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9349 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9350 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9351 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9352 'header_font_size' => '10',
9353 'body_font_size' => '10',
9354 'footer_font_size' => '8'
9355 );
9356 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && $invoice_params) {
9357 $default_params = array_merge($default_params, $invoice_params);
9358 }
9359 return array($content, $default_params);
9360 }
9361
9362 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
9363 {
9364 // clone the original template file source
9365 $parsed = $invoicetpl;
9366
9367 /**
9368 * Parse all conditional text rules.
9369 *
9370 * @since 1.14 (J) - 1.4.0 (WP)
9371 */
9372 self::getConditionalRulesInstance()
9373 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9374 ->parseTokens($parsed);
9375 //
9376
9377 $dbo = JFactory::getDbo();
9378 if (is_null($vbo_tn)) {
9379 $vbo_tn = self::getTranslator();
9380 }
9381 // availability helper
9382 $av_helper = self::getAvailabilityInstance();
9383
9384 $nowdf = self::getDateFormat();
9385 if ($nowdf == "%d/%m/%Y") {
9386 $df = 'd/m/Y';
9387 } elseif ($nowdf == "%m/%d/%Y") {
9388 $df = 'm/d/Y';
9389 } else {
9390 $df = 'Y/m/d';
9391 }
9392 $datesep = self::getDateSeparator();
9393
9394 /**
9395 * Split stay reservation.
9396 *
9397 * @since 1.16.0 (J) - 1.6.0 (WP)
9398 */
9399 $room_stay_dates = [];
9400 if ($booking['split_stay']) {
9401 if ($booking['status'] == 'confirmed') {
9402 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
9403 } else {
9404 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
9405 }
9406 // immediately count the number of nights of stay for each split room
9407 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
9408 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
9409 // overwrite values for compatibility with non-confirmed bookings
9410 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
9411 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
9412 }
9413 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
9414 // overwrite the whole array
9415 $room_stay_dates[$sps_r_k] = $sps_r_v;
9416 }
9417 }
9418
9419 $companylogo = self::getSiteLogo();
9420 $uselogo = '';
9421 if (!empty($companylogo)) {
9422 /**
9423 * Let's try to prevent TCPDF errors.
9424 *
9425 * @since August 2nd 2019
9426 */
9427 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9428 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9429 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9430 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9431 }
9432 }
9433 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
9434 $parsed = str_replace("{company_info}", $company_info, $parsed);
9435 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
9436 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
9437 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
9438 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
9439
9440 // custom fields replacement
9441 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
9442 if (is_array($cmatches[1]) && $cmatches[1]) {
9443 $cfids = array();
9444 foreach ($cmatches[1] as $cfid) {
9445 $cfids[] = $cfid;
9446 }
9447 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
9448 $dbo->setQuery($q);
9449 $cfields = $dbo->loadAssocList();
9450 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
9451 $cfmap = array();
9452 foreach ($cfields as $cf) {
9453 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
9454 }
9455 $cfmapreplace = array();
9456 $partsreceived = explode("\n", $booking['custdata']);
9457 if (count($partsreceived) > 0) {
9458 foreach ($partsreceived as $pst) {
9459 if (!empty($pst)) {
9460 $tmpdata = explode(":", $pst);
9461 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
9462 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
9463 }
9464 }
9465 }
9466 }
9467 foreach ($cmatches[1] as $cfid) {
9468 if (array_key_exists($cfid, $cfmapreplace)) {
9469 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
9470 } else {
9471 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
9472 }
9473 }
9474 }
9475
9476 // invoice price description - Start
9477 $rooms = array();
9478 $tars = array();
9479 $arrpeople = array();
9480 $is_package = !empty($booking['pkg']) ? true : false;
9481 $tot_adults = 0;
9482 $tot_children = 0;
9483 $tot_guests = 0;
9484 foreach ($booking_rooms as $kor => $or) {
9485 $num = $kor + 1;
9486 $rooms[$num] = $or;
9487
9488 // determine the days to consider for the count of the availability
9489 $room_nights = $booking['days'];
9490 $room_checkin = $booking['checkin'];
9491 $room_checkout = $booking['checkout'];
9492 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9493 $room_nights = $room_stay_dates[$kor]['nights'];
9494 $room_checkin = $room_stay_dates[$kor]['checkin'];
9495 $room_checkout = $room_stay_dates[$kor]['checkout'];
9496 }
9497
9498 $arrpeople[$num]['adults'] = $or['adults'];
9499 $arrpeople[$num]['children'] = $or['children'];
9500 $tot_adults += $or['adults'];
9501 $tot_children += $or['children'];
9502 $tot_guests += ($or['adults'] + $or['children']);
9503 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9504 //package or custom cost set from the back-end
9505 continue;
9506 }
9507 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
9508 $dbo->setQuery($q);
9509 $tar = $dbo->loadAssocList();
9510 if ($tar) {
9511 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
9512 //different usage
9513 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
9514 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
9515 //Occupancy Override
9516 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
9517 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
9518 //
9519 if (is_array($diffusageprice)) {
9520 //set a charge or discount to the price(s) for the different usage of the room
9521 foreach ($tar as $kpr => $vpr) {
9522 $tar[$kpr]['diffusage'] = $or['adults'];
9523 if ($diffusageprice['chdisc'] == 1) {
9524 //charge
9525 if ($diffusageprice['valpcent'] == 1) {
9526 //fixed value
9527 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9528 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9529 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
9530 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9531 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
9532 } else {
9533 //percentage value
9534 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9535 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
9536 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
9537 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9538 $tar[$kpr]['cost'] = $aduseval;
9539 }
9540 } else {
9541 //discount
9542 if ($diffusageprice['valpcent'] == 1) {
9543 //fixed value
9544 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9545 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9546 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
9547 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9548 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
9549 } else {
9550 //percentage value
9551 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9552 $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);
9553 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
9554 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9555 $tar[$kpr]['cost'] = $aduseval;
9556 }
9557 }
9558 }
9559 }
9560 }
9561 //
9562 $tars[$num] = $tar[0];
9563 }
9564 }
9565 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
9566 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
9567 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
9568 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
9569 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
9570 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
9571
9572 $isdue = 0;
9573 $tot_taxes = 0;
9574 $tot_city_taxes = 0;
9575 $tot_fees = 0;
9576 $pricestr = array();
9577 $optstr = array();
9578 // start building the tax summary
9579 VBOTaxonomySummary::start();
9580 foreach ($booking_rooms as $kor => $or) {
9581 $num = $kor + 1;
9582
9583 // determine the days to consider for the count of the availability
9584 $room_nights = $booking['days'];
9585 $room_checkin = $booking['checkin'];
9586 $room_checkout = $booking['checkout'];
9587 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9588 $room_nights = $room_stay_dates[$kor]['nights'];
9589 $room_checkin = $room_stay_dates[$kor]['checkin'];
9590 $room_checkout = $room_stay_dates[$kor]['checkout'];
9591 }
9592
9593 $pricestr[$num] = array();
9594 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9595 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
9596 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
9597 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
9598 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
9599 $pricestr[$num]['tot'] = $calctar;
9600 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
9601 $tot_taxes += ($calctar - $cost_minus_tax);
9602 $isdue += $calctar;
9603 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
9604 $pricestr[$num]['tax_rate'] = $tax_rate;
9605 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
9606 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9607 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
9608 $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'] : "");
9609 $pricestr[$num]['tot'] = $calctar;
9610 $tars[$num]['calctar'] = $calctar;
9611 $isdue += $calctar;
9612 if ($calctar == $display_rate) {
9613 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
9614 $tot_taxes += ($display_rate - $cost_minus_tax);
9615 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
9616 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
9617 $pricestr[$num]['tax_rate'] = $tax_rate;
9618 } else {
9619 $tot_taxes += ($calctar - $display_rate);
9620 $pricestr[$num]['tax'] = ($calctar - $display_rate);
9621 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
9622 $pricestr[$num]['tax_rate'] = $tax_rate;
9623 }
9624 }
9625 $optstr[$num] = array();
9626 $opt_ind = 0;
9627 if (!empty($or['optionals'])) {
9628 $stepo = explode(";", $or['optionals']);
9629 foreach ($stepo as $roptkey => $oo) {
9630 if (!empty($oo)) {
9631 $stept = explode(":", $oo);
9632 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
9633 $dbo->setQuery($q);
9634 $actopt = $dbo->loadAssocList();
9635 if ($actopt) {
9636 if (is_object($vbo_tn)) {
9637 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
9638 }
9639 $optstr[$num][$opt_ind] = array();
9640 $chvar = '';
9641 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
9642 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
9643 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
9644 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
9645 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
9646 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
9647 $agestept = explode('-', $stept[1]);
9648 $stept[1] = $agestept[0];
9649 $chvar = $agestept[1];
9650 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
9651 //percentage value of the adults tariff
9652 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9653 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9654 } else {
9655 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9656 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9657 }
9658 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
9659 //VBO 1.10 - percentage value of room base cost
9660 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9661 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9662 } else {
9663 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
9664 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9665 }
9666 }
9667 $actopt[0]['chageintv'] = $chvar;
9668 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
9669 $actopt[0]['quan'] = $stept[1];
9670 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
9671 } else {
9672 $actopt[0]['quan'] = $stept[1];
9673 // VBO 1.11 - options percentage cost of the room total fee
9674 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9675 $deftar_basecosts = $or['cust_cost'];
9676 } else {
9677 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9678 }
9679 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
9680 //
9681 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
9682 }
9683 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
9684 $realcost = $actopt[0]['maxprice'];
9685 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
9686 $realcost = $actopt[0]['maxprice'] * $stept[1];
9687 }
9688 }
9689 if ($actopt[0]['perperson'] == 1) {
9690 $realcost = $realcost * $or['adults'];
9691 }
9692 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
9693 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
9694 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
9695 $optstr[$num][$opt_ind]['tax'] = 0;
9696 if ($actopt[0]['is_citytax'] == 1) {
9697 $tot_city_taxes += $tmpopr;
9698 } elseif ($actopt[0]['is_fee'] == 1) {
9699 $tot_fees += $tmpopr;
9700 }
9701 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
9702 if ($tmpopr == $realcost) {
9703 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
9704 $tot_taxes += ($realcost - $opt_minus_tax);
9705 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
9706 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
9707 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9708 } else {
9709 $tot_taxes += ($tmpopr - $realcost);
9710 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
9711 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
9712 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9713 }
9714 //
9715 $opt_ind++;
9716 $isdue += $tmpopr;
9717 }
9718 }
9719 }
9720 }
9721
9722 // custom extra costs
9723 if (!empty($or['extracosts'])) {
9724 $cur_extra_costs = json_decode($or['extracosts'], true);
9725 foreach ($cur_extra_costs as $eck => $ecv) {
9726 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9727 $isdue += $ecplustax;
9728 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
9729 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
9730 $optstr[$num][$opt_ind]['tax'] = 0;
9731 if ($ecplustax == $ecv['cost']) {
9732 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9733 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
9734 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
9735 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
9736 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9737 } else {
9738 $tot_taxes += ($ecplustax - $ecv['cost']);
9739 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
9740 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
9741 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9742 }
9743 $opt_ind++;
9744 }
9745 }
9746 }
9747
9748 $usedcoupon = false;
9749 if (strlen($booking['coupon']) > 0) {
9750 $orig_isdue = $isdue;
9751 $expcoupon = explode(";", $booking['coupon']);
9752 $usedcoupon = $expcoupon;
9753 $isdue = $isdue - (float)$expcoupon[1];
9754 if ($isdue != $orig_isdue) {
9755 //lower taxes proportionally
9756 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9757 }
9758 }
9759 if ($booking['refund'] > 0) {
9760 $orig_isdue = $isdue;
9761 $isdue -= $booking['refund'];
9762 if ($isdue != $orig_isdue) {
9763 //lower taxes proportionally
9764 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9765 }
9766 }
9767 $rows_written = 0;
9768 $inv_rows = '';
9769 foreach ($pricestr as $num => $price_descr) {
9770 // add room split stay information, if any
9771 $split_stay_str = '';
9772 $kor = $num - 1;
9773 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
9774 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
9775 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
9776 }
9777
9778 $inv_rows .= '<tr>'."\n";
9779 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
9780 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($price_descr['tot'] - $price_descr['tax'])).'</td>'."\n";
9781 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tax']).'</td>'."\n";
9782 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tot']).'</td>'."\n";
9783 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9784 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
9785 }
9786 $inv_rows .= '</tr>'."\n";
9787 $rows_written++;
9788 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
9789 foreach ($optstr[$num] as $optk => $optv) {
9790 $inv_rows .= '<tr>'."\n";
9791 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
9792 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($optv['tot'] - $optv['tax'])).'</td>'."\n";
9793 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tax']).'</td>'."\n";
9794 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tot']).'</td>'."\n";
9795 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9796 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
9797 }
9798 $inv_rows .= '</tr>'."\n";
9799 $rows_written++;
9800 }
9801 }
9802 }
9803
9804 // if discount print row
9805 if ($usedcoupon !== false) {
9806 $inv_rows .= '<tr>'."\n";
9807 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9808 $inv_rows .= '</tr>'."\n";
9809 $inv_rows .= '<tr>'."\n";
9810 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
9811 $inv_rows .= '<td></td>'."\n";
9812 $inv_rows .= '<td></td>'."\n";
9813 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($usedcoupon[1]).'</td>'."\n";
9814 $inv_rows .= '</tr>'."\n";
9815 $rows_written += 2;
9816 }
9817 // if refunded amount, print row
9818 if ($booking['refund'] > 0) {
9819 $inv_rows .= '<tr>'."\n";
9820 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9821 $inv_rows .= '</tr>'."\n";
9822 $inv_rows .= '<tr>'."\n";
9823 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
9824 $inv_rows .= '<td></td>'."\n";
9825 $inv_rows .= '<td></td>'."\n";
9826 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($booking['refund']).'</td>'."\n";
9827 $inv_rows .= '</tr>'."\n";
9828 $rows_written += 2;
9829 }
9830 //
9831 $min_records = 8;
9832 if ($rows_written < $min_records) {
9833 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
9834 $inv_rows .= '<tr>'."\n";
9835 $inv_rows .= '<td></td>'."\n";
9836 $inv_rows .= '<td></td>'."\n";
9837 $inv_rows .= '<td></td>'."\n";
9838 $inv_rows .= '</tr>'."\n";
9839 }
9840 }
9841 //invoice price description - End
9842 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
9843 $parsed = str_replace("{invoice_totalnet}", $booking['currencyname'].' '.self::numberFormat(($isdue - $tot_taxes)), $parsed);
9844 $parsed = str_replace("{invoice_totaltax}", $booking['currencyname'].' '.self::numberFormat($tot_taxes), $parsed);
9845 $parsed = str_replace("{invoice_grandtotal}", $booking['currencyname'].' '.self::numberFormat($isdue), $parsed);
9846 $parsed = str_replace("{inv_notes}", ($booking['inv_notes'] ?? ''), $parsed);
9847
9848 // invoice tax summary
9849 $tax_summary = VBOTaxonomySummary::get();
9850 $tax_names = VBOTaxonomySummary::getNames();
9851 $taxsum_rows = "";
9852 foreach ($tax_summary as $tax_rate => $tax_amount) {
9853 if (!$tax_rate && !$tax_amount) {
9854 continue;
9855 }
9856 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
9857 $taxsum_rows .= '<tr>' . "\n";
9858 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
9859 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
9860 $taxsum_rows .= '<td>' . $booking['currencyname'] . ' ' . self::numberFormat($tax_amount) . '</td>' . "\n";
9861 $taxsum_rows .= '</tr>' . "\n";
9862 }
9863 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
9864
9865 return $parsed;
9866 }
9867
9868 /**
9869 * Specific method for parsing the template file for the custom (manual) invoices.
9870 *
9871 * @param string $invoicetpl the plain custom invoice template file before parsing
9872 * @param array $invoice the invoice record
9873 * @param array $customer the customer record
9874 *
9875 * @return string the HTML content of the parsed custom invoice template
9876 *
9877 * @since 1.11.1
9878 */
9879 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
9880 $nowdf = self::getDateFormat(true);
9881 if ($nowdf == "%d/%m/%Y") {
9882 $df = 'd/m/Y';
9883 } elseif ($nowdf == "%m/%d/%Y") {
9884 $df = 'm/d/Y';
9885 } else {
9886 $df = 'Y/m/d';
9887 }
9888 $datesep = self::getDateSeparator(true);
9889 $companylogo = self::getSiteLogo();
9890 $uselogo = '';
9891 if (!empty($companylogo)) {
9892 /**
9893 * Let's try to prevent TCPDF errors by checking if the file exists.
9894 *
9895 * @since August 2nd 2019
9896 */
9897 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9898 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9899 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9900 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9901 }
9902 }
9903 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
9904 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
9905 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
9906 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
9907 // customer information
9908 $custinfo = '';
9909 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
9910 $custinfo .= $customer['email'] . "\n";
9911 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
9912 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
9913 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
9914 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
9915 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
9916 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
9917 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
9918 // invoice notes
9919 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
9920
9921 return $invoicetpl;
9922 }
9923
9924 /**
9925 * Generates or updates an invoice for a specific reservation, always in PDF format, and electronic if some drivers were enabled.
9926 *
9927 * @param array $booking the booking information.
9928 * @param int $invoice_num the invoice number.
9929 * @param string $invoice_suff the invoice suffix.
9930 * @param string $invoice_date the invoice date.
9931 * @param string $company_info optional company information.
9932 * @param bool $translate whether the invoice contents should be translated in the booking language.
9933 * @param bool $refresh_pdf true to only refresh (re-generate) the PDF, in case some extra information is available.
9934 *
9935 * @return mixed false in case of failure, invoice record ID if the PDF invoice was generated, and the record stored.
9936 *
9937 * @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.
9938 */
9939 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false, $refresh_pdf = false)
9940 {
9941 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
9942 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
9943 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
9944
9945 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
9946 return false;
9947 }
9948
9949 /**
9950 * Trigger event to allow third party plugins to override some values for the invoice generation.
9951 *
9952 * @since 1.16.3 (J) - 1.6.3 (WP)
9953 */
9954 $event_args = [
9955 'invoice_num' => $invoice_num,
9956 'invoice_suff' => $invoice_suff,
9957 'invoice_date' => $invoice_date,
9958 'company_info' => $company_info,
9959 'translate' => $translate,
9960 ];
9961 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
9962 if (is_array($event_args) && $event_args) {
9963 // extract the new values to be used
9964 extract($event_args);
9965 }
9966
9967 $dbo = JFactory::getDbo();
9968 $vbo_tn = self::getTranslator();
9969
9970 // inject the currency name value
9971 $currencyname = self::getCurrencyName();
9972 $booking['currencyname'] = $currencyname;
9973
9974 // make sure we've got a customer ID assigned to the reservation record
9975 if (!isset($booking['idcustomer']) || !isset($booking['customer_name'])) {
9976 $booking_customer = self::getCPinInstance()->getCustomerFromBooking($booking['id']);
9977 if ($booking_customer) {
9978 $booking['idcustomer'] = $booking_customer['id'];
9979 $booking['customer_name'] = $booking_customer['first_name'] . ' ' . $booking_customer['last_name'];
9980 $booking['customer_pin'] = $booking_customer['pin'];
9981 $booking['country_name'] = $booking_customer['country_name'] ?? '';
9982 }
9983 }
9984
9985 $nowdf = self::getDateFormat(true);
9986 if ($nowdf == "%d/%m/%Y") {
9987 $df = 'd/m/Y';
9988 } elseif ($nowdf == "%m/%d/%Y") {
9989 $df = 'm/d/Y';
9990 } else {
9991 $df = 'Y/m/d';
9992 }
9993 $datesep = self::getDateSeparator(true);
9994
9995 if (empty($invoice_date)) {
9996 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
9997 $used_date = $booking['ts'];
9998 } else {
9999 /**
10000 * We could be re-generating an invoice for a booking that already had an invoice.
10001 * In order to modify some entries in the invoice, the whole PDF is re-generated.
10002 * It is now possible to keep the same invoice date as the previous one, so check
10003 * what value contains $invoice_date to see if it's different from today's date.
10004 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
10005 * to also check the length of the string $invoice_date before using that date.
10006 *
10007 * @since 1.10 - August 2018
10008 */
10009 $base_ts = time();
10010 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
10011 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
10012 }
10013 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
10014 $used_date = $base_ts;
10015 }
10016
10017 // load room reservation records
10018 $q = $dbo->getQuery(true)
10019 ->select($dbo->qn('or') . '.*')
10020 ->select($dbo->qn('r.name', 'room_name'))
10021 ->select($dbo->qn('r.fromadult'))
10022 ->select($dbo->qn('r.toadult'))
10023 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
10024 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
10025 ->where($dbo->qn('or.idorder') . ' = ' . (int)$booking['id'])
10026 ->order($dbo->qn('or.id') . ' ASC');
10027
10028 $dbo->setQuery($q);
10029 $booking_rooms = $dbo->loadAssocList();
10030
10031 if (!$booking_rooms) {
10032 return false;
10033 }
10034
10035 // check if an invoice for this reservation exists
10036 $invoice_id = 0;
10037 $dbo->setQuery(
10038 $dbo->getQuery(true)
10039 ->select('*')
10040 ->from($dbo->qn('#__vikbooking_invoices'))
10041 ->where($dbo->qn('idorder') . ' = ' . (int)$booking['id'])
10042 ->order($dbo->qn('id') . ' DESC')
10043 );
10044 $invoice_data = $dbo->loadAssoc();
10045 if ($invoice_data) {
10046 $invoice_id = $invoice_data['id'];
10047 }
10048
10049 // if requested, prepare data to refresh the PDF invoice
10050 if ($refresh_pdf === true) {
10051 if (!$invoice_id || !$invoice_data) {
10052 // cannot refresh the PDF if an invoice was never created before
10053 return false;
10054 }
10055
10056 // load the same exact previous information to refresh the invoice
10057 $invoice_num = $invoice_data['number'];
10058 $invoice_suff = $invoice_suff;
10059 $invoice_date = date(str_replace("/", $datesep, $df), $invoice_data['for_date']);
10060 $used_date = $invoice_data['for_date'];
10061
10062 // parse the previous (current) invoice number
10063 preg_match("/^([0-9]+)([\/\s-]?.+)/i", $invoice_data['number'], $matches);
10064 if ($matches) {
10065 // overwrite values
10066 $invoice_num = $matches[1] ?: $invoice_num;
10067 $invoice_suff = $matches[2] ?: $invoice_suff;
10068 }
10069 }
10070
10071 // translations for the invoices are disabled by default as well as the language definitions for the customer language
10072 if ($translate === true) {
10073 if (!empty($booking['lang'])) {
10074 $lang = JFactory::getLanguage();
10075 if ($lang->getTag() != $booking['lang']) {
10076 if (VBOPlatformDetection::isWordPress()) {
10077 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
10078 } else {
10079 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
10080 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10081 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10082 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10083 }
10084 }
10085 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10086 // force the translation to start because contents should be translated
10087 $vbo_tn::$force_tolang = $booking['lang'];
10088 }
10089 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10090 }
10091 }
10092
10093 // the PDF file name
10094 $pdffname = $booking['id'] . '_' . $booking['sid'] . '.pdf';
10095
10096 /**
10097 * If an analogic invoice is not available, make sure to create the record
10098 * before letting the e-invoicing drivers run to avoid a loop.
10099 *
10100 * @since 1.16.7 (J) - 1.6.7 (WP)
10101 */
10102 $retval = null;
10103 if (!$refresh_pdf && !$invoice_id) {
10104 // prepare record object
10105 $invoice_record = new stdClass;
10106 $invoice_record->number = $invoice_num . $invoice_suff;
10107 $invoice_record->file_name = $pdffname;
10108 $invoice_record->idorder = (int)$booking['id'];
10109 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10110 $invoice_record->created_on = time();
10111 $invoice_record->for_date = (int)$used_date;
10112
10113 // insert record
10114 if (!$dbo->insertObject('#__vikbooking_invoices', $invoice_record, 'id')) {
10115 return false;
10116 }
10117
10118 $retval = $invoice_record->id ?? false;
10119
10120 if (!$retval) {
10121 return false;
10122 }
10123 }
10124
10125 /**
10126 * The generation of the analogic invoices can trigger the drivers for the
10127 * generation of the e-Invoices if they are set to automatically run.
10128 * However, the e-Invoicing classes may be calling this method, so we need
10129 * to make sure the eInvoicing class is not running before proceeding.
10130 * This method could be called within a loop, and so the second iterations
10131 * may already have loaded the eInvocing class. For this we use a static variable.
10132 *
10133 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10134 * though additional information may be available only after the transmission. On top
10135 * of that, e-invoicing drivers always run, but they may not generate the e-invoice.
10136 */
10137 static $einvocing_can_run = false;
10138
10139 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10140 // allow second iterations calling this method to run
10141 $einvocing_can_run = true;
10142 }
10143
10144 // always load all drivers, db and plugin based ones
10145 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10146
10147 foreach ($drivers as $driver_obj) {
10148 // set the flag of the external call
10149 $driver_obj->externalCall = __METHOD__;
10150
10151 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10152 $driver_obj->externalData['einvnum'] = $invoice_num;
10153
10154 // let the driver eventually elaborate the booking information before invoicing
10155 $driver_obj->elaborateBookingDetails($booking, $booking_rooms);
10156
10157 if ($einvocing_can_run === true && !$refresh_pdf) {
10158 // generate the e-Invoice
10159 $driver_err = '';
10160 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10161 $driver_err = $driver_obj->getError();
10162 VikError::raiseWarning('', $driver_err);
10163 }
10164
10165 // Booking History - store log for the e-Invoice result
10166 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10167 }
10168 }
10169
10170 /**
10171 * Start the generation of the analogic (courtesy) PDF invoice.
10172 */
10173
10174 // load dependencies
10175 if (!class_exists('TCPDF')) {
10176 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10177 }
10178 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10179
10180 /**
10181 * Trigger event to allow third party plugins to return a specific font name.
10182 *
10183 * @since 1.16.0 (J) - 1.6.0 (WP)
10184 */
10185 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10186 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10187 $usepdffont = $custom_pdf_font[0];
10188 }
10189
10190 // set array variable to the template file
10191 $booking_info = self::getBookingInfoFromID($booking['id']);
10192
10193 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10194
10195 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10196
10197 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10198
10199 if (is_file($pathpdf)) {
10200 @unlink($pathpdf);
10201 }
10202
10203 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10204
10205 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10206 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10207
10208 // header for each page of the pdf
10209 if ($pdfparams['show_header'] == 1 && $pdfparams['header_data']) {
10210 $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]);
10211 }
10212
10213 // change some currencies to their unicode (decimal) value
10214 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10215 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10216 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10217 }
10218
10219 // header and footer fonts
10220 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10221 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10222
10223 // margins
10224 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10225 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10226 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10227
10228 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10229 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10230 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10231
10232 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10233 $pdf->SetPrintHeader(false);
10234 }
10235 if ($pdfparams['show_footer'] == 0) {
10236 $pdf->SetPrintFooter(false);
10237 }
10238
10239 $pdf->AddPage();
10240 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10241 $pdf->lastPage();
10242 $pdf->Output($pathpdf, 'F');
10243
10244 if (!is_file($pathpdf)) {
10245 return false;
10246 }
10247
10248 if (VBOPlatformDetection::isWordPress()) {
10249 /**
10250 * @wponly - trigger files mirroring
10251 */
10252 VikBookingLoader::import('update.manager');
10253 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10254 }
10255
10256 // Booking History
10257 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#' . $invoice_num . $invoice_suff);
10258
10259 if ($retval) {
10260 // a new invoice ID was inserted above
10261 return $retval;
10262 }
10263
10264 // prepare record object to be updated (if insert needed, it's done at first)
10265 $invoice_record = new stdClass;
10266 $invoice_record->id = (int)$invoice_id;
10267 $invoice_record->number = $invoice_num . $invoice_suff;
10268 $invoice_record->file_name = $pdffname;
10269 $invoice_record->idorder = (int)$booking['id'];
10270 $invoice_record->idcustomer = (int)$booking['idcustomer'];
10271 $invoice_record->created_on = time();
10272 $invoice_record->for_date = (int)$used_date;
10273
10274 // update record
10275 $dbo->updateObject('#__vikbooking_invoices', $invoice_record, 'id');
10276
10277 // return the PDF invoice ID
10278 return $invoice_id;
10279 }
10280
10281 /**
10282 * Generates an analogic invoice in PDF format for a custom list of services.
10283 * No bookings are assigned to this custom invoice. The method parses the same
10284 * invoice template as for the regular process with real booking IDs.
10285 * The invoice number must be stored before calling this method.
10286 *
10287 * @param int $invoice_id the ID of the custom invoice record
10288 *
10289 * @return bool
10290 *
10291 * @since 1.11.1 (J) - 1.1.1 (WP)
10292 */
10293 public static function generateCustomInvoice($invoice_id)
10294 {
10295 $dbo = JFactory::getDbo();
10296
10297 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
10298 $dbo->setQuery($q);
10299 $invoice = $dbo->loadAssoc();
10300 if (!$invoice) {
10301 return false;
10302 }
10303
10304 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
10305 $rawcont = is_array($rawcont) ? $rawcont : array();
10306 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
10307 $invoice['rawcont'] = $rawcont;
10308 $customer = self::getCPinInstance()->getCustomerByID($invoice['idcustomer']);
10309 if (!$rawcont || !$rows || !$customer) {
10310 // at least one invoice raw is mandatory as well as the customer
10311 return false;
10312 }
10313
10314 /**
10315 * Start the generation of the electronic invoice, if any driver was enabled.
10316 *
10317 * @since 1.16.7 (J) - 1.6.7 (WP) the electronic invoices run before the PDF invoices, even
10318 * though additional information would be available only after the transmission.
10319 */
10320 $drivers = VBOEinvoicingFactory::getInstance()->getDrivers();
10321
10322 if ($drivers) {
10323 // make sure the invoice number is just a number
10324 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
10325
10326 // invoke all drivers that should run automatically
10327 foreach ($drivers as $driver_obj) {
10328 // set the flag of the external call
10329 $driver_obj->externalCall = __METHOD__;
10330
10331 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
10332 $driver_obj->externalData['einvnum'] = $invoice['number'];
10333 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
10334 $driver_obj->externalData['einvcustom'] = $invoice;
10335
10336 // prepare data array for the generation of the e-Invoice
10337 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
10338
10339 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
10340 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
10341 if ($preveinv !== false) {
10342 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
10343 }
10344
10345 // generate the e-Invoice
10346 $driver_err = '';
10347 if (!$driver_obj->generateEInvoice($einvdata)) {
10348 $driver_err = $driver_obj->getError();
10349 VikError::raiseWarning('', $driver_err);
10350 }
10351 }
10352 }
10353
10354 /**
10355 * Start the generation of the analogic (courtesy) PDF invoice.
10356 */
10357
10358 // load dependencies
10359 if (!class_exists('TCPDF')) {
10360 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10361 }
10362 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10363
10364 /**
10365 * Trigger event to allow third party plugins to return a specific font name.
10366 *
10367 * @since 1.16.0 (J) - 1.6.0 (WP)
10368 */
10369 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10370 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10371 $usepdffont = $custom_pdf_font[0];
10372 }
10373
10374 // load invoice template file
10375 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
10376
10377 // parse invoice template file
10378 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
10379
10380 $pdffname = $invoice['file_name'];
10381 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10382
10383 if (is_file($pathpdf)) {
10384 @unlink($pathpdf);
10385 }
10386
10387 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10388
10389 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10390 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
10391
10392 // header for each page of the pdf
10393 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10394 $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]);
10395 }
10396
10397 // change some currencies to their unicode (decimal) value
10398 $currencyname = self::getCurrencyName();
10399 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10400 if (array_key_exists($currencyname, $unichr_map)) {
10401 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
10402 }
10403
10404 // header and footer fonts
10405 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10406 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10407
10408 // margins
10409 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10410 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10411 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10412
10413 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10414 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10415 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10416
10417 if ($pdfparams['show_header'] == 0 || !$pdfparams['header_data']) {
10418 $pdf->SetPrintHeader(false);
10419 }
10420 if ($pdfparams['show_footer'] == 0) {
10421 $pdf->SetPrintFooter(false);
10422 }
10423
10424 $pdf->AddPage();
10425 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10426 $pdf->lastPage();
10427 $pdf->Output($pathpdf, 'F');
10428
10429 if (!is_file($pathpdf)) {
10430 return false;
10431 }
10432
10433 if (VBOPlatformDetection::isWordPress()) {
10434 /**
10435 * @wponly - trigger files mirroring
10436 */
10437 VikBookingLoader::import('update.manager');
10438 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10439 }
10440
10441 // return the result of the generation of the PDF invoice
10442 return true;
10443 }
10444
10445 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '')
10446 {
10447 if (empty($invoice_id) || !is_array($booking) || empty($booking['custmail'])) {
10448 return false;
10449 }
10450
10451 $dbo = JFactory::getDbo();
10452
10453 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
10454 $dbo->setQuery($q);
10455 $invoice_data = $dbo->loadAssoc();
10456
10457 if (!$invoice_data) {
10458 return false;
10459 }
10460
10461 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
10462 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
10463 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
10464 if (!file_exists($invoice_file_path)) {
10465 return false;
10466 }
10467
10468 $vbo_app = self::getVboApplication();
10469 $admin_sendermail = self::getSenderMail();
10470
10471 $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);
10472
10473 // update record
10474 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
10475 $dbo->setQuery($q);
10476 $dbo->execute();
10477
10478 return true;
10479 }
10480
10481 public static function loadCheckinDocTmpl($booking_info = array(), $booking_rooms = array(), $customer = array()) {
10482 if (!defined('_VIKBOOKINGEXEC')) {
10483 define('_VIKBOOKINGEXEC', '1');
10484 }
10485 ob_start();
10486 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "checkins" . DIRECTORY_SEPARATOR . "checkin_tmpl.php";
10487 $content = ob_get_contents();
10488 ob_end_clean();
10489 $default_params = array(
10490 'show_header' => 0,
10491 'header_data' => array(),
10492 'show_footer' => 0,
10493 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10494 'pdf_unit' => 'PDF_UNIT',
10495 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10496 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10497 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10498 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10499 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10500 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10501 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10502 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10503 'header_font_size' => '10',
10504 'body_font_size' => '10',
10505 'footer_font_size' => '8'
10506 );
10507 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && is_array($checkin_params) && $checkin_params) {
10508 $default_params = array_merge($default_params, $checkin_params);
10509 }
10510 return array($content, $default_params);
10511 }
10512
10513 public static function parseCheckinDocTemplate($checkintpl, $booking, $booking_rooms, $customer) {
10514 $parsed = $checkintpl;
10515 $dbo = JFactory::getDbo();
10516 $app = JFactory::getApplication();
10517 $nowdf = self::getDateFormat();
10518 if ($nowdf == "%d/%m/%Y") {
10519 $df='d/m/Y';
10520 } elseif ($nowdf == "%m/%d/%Y") {
10521 $df='m/d/Y';
10522 } else {
10523 $df='Y/m/d';
10524 }
10525 $datesep = self::getDateSeparator();
10526 $companylogo = self::getSiteLogo();
10527 $uselogo = '';
10528 if (!empty($companylogo)) {
10529 /**
10530 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
10531 *
10532 * @since March 16th 2021
10533 */
10534 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10535 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10536 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
10537 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10538 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10539 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
10540 }
10541 }
10542 $company_name = self::getFrontTitle();
10543 $company_info = self::getInvoiceCompanyInfo();
10544 $parsed = str_replace("{company_name}", $company_name, $parsed);
10545 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10546 $parsed = str_replace("{company_info}", $company_info, $parsed);
10547 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10548 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10549 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10550 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10551 $tot_guests = 0;
10552 $tot_adults = 0;
10553 $tot_children = 0;
10554 foreach ($booking_rooms as $kor => $or) {
10555 $tot_guests += ($or['adults'] + $or['children']);
10556 $tot_adults += $or['adults'];
10557 $tot_children += $or['children'];
10558 }
10559 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10560 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10561 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10562 if (count($customer) && isset($customer['comments'])) {
10563 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
10564 }
10565 $termsconds = self::getTermsConditions();
10566 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
10567 //custom fields replace
10568 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10569 if (is_array($cmatches[1]) && $cmatches[1]) {
10570 $cfids = array();
10571 foreach ($cmatches[1] as $cfid) {
10572 $cfids[] = $cfid;
10573 }
10574 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10575 $dbo->setQuery($q);
10576 $cfields = $dbo->loadAssocList();
10577 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10578 $cfmap = array();
10579 foreach ($cfields as $cf) {
10580 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10581 }
10582 $cfmapreplace = array();
10583 $partsreceived = explode("\n", $booking['custdata']);
10584 if (count($partsreceived) > 0) {
10585 foreach ($partsreceived as $pst) {
10586 if (!empty($pst)) {
10587 $tmpdata = explode(":", $pst);
10588 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10589 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10590 }
10591 }
10592 }
10593 }
10594 foreach ($cmatches[1] as $cfid) {
10595 if (array_key_exists($cfid, $cfmapreplace)) {
10596 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10597 } else {
10598 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10599 }
10600 }
10601 }
10602 //end custom fields replace
10603
10604 return $parsed;
10605 }
10606
10607 /**
10608 * Returns an array of key-value pairs to be used for building the Guest Details
10609 * in the Check-in process. The keys will be compared to the fields of the table
10610 * #__customers to see if some values already exist. The values can use lang defs
10611 * of both front-end or back-end. To be called as list(fields, attributes).
10612 *
10613 * @param boolean $precheckin true if requested for front-end pre check-in.
10614 * @param string $type optional type of custom pax fields to force.
10615 *
10616 * @return array key-value pairs for showing and collecting guest details.
10617 *
10618 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
10619 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
10620 */
10621 public static function getPaxFields($precheckin = false, $type = null)
10622 {
10623 if (!$precheckin) {
10624 // back-end check-in ("registration") key-value pairs
10625
10626 // check the type of pax fields collection data
10627 $collection_type = !empty($type) ? $type : VBOFactory::getConfig()->getString('checkindata', 'basic');
10628
10629 // return the requested pax fields collection list
10630 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
10631
10632 if (is_array($custom_pax_fields) && count($custom_pax_fields)) {
10633 // requested driver returned a list of fields
10634 return $custom_pax_fields;
10635 }
10636
10637 // in case the driver is unsupported, fallback to "basic" (default) driver
10638 return VBOCheckinPax::getFields('basic');
10639 }
10640
10641 // front-end key-value pairs for pre check-in
10642 $precheckin_pax_fields = [
10643 [
10644 'first_name' => JText::translate('VBCCFIRSTNAME'),
10645 'last_name' => JText::translate('VBCCLASTNAME'),
10646 'date_birth' => JText::translate('ORDER_DBIRTH'),
10647 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
10648 'country' => JText::translate('ORDER_STATE'),
10649 'city' => JText::translate('ORDER_CITY'),
10650 'zip' => JText::translate('ORDER_ZIP'),
10651 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
10652 'gender' => JText::translate('VBOCUSTGENDER'),
10653 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
10654 'docnum' => JText::translate('VBOCUSTDOCNUM'),
10655 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
10656 ],
10657 [
10658 'first_name' => 'text',
10659 'last_name' => 'text',
10660 'date_birth' => 'calendar',
10661 'place_birth' => 'text',
10662 'country' => 'country',
10663 'city' => 'text',
10664 'zip' => 'text',
10665 'nationality' => 'country',
10666 'gender' => [
10667 'M' => JText::translate('VBOCUSTGENDERM'),
10668 'F' => JText::translate('VBOCUSTGENDERF'),
10669 ],
10670 'doctype' => 'text',
10671 'docnum' => 'text',
10672 'documents' => 'file',
10673 ],
10674 ];
10675
10676 // make a safe copy of the default precheckin fields
10677 $def_precheckin_pax_fields = $precheckin_pax_fields;
10678
10679 /**
10680 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
10681 *
10682 * @since 1.16.3 (J) - 1.6.3 (WP)
10683 */
10684 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
10685
10686 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
10687 // use the filtered pax fields
10688 $def_precheckin_pax_fields = $precheckin_pax_fields;
10689 }
10690
10691 // return the front-end key-value pairs for pre check-in
10692 return $def_precheckin_pax_fields;
10693 }
10694
10695 /**
10696 * Returns the associative list of countries from the DB.
10697 *
10698 * @param bool $tn whether to translate the country name.
10699 * @param bool $no_id whether to unset the ID of the country.
10700 *
10701 * @return array associative or empty array.
10702 *
10703 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
10704 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
10705 */
10706 public static function getCountriesArray($tn = true, $no_id = true)
10707 {
10708 $dbo = JFactory::getDbo();
10709 $all_countries = [];
10710
10711 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
10712 $dbo->setQuery($q);
10713 $dbo->execute();
10714 if (!$dbo->getNumRows()) {
10715 return [];
10716 }
10717 $countries = $dbo->loadAssocList();
10718
10719 if ($tn === true) {
10720 $vbo_tn = self::getTranslator();
10721 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
10722 // re-apply sorting by country name
10723 $sorting = [];
10724 foreach ($countries as $country) {
10725 $sorting[$country['country_name']] = $country;
10726 }
10727 ksort($sorting);
10728 $sorted = [];
10729 foreach ($sorting as $country) {
10730 $sorted[] = $country;
10731 }
10732 $countries = $sorted;
10733 unset($sorting, $sorted);
10734 }
10735
10736 foreach ($countries as $v) {
10737 if ($no_id) {
10738 // keep the original structure by unsetting the ID only needed for translation
10739 unset($v['id']);
10740 }
10741 $all_countries[$v['country_3_code']] = $v;
10742 }
10743
10744 return $all_countries;
10745 }
10746
10747 public static function getCountriesSelect($name, $all_countries = array(), $current_value = '', $empty_value = ' ')
10748 {
10749 if (!count($all_countries)) {
10750 $all_countries = self::getCountriesArray();
10751 }
10752
10753 $countries = '<select name="'.$name.'">'."\n";
10754 if (strlen($empty_value)) {
10755 $countries .= '<option value="">'.$empty_value.'</option>'."\n";
10756 }
10757 foreach ($all_countries as $v) {
10758 $countries .= '<option value="'.$v['country_3_code'].'"'.($v['country_3_code'] == $current_value ? ' selected="selected"' : '').'>'.$v['country_name'].'</option>'."\n";
10759 }
10760 $countries .= '</select>';
10761
10762 return $countries;
10763 }
10764
10765 public static function getThumbSize($skipsession = false)
10766 {
10767 static $thumb_size = null;
10768
10769 if ($thumb_size) {
10770 return $thumb_size;
10771 }
10772
10773 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
10774
10775 return $thumb_size;
10776 }
10777
10778 /**
10779 * Checks whether an iCal file for the reservation should be
10780 * attached to the confirmation email for customer and/or admin.
10781 *
10782 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
10783 *
10784 * @since 1.12.0 (J) - 1.2.0 (WP)
10785 */
10786 public static function attachIcal()
10787 {
10788 return VBOFactory::getConfig()->getInt('attachical', 1);
10789 }
10790
10791 /**
10792 * How the calculation of the orphan dates should take place.
10793 *
10794 * @return string "next" for only checking the bookings ahead, "prevnext" if
10795 * also the previous bookings should be checked.
10796 *
10797 * @since 1.13 (J) - 1.3.0 (WP)
10798 */
10799 public static function orphansCalculation()
10800 {
10801 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
10802 }
10803
10804 /**
10805 * Returns the name of the template file to use for the front-end View "search".
10806 *
10807 * @return string the name of the template file. New and upgraded user will both use the classic file.
10808 *
10809 * @since 1.13 (J) - 1.3.0 (WP)
10810 */
10811 public static function searchResultsTmpl()
10812 {
10813 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
10814 }
10815
10816 /**
10817 * Returns a string without any new-line characters
10818 * to be used for JavaScript values without facing
10819 * errors like 'unterminated string literal'.
10820 * By passing nl2br($str) as argument, we can keep
10821 * the wanted new-line HTML tags for PRE tags.
10822 * We use implode() with just one argument to
10823 * not use an empty string as glue for the string.
10824 *
10825 * @param $str string
10826 *
10827 * @return string
10828 */
10829 public static function strTrimLiteral($str) {
10830 $str = str_replace(array("\r\n", "\r"), "\n", $str);
10831 $lines = explode("\n", $str);
10832 $new_lines = array();
10833 foreach ($lines as $i => $line) {
10834 if (strlen($line)) {
10835 $new_lines[] = trim($line);
10836 }
10837 }
10838 return implode($new_lines);
10839 }
10840
10841 /**
10842 * Returns an instance of the VboApplication class.
10843 *
10844 * @return VboApplication
10845 */
10846 public static function getVboApplication()
10847 {
10848 // require library
10849 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
10850
10851 // return a new instance
10852 return new VboApplication();
10853 }
10854
10855 /**
10856 * Returns the translation of the given 0-indexed week day.
10857 *
10858 * @param int $wd the 0-indexed day of the week.
10859 * @param bool $short whether to get the short version of the weekday.
10860 *
10861 * @return string the name of the week day.
10862 *
10863 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10864 */
10865 public static function sayWeekDay($wd, $short = false)
10866 {
10867 switch ($wd) {
10868 case '6' :
10869 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
10870 break;
10871 case '5' :
10872 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
10873 break;
10874 case '4' :
10875 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
10876 break;
10877 case '3' :
10878 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
10879 break;
10880 case '2' :
10881 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
10882 break;
10883 case '1' :
10884 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
10885 break;
10886 default :
10887 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
10888 break;
10889 }
10890 return $ret;
10891 }
10892
10893 /**
10894 * Returns the translation of the given 1-indexed month of the year.
10895 *
10896 * @param int $idm the 1-indexed month of the year.
10897 * @param bool $short whether to get the short version of the month.
10898 *
10899 * @return string the name of the month.
10900 *
10901 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10902 */
10903 public static function sayMonth($idm, $short = false)
10904 {
10905 switch ($idm) {
10906 case '12' :
10907 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
10908 break;
10909 case '11' :
10910 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
10911 break;
10912 case '10' :
10913 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
10914 break;
10915 case '9' :
10916 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
10917 break;
10918 case '8' :
10919 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
10920 break;
10921 case '7' :
10922 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
10923 break;
10924 case '6' :
10925 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
10926 break;
10927 case '5' :
10928 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
10929 break;
10930 case '4' :
10931 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
10932 break;
10933 case '3' :
10934 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
10935 break;
10936 case '2' :
10937 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
10938 break;
10939 default :
10940 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
10941 break;
10942 }
10943 return $ret;
10944 }
10945
10946 public static function sayDayMonth($d) {
10947 switch ($d) {
10948 case '31' :
10949 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
10950 break;
10951 case '30' :
10952 $ret = JText::translate('VBDAYMONTHTHIRTY');
10953 break;
10954 case '29' :
10955 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
10956 break;
10957 case '28' :
10958 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
10959 break;
10960 case '27' :
10961 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
10962 break;
10963 case '26' :
10964 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
10965 break;
10966 case '25' :
10967 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
10968 break;
10969 case '24' :
10970 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
10971 break;
10972 case '23' :
10973 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
10974 break;
10975 case '22' :
10976 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
10977 break;
10978 case '21' :
10979 $ret = JText::translate('VBDAYMONTHTWENTYONE');
10980 break;
10981 case '20' :
10982 $ret = JText::translate('VBDAYMONTHTWENTY');
10983 break;
10984 case '19' :
10985 $ret = JText::translate('VBDAYMONTHNINETEEN');
10986 break;
10987 case '18' :
10988 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
10989 break;
10990 case '17' :
10991 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
10992 break;
10993 case '16' :
10994 $ret = JText::translate('VBDAYMONTHSIXTEEN');
10995 break;
10996 case '15' :
10997 $ret = JText::translate('VBDAYMONTHFIFTEEN');
10998 break;
10999 case '14' :
11000 $ret = JText::translate('VBDAYMONTHFOURTEEN');
11001 break;
11002 case '13' :
11003 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
11004 break;
11005 case '12' :
11006 $ret = JText::translate('VBDAYMONTHTWELVE');
11007 break;
11008 case '11' :
11009 $ret = JText::translate('VBDAYMONTHELEVEN');
11010 break;
11011 case '10' :
11012 $ret = JText::translate('VBDAYMONTHTEN');
11013 break;
11014 case '9' :
11015 $ret = JText::translate('VBDAYMONTHNINE');
11016 break;
11017 case '8' :
11018 $ret = JText::translate('VBDAYMONTHEIGHT');
11019 break;
11020 case '7' :
11021 $ret = JText::translate('VBDAYMONTHSEVEN');
11022 break;
11023 case '6' :
11024 $ret = JText::translate('VBDAYMONTHSIX');
11025 break;
11026 case '5' :
11027 $ret = JText::translate('VBDAYMONTHFIVE');
11028 break;
11029 case '4' :
11030 $ret = JText::translate('VBDAYMONTHFOUR');
11031 break;
11032 case '3' :
11033 $ret = JText::translate('VBDAYMONTHTHREE');
11034 break;
11035 case '2' :
11036 $ret = JText::translate('VBDAYMONTHTWO');
11037 break;
11038 default :
11039 $ret = JText::translate('VBDAYMONTHONE');
11040 break;
11041 }
11042 return $ret;
11043 }
11044
11045 public static function totElements($arr) {
11046 $n = 0;
11047 if (is_array($arr)) {
11048 foreach ($arr as $a) {
11049 if (!empty($a)) {
11050 $n++;
11051 }
11052 }
11053 return $n;
11054 }
11055 return false;
11056 }
11057
11058 /**
11059 * Returns a list of documents that were uploaded
11060 * for the specified customer.
11061 *
11062 * @param integer $id The customer ID.
11063 *
11064 * @return array A list of documents.
11065 *
11066 * @since 1.3.0
11067 */
11068 public static function getCustomerDocuments($id)
11069 {
11070 $dbo = JFactory::getDbo();
11071
11072 $q = $dbo->getQuery(true)
11073 ->select($dbo->qn('docsfolder'))
11074 ->from($dbo->qn('#__vikbooking_customers'))
11075 ->where($dbo->qn('id') . ' = ' . (int) $id);
11076
11077 $dbo->setQuery($q, 0, 1);
11078 $dbo->execute();
11079
11080 if (!$dbo->getNumRows())
11081 {
11082 // customer not found
11083 return array();
11084 }
11085
11086 // retrieve customer documents directory name
11087 $dirname = $dbo->loadResult();
11088
11089 if (empty($dirname))
11090 {
11091 // no available directory
11092 return array();
11093 }
11094
11095 // build documents folder path
11096 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11097
11098 if (!is_dir($dirname))
11099 {
11100 // the customer directory doesn't exist
11101 return array();
11102 }
11103
11104 // read all files from customer directory
11105 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11106
11107 $files = array();
11108
11109 foreach ($glob as $path)
11110 {
11111 // skip "index.html"
11112 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11113 {
11114 // extract name and extension from file path
11115 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11116 {
11117 $name = $match[1];
11118 $ext = $match[2];
11119 }
11120 else
11121 {
11122 $name = basename($path);
11123 $ext = '';
11124 }
11125
11126 $file = new stdClass;
11127 $file->path = $path;
11128 $file->name = $name;
11129 $file->ext = $ext;
11130 $file->basename = $file->name . '.' . $file->ext;
11131 $file->size = filesize($path);
11132 $file->date = filemtime($path);
11133 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11134
11135 $files[] = $file;
11136 }
11137 }
11138
11139 // sort files by creation date
11140 usort($files, function($a, $b)
11141 {
11142 return $b->date - $a->date;
11143 });
11144
11145 return $files;
11146 }
11147
11148 public static function displayPaymentParameters($pfile, $pparams = '') {
11149 $html = '<p>---------</p>';
11150
11151 /**
11152 * @wponly The payment gateway is now loaded
11153 * using the apposite dispatcher.
11154 *
11155 * @since 1.0.5
11156 */
11157 JLoader::import('adapter.payment.dispatcher');
11158
11159 try
11160 {
11161 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11162 }
11163 catch (Exception $e)
11164 {
11165 // payment not found
11166 $html = $e->getMessage();
11167
11168 if ($code = $e->getCode())
11169 {
11170 $html = '<b>' . $code . '</b> : ' . $html;
11171 }
11172
11173 return $html;
11174 }
11175 //
11176
11177 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11178 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11179
11180 // get admin parameters
11181 $pconfig = $payment->getAdminParameters();
11182
11183 if (count($pconfig) > 0) {
11184 $html = '';
11185 foreach ($pconfig as $value => $cont) {
11186 if (empty($value)) {
11187 continue;
11188 }
11189 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11190 $label = $labelparts[0];
11191 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11192 if (!empty($cont['help'])) {
11193 $labelhelp = $cont['help'];
11194 }
11195 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11196 $html .= '<div class="vbo-param-container">';
11197 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11198 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11199 }
11200 $html .= '<div class="vbo-param-setting">';
11201 switch ($cont['type']) {
11202 case 'custom':
11203 $html .= $cont['html'];
11204 break;
11205 case 'select':
11206 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11207 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11208 if (isset($cont['multiple']) && $cont['multiple']) {
11209 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11210 } else {
11211 $html .= '<select name="vikpaymentparams['.$value.']">';
11212 }
11213 foreach ($options as $optkey => $poption) {
11214 $checkval = $is_assoc ? $optkey : $poption;
11215 $selected = false;
11216 if (isset($arrparams[$value])) {
11217 if (is_array($arrparams[$value])) {
11218 $selected = in_array($checkval, $arrparams[$value]);
11219 } else {
11220 $selected = ($checkval == $arrparams[$value]);
11221 }
11222 } elseif (isset($default_paramv)) {
11223 if (is_array($default_paramv)) {
11224 $selected = in_array($checkval, $default_paramv);
11225 } else {
11226 $selected = ($checkval == $default_paramv);
11227 }
11228 }
11229 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11230 }
11231 $html .= '</select>';
11232 break;
11233 case 'password':
11234 $html .= '<div class="btn-wrapper input-append">';
11235 $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"/>';
11236 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11237 $html .= '</div>';
11238 break;
11239 case 'number':
11240 $number_attr = array();
11241 if (isset($cont['min'])) {
11242 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11243 }
11244 if (isset($cont['max'])) {
11245 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11246 }
11247 if (isset($cont['step'])) {
11248 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11249 }
11250 $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) . '/>';
11251 break;
11252 case 'textarea':
11253 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11254 break;
11255 case 'hidden':
11256 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
11257 break;
11258 case 'checkbox':
11259 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11260 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
11261 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
11262 break;
11263 default:
11264 $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"/>';
11265 break;
11266 }
11267 if (strlen($labelhelp) > 0) {
11268 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11269 }
11270 $html .= '</div>';
11271 $html .= '</div>';
11272 }
11273 // JS helper function to toggle the password fields
11274 $html .= "\n" . '<script>' . "\n";
11275 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
11276 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11277 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11278 $html .= ' var inp_type = inp.attr("type");' . "\n";
11279 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11280 $html .= '}' . "\n";
11281 $html .= "\n" . '</script>' . "\n";
11282 }
11283
11284 return $html;
11285 }
11286
11287 public static function displaySMSParameters($pfile, $params = null)
11288 {
11289 $html = '---------';
11290
11291 $params = is_string($params) ? json_decode($params, true) : $params;
11292 $params = is_array($params) ? $params : [];
11293
11294 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
11295 return $html;
11296 }
11297
11298 // attempt to load the class file
11299 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
11300
11301 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
11302 return $html;
11303 }
11304
11305 // load the gateway parameters
11306 $config = VikSmsApi::getAdminParameters();
11307
11308 if (!is_array($config) || !count($config)) {
11309 return $html;
11310 }
11311
11312 // flags for JS helpers
11313 $js_helpers = array();
11314
11315 $html = '';
11316 foreach ($config as $value => $cont) {
11317 if (empty($value)) {
11318 continue;
11319 }
11320 $inp_attr = '';
11321 if (isset($cont['attributes'])) {
11322 foreach ($cont['attributes'] as $inpk => $inpv) {
11323 $inp_attr .= $inpk.'="'.$inpv.'" ';
11324 }
11325 $inp_attr = ' ' . rtrim($inp_attr);
11326 }
11327 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11328 $label = $labelparts[0];
11329 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11330 if (!empty($cont['help'])) {
11331 $labelhelp = $cont['help'];
11332 }
11333 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11334 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
11335 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11336 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11337 }
11338 $html .= '<div class="vbo-param-setting">';
11339 switch ($cont['type']) {
11340 case 'custom':
11341 $html .= $cont['html'];
11342 break;
11343 case 'select':
11344 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11345 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11346 if (isset($cont['multiple']) && $cont['multiple']) {
11347 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11348 } else {
11349 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
11350 }
11351 foreach ($options as $optkey => $poption) {
11352 $checkval = $is_assoc ? $optkey : $poption;
11353 $selected = false;
11354 if (isset($params[$value])) {
11355 if (is_array($params[$value])) {
11356 $selected = in_array($checkval, $params[$value]);
11357 } else {
11358 $selected = ($checkval == $params[$value]);
11359 }
11360 } elseif (isset($default_paramv)) {
11361 if (is_array($default_paramv)) {
11362 $selected = in_array($checkval, $default_paramv);
11363 } else {
11364 $selected = ($checkval == $default_paramv);
11365 }
11366 }
11367 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11368 }
11369 $html .= '</select>';
11370 break;
11371 case 'password':
11372 $html .= '<div class="btn-wrapper input-append">';
11373 $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 . '/>';
11374 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11375 $html .= '</div>';
11376 // set flag for JS helper
11377 $js_helpers[] = $cont['type'];
11378 break;
11379 case 'number':
11380 $number_attr = array();
11381 if (isset($cont['min'])) {
11382 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11383 }
11384 if (isset($cont['max'])) {
11385 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11386 }
11387 if (isset($cont['step'])) {
11388 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11389 }
11390 $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 . '/>';
11391 break;
11392 case 'textarea':
11393 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11394 break;
11395 case 'visual_html':
11396 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11397 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
11398 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
11399 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
11400 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11401 break;
11402 case 'hidden':
11403 $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 . '/>';
11404 break;
11405 case 'checkbox':
11406 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11407 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
11408 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11409 break;
11410 default:
11411 $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 . '/>';
11412 break;
11413 }
11414 if (strlen($labelhelp) > 0) {
11415 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11416 }
11417 $html .= '</div>';
11418 $html .= '</div>';
11419 }
11420
11421 // JS helper functions
11422 if (in_array('password', $js_helpers)) {
11423 // toggle the password fields
11424 $html .= "\n" . '<script>' . "\n";
11425 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
11426 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11427 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11428 $html .= ' var inp_type = inp.attr("type");' . "\n";
11429 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11430 $html .= '}' . "\n";
11431 $html .= "\n" . '</script>' . "\n";
11432 }
11433
11434 return $html;
11435 }
11436
11437 /**
11438 * Renders the params of the requested cron job file-class.
11439 *
11440 * @param string $file the name of the cron job driver file.
11441 * @param array $params the parameters array.
11442 *
11443 * @return string the necessary HTML content to render.
11444 *
11445 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
11446 */
11447 public static function displayCronParameters($file, $params = [])
11448 {
11449 try
11450 {
11451 // attempt to create a new instance
11452 $job = VBOFactory::getCronFactory()->createInstance($file);
11453 }
11454 catch (Exception $e)
11455 {
11456 // something went wrong, display error message
11457 return '<p>' . $e->getMessage() . '<p>';
11458 }
11459
11460 // get admin parameters
11461 $config = $job->getForm();
11462
11463 if (!is_array($config) || !count($config))
11464 {
11465 return '<p>---------</p>';
11466 }
11467
11468 // flags for JS helpers
11469 $js_helpers = array();
11470
11471 $html = '';
11472 foreach ($config as $value => $cont) {
11473 if (empty($value)) {
11474 continue;
11475 }
11476
11477 $inp_attr = '';
11478 if (isset($cont['attributes']) && is_array($cont['attributes'])) {
11479 foreach ($cont['attributes'] as $inpk => $inpv) {
11480 $inp_attr .= $inpk . '="' . $inpv . '" ';
11481 }
11482 $inp_attr = ' ' . rtrim($inp_attr);
11483 }
11484
11485 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11486 $label = $labelparts[0];
11487 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11488 if (!empty($cont['help'])) {
11489 $labelhelp = $cont['help'];
11490 }
11491
11492 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11493 $nested_style = (isset($cont['nested']) && $cont['nested']);
11494 $hidden_wrapper = false;
11495 if (isset($cont['conditional']) && isset($config[$cont['conditional']])) {
11496 $check_cond = isset($config[$cont['conditional']]['default']) ? $config[$cont['conditional']]['default'] : null;
11497 $check_cond = isset($params[$cont['conditional']]) ? $params[$cont['conditional']] : $check_cond;
11498 if (!is_null($check_cond) && (!$check_cond || !strcasecmp($check_cond, 'off'))) {
11499 // hide current field because the field to who this is dependant is "off" or disabled (i.e. 0)
11500 $hidden_wrapper = true;
11501 }
11502 }
11503
11504 $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;"' : '') . '>';
11505 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11506 $html .= '<div class="vbo-param-label">' . $label . '</div>';
11507 }
11508 $html .= '<div class="vbo-param-setting">';
11509
11510 switch ($cont['type']) {
11511 case 'custom':
11512 $html .= $cont['html'];
11513 break;
11514 case 'select':
11515 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11516 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11517 if (isset($cont['multiple']) && $cont['multiple']) {
11518 $html .= '<select name="vikcronparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11519 } else {
11520 $html .= '<select name="vikcronparams['.$value.']"' . $inp_attr . '>';
11521 }
11522 foreach ($options as $optkey => $poption) {
11523 $checkval = $is_assoc ? $optkey : $poption;
11524 $selected = false;
11525 if (isset($params[$value])) {
11526 if (is_array($params[$value])) {
11527 $selected = in_array($checkval, $params[$value]);
11528 } else {
11529 $selected = ($checkval == $params[$value]);
11530 }
11531 } elseif (isset($default_paramv)) {
11532 if (is_array($default_paramv)) {
11533 $selected = in_array($checkval, $default_paramv);
11534 } else {
11535 $selected = ($checkval == $default_paramv);
11536 }
11537 }
11538 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11539 }
11540 $html .= '</select>';
11541 break;
11542 case 'password':
11543 $html .= '<div class="btn-wrapper input-append">';
11544 $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 . '/>';
11545 $html .= '<button type="button" class="btn btn-primary" onclick="vikCronParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11546 $html .= '</div>';
11547 // set flag for JS helper
11548 $js_helpers[] = $cont['type'];
11549 break;
11550 case 'number':
11551 $number_attr = [];
11552 if (isset($cont['min'])) {
11553 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11554 }
11555 if (isset($cont['max'])) {
11556 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11557 }
11558 if (isset($cont['step'])) {
11559 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11560 }
11561 $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 . '/>';
11562 break;
11563 case 'textarea':
11564 $html .= '<textarea name="vikcronparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11565 break;
11566 case 'visual_html':
11567 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11568 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : [];
11569 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : [];
11570 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : [];
11571 $html .= self::getVboApplication()->renderVisualEditor('vikcronparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11572 break;
11573 case 'codemirror':
11574 $editor = JEditor::getInstance('codemirror');
11575 $e_options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11576 $e_name = 'vikcronparams[' . $value . ']';
11577 $e_value = isset($params[$value]) ? $params[$value] : $default_paramv;
11578 $e_width = isset($e_options['width']) ? $e_options['width'] : '100%';
11579 $e_height = isset($e_options['height']) ? $e_options['height'] : 300;
11580 $e_col = isset($e_options['col']) ? $e_options['col'] : 70;
11581 $e_row = isset($e_options['row']) ? $e_options['row'] : 20;
11582 $e_buttons = isset($e_options['buttons']) ? (bool)$e_options['buttons'] : true;
11583 $e_id = isset($e_options['id']) ? $e_options['id'] : 'vikcronparams_' . $value;
11584 $e_params = isset($e_options['params']) && is_array($e_options['params']) ? $e_options['params'] : [];
11585 if (interface_exists('Throwable')) {
11586 /**
11587 * With PHP >= 7 supporting throwable exceptions for Fatal Errors
11588 * we try to avoid issues with third party plugins that make use
11589 * of the WP native function get_current_screen().
11590 *
11591 * @wponly
11592 */
11593 try {
11594 $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);
11595 } catch (Throwable $t) {
11596 $html .= $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine() . '<br/>';
11597 $html .= '<textarea name="vikcronparams[' . $value . ']"' . $inp_attr . '>' . (isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)) . '</textarea>';
11598 }
11599 } else {
11600 $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);
11601 }
11602 break;
11603 case 'hidden':
11604 $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 . '/>';
11605 break;
11606 case 'checkbox':
11607 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11608 $html .= '<input type="hidden" name="vikcronparams['.$value.']" value="0" />';
11609 $html .= self::getVboApplication()->printYesNoButtons('vikcronparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11610 break;
11611 default:
11612 $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 . '/>';
11613 break;
11614 }
11615
11616 if (strlen($labelhelp)) {
11617 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11618 }
11619
11620 $html .= '</div>';
11621 $html .= '</div>';
11622 }
11623
11624 // JS helper functions
11625 if (in_array('password', $js_helpers)) {
11626 // toggle the password fields
11627 $html .= "\n" . '<script>' . "\n";
11628 $html .= 'function vikCronParamTogglePwd(elem) {' . "\n";
11629 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11630 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11631 $html .= ' var inp_type = inp.attr("type");' . "\n";
11632 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11633 $html .= '}' . "\n";
11634 $html .= "\n" . '</script>' . "\n";
11635 }
11636
11637 return $html;
11638 }
11639
11640 public static function invokeChannelManager($skiporder = true, $order = array()) {
11641 $task = VikRequest::getString('task', '', 'request');
11642 $view = VikRequest::getString('view', '', 'request');
11643 $tmpl = VikRequest::getString('tmpl', '', 'request');
11644 $noimpression = array('vieworder', 'booking');
11645 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')) {
11646 //VCM Channel Impression
11647 if (!class_exists('VikChannelManagerConfig')) {
11648 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11649 }
11650 if (!class_exists('VikChannelManager')) {
11651 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11652 }
11653 VikChannelManager::invokeChannelImpression();
11654 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11655 //VCM Channel Conversion-Impression
11656 if (!class_exists('VikChannelManagerConfig')) {
11657 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11658 }
11659 if (!class_exists('VikChannelManager')) {
11660 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11661 }
11662 VikChannelManager::invokeChannelConversionImpression($order);
11663 }
11664 }
11665
11666 public static function validEmail($email) {
11667 $isValid = true;
11668 $atIndex = strrpos($email, "@");
11669 if (is_bool($atIndex) && !$atIndex) {
11670 $isValid = false;
11671 } else {
11672 $domain = substr($email, $atIndex +1);
11673 $local = substr($email, 0, $atIndex);
11674 $localLen = strlen($local);
11675 $domainLen = strlen($domain);
11676 if ($localLen < 1 || $localLen > 64) {
11677 // local part length exceeded
11678 $isValid = false;
11679 } else
11680 if ($domainLen < 1 || $domainLen > 255) {
11681 // domain part length exceeded
11682 $isValid = false;
11683 } else
11684 if ($local[0] == '.' || $local[$localLen -1] == '.') {
11685 // local part starts or ends with '.'
11686 $isValid = false;
11687 } else
11688 if (preg_match('/\\.\\./', $local)) {
11689 // local part has two consecutive dots
11690 $isValid = false;
11691 } else
11692 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
11693 // character not valid in domain part
11694 $isValid = false;
11695 } else
11696 if (preg_match('/\\.\\./', $domain)) {
11697 // domain part has two consecutive dots
11698 $isValid = false;
11699 } else
11700 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
11701 // character not valid in local part unless
11702 // local part is quoted
11703 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
11704 $isValid = false;
11705 }
11706 }
11707 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
11708 // domain not found in DNS
11709 $isValid = false;
11710 }
11711 }
11712 return $isValid;
11713 }
11714
11715 public static function caniWrite($path) {
11716 if ($path[strlen($path) - 1] == '/') {
11717 // ricorsivo return a temporary file path
11718 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
11719 }
11720 if (is_dir($path)) {
11721 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
11722 }
11723 // check tmp file for read/write capabilities
11724 $rm = file_exists($path);
11725 $f = @fopen($path, 'a');
11726 if ($f === false) {
11727 return false;
11728 }
11729 fclose($f);
11730 if (!$rm) {
11731 unlink($path);
11732 }
11733 return true;
11734 }
11735
11736 /**
11737 * Alias method of JFile::upload to unify any
11738 * upload function into one.
11739 *
11740 * @param string $src The name of the php (temporary) uploaded file.
11741 * @param string $dest The path (including filename) to move the uploaded file to.
11742 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
11743 *
11744 * @return boolean True on success.
11745 *
11746 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
11747 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
11748 */
11749 public static function uploadFile($src, $dest, $copy_only = false) {
11750 // always attempt to include the File class
11751 jimport('joomla.filesystem.file');
11752
11753 // upload the file
11754 if (!$copy_only) {
11755 $result = JFile::upload($src, $dest);
11756 } else {
11757 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
11758 $result = JFile::copy($src, $dest);
11759 }
11760
11761 /**
11762 * @wponly in order to not lose uploaded files after installing an update,
11763 * we need to move any uploaded file onto a recovery folder.
11764 */
11765 if ($result) {
11766 VikBookingLoader::import('update.manager');
11767 VikBookingUpdateManager::triggerUploadBackup($dest);
11768 }
11769 //
11770
11771 // return upload result
11772 return $result;
11773 }
11774
11775 /**
11776 * Helper method used to upload the given file (retrieved from $_FILES)
11777 * into the specified destination.
11778 *
11779 * @param array $file An associative array with the file details.
11780 * @param string $dest The destination path.
11781 * @param string $filters A string (or a regex) containing the allowed extensions.
11782 *
11783 * @return array The uploading result.
11784 *
11785 * @throws RuntimeException
11786 *
11787 * @since 1.3.0
11788 */
11789 public static function uploadFileFromRequest($file, $dest, $filters = '*')
11790 {
11791 jimport('joomla.filesystem.file');
11792
11793 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
11794
11795 if (empty($file['name']))
11796 {
11797 throw new RuntimeException('Missing file', 400);
11798 }
11799
11800 /**
11801 * Make sure the upload of the file didn't raise any errors.
11802 *
11803 * @link https://www.php.net/manual/en/features.file-upload.errors.php
11804 */
11805 if ((int) $file['error'])
11806 {
11807 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
11808 {
11809 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
11810 }
11811 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
11812 {
11813 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
11814 }
11815 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
11816 {
11817 $error = 'The uploaded file was only partially uploaded.';
11818 }
11819 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
11820 {
11821 $error = 'No file was uploaded.';
11822 }
11823 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
11824 {
11825 $error = 'Missing a temporary folder.';
11826 }
11827 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
11828 {
11829 $error = 'Failed to write file to disk.';
11830 }
11831 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
11832 {
11833 $error = 'A PHP extension stopped the file upload.';
11834 }
11835 else
11836 {
11837 $error = 'Unknown error.';
11838 }
11839
11840 throw new RuntimeException($error, 500);
11841 }
11842
11843 $src = $file['tmp_name'];
11844
11845 // extract file name and extension
11846 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
11847 {
11848 $filename = $match[1];
11849 $fileext = $match[2];
11850 }
11851 else
11852 {
11853 // probably no extension provided
11854 $filename = basename($file['name']);
11855 $fileext = '';
11856 }
11857
11858 $j = '';
11859
11860 if (file_exists($dest . $filename . $fileext))
11861 {
11862 $j = 2;
11863
11864 while (file_exists($dest . $filename . '-' . $j . $fileext))
11865 {
11866 $j++;
11867 }
11868
11869 $j = '-' . $j;
11870 }
11871
11872 $finaldest = $dest . $filename . $j . $fileext;
11873
11874 if ($filters !== '*')
11875 {
11876 $ext = $file['type'];
11877
11878 // check if we have a regex
11879 if (preg_match("/^[#\/]/", $filters) && preg_match("/[#\/][a-z]*$/", $filters))
11880 {
11881 if (!preg_match($filters, $ext))
11882 {
11883 // extension not supported
11884 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11885 }
11886 }
11887 else
11888 {
11889 // get all supported types
11890 $types = array_map('strtolower', array_filter(explode(',', $filters)));
11891
11892 if (!in_array($ext, $types))
11893 {
11894 // extension not supported
11895 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11896 }
11897 }
11898 }
11899
11900 // try to upload the file
11901 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
11902 {
11903 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
11904 }
11905
11906 $file = new stdClass;
11907 $file->name = $filename . $j;
11908 $file->ext = ltrim($fileext, '.');
11909 $file->filename = basename($finaldest);
11910 $file->path = $finaldest;
11911
11912 return $file;
11913 }
11914
11915 /**
11916 * Gets the instance of a specific report class.
11917 *
11918 * @param string $report the name of the report to load.
11919 *
11920 * @return mixed false or report object instance.
11921 *
11922 * @since 1.3.0
11923 */
11924 public static function getReportInstance($report)
11925 {
11926 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
11927
11928 return VikBookingReport::getInstanceOf($report);
11929 }
11930
11931 /**
11932 * Checks whether the guest reviews are enabled and VCM is installed.
11933 *
11934 * @return boolean true if enabled, false otherwise.
11935 *
11936 * @since 1.3.0
11937 */
11938 public static function allowGuestReviews()
11939 {
11940 $dbo = JFactory::getDbo();
11941 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
11942 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
11943 $dbo->setQuery($q);
11944 $dbo->execute();
11945 if ($dbo->getNumRows()) {
11946 $s = $dbo->loadResult();
11947 return ((int)$s === 1 && $vcm_installed);
11948 }
11949 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
11950 $dbo->setQuery($q);
11951 $dbo->execute();
11952 return $vcm_installed;
11953 }
11954
11955 /**
11956 * Gets the minimum chars for the guest review message.
11957 *
11958 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
11959 *
11960 * @since 1.3.0
11961 */
11962 public static function guestReviewMinChars()
11963 {
11964 $dbo = JFactory::getDbo();
11965 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
11966 $dbo->setQuery($q);
11967 $dbo->execute();
11968 if ($dbo->getNumRows() > 0) {
11969 return (int)$dbo->loadResult();
11970 }
11971 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
11972 $dbo->setQuery($q);
11973 $dbo->execute();
11974 return 15;
11975 }
11976
11977 /**
11978 * The approval type for new guest reviews.
11979 *
11980 * @return string auto or manual.
11981 *
11982 * @since 1.3.0
11983 */
11984 public static function guestReviewsApproval()
11985 {
11986 $dbo = JFactory::getDbo();
11987 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
11988 $dbo->setQuery($q);
11989 $dbo->execute();
11990 if ($dbo->getNumRows() > 0) {
11991 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
11992 }
11993 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
11994 $dbo->setQuery($q);
11995 $dbo->execute();
11996 return 'auto';
11997 }
11998
11999 /**
12000 * The type of reviews guests should leave.
12001 *
12002 * @return string global or service.
12003 *
12004 * @since 1.3.0
12005 */
12006 public static function guestReviewsType()
12007 {
12008 $dbo = JFactory::getDbo();
12009 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
12010 $dbo->setQuery($q);
12011 $dbo->execute();
12012 if ($dbo->getNumRows() > 0) {
12013 return $dbo->loadResult() == 'global' ? 'global' : 'service';
12014 }
12015 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
12016 $dbo->setQuery($q);
12017 $dbo->execute();
12018 return 'service';
12019 }
12020
12021 /**
12022 * The services to be reviewed by the guests.
12023 *
12024 * @return array list, empty or not, of the services to be reviewed.
12025 *
12026 * @since 1.3.0
12027 */
12028 public static function guestReviewsServices()
12029 {
12030 $dbo = JFactory::getDbo();
12031 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
12032 $dbo->setQuery($q);
12033 $dbo->execute();
12034 if ($dbo->getNumRows()) {
12035 return $dbo->loadAssocList();
12036 }
12037 // insert the default services
12038 $default_services = array(
12039 JText::translate('VBOGREVVALUE'),
12040 JText::translate('VBOGREVLOCATION'),
12041 JText::translate('VBOGREVSTAFF'),
12042 JText::translate('VBOGREVCLEAN'),
12043 JText::translate('VBOGREVCOMFORT'),
12044 JText::translate('VBOGREVFACILITIES'),
12045 );
12046 foreach ($default_services as $def_service) {
12047 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
12048 $dbo->setQuery($q);
12049 $dbo->execute();
12050 }
12051 return array();
12052 }
12053
12054 /**
12055 * Checks whether the guest reviews should be downloaded (max 1 per day).
12056 * VCM must be installed in order to download the new reviews.
12057 *
12058 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
12059 *
12060 * @since 1.3.0
12061 */
12062 public static function shouldDownloadReviews()
12063 {
12064 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
12065 return -1;
12066 }
12067
12068 $today = date('Y-m-d');
12069
12070 $dbo = JFactory::getDbo();
12071 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
12072 $dbo->setQuery($q);
12073 $dbo->execute();
12074 if ($dbo->getNumRows()) {
12075 $last_download = $dbo->loadResult();
12076 // update last download day
12077 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
12078 $dbo->setQuery($q);
12079 $dbo->execute();
12080 //
12081 return ($last_download != $today ? 1 : 0);
12082 }
12083 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
12084 $dbo->setQuery($q);
12085 $dbo->execute();
12086 return 1;
12087 }
12088
12089 /**
12090 * Tells whether a booking can be reviewed.
12091 *
12092 * @param array $booking booking array details.
12093 *
12094 * @return boolean true if a review can be left, false otherwise.
12095 *
12096 * @since 1.3.0
12097 */
12098 public static function canBookingBeReviewed($booking)
12099 {
12100 // reviews must be enabled and supported
12101 if (!self::allowGuestReviews()) {
12102 return false;
12103 }
12104
12105 // booking status must be confirmed
12106 if ($booking['status'] != 'confirmed') {
12107 return false;
12108 }
12109
12110 // review can be left starting from the check-out day
12111 $checkout_info = getdate($booking['checkout']);
12112 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12113 if (time() < $checkout_midn) {
12114 return false;
12115 }
12116
12117 // make sure a review for this booking does not exist
12118 $noreview = true;
12119 $dbo = JFactory::getDbo();
12120 try {
12121 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12122 $dbo->setQuery($q);
12123 $dbo->execute();
12124 $noreview = ($dbo->getNumRows() < 1);
12125 } catch (Exception $e) {
12126 // if the query fails, we do not allow the review to be left
12127 $noreview = false;
12128 }
12129
12130 return $noreview;
12131 }
12132
12133 /**
12134 * Gets the review for a booking, no matter if it's published or not.
12135 * Review will be returned only if reviews are enabled and supported.
12136 * Only reviews left through the website, no OTA reviews as this method
12137 * should be used in the front-end to display the review to the guest.
12138 *
12139 * @param array $booking booking array details.
12140 *
12141 * @return mixed associative array or false.
12142 *
12143 * @since 1.3.0
12144 */
12145 public static function getBookingReview($booking)
12146 {
12147 // reviews must be enabled and supported
12148 if (!self::allowGuestReviews()) {
12149 return false;
12150 }
12151
12152 if (empty($booking['id'])) {
12153 return false;
12154 }
12155
12156 // make sure a review for this booking exists
12157 $dbo = JFactory::getDbo();
12158 $review = array();
12159 try {
12160 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12161 $dbo->setQuery($q);
12162 $dbo->execute();
12163 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12164 } catch (Exception $e) {
12165 // query has failed
12166 $review = array();
12167 }
12168
12169 if (count($review)) {
12170 // make sure the review was left from the website
12171 $review['uniquekey'] = (int)$review['uniquekey'];
12172 }
12173
12174 if (count($review) && !empty($review['uniquekey'])) {
12175 // this is an OTA review
12176 return false;
12177 }
12178
12179 if (count($review) && !empty($review['content'])) {
12180 // decode content
12181 $review['content'] = json_decode($review['content'], true);
12182 }
12183
12184 return count($review) ? $review : false;
12185 }
12186
12187 /**
12188 * Attempts to find a translation for a raw customer data label.
12189 * Bookings downloaded from the OTAs will save a raw string of information
12190 * composed of pairs of label-value separated by new line feeds.
12191 * We try to translate the given label into the current language.
12192 *
12193 * @param string $label the raw string label to translate.
12194 *
12195 * @return string either the original or the translated label.
12196 *
12197 * @since 1.3.5
12198 * @since 1.4.0 back-end support added.
12199 */
12200 public static function tnCustomerRawDataLabel($label)
12201 {
12202 // this is a map of the known labels
12203 $known_lbls = array(
12204 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12205 'LASTNAME' => 'ORDER_LNAME',
12206 'COUNTRY' => 'ORDER_STATE',
12207 'EMAIL' => 'VBMAIL',
12208 'TELEPHONE' => 'VBPHONE',
12209 'PHONE' => 'VBPHONE',
12210 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12211 'MEAL_PLAN' => 'VBOMEALPLAN',
12212 'CITY' => 'VBCITY',
12213 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12214 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12215 );
12216
12217 if (self::isAdmin()) {
12218 // override the map of known translation strings
12219 $known_lbls = array(
12220 'NAME' => 'ORDER_NAME',
12221 'LASTNAME' => 'ORDER_LNAME',
12222 'COUNTRY' => 'ORDER_STATE',
12223 'ADDRESS' => 'ORDER_ADDRESS',
12224 'CITY' => 'ORDER_CITY',
12225 'LOCATION' => 'ORDER_CITY',
12226 'EMAIL' => 'ORDER_EMAIL',
12227 'TELEPHONE' => 'ORDER_PHONE',
12228 'PHONE' => 'ORDER_PHONE',
12229 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12230 );
12231 }
12232
12233 // we get rid of any empty space by keeping the underscores
12234 $converted = str_replace(' ', '', strtoupper($label));
12235
12236 if (isset($known_lbls[$converted])) {
12237 // this language definition has been mapped
12238 return JText::translate($known_lbls[$converted]);
12239 }
12240
12241 // we try to guess the translation string by prepending VBO
12242 $guessed = JText::translate('VBO' . $converted);
12243 if ($guessed != 'VBO' . $converted) {
12244 // the label was translated correctly
12245 return $guessed;
12246 }
12247
12248 // this label could not be translated, so we return the plain string
12249 return $label;
12250 }
12251
12252 /**
12253 * We check whether some bookings are available for import from third party plugins.
12254 *
12255 * @return mixed array with list of plugins supported, false otherwise.
12256 *
12257 * @wponly this method is only useful for WordPress.
12258 *
12259 * @since 1.3.5
12260 */
12261 public static function canImportBookingsFromThirdPartyPlugins()
12262 {
12263 $dbo = JFactory::getDbo();
12264
12265 $plugins = array();
12266
12267 /**
12268 * As requested from hundreds of our clients, for the moment we check only the custom
12269 * post types of type "mphb_booking" to see if some bookings are available for import.
12270 */
12271 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12272 $dbo->setQuery($q);
12273 $dbo->execute();
12274
12275 if (!$dbo->getNumRows()) {
12276 return false;
12277 }
12278
12279 // push this third party plugin
12280 $plugins['mphb'] = 'MotoPress Hotel Booking';
12281
12282 return count($plugins) ? $plugins : false;
12283 }
12284
12285 /**
12286 * This method returns a list of the known languages sorted by the
12287 * administrator custom preferences. Useful for the phone input fields.
12288 *
12289 * @param boolean $code_assoc whether to get an associative array with the lang name.
12290 *
12291 * @return array the sorted list of preferred countries.
12292 *
12293 * @since 1.3.11
12294 */
12295 public static function preferredCountriesOrdering($code_assoc = false)
12296 {
12297 $preferred_countries = array();
12298
12299 // try to get the preferred countries from db
12300 $dbo = JFactory::getDbo();
12301 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12302 $dbo->setQuery($q);
12303 $setting = $dbo->loadResult();
12304 if (!$setting) {
12305 // create empty configuration record
12306 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12307 $dbo->setQuery($q);
12308 $dbo->execute();
12309 } else {
12310 $preferred_countries = json_decode($setting);
12311 }
12312
12313 // get the default known languages
12314 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12315
12316 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12317 // sort the default known languages by country code alphabetically
12318 ksort($sorted_known_langs);
12319 foreach ($sorted_known_langs as $k => $v) {
12320 $langsep = strpos($k, '_') !== false ? '_' : '-';
12321 $langparts = explode($langsep, $k);
12322 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
12323 }
12324 // update the database record
12325 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
12326 $dbo->setQuery($q);
12327 $dbo->execute();
12328 }
12329
12330 if ($code_assoc) {
12331 // this is useful for displaying the preferred countries codes together with the language name
12332 $map = array();
12333 foreach ($preferred_countries as $ccode) {
12334 // look for the current country code in the keys of the known language tags
12335 $match_found = false;
12336 foreach ($sorted_known_langs as $langtag => $langinfo) {
12337 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
12338 $langparts = explode($langsep, $langtag);
12339 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
12340 // match found
12341 $match_found = true;
12342 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12343 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
12344 // match found
12345 $match_found = true;
12346 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12347 }
12348 }
12349 if (!$match_found) {
12350 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
12351 $map[$ccode] = strtoupper($ccode);
12352 }
12353 }
12354 if (count($map)) {
12355 // set the associatve array to be returned
12356 $preferred_countries = $map;
12357 }
12358 }
12359
12360 return $preferred_countries;
12361 }
12362
12363 /**
12364 * Gets the instance of the admin widgets helper class.
12365 *
12366 * @return VikBookingHelperAdminWidgets
12367 *
12368 * @since 1.4.0
12369 */
12370 public static function getAdminWidgetsInstance()
12371 {
12372 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
12373
12374 return VikBookingHelperAdminWidgets::getInstance();
12375 }
12376
12377 /**
12378 * Gets the instance of the conditional rules helper class.
12379 *
12380 * @param bool $require_only whether to return the object.
12381 *
12382 * @return mixed VikBookingHelperConditionalRules or true.
12383 *
12384 * @since 1.4.0
12385 */
12386 public static function getConditionalRulesInstance($require_only = false)
12387 {
12388 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
12389
12390 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
12391 }
12392
12393 /**
12394 * Gets the instance of the geocoding helper class.
12395 *
12396 * @param bool $require_only whether to return the object.
12397 *
12398 * @return mixed VikBookingHelperGeocoding or true.
12399 *
12400 * @since 1.4.0
12401 */
12402 public static function getGeocodingInstance($require_only = false)
12403 {
12404 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
12405
12406 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
12407 }
12408
12409 /**
12410 * Helper method to cope with the removal of the same method
12411 * in the JApplication class introduced with Joomla 4. Using
12412 * isClient() would break the compatibility with J < 3.7 so
12413 * we can rely on this helper method to avoid Fatal Errors.
12414 *
12415 * @return boolean
12416 *
12417 * @since October 2020
12418 */
12419 public static function isAdmin()
12420 {
12421 $app = JFactory::getApplication();
12422 if (method_exists($app, 'isClient')) {
12423 return $app->isClient('administrator');
12424 }
12425
12426 return $app->isAdmin();
12427 }
12428
12429 /**
12430 * Helper method to cope with the removal of the same method
12431 * in the JApplication class introduced with Joomla 4. Using
12432 * isClient() would break the compatibility with J < 3.7 so
12433 * we can rely on this helper method to avoid Fatal Errors.
12434 *
12435 * @return boolean
12436 *
12437 * @since October 2020
12438 */
12439 public static function isSite()
12440 {
12441 $app = JFactory::getApplication();
12442 if (method_exists($app, 'isClient')) {
12443 return $app->isClient('site');
12444 }
12445
12446 return $app->isSite();
12447 }
12448
12449 /**
12450 * Gets the Google Maps API Key.
12451 *
12452 * @return string
12453 *
12454 * @since 1.14 (J) - 1.4.0 (WP)
12455 */
12456 public static function getGoogleMapsKey()
12457 {
12458 $dbo = JFactory::getDbo();
12459 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
12460 $dbo->setQuery($q);
12461 $dbo->execute();
12462 if ($dbo->getNumRows()) {
12463 return $dbo->loadResult();
12464 }
12465 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
12466 $dbo->setQuery($q);
12467 $dbo->execute();
12468 return '';
12469 }
12470
12471 /**
12472 * Checks whether the interactive map booking is enabled.
12473 *
12474 * @return bool
12475 *
12476 * @since 1.14 (J) - 1.4.0 (WP)
12477 */
12478 public static function interactiveMapEnabled()
12479 {
12480 $dbo = JFactory::getDbo();
12481 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
12482 $dbo->setQuery($q);
12483 $dbo->execute();
12484 if ($dbo->getNumRows()) {
12485 return ((int)$dbo->loadResult() > 0);
12486 }
12487
12488 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
12489 $dbo->setQuery($q);
12490 $dbo->execute();
12491
12492 return false;
12493 }
12494
12495 /**
12496 * Gets the preferred colors saved in the configuration, if any.
12497 *
12498 * @return array
12499 *
12500 * @since 1.14 (J) - 1.4.0 (WP)
12501 */
12502 public static function getPreferredColors()
12503 {
12504 $dbo = JFactory::getDbo();
12505 $pref_colors = array(
12506 'textcolor' => '',
12507 'bgcolor' => '',
12508 'fontcolor' => '',
12509 'bgcolorhov' => '',
12510 'fontcolorhov' => '',
12511 );
12512
12513 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
12514 $dbo->setQuery($q);
12515 $dbo->execute();
12516 if ($dbo->getNumRows()) {
12517 $colors = json_decode($dbo->loadResult(), true);
12518 if (!is_array($colors) || !isset($colors['textcolor'])) {
12519 return $pref_colors;
12520 }
12521 return $colors;
12522 }
12523 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
12524 $dbo->setQuery($q);
12525 $dbo->execute();
12526 return $pref_colors;
12527 }
12528
12529 /**
12530 * Adds to the document inline styles for the preferred colors, if any.
12531 *
12532 * @return void
12533 *
12534 * @since 1.14 (J) - 1.4.0 (WP)
12535 */
12536 public static function loadPreferredColorStyles()
12537 {
12538 $view = VikRequest::getString('view', '', 'request');
12539 $pref_colors = self::getPreferredColors();
12540
12541 $css_classes = array();
12542
12543 if (!empty($pref_colors['textcolor'])) {
12544 // titles and headings
12545 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
12546 // stepbar, oconfirm
12547 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; }');
12548 // buttons secondary color
12549 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
12550 if (!empty($pref_colors['fontcolor'])) {
12551 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
12552 }
12553 // datepicker
12554 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
12555 color: ' . $pref_colors['textcolor'] . ' !important;
12556 border-color: ' . $pref_colors['textcolor'] . ' !important;
12557 }');
12558 array_push($css_classes, '.ui-datepicker .ui-datepicker-today a {
12559 color: ' . $pref_colors['textcolor'] . ' !important;
12560 }');
12561 // operators tableaux
12562 if ($view == 'tableaux') {
12563 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
12564 }
12565 }
12566
12567 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
12568 // elements with backgrounds
12569 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12570 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12571 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12572 // buttons with backgrounds
12573 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12574 // stepbar
12575 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12576 // datepicker
12577 array_push($css_classes, '.ui-datepicker table td:hover {
12578 border-color: ' . $pref_colors['bgcolor'] . ' !important;
12579 }');
12580 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day {
12581 background: ' . $pref_colors['bgcolor'] . ' !important;
12582 color: ' . $pref_colors['fontcolor'] . ' !important;
12583 }');
12584 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
12585 color: ' . $pref_colors['fontcolor'] . ' !important;
12586 }');
12587 // operators tableaux
12588 if ($view == 'tableaux') {
12589 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; }');
12590 }
12591 }
12592
12593 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
12594 // buttons with backgrounds during hover state
12595 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12596 // operators tableaux
12597 if ($view == 'tableaux') {
12598 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12599 }
12600 }
12601
12602 if (!count($css_classes)) {
12603 return;
12604 }
12605
12606 // add in-line style declaration
12607 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
12608 }
12609
12610 /**
12611 * Given the full endpoint URL for the AJAX request, it
12612 * returns an appropriate URI for the current platform.
12613 *
12614 * @param mixed $query The query string or a routed URL.
12615 * @param boolean $xhtml Replace & by &amp; for XML compliance.
12616 *
12617 * @return string The AJAX end-point URI.
12618 *
12619 * @since 1.14 (J) - 1.4.0 (WP)
12620 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
12621 */
12622 public static function ajaxUrl($query = '', $xhtml = false)
12623 {
12624 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
12625 }
12626
12627 /**
12628 * Tells whether no tax rates have been defined so far.
12629 *
12630 * @return bool True if no tax rates defined, false otherwise.
12631 *
12632 * @since 1.14 (J) - 1.4.0 (WP)
12633 */
12634 public static function noTaxRates()
12635 {
12636 $dbo = JFactory::getDbo();
12637
12638 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
12639 $dbo->setQuery($q);
12640 $dbo->execute();
12641
12642 return ($dbo->getNumRows() < 1);
12643 }
12644
12645 /**
12646 * Booking Type feature is strictly connected to VCM and OTAs. Updated
12647 * versions of VBO will always support it as long as VCM is installed.
12648 * Needed by VCM to understand whether certain SQL queries can be performed.
12649 *
12650 * @return bool true if VCM exists, false otherwise.
12651 *
12652 * @since 1.14 (J) - 1.4.0 (WP)
12653 */
12654 public static function isBookingTypeSupported()
12655 {
12656 return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12657 }
12658
12659 /**
12660 * Returns an instance of the Rates Flow helper class of VCM, if available.
12661 *
12662 * @return mixed VCMRatesFlow if VCM available and updated, or null.
12663 *
12664 * @since 1.15.0 (J) - 1.5.0 (WP)
12665 */
12666 public static function getRatesFlowInstance()
12667 {
12668 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
12669 if (!is_file($vcm_fpath)) {
12670 return null;
12671 }
12672
12673 // load dependencies
12674 require_once $vcm_fpath;
12675
12676 // return the instance of the class
12677 return VCMRatesFlow::getInstance();
12678 }
12679
12680 /**
12681 * Returns the current appearance preference.
12682 *
12683 * @return string light, auto or dark.
12684 *
12685 * @since 1.15.0 (J) - 1.5.0 (WP)
12686 */
12687 public static function getAppearancePref()
12688 {
12689 static $preference = null;
12690
12691 if ($preference !== null) {
12692 return $preference;
12693 }
12694
12695 // auto is the default appearance preference
12696 $default_pref = 'auto';
12697
12698 // accepted preferences
12699 $valid_pref = [
12700 'auto',
12701 'light',
12702 'dark',
12703 ];
12704
12705 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
12706 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
12707
12708 return $preference;
12709 }
12710
12711 /**
12712 * According to the appearance preferences, the apposite
12713 * CSS assets are loaded within the document.
12714 *
12715 * @param bool $get_info true to not load any assets.
12716 *
12717 * @return mixed string light, auto, dark, info array or false.
12718 *
12719 * @since 1.15.0 (J) - 1.5.0 (WP)
12720 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
12721 */
12722 public static function loadAppearancePreferenceAssets($get_info = false)
12723 {
12724 // get document object
12725 $document = JFactory::getDocument();
12726
12727 // load current color scheme preference
12728 $current_pref = self::getAppearancePref();
12729
12730 // define caching values
12731 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
12732
12733 // define file attributes
12734 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
12735
12736 // apposite file path and URI for back-end
12737 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
12738 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
12739
12740 // check if this is for the front-end ("auto" file only)
12741 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
12742 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
12743 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
12744 // check if front-end appearance is disabled
12745 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
12746 // do nothing for the front-end by default
12747 return false;
12748 }
12749 }
12750
12751 if (VBOPlatformDetection::isJoomla()) {
12752 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
12753 $css_uri = str_replace('resources/', '', $css_uri);
12754 }
12755
12756 if (!is_file($css_path)) {
12757 // this preference does not require a specific stylesheet
12758 return false;
12759 }
12760
12761 if ($get_info) {
12762 // do not actually load any assets
12763 return [
12764 'id' => $file_attr['id'],
12765 'href' => $css_uri,
12766 ];
12767 }
12768
12769 // load the apposite CSS file
12770 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
12771
12772 return $current_pref;
12773 }
12774
12775 /**
12776 * VBO trigger for VCM subscription status and updates. If called within the admin section
12777 * returns the expiration reminder array to display a modal window, and sets a warning
12778 * message. If called within the front-end, may trigger an email alert.
12779 *
12780 * @return mixed null, array or boolean depending on client position.
12781 *
12782 * @since 1.15.0 (J) - 1.5.0 (WP)
12783 */
12784 public static function getVCMSubscriptionStatus()
12785 {
12786 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
12787 if (!is_file($vcm_lib_path)) {
12788 // do nothing
12789 return null;
12790 }
12791
12792 // determine client position
12793 $is_admin = self::isAdmin();
12794
12795 try {
12796 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
12797 // VCM is outdated
12798 return null;
12799 }
12800
12801 // always attempt to load the admin lang handler of VCM
12802 $lang = JFactory::getLanguage();
12803
12804 // load the VCM admin language file
12805 $vcm_admin_lang_path = '';
12806 if (VBOPlatformDetection::isJoomla()) {
12807 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
12808 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
12809 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
12810 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
12811 /**
12812 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
12813 */
12814 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
12815 }
12816 // load translation file
12817 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
12818 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
12819 /**
12820 * @wponly load language admin handler as well for WP.
12821 * We do this only because of WordPress, but in a way also compatible with Joomla as
12822 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
12823 */
12824 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
12825 }
12826
12827 if ($is_admin) {
12828 // back-end section of the site: check status
12829 if (VikChannelManager::loadExpirationDetails() === false) {
12830 // download and update the expiration details
12831 VikChannelManager::downloadExpirationDetails();
12832 }
12833 $expiration_reminder = null;
12834 if (VikChannelManager::isSubscriptionExpired()) {
12835 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
12836 } else {
12837 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
12838 // we also check if an update is available
12839 if (VikChannelManager::checkUpdates()) {
12840 // an update is more important
12841 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
12842 exit;
12843 }
12844 }
12845 // return reminder status
12846 return $expiration_reminder;
12847 }
12848
12849 // front-end section of the site: trigger alert, if any (bool)
12850 return VikChannelManager::checkSubscriptionReminder();
12851
12852 } catch (Exception $e) {
12853 // do nothing
12854 }
12855
12856 return null;
12857 }
12858
12859 /**
12860 * Given a list of website rate plans, returns the sorted list.
12861 * Naming score is the highest sorting method, pricing scoring is also supported.
12862 *
12863 * @param array $rate_plans the list of rate plan arrays to sort.
12864 * @param bool $assoc whether to keep the original array keys.
12865 *
12866 * @return array the sorted list of rate plan arrays.
12867 *
12868 * @since 1.15.0 (J) - 1.5.0 (WP)
12869 */
12870 public static function sortRatePlans($rate_plans, $assoc = false)
12871 {
12872 if (!is_array($rate_plans) || count($rate_plans) < 2) {
12873 return $rate_plans;
12874 }
12875
12876 foreach ($rate_plans as $rp_ind => $rate_plan) {
12877 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
12878 // do not proceed in case of a weird array structure
12879 return $rate_plans;
12880 }
12881 // count the sorting-score of this rate plan
12882 $sort_score = 0;
12883 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
12884 // "standard rate" rate plans should go first
12885 $sort_score += 3;
12886 } else {
12887 $sort_score--;
12888 }
12889 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
12890 // this doesn't seem to be a "non refundable rate" so it should go first
12891 $sort_score += 2;
12892 }
12893
12894 // set sorting score for this rate plan ID
12895 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
12896 }
12897
12898 // apply custom sorting and keep index association
12899 uasort($rate_plans, function($a, $b) {
12900 // sort by name-scoring DESC
12901 $sort_diff = $b['sort_score'] - $a['sort_score'];
12902 if (isset($b['cost'])) {
12903 // apply also sorting by price ASC
12904 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
12905 }
12906 return $sort_diff;
12907 });
12908
12909 return !$assoc ? array_values($rate_plans) : $rate_plans;
12910 }
12911
12912 /**
12913 * Returns an instance of the Availability helper class.
12914 *
12915 * @return VikBookingAvailability
12916 *
12917 * @since 1.15.0 (J) - 1.5.0 (WP)
12918 */
12919 public static function getAvailabilityInstance()
12920 {
12921 // require library
12922 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
12923
12924 // return the instance of the class
12925 return VikBookingAvailability::getInstance();
12926 }
12927
12928 /**
12929 * Helper method to quickly guess the internal dependency to import.
12930 *
12931 * @param string $path_id either a full path to a file or its identifier.
12932 *
12933 * @return bool true if the requested library was imported or false.
12934 *
12935 * @since 1.15.0 (J) - 1.5.0 (WP)
12936 */
12937 public static function import($path_id)
12938 {
12939 if (empty($path_id)) {
12940 return false;
12941 }
12942
12943 // always append .php extension
12944 $path_id = basename($path_id, '.php') . '.php';
12945
12946 if (is_file($path_id)) {
12947 // full path given
12948 require_once $path_id;
12949
12950 return true;
12951 }
12952
12953 // try guessing where the file id is located
12954 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12955 // library was found
12956 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12957
12958 return true;
12959 }
12960
12961 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12962 // library was found
12963 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12964
12965 return true;
12966 }
12967
12968 return false;
12969 }
12970 }
12971
12972 if (!class_exists('VikResizer'))
12973 {
12974 class VikResizer
12975 {
12976 public function __construct()
12977 {
12978 // objects of this class can also be instantiated without calling the methods statically.
12979 }
12980
12981 /**
12982 * Resizes an image proportionally. For PNG files it can optionally
12983 * trim the image to exclude the transparency, and add some padding to it.
12984 * All PNG files keep the alpha background in the resized version.
12985 *
12986 * @param string $fileimg path to original image file
12987 * @param string $dest path to destination image file
12988 * @param int $towidth
12989 * @param int $toheight
12990 * @param bool $trim_png remove empty background from image
12991 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
12992 *
12993 * @return boolean
12994 */
12995 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
12996 {
12997 if (!is_file($fileimg)) {
12998 return false;
12999 }
13000 if (empty($towidth) && empty($toheight)) {
13001 return copy($fileimg, $dest);
13002 }
13003
13004 $result = true;
13005
13006 list ($owid, $ohei, $type) = getimagesize($fileimg);
13007
13008 if ($owid > $towidth || $ohei > $toheight) {
13009 $xscale = $owid / $towidth;
13010 $yscale = $ohei / $toheight;
13011 if ($yscale > $xscale) {
13012 $new_width = round($owid * (1 / $yscale));
13013 $new_height = round($ohei * (1 / $yscale));
13014 } else {
13015 $new_width = round($owid * (1 / $xscale));
13016 $new_height = round($ohei * (1 / $xscale));
13017 }
13018
13019 $imageresized = imagecreatetruecolor($new_width, $new_height);
13020
13021 switch ($type) {
13022 case '1' :
13023 $imagetmp = imagecreatefromgif ($fileimg);
13024 break;
13025 case '2' :
13026 $imagetmp = imagecreatefromjpeg($fileimg);
13027 break;
13028 case '18' :
13029 $imagetmp = imagecreatefromwebp($fileimg);
13030 break;
13031 default :
13032 //keep alpha for PNG files
13033 $background = imagecolorallocate($imageresized, 0, 0, 0);
13034 imagecolortransparent($imageresized, $background);
13035 imagealphablending($imageresized, false);
13036 imagesavealpha($imageresized, true);
13037 //
13038 $imagetmp = imagecreatefrompng($fileimg);
13039 break;
13040 }
13041
13042 if (!$imagetmp) {
13043 return false;
13044 }
13045
13046 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13047
13048 switch ($type) {
13049 case '1' :
13050 imagegif($imageresized, $dest);
13051 break;
13052 case '2' :
13053 imagejpeg($imageresized, $dest);
13054 break;
13055 case '18' :
13056 imagewebp($imageresized, $dest);
13057 break;
13058 default :
13059 if ($trim_png) {
13060 self::imageTrim($imageresized, $background, $trim_pad);
13061 }
13062 imagepng($imageresized, $dest);
13063 break;
13064 }
13065
13066 imagedestroy($imageresized);
13067 } else {
13068 $result = copy($fileimg, $dest);
13069 }
13070
13071 if (VBOPlatformDetection::isWordPress()) {
13072 /**
13073 * @wponly in order to not lose resized files after installing an update,
13074 * we need to move any uploaded file onto a recovery folder.
13075 */
13076 VikBookingLoader::import('update.manager');
13077 VikBookingUpdateManager::triggerUploadBackup($dest);
13078 }
13079
13080 return $result;
13081 }
13082
13083 /**
13084 * (BETA) Resizes an image proportionally. For PNG files it can optionally
13085 * trim the image to exclude the transparency, and add some padding to it.
13086 * All PNG files keep the alpha background in the resized version.
13087 *
13088 * @param resource $im Image link resource (reference)
13089 * @param int $bg imagecolorallocate color identifier
13090 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
13091 *
13092 * @return void
13093 */
13094 public static function imagetrim(&$im, $bg, $pad = null)
13095 {
13096 // Calculate padding for each side.
13097 if (isset($pad)) {
13098 $pp = explode(' ', $pad);
13099 if (isset($pp[3])) {
13100 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13101 } elseif (isset($pp[2])) {
13102 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13103 } elseif (isset($pp[1])) {
13104 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13105 } else {
13106 $p = array_fill(0, 4, (int) $pp[0]);
13107 }
13108 } else {
13109 $p = array_fill(0, 4, 0);
13110 }
13111
13112 // Get the image width and height.
13113 $imw = imagesx($im);
13114 $imh = imagesy($im);
13115
13116 // Set the X variables.
13117 $xmin = $imw;
13118 $xmax = 0;
13119
13120 // Start scanning for the edges.
13121 for ($iy=0; $iy<$imh; $iy++) {
13122 $first = true;
13123 for ($ix=0; $ix<$imw; $ix++) {
13124 $ndx = imagecolorat($im, $ix, $iy);
13125 if ($ndx != $bg) {
13126 if ($xmin > $ix) {
13127 $xmin = $ix;
13128 }
13129 if ($xmax < $ix) {
13130 $xmax = $ix;
13131 }
13132 if (!isset($ymin)) {
13133 $ymin = $iy;
13134 }
13135 $ymax = $iy;
13136 if ($first) {
13137 $ix = $xmax;
13138 $first = false;
13139 }
13140 }
13141 }
13142 }
13143
13144 // The new width and height of the image. (not including padding)
13145 $imw = 1+$xmax-$xmin; // Image width in pixels
13146 $imh = 1+$ymax-$ymin; // Image height in pixels
13147
13148 // Make another image to place the trimmed version in.
13149 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13150
13151 // Make the background of the new image the same as the background of the old one.
13152 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13153 imagefill($im2, 0, 0, $bg2);
13154
13155 // Copy it over to the new image.
13156 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13157
13158 // To finish up, we replace the old image which is referenced.
13159 $im = $im2;
13160 }
13161
13162 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13163 {
13164 if (!file_exists($fileimg)) {
13165 return false;
13166 }
13167 if (empty($towidth) && empty($toheight)) {
13168 copy($fileimg, $dest);
13169 return true;
13170 }
13171
13172 $exp = explode(",", $rgb);
13173 if (count($exp) == 3) {
13174 $r = trim($exp[0]);
13175 $g = trim($exp[1]);
13176 $b = trim($exp[2]);
13177 } else {
13178 $r = 0;
13179 $g = 0;
13180 $b = 0;
13181 }
13182
13183 list ($owid, $ohei, $type) = getimagesize($fileimg);
13184
13185 if ($owid > $towidth || $ohei > $toheight) {
13186 $xscale = $owid / $towidth;
13187 $yscale = $ohei / $toheight;
13188 if ($yscale > $xscale) {
13189 $new_width = round($owid * (1 / $yscale));
13190 $new_height = round($ohei * (1 / $yscale));
13191 $ydest = 0;
13192 $diff = $towidth - $new_width;
13193 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13194 } else {
13195 $new_width = round($owid * (1 / $xscale));
13196 $new_height = round($ohei * (1 / $xscale));
13197 $xdest = 0;
13198 $diff = $toheight - $new_height;
13199 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13200 }
13201
13202 $imageresized = imagecreatetruecolor($towidth, $toheight);
13203
13204 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13205 imagefill($imageresized, 0, 0, $bgColor);
13206
13207 switch ($type) {
13208 case '1' :
13209 $imagetmp = imagecreatefromgif ($fileimg);
13210 break;
13211 case '2' :
13212 $imagetmp = imagecreatefromjpeg($fileimg);
13213 break;
13214 default :
13215 $imagetmp = imagecreatefrompng($fileimg);
13216 break;
13217 }
13218
13219 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13220
13221 switch ($type) {
13222 case '1' :
13223 imagegif ($imageresized, $dest);
13224 break;
13225 case '2' :
13226 imagejpeg($imageresized, $dest);
13227 break;
13228 default :
13229 imagepng($imageresized, $dest);
13230 break;
13231 }
13232
13233 imagedestroy($imageresized);
13234
13235 return true;
13236 } else {
13237 copy($fileimg, $dest);
13238 }
13239 return true;
13240 }
13241
13242 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13243 {
13244 if (!file_exists($fileimg)) {
13245 return false;
13246 }
13247 if (empty($towidth) && empty($toheight)) {
13248 copy($fileimg, $dest);
13249 return true;
13250 }
13251
13252 list ($owid, $ohei, $type) = getimagesize($fileimg);
13253
13254 if ($owid <= $ohei) {
13255 $new_width = $towidth;
13256 $new_height = ($towidth / $owid) * $ohei;
13257 } else {
13258 $new_height = $toheight;
13259 $new_width = ($new_height / $ohei) * $owid;
13260 }
13261
13262 switch ($type) {
13263 case '1' :
13264 $img_src = imagecreatefromgif ($fileimg);
13265 $img_dest = imagecreate($new_width, $new_height);
13266 break;
13267 case '2' :
13268 $img_src = imagecreatefromjpeg($fileimg);
13269 $img_dest = imagecreatetruecolor($new_width, $new_height);
13270 break;
13271 default :
13272 $img_src = imagecreatefrompng($fileimg);
13273 $img_dest = imagecreatetruecolor($new_width, $new_height);
13274 break;
13275 }
13276
13277 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13278
13279 switch ($type) {
13280 case '1' :
13281 $cropped = imagecreate($towidth, $toheight);
13282 break;
13283 case '2' :
13284 $cropped = imagecreatetruecolor($towidth, $toheight);
13285 break;
13286 default :
13287 $cropped = imagecreatetruecolor($towidth, $toheight);
13288 break;
13289 }
13290
13291 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
13292
13293 switch ($type) {
13294 case '1' :
13295 imagegif ($cropped, $dest);
13296 break;
13297 case '2' :
13298 imagejpeg($cropped, $dest);
13299 break;
13300 default :
13301 imagepng($cropped, $dest);
13302 break;
13303 }
13304
13305 imagedestroy($img_dest);
13306 imagedestroy($cropped);
13307
13308 return true;
13309 }
13310 }
13311 }
13312