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

13,215 lines 449.2 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 $dbo = JFactory::getDbo();
3265 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='minuteslock';";
3266 $dbo->setQuery($q);
3267 $dbo->execute();
3268 if (!$dbo->getNumRows()) {
3269 return 0;
3270 }
3271 $s = $dbo->loadAssocList();
3272 if ($conv) {
3273 return (time() + ((int)$s[0]['setting'] * 60));
3274 }
3275 return (int)$s[0]['setting'];
3276 }
3277
3278 /**
3279 * Checks if the room units are temporarily locked on the given dates.
3280 *
3281 * @param int $idroom the room ID.
3282 * @param int $units the total number of room units.
3283 * @param int $first the check-in timestamp.
3284 * @param int $second the check-out timestamp.
3285 * @param bool $occupied if true, the system will also consider the booked units.
3286 * @param array $skip_busy_ids optional list of busy record IDs to skip (booking modification).
3287 *
3288 * @return bool true if the room is available, false if fully locked/occupied, hence not bookable.
3289 *
3290 * @since 1.16.0 (J) - 1.6.0 (WP) added $occupied argument.
3291 * @since 1.16.3 (J) - 1.6.3 (WP) added $skip_busy_ids argument to support usage for booking modifications.
3292 */
3293 public static function roomNotLocked($idroom, $units, $first, $second, $occupied = false, $skip_busy_ids = [])
3294 {
3295 $dbo = JFactory::getDbo();
3296
3297 if ($units < 1) {
3298 return false;
3299 }
3300
3301 $actnow = time();
3302
3303 // clean up expired records
3304 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $dbo->quote($actnow) . ";";
3305 $dbo->setQuery($q);
3306 $dbo->execute();
3307
3308 $secdiff = $second - $first;
3309 $daysdiff = $secdiff / 86400;
3310 if (is_int($daysdiff)) {
3311 if ($daysdiff < 1) {
3312 $daysdiff = 1;
3313 }
3314 } else {
3315 if ($daysdiff < 1) {
3316 $daysdiff = 1;
3317 } else {
3318 $sum = floor($daysdiff) * 86400;
3319 $newdiff = $secdiff - $sum;
3320 $maxhmore = self::getHoursMoreRb() * 3600;
3321 if ($maxhmore >= $newdiff) {
3322 $daysdiff = floor($daysdiff);
3323 } else {
3324 $daysdiff = ceil($daysdiff);
3325 }
3326 }
3327 }
3328 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3329 $tmplock_map = [];
3330
3331 // check temporarily locked records
3332 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `until`>=" . $dbo->quote($actnow) . ";";
3333 $dbo->setQuery($check);
3334 $busy = $dbo->loadAssocList();
3335 if ($busy) {
3336 foreach ($groupdays as $kg => $gday) {
3337 $bfound = 0;
3338 foreach ($busy as $bu) {
3339 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3340 $bfound++;
3341 }
3342 }
3343 $tmplock_map[$kg] = $bfound;
3344 if ($bfound >= $units) {
3345 return false;
3346 }
3347 }
3348 }
3349
3350 if ($occupied) {
3351 // check also the busy records and some the occupied units to the temporarily locked ones
3352 $check = "SELECT `id`,`checkin`,`realback` FROM `#__vikbooking_busy` WHERE `idroom`=" . $dbo->quote($idroom) . " AND `realback`>=" . $first . ";";
3353 $dbo->setQuery($check);
3354 $busy = $dbo->loadAssocList();
3355 if (!$busy) {
3356 return true;
3357 }
3358
3359 foreach ($groupdays as $kg => $gday) {
3360 $bfound = 0;
3361 foreach ($busy as $bu) {
3362 if (in_array($bu['id'], $skip_busy_ids)) {
3363 // Booking modification
3364 continue;
3365 }
3366 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3367 $bfound++;
3368 }
3369 }
3370 // sum units occupied to the units temporarily locked
3371 $bfound += isset($tmplock_map[$kg]) ? (int)$tmplock_map[$kg] : 0;
3372 if ($bfound >= $units) {
3373 return false;
3374 }
3375 }
3376 }
3377
3378 return true;
3379 }
3380
3381 public static function getGroupDays($first, $second, $daysdiff) {
3382 $ret = array();
3383 $ret[] = $first;
3384 if ($daysdiff > 1) {
3385 $start = getdate($first);
3386 $end = getdate($second);
3387 $endcheck = mktime(0, 0, 0, $end['mon'], $end['mday'], $end['year']);
3388 for($i = 1; $i < $daysdiff; $i++) {
3389 $checkday = $start['mday'] + $i;
3390 $dayts = mktime(0, 0, 0, $start['mon'], $checkday, $start['year']);
3391 if ($dayts != $endcheck) {
3392 $ret[] = $dayts;
3393 }
3394 }
3395 }
3396 $ret[] = $second;
3397 return $ret;
3398 }
3399
3400 /**
3401 * Counts the hours of difference between the current
3402 * server time and the selected check-in date and time.
3403 *
3404 * @param int $checkin_ts
3405 * @param [int] $now_ts
3406 *
3407 * @return int
3408 */
3409 public static function countHoursToArrival($checkin_ts, $now_ts = '') {
3410 $hoursdiff = 0;
3411
3412 if (empty($now_ts)) {
3413 $now_ts = time();
3414 }
3415
3416 if ($now_ts >= $checkin_ts) {
3417 return $hoursdiff;
3418 }
3419
3420 $hoursdiff = floor(($checkin_ts - $now_ts) / 3600);
3421
3422 return $hoursdiff;
3423 }
3424
3425 /**
3426 * Loads all the occupied records for the given rooms and offset.
3427 *
3428 * @param array $roomids the optional list of room IDs to filter.
3429 * @param int $from_ts the optional base date to use.
3430 * @param int $max_ts the optional max date to use.
3431 * @param bool $closures whether to ignore, include or exclude closures.
3432 *
3433 * @return array
3434 *
3435 * @since 1.15.0 (J) - 1.5.0 (WP) changed arguments signature with 3rd arg.
3436 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 4th arg.
3437 */
3438 public static function loadBusyRecords($roomids = array(), $from_ts = 0, $max_ts = 0, $closures = null)
3439 {
3440 $base_ts = empty($from_ts) ? time() : $from_ts;
3441 $busy = [];
3442 $clauses = [];
3443
3444 if (is_array($roomids) && count($roomids)) {
3445 // filter busy records only by the requested room ids
3446 $roomids = array_map(function($rid) {
3447 return (int)$rid;
3448 }, $roomids);
3449 $clauses[] = "`b`.`idroom` IN (" . implode(', ', $roomids) . ")";
3450 }
3451
3452 // exclude past reservations
3453 $clauses[] = "`b`.`checkout` >= {$base_ts}";
3454
3455 if (!empty($max_ts)) {
3456 // exclude future reservations (recommended for large datasets)
3457 $clauses[] = "`b`.`checkin` <= {$max_ts}";
3458 }
3459
3460 if (is_bool($closures)) {
3461 // include or exclude closures
3462 if ($closures) {
3463 $clauses[] = "`o`.`closure` = 1";
3464 } else {
3465 $clauses[] = "`o`.`closure` != 1";
3466 }
3467 }
3468
3469 $dbo = JFactory::getDbo();
3470
3471 $q = "SELECT `b`.*, `ob`.`idorder`, `o`.`closure` FROM `#__vikbooking_busy` AS `b`
3472 LEFT JOIN `#__vikbooking_ordersbusy` AS `ob` ON `ob`.`idbusy`=`b`.`id`
3473 LEFT JOIN `#__vikbooking_orders` AS `o` ON `o`.`id`=`ob`.`idorder`
3474 WHERE " . implode(' AND ', $clauses) . ";";
3475 $dbo->setQuery($q);
3476 $allbusy = $dbo->loadAssocList();
3477 if ($allbusy) {
3478 foreach ($allbusy as $br) {
3479 if (!isset($busy[$br['idroom']])) {
3480 $busy[$br['idroom']] = [];
3481 }
3482 $busy[$br['idroom']][] = $br;
3483 }
3484 }
3485
3486 return $busy;
3487 }
3488
3489 /**
3490 * Loads all the busy records by excluding the closures.
3491 * Built for the page dashboard to calculate the actual
3492 * rooms occupancy by excluding the rooms closed.
3493 *
3494 * @param array $roomids the optional list of room IDs to filter.
3495 * @param int $from_ts the optional base date to use.
3496 * @param int $max_ts the optional max date to use.
3497 *
3498 * @return array the list of busy records.
3499 *
3500 * @since 1.12
3501 * @since 1.15.2 (J) - 1.5.5 (WP) changed arguments signature with 3rd arg.
3502 */
3503 public static function loadBusyRecordsUnclosed($roomids = array(), $from_ts = 0, $max_ts = 0)
3504 {
3505 if (!is_array($roomids) || !count($roomids)) {
3506 return [];
3507 }
3508
3509 return self::loadBusyRecords($roomids, $from_ts, $max_ts, false);
3510 }
3511
3512 public static function loadBookingBusyIds($idorder) {
3513 $busy = array();
3514 if (empty($idorder)) {
3515 return $busy;
3516 }
3517 $dbo = JFactory::getDbo();
3518 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$idorder.";";
3519 $dbo->setQuery($q);
3520 $allbusy = $dbo->loadAssocList();
3521 if ($allbusy) {
3522 foreach ($allbusy as $b) {
3523 array_push($busy, $b['idbusy']);
3524 }
3525 }
3526 return $busy;
3527 }
3528
3529 public static function loadLockedRecords($roomids, $ts = 0) {
3530 $dbo = JFactory::getDbo();
3531 $actnow = empty($ts) ? time() : $ts;
3532 $locked = array();
3533 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `until`<" . $actnow . ";";
3534 $dbo->setQuery($q);
3535 $dbo->execute();
3536 if (!is_array($roomids) || !(count($roomids) > 0)) {
3537 return $locked;
3538 }
3539 $check = "SELECT `id`,`idroom`,`checkin`,`realback` FROM `#__vikbooking_tmplock` WHERE `idroom` IN (".implode(', ', $roomids).") AND `until` > ".$actnow.";";
3540 $dbo->setQuery($check);
3541 $all_locked = $dbo->loadAssocList();
3542 if ($all_locked) {
3543 foreach ($all_locked as $kb => $br) {
3544 $locked[$br['idroom']][$kb] = $br;
3545 }
3546 }
3547 return $locked;
3548 }
3549
3550 public static function getRoomBookingsFromBusyIds($idroom, $arr_bids) {
3551 $bookings = [];
3552 if (empty($idroom) || !is_array($arr_bids) || !(count($arr_bids) > 0)) {
3553 return $bookings;
3554 }
3555 $dbo = JFactory::getDbo();
3556 $q = "SELECT `ob`.`idorder`,`ob`.`idbusy` FROM `#__vikbooking_ordersbusy` AS `ob` WHERE `ob`.`idbusy` IN (".implode(',', $arr_bids).") GROUP BY `ob`.`idorder`,`ob`.`idbusy`;";
3557 $dbo->setQuery($q);
3558 $all_booking_ids = $dbo->loadAssocList();
3559 if ($all_booking_ids) {
3560 $oids = array();
3561 foreach ($all_booking_ids as $bid) {
3562 $oids[] = $bid['idorder'];
3563 }
3564 $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` ".
3565 "FROM `#__vikbooking_ordersrooms` AS `or` ".
3566 "LEFT JOIN `#__vikbooking_orders` `o` ON `o`.`id`=`or`.`idorder` ".
3567 "WHERE `or`.`idorder` IN (".implode(',', $oids).") AND `or`.`idroom`=".(int)$idroom." AND `o`.`status`='confirmed' ".
3568 "ORDER BY `o`.`checkout` ASC;";
3569 $dbo->setQuery($q);
3570 $bookings = $dbo->loadAssocList();
3571 if (!$bookings) {
3572 $bookings = [];
3573 }
3574 }
3575 return $bookings;
3576 }
3577
3578 public static function roomBookable($idroom, $units, $first, $second, $skip_busy_ids = [])
3579 {
3580 $dbo = JFactory::getDbo();
3581
3582 if ($units < 1) {
3583 return false;
3584 }
3585
3586 $room_info = self::getRoomInfo($idroom, ['id', 'name', 'units']);
3587 if (!$room_info) {
3588 return false;
3589 }
3590
3591 $secdiff = $second - $first;
3592 $daysdiff = $secdiff / 86400;
3593 if (is_int($daysdiff)) {
3594 if ($daysdiff < 1) {
3595 $daysdiff = 1;
3596 }
3597 } else {
3598 if ($daysdiff < 1) {
3599 $daysdiff = 1;
3600 } else {
3601 $sum = floor($daysdiff) * 86400;
3602 $newdiff = $secdiff - $sum;
3603 $maxhmore = self::getHoursMoreRb() * 3600;
3604 if ($maxhmore >= $newdiff) {
3605 $daysdiff = floor($daysdiff);
3606 } else {
3607 $daysdiff = ceil($daysdiff);
3608 }
3609 }
3610 }
3611
3612 $q = $dbo->getQuery(true)
3613 ->select($dbo->qn([
3614 'id',
3615 'checkin',
3616 'realback',
3617 ]))
3618 ->from($dbo->qn('#__vikbooking_busy'))
3619 ->where($dbo->qn('idroom') . ' = ' . (int)$idroom)
3620 ->where($dbo->qn('realback') . ' >= ' . $first);
3621
3622 $dbo->setQuery($q);
3623 $busy = $dbo->loadAssocList();
3624 if (!$busy) {
3625 return ($units <= $room_info['units']);
3626 }
3627
3628 $groupdays = self::getGroupDays($first, $second, $daysdiff);
3629 foreach ($groupdays as $gday) {
3630 $bfound = 0;
3631 foreach ($busy as $bu) {
3632 if (in_array($bu['id'], $skip_busy_ids)) {
3633 // booking modification
3634 continue;
3635 }
3636 if ($gday >= $bu['checkin'] && $gday <= $bu['realback']) {
3637 $bfound++;
3638 }
3639 }
3640 if ($bfound >= $units) {
3641 return false;
3642 }
3643 }
3644
3645 return ($units <= $room_info['units']);
3646 }
3647
3648 public static function payTotal()
3649 {
3650 return VBOFactory::getConfig()->getBool('paytotal', true);
3651 }
3652
3653 public static function getDepositIfDays()
3654 {
3655 return VBOFactory::getConfig()->getInt('depifdaysadv', 0);
3656 }
3657
3658 public static function depositAllowedDaysAdv($checkints) {
3659 $days_adv = self::getDepositIfDays();
3660 if (!($days_adv > 0) || !($checkints > 0)) {
3661 return true;
3662 }
3663 $now_info = getdate();
3664 $maxts = mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + $days_adv), $now_info['year']);
3665 return $maxts > $checkints ? false : true;
3666 }
3667
3668 public static function depositCustomerChoice() {
3669 $dbo = JFactory::getDbo();
3670 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depcustchoice';";
3671 $dbo->setQuery($q);
3672 $dbo->execute();
3673 $s = $dbo->loadAssocList();
3674 return (intval($s[0]['setting']) == 1 ? true : false);
3675 }
3676
3677 public static function getDepositOverrides($getjson = false) {
3678 $dbo = JFactory::getDbo();
3679 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='depoverrides';";
3680 $dbo->setQuery($q);
3681 $dbo->execute();
3682 if ($dbo->getNumRows() > 0) {
3683 $s = $dbo->loadResult();
3684 return $getjson ? $s : json_decode($s, true);
3685 }
3686 //count of this array will be at least 1 to store the "more" property
3687 $def_arr = array('more' => '');
3688 $def_val = json_encode($def_arr);
3689 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('depoverrides', ".$dbo->quote($def_val).");";
3690 $dbo->setQuery($q);
3691 $dbo->execute();
3692 return $getjson ? $def_val : $def_arr;
3693 }
3694
3695 public static function calcDepositOverride($amount_deposit, $nights) {
3696 $overrides = self::getDepositOverrides();
3697 $nights = intval($nights);
3698 $andmore = intval($overrides['more']);
3699 if (!(count($overrides) > 1)) {
3700 //no overrides
3701 return $amount_deposit;
3702 }
3703 foreach ($overrides as $k => $v) {
3704 if ((int)$k == $nights && strlen($v) > 0) {
3705 //exact override found
3706 return (float)$v;
3707 }
3708 }
3709 if ($andmore > 0 && $andmore <= $nights) {
3710 foreach ($overrides as $k => $v) {
3711 if ((int)$k == $andmore && strlen($v) > 0) {
3712 //"and more" nights found
3713 return (float)$v;
3714 }
3715 }
3716 }
3717 //nothing was found
3718 return $amount_deposit;
3719 }
3720
3721 public static function noDepositForNonRefund() {
3722 $dbo = JFactory::getDbo();
3723 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='nodepnonrefund';";
3724 $dbo->setQuery($q);
3725 $dbo->execute();
3726 if ($dbo->getNumRows() > 0) {
3727 $s = $dbo->loadResult();
3728 return ((int)$s == 1);
3729 }
3730 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('nodepnonrefund', '0');";
3731 $dbo->setQuery($q);
3732 $dbo->execute();
3733 //default to false
3734 return false;
3735 }
3736
3737 /**
3738 * This method returns the room-rate array with the lowest price
3739 * that matches the preferred rate plan parameter (if available).
3740 * The array $website_rates could not be an array, or it could be
3741 * an array with the error string (response from the TACVBO Class).
3742 * The method has been introduced in VBO 1.10 and it's mainly used
3743 * by the module mod_vikbooking_channelrates and its ajax requests.
3744 *
3745 * @param array $website_rates the array of the website rates returned by the method fetchWebsiteRates()
3746 * @param int $def_rplan the id of the default type of price to take for display. If empty, take the lowest rate
3747 *
3748 * @return array
3749 */
3750 public static function getBestRoomRate($website_rates, $def_rplan) {
3751 if (!is_array($website_rates) || !(count($website_rates) > 0) || (is_array($website_rates) && isset($website_rates['e4j.error']))) {
3752 return array();
3753 }
3754 $best_room_rate = array();
3755 foreach ($website_rates as $rid => $tars) {
3756 foreach ($tars as $tar) {
3757 if (empty($def_rplan) || (int)$tar['idprice'] == $def_rplan) {
3758 //the array $website_rates is already sorted by price ASC, so we take the first useful array
3759 $best_room_rate = $tar;
3760 break 2;
3761 }
3762 }
3763 }
3764 if (!(count($best_room_rate) > 0)) {
3765 //the default rate plan is not available if we enter this statement, so we take the first and lowest rate
3766 foreach ($website_rates as $rid => $tars) {
3767 foreach ($tars as $tar) {
3768 $best_room_rate = $tar;
3769 break 2;
3770 }
3771 }
3772 }
3773
3774 return $best_room_rate;
3775 }
3776
3777 /**
3778 * This method returns an array with the details
3779 * of all channels in VCM that supports AV requests,
3780 * and that have at least one room type mapped.
3781 * The method invokes the Logos Class to return details
3782 * about the name and logo URL of the channel.
3783 * The method has been introduced in VBO 1.10 and it's mainly used
3784 * by the module mod_vikbooking_channelrates and its ajax requests.
3785 *
3786 * @param array $channels an array of channel IDs to be mapped on the VCM relations
3787 *
3788 * @return array
3789 */
3790 public static function getChannelsMap($channels) {
3791 if (!is_array($channels) || !(count($channels))) {
3792 return array();
3793 }
3794 $vcm_logos = self::getVcmChannelsLogo('', true);
3795 if (!is_object($vcm_logos)) {
3796 return array();
3797 }
3798 $channels_ids = array();
3799 foreach ($channels as $chid) {
3800 $ichid = (int)$chid;
3801 if ($ichid < 1) {
3802 continue;
3803 }
3804 array_push($channels_ids, $ichid);
3805 }
3806 if (!(count($channels_ids))) {
3807 return array();
3808 }
3809 $dbo = JFactory::getDbo();
3810 $q = "SELECT `idchannel`, `channel` FROM `#__vikchannelmanager_roomsxref` WHERE `idchannel` IN (".implode(', ', $channels_ids).") GROUP BY `idchannel`,`channel` ORDER BY `channel` ASC;";
3811 $dbo->setQuery($q);
3812 $dbo->execute();
3813 if ($dbo->getNumRows() < 1) {
3814 return array();
3815 }
3816 $channels_names = $dbo->loadAssocList();
3817 $channels_map = array();
3818 foreach ($channels_names as $ch) {
3819 $ota_logo_url = $vcm_logos->setProvenience($ch['channel'])->getLogoURL();
3820 $ota_logo_url = $ota_logo_url === false ? '' : $ota_logo_url;
3821 $chdata = array(
3822 'id' => $ch['idchannel'],
3823 'name' => ucwords($ch['channel']),
3824 'logo' => $ota_logo_url
3825 );
3826 array_push($channels_map, $chdata);
3827 }
3828 return $channels_map;
3829 }
3830
3831 /**
3832 * This method returns a string to calculate the rates for the OTAs. Data is taken from the Bulk Rates Cache
3833 * of Vik Channel Manager. The string returned contains the charge/discount operator at the position 0 (+ or -),
3834 * and the percentage char (%) at the last position (if percent). Between the first and last position there is
3835 * the float value. The method has been introduced in VBO 1.10 and it's mainly used by the "channel rates"
3836 * module/widget and its ajax requests.
3837 *
3838 * @param array $best_room_rate array containing a specific tariff returned by getBestRoomRate().
3839 * @param bool $per_channel if true, an associative array will be returned for each channel alteration.
3840 *
3841 * @return mixed string (empty or indicating the alteration), or associative array per channel.
3842 *
3843 * @since 1.15.0 (J) - 1.5.0 (WP) added support for VCM different alterations per channel.
3844 */
3845 public static function getOtasRatesVal($best_room_rate, $per_channel = false)
3846 {
3847 $otas_rates_val = '';
3848 if (!is_array($best_room_rate) || !count($best_room_rate) || !is_file(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
3849 return $otas_rates_val;
3850 }
3851 if (!class_exists('VikChannelManager')) {
3852 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
3853 }
3854 $bulk_rates_cache = VikChannelManager::getBulkRatesCache();
3855 if (count($bulk_rates_cache) && isset($best_room_rate['idprice'])) {
3856 if (isset($bulk_rates_cache[$best_room_rate['idroom']]) && isset($bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']])) {
3857 // the Bulk Rates Cache contains data for this room type and rate plan
3858 $data_cont = $bulk_rates_cache[$best_room_rate['idroom']][$best_room_rate['idprice']];
3859 // build rates modification string
3860 if ((int)$data_cont['rmod'] > 0) {
3861 // rates were modified for the OTAs, check how
3862 $rmodop = (int)$data_cont['rmodop'] > 0 ? '+' : '-';
3863 $rmodpcent = (int)$data_cont['rmodval'] > 0 ? '%' : '';
3864 $otas_rates_val = $rmodop.(float)$data_cont['rmodamount'].$rmodpcent;
3865 // check alterations per channel, if requested
3866 if ($per_channel && !empty($data_cont['rmod_channels']) && is_array($data_cont['rmod_channels'])) {
3867 // we compose an associative array with the alteration for each channel (0 = generic rule)
3868 $channel_alterations = array($otas_rates_val);
3869 foreach ($data_cont['rmod_channels'] as $ch_id => $ch_mods) {
3870 if ((int)$ch_mods['rmod'] > 0) {
3871 // custom rate alteration for this channel
3872 $rmodop = (int)$ch_mods['rmodop'] > 0 ? '+' : '-';
3873 $rmodpcent = (int)$ch_mods['rmodval'] > 0 ? '%' : '';
3874 $ch_alter_string = $rmodop . (float)$ch_mods['rmodamount'] . $rmodpcent;
3875 } else {
3876 // no rates alteration for this channel identifier
3877 $ch_alter_string = '+0%';
3878 }
3879 $channel_alterations[$ch_id] = $ch_alter_string;
3880 }
3881 // overwrite return value with associative array
3882 $otas_rates_val = $channel_alterations;
3883 }
3884 }
3885 }
3886 }
3887
3888 return $otas_rates_val;
3889 }
3890
3891 /**
3892 * This method checks if some non-refundable rates were selected
3893 * (`free_cancellation`=0), the only argument is an array of tariffs.
3894 * The property 'idprice' must be defined on each sub-array.
3895 *
3896 * @param $tars array
3897 *
3898 * @return boolean
3899 **/
3900 public static function findNonRefundableRates($tars = [])
3901 {
3902 $id_prices = [];
3903
3904 foreach ($tars as $tar) {
3905 if (isset($tar['idprice'])) {
3906 if (!in_array($tar['idprice'], $id_prices)) {
3907 array_push($id_prices, (int)$tar['idprice']);
3908 }
3909 continue;
3910 }
3911 foreach ($tar as $t) {
3912 if (isset($t['idprice'])) {
3913 if (!in_array($t['idprice'], $id_prices)) {
3914 array_push($id_prices, (int)$t['idprice']);
3915 }
3916 }
3917 }
3918 }
3919
3920 if (!count($id_prices)) {
3921 // no price IDs found (probably a package)
3922 return false;
3923 }
3924
3925 $dbo = JFactory::getDbo();
3926
3927 $q = "SELECT `id`,`name` FROM `#__vikbooking_prices` WHERE `id` IN (".implode(', ', $id_prices).") AND `free_cancellation`=0;";
3928 $dbo->setQuery($q);
3929 $dbo->execute();
3930
3931 return (bool)($dbo->getNumRows() > 0);
3932 }
3933
3934 /**
3935 * This method checks if the deposit is allowed depending on
3936 * the selected rate plans (idprice) for the rooms reserved.
3937 * If the configuration setting is enabled, and if some
3938 * non-refundable rates were selected (`free_cancellation`=0),
3939 * the method will return false, because the deposit is not allowed.
3940 * The only argument is an array of tariffs. The property 'idprice'
3941 * must be defined on each sub-array (multi-dimension supported)
3942 * throgh the method findNonRefundableRates();
3943 *
3944 * @param $tars array
3945 *
3946 * @return boolean
3947 **/
3948 public static function allowDepositFromRates($tars) {
3949 if (!self::noDepositForNonRefund()) {
3950 //deposit can be paid also if non-refundable rates
3951 return true;
3952 }
3953 return !self::findNonRefundableRates($tars);
3954 }
3955
3956 public static function showSearchSuggestions() {
3957 $dbo = JFactory::getDbo();
3958 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='searchsuggestions';";
3959 $dbo->setQuery($q);
3960 $dbo->execute();
3961 if ($dbo->getNumRows() > 0) {
3962 return (int)$dbo->loadResult();
3963 }
3964 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('searchsuggestions', '1');";
3965 $dbo->setQuery($q);
3966 $dbo->execute();
3967 return 1;
3968 }
3969
3970 /**
3971 * Given a code, returns the coupon information array.
3972 *
3973 * @param string $code the coupon code.
3974 *
3975 * @return array empty array or coupon record.
3976 *
3977 * @since 1.16.0 (J) - 1.6.0 (WP) the customers assigned are also returned.
3978 */
3979 public static function getCouponInfo($code)
3980 {
3981 $dbo = JFactory::getDbo();
3982
3983 $q = "SELECT * FROM `#__vikbooking_coupons` WHERE `code`=".$dbo->quote($code);
3984 $dbo->setQuery($q, 0, 1);
3985 $coupon = $dbo->loadAssoc();
3986 if (!$coupon) {
3987 return [];
3988 }
3989
3990 // check for customers assigned
3991 $q = "SELECT `idcustomer`, `automatic` FROM `#__vikbooking_customers_coupons` WHERE `idcoupon`=" . (int)$coupon['id'];
3992 $dbo->setQuery($q);
3993 $coupon_customers = $dbo->loadAssocList();
3994 if ($coupon_customers) {
3995 // set customers and automatic properties
3996 $coupon['customers'] = [];
3997 $coupon['automatic'] = $coupon_customers[0]['automatic'];
3998 foreach ($coupon_customers as $coupon_customer) {
3999 // push customer ID
4000 $coupon['customers'][] = $coupon_customer['idcustomer'];
4001 }
4002 }
4003
4004 return $coupon;
4005 }
4006
4007 public static function getRoomInfo($idroom, $columns = [])
4008 {
4009 static $room_infos = [];
4010
4011 if (isset($room_infos[$idroom])) {
4012 return $room_infos[$idroom];
4013 }
4014
4015 $dbo = JFactory::getDbo();
4016
4017 if ($columns) {
4018 $columns = array_map([$dbo, 'qn'], $columns);
4019 }
4020
4021 $q = $dbo->getQuery(true)
4022 ->select($columns ? $columns : '*')
4023 ->from($dbo->qn('#__vikbooking_rooms'))
4024 ->where($dbo->qn('id') . ' = ' . (int)$idroom);
4025
4026 $dbo->setQuery($q);
4027 $room = $dbo->loadAssoc();
4028
4029 $room_infos[$idroom] = $room ? $room : [];
4030
4031 return $room_infos[$idroom];
4032 }
4033
4034 public static function loadOrdersRoomsData($idorder)
4035 {
4036 $dbo = JFactory::getDbo();
4037
4038 $q = $dbo->getQuery(true)
4039 ->select($dbo->qn('or') . '.*')
4040 ->select($dbo->qn('r.name', 'room_name'))
4041 ->select($dbo->qn('r.params'))
4042 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
4043 ->leftJoin($dbo->qn('#__vikbooking_rooms', 'r') . ' ON ' . $dbo->qn('or.idroom') . ' = ' . $dbo->qn('r.id'))
4044 ->where($dbo->qn('or.idorder') . ' = ' . (int)$idorder)
4045 ->order($dbo->qn('or.id') . ' ASC');
4046
4047 $dbo->setQuery($q);
4048
4049 return $dbo->loadAssocList();
4050 }
4051
4052 public static function sayCategory($ids, $vbo_tn = null)
4053 {
4054 $dbo = JFactory::getDbo();
4055 $split = explode(";", $ids);
4056 $say = "";
4057 foreach ($split as $k => $s) {
4058 if (!strlen($s)) {
4059 continue;
4060 }
4061 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . (int)$s . ";";
4062 $dbo->setQuery($q);
4063 $nam = $dbo->loadAssocList();
4064 if (!$nam) {
4065 continue;
4066 }
4067 if (is_object($vbo_tn)) {
4068 $vbo_tn->translateContents($nam, '#__vikbooking_categories');
4069 }
4070 $say .= $nam[0]['name'];
4071 $say .= (strlen($split[($k +1)]) && end($split) != $s ? ", " : "");
4072 }
4073 return $say;
4074 }
4075
4076 public static function getRoomCaratOriz($idc, $vbo_tn = null)
4077 {
4078 $dbo = JFactory::getDbo();
4079 $split = explode(";", $idc);
4080 $carat = "";
4081 $arr = array();
4082 $where = array();
4083 foreach ($split as $s) {
4084 if (!empty($s)) {
4085 $where[] = $s;
4086 }
4087 }
4088 if ($where) {
4089 $q = "SELECT `c`.* FROM `#__vikbooking_characteristics` AS `c` WHERE `c`.`id` IN (" . implode(",", $where) . ") ORDER BY `c`.`ordering` ASC;";
4090 $dbo->setQuery($q);
4091 $arr = $dbo->loadAssocList();
4092 if ($arr && is_object($vbo_tn)) {
4093 $vbo_tn->translateContents($arr, '#__vikbooking_characteristics');
4094 }
4095 }
4096 if ($arr) {
4097 $carat .= "<div class=\"vbo-room-carats\">\n";
4098 foreach ($arr as $a) {
4099 if (!empty($a['textimg'])) {
4100 //tooltip icon text is not empty
4101 if (!empty($a['icon'])) {
4102 //an icon has been uploaded: display the image
4103 $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";
4104 } else {
4105 if (strpos($a['textimg'], '</i>') !== false) {
4106 //the tooltip icon text is a font-icon, we can use the name as tooltip
4107 $carat .= "<span class=\"vbo-room-carat\"><span class=\"vbo-expl\" data-vbo-expl=\"".$a['name']."\">".$a['textimg']."</span></span>\n";
4108 } else {
4109 //display just the text
4110 $carat .= "<span class=\"vbo-room-carat\">".$a['textimg']."</span>\n";
4111 }
4112 }
4113 } else {
4114 $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");
4115 }
4116 }
4117 $carat .= "</div>\n";
4118 }
4119 return $carat;
4120 }
4121
4122 public static function getRoomOptionals($idopts, $vbo_tn = null)
4123 {
4124 $dbo = JFactory::getDbo();
4125 $split = explode(";", $idopts);
4126 $fetch = array();
4127 foreach ($split as $s) {
4128 if (!empty($s)) {
4129 $fetch[] = $s;
4130 }
4131 }
4132 if ($fetch) {
4133 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id` IN (".implode(", ", $fetch).") ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
4134 $dbo->setQuery($q);
4135 $arr = $dbo->loadAssocList();
4136 if ($arr) {
4137 if (is_object($vbo_tn)) {
4138 $vbo_tn->translateContents($arr, '#__vikbooking_optionals');
4139 }
4140 return $arr;
4141 }
4142 }
4143 return [];
4144 }
4145
4146 public static function getSingleOption($idopt, $vbo_tn = null)
4147 {
4148 $dbo = JFactory::getDbo();
4149 $opt = array();
4150 if (!empty($idopt)) {
4151 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$idopt . ";";
4152 $dbo->setQuery($q);
4153 $opt = $dbo->loadAssoc();
4154 if ($opt && is_object($vbo_tn)) {
4155 $vbo_tn->translateContents($opt, '#__vikbooking_optionals');
4156 }
4157 }
4158 return $opt;
4159 }
4160
4161 /**
4162 * Unsets the options that are not available in the requested dates.
4163 *
4164 * @param array $optionals the array of options passed by reference
4165 * @param int $checkin the timestamp of the check-in date and time
4166 * @param int $checkout the timestamp of the check-out date and time
4167 *
4168 * @return void
4169 *
4170 * @since 1.11
4171 */
4172 public static function filterOptionalsByDate(&$optionals, $checkin, $checkout) {
4173 if (!is_array($optionals) || !count($optionals) || empty($checkin) || empty($checkout)) {
4174 return;
4175 }
4176 foreach ($optionals as $k => $v) {
4177 if (!empty($v['alwaysav'])) {
4178 $dates = explode(';', $v['alwaysav']);
4179 if (empty($dates[0]) || empty($dates[1])) {
4180 continue;
4181 }
4182 // it is sufficient that the check-in is included within the validity dates, we ignore the checkout
4183 if (!($checkin >= $dates[0]) || !($checkin <= $dates[1])) {
4184 unset($optionals[$k]);
4185 }
4186 }
4187 }
4188 if (!count($optionals)) {
4189 // the system requires it to be an empty string
4190 $optionals = '';
4191 }
4192 }
4193
4194 /**
4195 * Unsets the options that are not suited for the requested room party.
4196 *
4197 * @param array $optionals the array of options passed by reference.
4198 * @param int $adults the number of adults in the room party.
4199 * @param int $children the number of children in the room party.
4200 *
4201 * @return void
4202 *
4203 * @since 1.13.5
4204 */
4205 public static function filterOptionalsByParty(&$optionals, $adults, $children) {
4206 if (!is_array($optionals) || !count($optionals) || (empty($adults) && empty($children))) {
4207 return;
4208 }
4209 foreach ($optionals as $k => $v) {
4210 if (empty($v['oparams'])) {
4211 continue;
4212 }
4213 $v['oparams'] = json_decode($v['oparams'], true);
4214 if (!is_array($v['oparams']) || !count($v['oparams']) || (empty($v['oparams']['minguestsnum']) && empty($v['oparams']['maxguestsnum']))) {
4215 continue;
4216 }
4217 if (!empty($v['oparams']['minguestsnum'])) {
4218 // filter by minimum adults/guests
4219 if ($v['oparams']['mingueststype'] == 'adults' && $adults <= $v['oparams']['minguestsnum']) {
4220 // minimum number of adults not sufficient, unset option
4221 unset($optionals[$k]);
4222 } elseif ($v['oparams']['mingueststype'] == 'guests' && ($adults + $children) <= $v['oparams']['minguestsnum']) {
4223 // minimum number of total guests not sufficient, unset option
4224 unset($optionals[$k]);
4225 }
4226 }
4227 if (!empty($v['oparams']['maxguestsnum'])) {
4228 // filter by maximum adults/guests
4229 if ($v['oparams']['maxgueststype'] == 'adults' && $adults >= $v['oparams']['maxguestsnum']) {
4230 // maximum number of adults exceeded, unset option
4231 unset($optionals[$k]);
4232 } elseif ($v['oparams']['maxgueststype'] == 'guests' && ($adults + $children) >= $v['oparams']['maxguestsnum']) {
4233 // maximum number of total guests exceeded, unset option
4234 unset($optionals[$k]);
4235 }
4236 }
4237 }
4238 if (!count($optionals)) {
4239 // the system requires it to be an empty string
4240 $optionals = '';
4241 }
4242 }
4243
4244 public static function getMandatoryTaxesFees($id_rooms, $num_adults, $num_nights) {
4245 $dbo = JFactory::getDbo();
4246 $taxes = 0;
4247 $fees = 0;
4248 $options_data = array();
4249 $id_options = array();
4250 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id` IN (".implode(", ", $id_rooms).");";
4251 $dbo->setQuery($q);
4252 $dbo->execute();
4253 if ($dbo->getNumRows() > 0) {
4254 $assocs = $dbo->loadAssocList();
4255 foreach ($assocs as $opts) {
4256 if (!empty($opts['idopt'])) {
4257 $r_ido = explode(';', rtrim($opts['idopt']));
4258 foreach ($r_ido as $ido) {
4259 if (!empty($ido) && !in_array($ido, $id_options)) {
4260 $id_options[] = $ido;
4261 }
4262 }
4263 }
4264 }
4265 }
4266 if (count($id_options) > 0) {
4267 $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);";
4268 $dbo->setQuery($q);
4269 $dbo->execute();
4270 if ($dbo->getNumRows() > 0) {
4271 $alltaxesfees = $dbo->loadAssocList();
4272 foreach ($alltaxesfees as $tf) {
4273 $realcost = (intval($tf['perday']) == 1 ? ($tf['cost'] * $num_nights) : $tf['cost']);
4274 if (!empty($tf['maxprice']) && $tf['maxprice'] > 0 && $realcost > $tf['maxprice']) {
4275 $realcost = $tf['maxprice'];
4276 }
4277 $realcost = $tf['perperson'] == 1 ? ($realcost * $num_adults) : $realcost;
4278 $realcost = self::sayOptionalsPlusIva($realcost, $tf['idiva']);
4279 if ($tf['is_citytax'] == 1) {
4280 $taxes += $realcost;
4281 } elseif ($tf['is_fee'] == 1) {
4282 $fees += $realcost;
4283 }
4284 $optsett = explode('-', $tf['forceval']);
4285 $options_data[] = $tf['id'].':'.$optsett[0];
4286 }
4287 }
4288 }
4289 return array('city_taxes' => $taxes, 'fees' => $fees, 'options' => $options_data);
4290 }
4291
4292 /**
4293 * From a list of option/extras records, it returns an array with the options/extras
4294 * that do not support age intervals, and options for the children age intervals.
4295 * To be used as list($optionals, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionals).
4296 *
4297 * @param array $optionals the full list of options/extras records.
4298 * @param int $adults the number of adults in the room requested.
4299 * @param int $children the number of children in the room requested.
4300 *
4301 * @return array the filtered regular and children age intervals options.
4302 */
4303 public static function loadOptionAgeIntervals($optionals, $adults = null, $children = null)
4304 {
4305 // container for age intervals
4306 $ageintervals = [];
4307
4308 // regular options should start as an array
4309 if (!is_array($optionals)) {
4310 $optionals = [];
4311 }
4312
4313 // check for the first valid age intervals option
4314 foreach ($optionals as $kopt => $opt) {
4315 if (!empty($opt['ageintervals'])) {
4316 $intervals = explode(';;', $opt['ageintervals']);
4317 foreach ($intervals as $intv) {
4318 if (empty($intv)) {
4319 continue;
4320 }
4321 $parts = explode('_', $intv);
4322 if (count($parts) >= 3) {
4323 // age intervals option found, get the first and only one
4324 $ageintervals = $optionals[$kopt];
4325 break 2;
4326 }
4327 }
4328 }
4329 }
4330
4331 if ($ageintervals) {
4332 /**
4333 * We allow price adjustments for a minimum total guests by overriding the costs
4334 * for each age interval for specific child numbers when the party covers some children.
4335 * For example, when children should pay only starting from the 5th guest (minguestsnum=4),
4336 * in case of a room party for 2 adults and 3 children, only the 3rd child should pay the
4337 * regular fees, while the 1st and second children should have all age intervals to 0.00.
4338 *
4339 * @since 1.13.5
4340 */
4341 $oparams = !empty($ageintervals['oparams']) ? json_decode($ageintervals['oparams'], true) : array();
4342 $oparams = !is_array($oparams) ? array() : $oparams;
4343 $adults = (int)$adults;
4344 $children = (int)$children;
4345 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4346 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4347 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4348 if ($children_to_pay < $children) {
4349 // compose string for free age intervals
4350 $intervals = explode(';;', $ageintervals['ageintervals']);
4351 foreach ($intervals as $k => $v) {
4352 $parts = explode('_', $v);
4353 // set cost to 0 (key = 2)
4354 $parts[2] = '0';
4355 $intervals[$k] = implode('_', $parts);
4356 }
4357 $free_ageintervals = implode(';;', $intervals);
4358 for ($i = 1; $i <= $children; $i++) {
4359 if (($children - $i + 1) <= $children_to_pay) {
4360 // costs for this Nth last child should be regular as this child should pay
4361 $ageintervals['ageintervals_child' . $i] = $ageintervals['ageintervals'];
4362 } else {
4363 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4364 $ageintervals['ageintervals_child' . $i] = $free_ageintervals;
4365 }
4366 }
4367 }
4368 }
4369
4370 // remove age intervals from regular options
4371 foreach ($optionals as $kopt => $opt) {
4372 if ($opt['id'] == $ageintervals['id'] || !empty($opt['ageintervals'])) {
4373 // unset the option of type age intervals from the regular options
4374 unset($optionals[$kopt]);
4375 }
4376 }
4377 }
4378
4379 // return the filtered list of regular options and age intervals options
4380 return [$optionals, $ageintervals];
4381 }
4382
4383 /**
4384 * Returns an array of overrides (if any) for the specific children Nth number in the party.
4385 * This is because some children in the party may not need to pay anything due to the min guests.
4386 *
4387 * @param array $optional the option full record to parse.
4388 * @param int $adults the number of adults in the room requested.
4389 * @param int $children the number of children in the room requested.
4390 *
4391 * @return array associative array with default age intervals and override strings.
4392 *
4393 * @since 1.13.5
4394 */
4395 public static function getOptionIntervalChildOverrides($optional, $adults, $children)
4396 {
4397 if (!is_array($optional)) {
4398 $optional = array();
4399 }
4400
4401 $overrides = array(
4402 'ageintervals' => (!empty($optional['ageintervals']) ? $optional['ageintervals'] : '')
4403 );
4404
4405 $oparams = !empty($optional['oparams']) ? json_decode($optional['oparams'], true) : array();
4406 $oparams = !is_array($oparams) ? array() : $oparams;
4407 $adults = (int)$adults;
4408 $children = (int)$children;
4409
4410 if (!count($oparams)) {
4411 return $overrides;
4412 }
4413
4414 $valid_guesttype = (!empty($oparams['mingueststype']) && $oparams['mingueststype'] == 'guests');
4415 if ($children > 0 && !empty($oparams['minguestsnum']) && $valid_guesttype && ($adults + $children) > $oparams['minguestsnum']) {
4416 $children_to_pay = ($adults + $children) - $oparams['minguestsnum'];
4417 if ($children_to_pay < $children) {
4418 // compose string for free age intervals
4419 $intervals = explode(';;', $optional['ageintervals']);
4420 foreach ($intervals as $k => $v) {
4421 $parts = explode('_', $v);
4422 // set cost to 0 (key = 2)
4423 $parts[2] = '0';
4424 $intervals[$k] = implode('_', $parts);
4425 }
4426 $free_ageintervals = implode(';;', $intervals);
4427 for ($i = 1; $i <= $children; $i++) {
4428 if (($children - $i + 1) <= $children_to_pay) {
4429 // costs for this Nth last child should be regular as this child should pay
4430 $overrides['ageintervals_child' . $i] = $optional['ageintervals'];
4431 } else {
4432 // override costs to 0.00 for this Nth first child since it's covered by the min guests number
4433 $overrides['ageintervals_child' . $i] = $free_ageintervals;
4434 }
4435 }
4436 }
4437 }
4438
4439 return $overrides;
4440 }
4441
4442 /**
4443 * Returns the number/index of the children being parsed given the full list of room options string,
4444 * the current key in the array of the split room options string, the ID of the ageintervals option,
4445 * and the total number of children. This is useful for later applying the cost overrides for the
4446 * children ages through the method getOptionIntervalChildOverrides().
4447 *
4448 * @param string $roptstr plain room option string of "#__vikbooking_ordersrooms".
4449 * @param int $optid the ID of the option of type age intervals to check.
4450 * @param int $roptkey the current position in the loop of the room options string.
4451 * @param int $children the number of children in the room requested.
4452 *
4453 * @return int the position/number of the children being parsed, -1 if not found.
4454 *
4455 * @since 1.13.5
4456 */
4457 public static function getRoomOptionChildNumber($roptstr, $optid, $roptkey, $children)
4458 {
4459 // default to -1 for not found
4460 $child_num = -1;
4461
4462 $valid_opt_counter = 0;
4463
4464 $roptions = explode(";", $roptstr);
4465 foreach ($roptions as $k => $opt) {
4466 if (empty($opt)) {
4467 continue;
4468 }
4469 $optvals = explode(":", $opt);
4470 /**
4471 * In some cases, like the "saveorder" task, the room option string may contain
4472 * the room number beside the option ID, separated with an underscore.
4473 * If an underscore is present, we need to split it to find the option ID.
4474 */
4475 if (strpos($optvals[0], '_') !== false) {
4476 // underscore found in room number portion, extract the option ID
4477 $rn_parts = explode('_', $optvals[0]);
4478 // 0th element is the room number in the party, 1st elem is the option ID
4479 $optvals[0] = $rn_parts[1];
4480 }
4481 //
4482 if ((int)$optvals[0] != (int)$optid) {
4483 // we are not interested into this option ID
4484 continue;
4485 }
4486 // increase counter for this option ID
4487 $valid_opt_counter++;
4488
4489 if ((int)$k == (int)$roptkey && $valid_opt_counter <= $children) {
4490 // children position found, we need to return it as a 0th base
4491 $child_num = ($valid_opt_counter - 1);
4492 break;
4493 }
4494 }
4495
4496 return $child_num;
4497 }
4498
4499 public static function getOptionIntervalsCosts($intvstr) {
4500 $optcosts = array();
4501 $intervals = explode(';;', $intvstr);
4502 foreach ($intervals as $kintv => $intv) {
4503 if (empty($intv)) continue;
4504 $parts = explode('_', $intv);
4505 if (count($parts) >= 3) {
4506 $optcosts[$kintv] = (float)$parts[2];
4507 }
4508 }
4509 return $optcosts;
4510 }
4511
4512 public static function getOptionIntervalsAges($intvstr) {
4513 $optages = array();
4514 $intervals = explode(';;', $intvstr);
4515 foreach ($intervals as $kintv => $intv) {
4516 if (empty($intv)) continue;
4517 $parts = explode('_', $intv);
4518 if (count($parts) >= 3) {
4519 $optages[$kintv] = $parts[0].' - '.$parts[1];
4520 }
4521 }
4522 return $optages;
4523 }
4524
4525 public static function getOptionIntervalsPercentage($intvstr) {
4526 /* returns an associative array to tell whether an interval has a percentage cost (VBO 1.8) */
4527 $optcostspcent = array();
4528 $intervals = explode(';;', $intvstr);
4529 foreach ($intervals as $kintv => $intv) {
4530 if (empty($intv)) continue;
4531 $parts = explode('_', $intv);
4532 if (count($parts) >= 3) {
4533 //fixed amount
4534 $setval = 0;
4535 if (array_key_exists(3, $parts) && strpos($parts[3], '%b') !== false) {
4536 //percentage value of the room base cost (VBO 1.10)
4537 $setval = 2;
4538 } elseif (array_key_exists(3, $parts) && strpos($parts[3], '%') !== false) {
4539 //percentage value of the adults tariff
4540 $setval = 1;
4541 }
4542 $optcostspcent[$kintv] = $setval;
4543 }
4544 }
4545 return $optcostspcent;
4546 }
4547
4548 public static function dayValidTs($days, $first, $second)
4549 {
4550 $secdiff = $second - $first;
4551 $daysdiff = $secdiff / 86400;
4552 if (is_int($daysdiff)) {
4553 if ($daysdiff < 1) {
4554 $daysdiff = 1;
4555 }
4556 } else {
4557 if ($daysdiff < 1) {
4558 $daysdiff = 1;
4559 } else {
4560 $sum = floor($daysdiff) * 86400;
4561 $newdiff = $secdiff - $sum;
4562 $maxhmore = self::getHoursMoreRb() * 3600;
4563 if ($maxhmore >= $newdiff) {
4564 $daysdiff = floor($daysdiff);
4565 } else {
4566 $daysdiff = ceil($daysdiff);
4567 }
4568 }
4569 }
4570
4571 return ($daysdiff == $days);
4572 }
4573
4574 /**
4575 * Calculates the stay fee inclusive of taxes.
4576 *
4577 * @param float $cost the cost to evaluate.
4578 * @param int $idprice the rate plan ID associated.
4579 *
4580 * @return float the given cost after tax, if taxes had to be applied.
4581 */
4582 public static function sayCostPlusIva($cost, $idprice)
4583 {
4584 $ivainclusa = self::ivaInclusa();
4585 if ($ivainclusa) {
4586 return $cost;
4587 }
4588
4589 static $tax_rplans_map = [];
4590
4591 if (array_key_exists($idprice, $tax_rplans_map)) {
4592 $tax_rate = $tax_rplans_map[$idprice];
4593 } else {
4594 $dbo = JFactory::getDbo();
4595 $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 . ";";
4596 $dbo->setQuery($q);
4597 $tax_rate = $dbo->loadAssoc();
4598
4599 // cache value
4600 $tax_rplans_map[$idprice] = $tax_rate;
4601 }
4602
4603 if (!$tax_rate || empty($tax_rate['aliq'])) {
4604 return $cost;
4605 }
4606
4607 $subt = 100 + $tax_rate['aliq'];
4608 $op = ($cost * $subt / 100);
4609
4610 /**
4611 * Tax Cap implementation for prices tax excluded (most common).
4612 *
4613 * @since 1.12
4614 */
4615 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4616 $op = ($cost + $tax_rate['taxcap']);
4617 }
4618
4619 // apply rounding to avoid issues with multiple tax rates when tax excluded
4620 $formatvals = self::getNumberFormatData();
4621 $formatparts = explode(':', $formatvals);
4622 $rounded_op = round($op, (int)$formatparts[0]);
4623 if ($rounded_op > $op) {
4624 return $rounded_op;
4625 }
4626
4627 /**
4628 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4629 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4630 *
4631 * @since 1.11.1
4632 */
4633 return round($op, (int)$formatparts[0]);
4634 }
4635
4636 /**
4637 * Calculates the stay fee exclusive of taxes.
4638 *
4639 * @param float $cost the cost to evaluate.
4640 * @param int $idprice the rate plan ID associated.
4641 *
4642 * @return float the given cost before tax.
4643 */
4644 public static function sayCostMinusIva($cost, $idprice)
4645 {
4646 $ivainclusa = self::ivaInclusa();
4647 if (!$ivainclusa) {
4648 return $cost;
4649 }
4650
4651 static $tax_rplans_map = [];
4652
4653 if (array_key_exists($idprice, $tax_rplans_map)) {
4654 $tax_rate = $tax_rplans_map[$idprice];
4655 } else {
4656 $dbo = JFactory::getDbo();
4657 $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 . ";";
4658 $dbo->setQuery($q);
4659 $tax_rate = $dbo->loadAssoc();
4660
4661 // cache value
4662 $tax_rplans_map[$idprice] = $tax_rate;
4663 }
4664
4665 if (!$tax_rate || empty($tax_rate['aliq'])) {
4666 return $cost;
4667 }
4668
4669 $subt = 100 + $tax_rate['aliq'];
4670 $op = ($cost * 100 / $subt);
4671
4672 /**
4673 * Tax Cap implementation also when prices tax included.
4674 *
4675 * @since 1.12
4676 */
4677 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4678 $op = ($cost - $tax_rate['taxcap']);
4679 }
4680
4681 // apply rounding to avoid issues with multiple tax rates when tax included
4682 $formatvals = self::getNumberFormatData();
4683 $formatparts = explode(':', $formatvals);
4684 $rounded_op = round($op, (int)$formatparts[0]);
4685 if ($rounded_op < $op) {
4686 return $rounded_op;
4687 }
4688
4689 /**
4690 * When using base costs with decimals, and no tax rates assigned, maybe for a foreigners rate plan,
4691 * no rounding is ever made, and so we should always apply such rounding to avoid getting decimals if they should be 0.
4692 *
4693 * @since 1.11.1
4694 */
4695 return round($op, (int)$formatparts[0]);
4696 }
4697
4698 /**
4699 * Given an option/extra cost, determines whether taxes should be applied over it.
4700 * Can also be used to calculate taxes on the extra costs per room in the bookings
4701 *
4702 * @param float $cost the cost to evaluate, option or extra service fee.
4703 * @param int $idiva the ID of the tax rate to use to apply taxes.
4704 * @param bool $force if true, taxes will always be applied.
4705 *
4706 * @return float the given option/extra cost plus tax, if taxes had to be applied.
4707 *
4708 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4709 */
4710 public static function sayOptionalsPlusIva($cost, $idiva, $force = false)
4711 {
4712 $ivainclusa = self::ivaInclusa();
4713 if ($ivainclusa && $force !== true) {
4714 return $cost;
4715 }
4716
4717 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4718 if (!$tax_rate) {
4719 return $cost;
4720 }
4721
4722 $subt = 100 + $tax_rate['aliq'];
4723 $op = ($cost * $subt / 100);
4724
4725 /**
4726 * Tax Cap implementation for prices tax excluded (most common).
4727 *
4728 * @since 1.12
4729 */
4730 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4731 $op = ($cost + $tax_rate['taxcap']);
4732 }
4733
4734 // apply rounding to avoid issues with multiple tax rates when tax excluded
4735 $formatvals = self::getNumberFormatData();
4736 $formatparts = explode(':', $formatvals);
4737 $rounded_op = round($op, (int)$formatparts[0]);
4738 if ($rounded_op > $op) {
4739 return $rounded_op;
4740 }
4741
4742 return $op;
4743 }
4744
4745 /**
4746 * Calculates the net amount for an option/extra service.
4747 *
4748 * @param float $cost the cost to evaluate, option or extra service fee.
4749 * @param int $idiva the ID of the tax rate to use to apply taxes.
4750 *
4751 * @return float the given option/extra cost before tax, if taxes were inclusive.
4752 */
4753 public static function sayOptionalsMinusIva($cost, $idiva)
4754 {
4755 if (empty($idiva)) {
4756 return $cost;
4757 }
4758
4759 $ivainclusa = self::ivaInclusa();
4760 if (!$ivainclusa) {
4761 return $cost;
4762 }
4763
4764 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4765 if (!$tax_rate) {
4766 return $cost;
4767 }
4768
4769 $subt = 100 + $tax_rate['aliq'];
4770 $op = ($cost * 100 / $subt);
4771
4772 /**
4773 * Tax Cap implementation also when prices tax included.
4774 *
4775 * @since 1.12
4776 */
4777 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4778 $op = ($cost - $tax_rate['taxcap']);
4779 }
4780
4781 // apply rounding to avoid issues with multiple tax rates when tax included
4782 $formatvals = self::getNumberFormatData();
4783 $formatparts = explode(':', $formatvals);
4784 $rounded_op = round($op, (int)$formatparts[0]);
4785 if ($rounded_op < $op) {
4786 return $rounded_op;
4787 }
4788
4789 return $op;
4790 }
4791
4792 /**
4793 * Given a cost, determines whether taxes should be applied over it.
4794 *
4795 * @param float $cost the cost to evaluate, could be a room custom price.
4796 * @param int $idiva the ID of the tax rate to use to apply taxes.
4797 * @param bool $force if true, taxes will always be applied.
4798 *
4799 * @return float the given cost plus tax, if taxes had to be applied.
4800 *
4801 * @since 1.15.0 (J) - 1.5.0 (WP) added third argument $force to comply with VCM.
4802 */
4803 public static function sayPackagePlusIva($cost, $idiva, $force = false)
4804 {
4805 $ivainclusa = self::ivaInclusa();
4806 if ($ivainclusa && $force !== true) {
4807 return $cost;
4808 }
4809
4810 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4811 if (!$tax_rate) {
4812 return $cost;
4813 }
4814
4815 $subt = 100 + $tax_rate['aliq'];
4816 $op = ($cost * $subt / 100);
4817
4818 /**
4819 * Tax Cap implementation for prices tax excluded (most common).
4820 *
4821 * @since 1.12
4822 */
4823 if ($tax_rate['taxcap'] > 0 && ($op - $cost) > $tax_rate['taxcap']) {
4824 $op = ($cost + $tax_rate['taxcap']);
4825 }
4826
4827 // apply rounding to avoid issues with multiple tax rates when tax excluded
4828 $formatvals = self::getNumberFormatData();
4829 $formatparts = explode(':', $formatvals);
4830 $rounded_op = round($op, (int)$formatparts[0]);
4831 if ($rounded_op > $op) {
4832 return $rounded_op;
4833 }
4834
4835 return $op;
4836 }
4837
4838 /**
4839 * Calculates the net amount for a package.
4840 *
4841 * @param float $cost the cost to evaluate, option or extra service fee.
4842 * @param int $idiva the ID of the tax rate to use to apply taxes.
4843 * @param bool $force_invoice_excltax whether to force the deduction of taxes.
4844 *
4845 * @return float the given cost before tax.
4846 */
4847 public static function sayPackageMinusIva($cost, $idiva, $force_invoice_excltax = false)
4848 {
4849 $ivainclusa = self::ivaInclusa();
4850 if (!$ivainclusa && $force_invoice_excltax !== true) {
4851 return $cost;
4852 }
4853
4854 $tax_rate = VBOTaxonomySummary::getTaxRateRecord($idiva);
4855 if (!$tax_rate) {
4856 return $cost;
4857 }
4858
4859 $subt = 100 + $tax_rate['aliq'];
4860 $op = ($cost * 100 / $subt);
4861
4862 /**
4863 * Tax Cap implementation also when prices tax included.
4864 *
4865 * @since 1.12
4866 */
4867 if ($tax_rate['taxcap'] > 0 && ($cost - $op) > $tax_rate['taxcap']) {
4868 $op = ($cost - $tax_rate['taxcap']);
4869 }
4870
4871 // apply rounding to avoid issues with multiple tax rates when tax included
4872 $formatvals = self::getNumberFormatData();
4873 $formatparts = explode(':', $formatvals);
4874 $rounded_op = round($op, (int)$formatparts[0]);
4875 if ($rounded_op < $op) {
4876 return $rounded_op;
4877 }
4878
4879 return $op;
4880 }
4881
4882 public static function getSecretLink()
4883 {
4884 $dbo = JFactory::getDbo();
4885
4886 $sid = mt_rand();
4887
4888 $q = "SELECT `sid` FROM `#__vikbooking_orders`;";
4889 $dbo->setQuery($q);
4890 $all = $dbo->loadAssocList();
4891 if ($all) {
4892 $list = [];
4893 foreach ($all as $s) {
4894 $list[] = $s['sid'];
4895 }
4896 if (in_array($sid, $list)) {
4897 while(in_array($sid, $list)) {
4898 $sid++;
4899 }
4900 }
4901 }
4902
4903 return $sid;
4904 }
4905
4906 /**
4907 * Generates a confirmation number for the given booking.
4908 * Modified the way random string-numbers are obtained to
4909 * avoid any possible brute-force attempt.
4910 *
4911 * @since 1.15.1 (J) - 1.5.4 (WP)
4912 */
4913 public static function generateConfirmNumber($oid, $update = true)
4914 {
4915 $dbo = JFactory::getDbo();
4916
4917 $confirmnumb = date('y') . $oid;
4918 for ($i = 0; $i < 8; $i++) {
4919 $confirmnumb .= rand(0, 9);
4920 }
4921
4922 if ($update) {
4923 $q = "UPDATE `#__vikbooking_orders` SET `confirmnumber`=" . $dbo->quote($confirmnumb) . " WHERE `id`=" . (int)$oid . ";";
4924 $dbo->setQuery($q);
4925 $dbo->execute();
4926 }
4927
4928 return $confirmnumb;
4929 }
4930
4931 public static function buildCustData($arr, $sep) {
4932 $cdata = "";
4933 foreach ($arr as $k => $e) {
4934 if (strlen($e)) {
4935 $cdata .= (strlen($k) > 0 ? $k . ": " : "") . $e . $sep;
4936 }
4937 }
4938 return $cdata;
4939 }
4940
4941 /**
4942 * This method parses the Joomla menu object to see if a menu item of a
4943 * specific type is available, to get its ID. Useful when links should be
4944 * displayed in pages where there is no Itemid set (booking details pages).
4945 *
4946 * @param array $viewtypes list of accepted menu items
4947 * @param string $lang the optional language to use.
4948 *
4949 * @return int
4950 *
4951 * @since 1.15.6 (J) - 1.5.12 (WP) added second argument $lang.
4952 */
4953 public static function findProperItemIdType($viewtypes, $lang = null)
4954 {
4955 if (VBOPlatformDetection::isWordPress()) {
4956 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
4957
4958 $itemid = $model->best($viewtypes, $lang);
4959
4960 if ($itemid) {
4961 return $itemid;
4962 }
4963
4964 return 0;
4965 }
4966
4967 $bestitemid = 0;
4968
4969 $current_lang = !empty($lang) ? $lang : JFactory::getLanguage()->getTag();
4970
4971 $app = JFactory::getApplication();
4972
4973 $menu = $app->getMenu('site');
4974
4975 if (!$menu) {
4976 return 0;
4977 }
4978
4979 $menu_items = $menu->getMenu();
4980
4981 if (!$menu_items) {
4982 return 0;
4983 }
4984
4985 foreach ($menu_items as $itemid => $item) {
4986 if (isset($item->query['option']) && $item->query['option'] == 'com_vikbooking' && in_array($item->query['view'], $viewtypes)) {
4987 // proper menu item type found
4988 $bestitemid = empty($bestitemid) ? $itemid : $bestitemid;
4989
4990 if (isset($item->language) && $item->language == $current_lang) {
4991 // we found the exact menu item type for the given language
4992 return $itemid;
4993 }
4994 }
4995 }
4996
4997 return $bestitemid;
4998 }
4999
5000 /**
5001 * Rewrites an internal URI that needs to be used outside of the website.
5002 * This means that the routed URI MUST start with the base path of the site.
5003 *
5004 * @param mixed $query The query string or an associative array of data.
5005 * @param boolean $xhtml Replace & by &amp; for XML compliance.
5006 * @param mixed $itemid The itemid to use. If null, the current one will be used.
5007 *
5008 * @return string The complete routed URI.
5009 *
5010 * @since 1.15.0 (J) - 1.5.0 (WP) adopted use of VBOPlatformUriAware, which also supports
5011 * routing from back-end (if available on the CMS version).
5012 */
5013 public static function externalroute($query = '', $xhtml = true, $itemid = null)
5014 {
5015 return VBOFactory::getPlatform()->getUri()->route($query, $xhtml, $itemid);
5016 }
5017
5018 /**
5019 * Generates an iCal file to be attached to the email message for the
5020 * customer or the administrator with some basic booking details.
5021 *
5022 * @param string $recip either admin or customer.
5023 * @param array $booking the booking array or some keys.
5024 *
5025 * @return mixed string in case of success, false otherwise.
5026 *
5027 * @since 1.12.0
5028 */
5029 public static function getEmailIcal($recip, $booking) {
5030 // load configuration setting
5031 $attachical = self::attachIcal();
5032
5033 if ($attachical === 0) {
5034 // do not attach any iCal file
5035 return false;
5036 }
5037
5038 if ($attachical === 2 && strpos($recip, 'admin') === false) {
5039 // skip the iCal for the admin
5040 return false;
5041 }
5042
5043 if ($attachical === 3 && strpos($recip, 'admin') !== false) {
5044 // skip the iCal for the customer
5045 return false;
5046 }
5047
5048 if (strpos($recip, 'admin') !== false) {
5049 // prepare event description and summary for the admin
5050 $description = $booking['custdata'];
5051 $summary = !empty($booking['subject']) ? $booking['subject'] : '';
5052 $fname = $booking['ts'] . '.ics';
5053 } else {
5054 // event description and summary for the customer
5055 $description = '';
5056 $summary = self::getFrontTitle();
5057 $fname = 'reservation_reminder.ics';
5058 }
5059
5060 // prepare iCal head
5061 $company_name = self::getFrontTitle();
5062 $ics_str = "BEGIN:VCALENDAR\r\n" .
5063 "PRODID:-//".$company_name."//".JUri::root()." 1.0//EN\r\n" .
5064 "CALSCALE:GREGORIAN\r\n" .
5065 "VERSION:2.0\r\n";
5066 // compose iCal body
5067 $ics_str .= 'BEGIN:VEVENT'."\r\n";
5068 $ics_str .= 'DTEND;VALUE=DATE:'.date('Ymd', $booking['checkout'])."\r\n";
5069 $ics_str .= 'DTSTART;VALUE=DATE:'.date('Ymd', $booking['checkin'])."\r\n";
5070 $ics_str .= 'UID:'.sha1($booking['ts'])."\r\n";
5071 $ics_str .= 'DESCRIPTION:'.preg_replace('/([\,;])/','\\\$1', $description)."\r\n";
5072 $ics_str .= 'SUMMARY:'.preg_replace('/([\,;])/','\\\$1', $summary)."\r\n";
5073 $ics_str .= 'LOCATION:'.preg_replace('/([\,;])/','\\\$1', $company_name)."\r\n";
5074 $ics_str .= 'END:VEVENT'."\r\n";
5075 // close iCal file content
5076 $ics_str .= "END:VCALENDAR";
5077
5078 /**
5079 * Trigger event to allow third party plugins to overwrite the iCal file.
5080 *
5081 * @since 1.16.0 (J) - 1.6.0 (WP)
5082 */
5083 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateMailIcalVikBooking', [$recip, $booking, &$ics_str]);
5084
5085 if (empty($ics_str)) {
5086 return false;
5087 }
5088
5089 // store the event onto a .ics file. We use the resources folder in back-end.
5090 $fpath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $fname;
5091 $fp = fopen($fpath, 'w+');
5092 $bytes = fwrite($fp, $ics_str);
5093 fclose($fp);
5094
5095 return $bytes ? $fpath : false;
5096 }
5097
5098 public static function loadEmailTemplate($booking_info = array()) {
5099 define('_VIKBOOKINGEXEC', '1');
5100 ob_start();
5101 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "email_tmpl.php";
5102 $content = ob_get_contents();
5103 ob_end_clean();
5104 return $content;
5105 }
5106
5107 /**
5108 * Parses the raw HTML content of the booking email template.
5109 *
5110 * @param string $tmpl the raw content of the template.
5111 * @param mixed $bid int for the booking ID or booking array.
5112 * @param array $rooms list of rooms booked and translated.
5113 * @param array $rates list of translated rates for the booked rooms.
5114 * @param array $options list of translated options booked.
5115 * @param float [$total] the booking total amount (in case it has changed).
5116 * @param string [$link] the booking link can be passed for the no-deposit.
5117 *
5118 * @return string the HTML content of the parsed email template.
5119 *
5120 * @since 1.13 with different arguments.
5121 */
5122 public static function parseEmailTemplate($tmpl, $bid, $rooms, $rates, $options, $total = 0, $link = null)
5123 {
5124 $app = JFactory::getApplication();
5125 $dbo = JFactory::getDbo();
5126 $vbo_tn = self::getTranslator();
5127
5128 // availability helper
5129 $av_helper = self::getAvailabilityInstance();
5130
5131 // get necessary values
5132 if (is_array($bid)) {
5133 // we got the full booking record
5134 $order_info = $bid;
5135 $bid = $order_info['id'];
5136 } else {
5137 $order_info = array();
5138 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5139 $dbo->setQuery($q);
5140 $order_info = $dbo->loadAssoc();
5141 if (!$order_info) {
5142 throw new Exception('Booking not found', 404);
5143 }
5144 }
5145
5146 $tars_info = array();
5147 $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'] . ";";
5148 $dbo->setQuery($q);
5149 $tars_info = $dbo->loadAssocList();
5150 if (!$tars_info) {
5151 throw new Exception('No rooms found', 404);
5152 }
5153
5154 /**
5155 * Split stay reservation.
5156 *
5157 * @since 1.16.0 (J) - 1.6.0 (WP)
5158 */
5159 $room_stay_dates = [];
5160 if ($order_info['split_stay']) {
5161 if ($order_info['status'] == 'confirmed') {
5162 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
5163 } else {
5164 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
5165 }
5166 // immediately count the number of nights of stay for each split room
5167 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5168 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5169 // overwrite values for compatibility with non-confirmed bookings
5170 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5171 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5172 }
5173 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5174 // overwrite the whole array
5175 $room_stay_dates[$sps_r_k] = $sps_r_v;
5176 }
5177 }
5178
5179 // check if the language in use is the same as the one used during the checkout
5180 $lang = JFactory::getLanguage();
5181 if (!empty($order_info['lang'])) {
5182 if ($lang->getTag() != $order_info['lang']) {
5183 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $order_info['lang'], true);
5184 if (VBOPlatformDetection::isJoomla()) {
5185 $lang->load('joomla', JPATH_SITE, $order_info['lang'], true);
5186 }
5187 }
5188 if ($vbo_tn->getDefaultLang() != $order_info['lang']) {
5189 // force the translation to start because contents should be translated
5190 $vbo_tn::$force_tolang = $order_info['lang'];
5191 }
5192 }
5193
5194 // values for replacements
5195 $company_name = self::getFrontTitle();
5196 $currencyname = self::getCurrencyName();
5197 $sitelogo = self::getSiteLogo();
5198 $footermess = self::getFooterOrdMail($vbo_tn);
5199 $dateformat = self::getDateFormat();
5200 $datesep = self::getDateSeparator();
5201 if ($dateformat == "%d/%m/%Y") {
5202 $df = 'd/m/Y';
5203 } elseif ($dateformat == "%m/%d/%Y") {
5204 $df = 'm/d/Y';
5205 } else {
5206 $df = 'Y/m/d';
5207 }
5208 $create_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['ts']);
5209 $checkin_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkin']);
5210 $checkout_date = date(str_replace("/", $datesep, $df) . ' H:i', $order_info['checkout']);
5211 $customer_info = nl2br($order_info['custdata']);
5212 $company_logo = '';
5213 if (!empty($sitelogo) && is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources'. DIRECTORY_SEPARATOR . $sitelogo)) {
5214 $company_logo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $sitelogo . '" alt="' . $company_name . '" />';
5215 }
5216 if ($order_info['status'] == 'cancelled') {
5217 $confirmnumber = '';
5218 $status_str = JText::translate('VBCANCELLED');
5219 } elseif ($order_info['status'] == 'standby') {
5220 $confirmnumber = '';
5221 $status_str = JText::translate('VBWAITINGFORPAYMENT');
5222 if (!empty($order_info['type']) && !strcasecmp($order_info['type'], 'Inquiry')) {
5223 // this is an inquiry reservation
5224 $status_str = JText::translate('VBO_INQUIRY_PENDING');
5225 }
5226 } else {
5227 $confirmnumber = $order_info['confirmnumber'];
5228 $status_str = JText::translate('VBCOMPLETED');
5229 }
5230 // booking total amount
5231 $total = $total === 0 ? (float)$order_info['total'] : (float)$total;
5232 // booking link
5233 $use_sid = !empty($order_info['idorderota']) && !empty($order_info['channel']) ? $order_info['idorderota'] : $order_info['sid'];
5234 if (is_null($link)) {
5235 $lang_link = !empty($order_info['lang']) ? "&lang={$order_info['lang']}" : '';
5236 $link = self::externalroute("index.php?option=com_vikbooking&view=booking&sid={$use_sid}&ts={$order_info['ts']}{$lang_link}", false);
5237 }
5238
5239 // raw HTML content
5240 $parsed = $tmpl;
5241
5242 /**
5243 * Parse all conditional text rules.
5244 *
5245 * @since 1.14 (J) - 1.4.0 (WP)
5246 */
5247 self::getConditionalRulesInstance()
5248 ->set(array('booking', 'rooms'), array($order_info, $rooms))
5249 ->parseTokens($parsed);
5250 //
5251
5252 // special tokens (tags) replacement
5253 $parsed = str_replace("{logo}", $company_logo, $parsed);
5254 $parsed = str_replace("{company_name}", $company_name, $parsed);
5255 $parsed = str_replace("{order_id}", $order_info['id'], $parsed);
5256 $statusclass = $order_info['status'] == 'confirmed' ? "confirmed" : "standby";
5257 $statusclass = $order_info['status'] == 'cancelled' ? "cancelled" : $statusclass;
5258 $parsed = str_replace("{order_status_class}", $statusclass, $parsed);
5259 $parsed = str_replace("{order_status}", $status_str, $parsed);
5260 $parsed = str_replace("{order_date}", $create_date, $parsed);
5261
5262 // PIN Code
5263 if ($order_info['status'] == 'confirmed' && self::customersPinEnabled()) {
5264 $cpin = self::getCPinIstance();
5265 $customer_pin = $cpin->getPinCodeByOrderId($order_info['id']);
5266 if (!empty($customer_pin)) {
5267 $customer_info .= '<h3>'.JText::translate('VBYOURPIN').': '.$customer_pin.'</h3>';
5268 }
5269 }
5270
5271 $parsed = str_replace("{customer_info}", $customer_info, $parsed);
5272 // Confirmation Number
5273 if (strlen($confirmnumber) > 0) {
5274 $parsed = str_replace("{confirmnumb}", $confirmnumber, $parsed);
5275 } else {
5276 $parsed = preg_replace('#('.preg_quote('{confirmnumb_delimiter}').')(.*)('.preg_quote('{/confirmnumb_delimiter}').')#si', '$1'.' '.'$3', $parsed);
5277 }
5278 $parsed = str_replace("{confirmnumb_delimiter}", "", $parsed);
5279 $parsed = str_replace("{/confirmnumb_delimiter}", "", $parsed);
5280
5281 $roomsnum = count($rooms);
5282 $parsed = str_replace("{rooms_count}", $roomsnum, $parsed);
5283 $roomstr = "";
5284 // Rooms Distinctive Features
5285 preg_match_all('/\{roomfeature ([a-zA-Z0-9 ]+)\}/U', $parsed, $matches);
5286 //
5287 foreach ($rooms as $num => $r) {
5288 $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/>";
5289 // Rooms Distinctive Features
5290 if (is_array($matches[1]) && count($matches[1])) {
5291 $distinctive_features = array();
5292 $rparams = (array)json_decode($r['params'], true);
5293 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'])) {
5294 $distinctive_features = $rparams['features'][$r['roomindex']];
5295 }
5296 $docheck = (count($distinctive_features) > 0);
5297 foreach ($matches[1] as $reqf) {
5298 $feature_found = false;
5299 if ($docheck) {
5300 foreach ($distinctive_features as $dfk => $dfv) {
5301 if (stripos($dfk, $reqf) !== false) {
5302 $feature_found = $dfk;
5303 if (strlen(trim($dfk)) == strlen(trim($reqf))) {
5304 break;
5305 }
5306 }
5307 }
5308 }
5309 if ($feature_found !== false && strlen($distinctive_features[$feature_found]) > 0) {
5310 $roomstr .= JText::translate($feature_found).': '.$distinctive_features[$feature_found].'<br/>';
5311 }
5312 $parsed = str_replace("{roomfeature ".$reqf."}", "", $parsed);
5313 }
5314 }
5315 }
5316
5317 // custom fields replace
5318 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
5319 if (is_array($cmatches[1]) && count($cmatches[1])) {
5320 $cfids = array();
5321 foreach ($cmatches[1] as $cfid ) {
5322 $cfids[] = $cfid;
5323 }
5324 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
5325 $dbo->setQuery($q);
5326 $dbo->execute();
5327 $cfields = $dbo->getNumRows() ? $dbo->loadAssocList() : array();
5328 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
5329 $cfmap = array();
5330 foreach ($cfields as $cf) {
5331 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
5332 }
5333 $cfmapreplace = array();
5334 $partsreceived = explode("\n", $order_info['custdata']);
5335 if (count($partsreceived) > 0) {
5336 foreach ($partsreceived as $pst) {
5337 if (!empty($pst)) {
5338 $tmpdata = explode(":", $pst);
5339 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
5340 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
5341 }
5342 }
5343 }
5344 }
5345 foreach ($cmatches[1] as $cfid ) {
5346 if (array_key_exists($cfid, $cfmapreplace)) {
5347 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
5348 } else {
5349 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
5350 }
5351 }
5352 }
5353
5354 $parsed = str_replace("{rooms_info}", $roomstr, $parsed);
5355 $parsed = str_replace("{checkin_date}", $checkin_date, $parsed);
5356 $parsed = str_replace("{checkout_date}", $checkout_date, $parsed);
5357
5358 // order details
5359 $orderdetails = "";
5360 $room_loop_ind = isset($rooms[0]) ? 0 : 1;
5361 foreach ($rooms as $num => $r) {
5362 $room_id = !empty($r['idroom']) ? $r['idroom'] : $r['id'];
5363 $use_room_ind = $num - $room_loop_ind;
5364 $split_stay_str = '';
5365 $split_stay_info = [];
5366 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) {
5367 $split_stay_info[] = $room_stay_dates[$use_room_ind]['nights'] . ' ' . ($room_stay_dates[$use_room_ind]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY'));
5368 $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']);
5369 $split_stay_str .= '<br/>' . implode(', ', $split_stay_info);
5370 }
5371
5372 $expdet = explode("\n", isset($rates[$num]) ? $rates[$num] : '-----');
5373 $faredets = explode(":", $expdet[0]);
5374 $orderdetails .= '<div class="roombooked"><strong>' . $r['name'] . '</strong>' . $split_stay_str . '<br/>' . $faredets[0];
5375 if (!empty($expdet[1])) {
5376 $attrfaredets = explode(":", $expdet[1]);
5377 if (strlen($attrfaredets[1]) > 0) {
5378 $orderdetails .= ' - '.$attrfaredets[0].':'.$attrfaredets[1];
5379 }
5380 }
5381 $fareprice = isset($faredets[1]) ? trim(str_replace($currencyname, "", $faredets[1])) : 0;
5382 $orderdetails .= '<div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($fareprice).'</span></div></div>';
5383 // options
5384 if (isset($options[$num]) && is_array($options[$num]) && count($options[$num]) > 0) {
5385 foreach ($options[$num] as $oo) {
5386 $expopts = explode("\n", $oo);
5387 foreach ($expopts as $optinfo) {
5388 if (!empty($optinfo)) {
5389 $splitopt = explode(":", $optinfo);
5390 $optprice = trim(str_replace($currencyname, "", $splitopt[1]));
5391 $orderdetails .= '<div class="roomoption"><span>'.$splitopt[0].'</span><div class="service-amount" style="float: right;"><span>'.$currencyname.' '.self::numberFormat($optprice).'</span></div></div>';
5392 }
5393 }
5394 }
5395 }
5396 //
5397 if ($roomsnum > 1 && $num < $roomsnum) {
5398 $orderdetails .= '<br/>';
5399 }
5400 }
5401 //
5402 // coupon
5403 if (!empty($order_info['coupon'])) {
5404 $expcoupon = explode(";", $order_info['coupon']);
5405 $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>';
5406 }
5407 //
5408 // discount payment method
5409 if ($order_info['status'] != 'cancelled') {
5410 $idpayment = $order_info['idpayment'];
5411 if (!empty($idpayment)) {
5412 $exppay = explode('=', $idpayment);
5413 $payment = self::getPayment($exppay[0], $vbo_tn);
5414 if (is_array($payment)) {
5415 if ($payment['charge'] > 0.00 && $payment['ch_disc'] != 1) {
5416 // Discount (not charge)
5417 if ($payment['val_pcent'] == 1) {
5418 // fixed value
5419 $total -= $payment['charge'];
5420 $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>';
5421 } else {
5422 // percent value
5423 $percent_disc = $total * $payment['charge'] / 100;
5424 $total -= $percent_disc;
5425 $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>';
5426 }
5427 }
5428 }
5429 }
5430 }
5431 //
5432 $parsed = str_replace("{order_details}", $orderdetails, $parsed);
5433 //
5434 $parsed = str_replace("{order_total}", $currencyname.' '.self::numberFormat($total), $parsed);
5435 $parsed = str_replace("{footer_emailtext}", $footermess, $parsed);
5436 $parsed = str_replace("{order_link}", '<a href="'.$link.'">'.$link.'</a>', $parsed);
5437 $parsed = str_replace("{booking_link}", $link, $parsed);
5438
5439 // deposit
5440 $deposit_str = '';
5441 if (!in_array($order_info['status'], array('confirmed', 'cancelled')) && !self::payTotal() && self::allowDepositFromRates($tars_info)) {
5442 $percentdeposit = self::getAccPerCent();
5443 $percentdeposit = self::calcDepositOverride($percentdeposit, $order_info['days']);
5444 if ($percentdeposit > 0 && self::depositAllowedDaysAdv($order_info['checkin'])) {
5445 if (self::getTypeDeposit() == "fixed") {
5446 $deposit_amount = $percentdeposit;
5447 } else {
5448 $deposit_amount = $total * $percentdeposit / 100;
5449 }
5450 if ($deposit_amount > 0) {
5451 $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>';
5452 }
5453 }
5454 }
5455 $parsed = str_replace("{order_deposit}", $deposit_str, $parsed);
5456 //
5457 // Amount Paid - Remaining Balance - Refunded Amount - Cancellation Fee
5458 $totpaid_str = '';
5459 if ($order_info['refund'] > 0) {
5460 $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>';
5461 }
5462 if ($order_info['status'] != 'cancelled') {
5463 $tot_paid = $order_info['totpaid'];
5464 $diff_topay = (float)$total - (float)$tot_paid;
5465 if ((float)$tot_paid > 0) {
5466 $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>';
5467 // only in case the remaining balance is greater than 1 to avoid commissions issues
5468 if ($diff_topay > 1) {
5469 $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>';
5470 }
5471 }
5472 }
5473 if ($order_info['status'] == 'cancelled' && isset($order_info['canc_fee']) && $order_info['canc_fee'] > 0) {
5474 $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>';
5475 }
5476 $parsed = str_replace("{order_total_paid}", $totpaid_str, $parsed);
5477
5478 /**
5479 * Language direction (LTR or RTL).
5480 *
5481 * @since 1.16.4 (J) - 1.6.4 (WP)
5482 */
5483 $lang_direction = 'ltr';
5484 if (!empty($order_info['lang'])) {
5485 // attempt to define the language direction and replace the special tag
5486 try {
5487 $lang_direction = JLanguage::getInstance($order_info['lang'])->isRtl() ? 'rtl' : 'ltr';
5488 } catch (Throwable $e) {
5489 // do nothing, default to ltr
5490 $lang_direction = 'ltr';
5491 }
5492 // static map for Arabic and Hebrew
5493 $lang_direction = !strcasecmp($order_info['lang'], 'ar') || !strcasecmp($order_info['lang'], 'he') ? 'rtl' : $lang_direction;
5494 }
5495 $parsed = str_replace("{lang_direction}", $lang_direction, $parsed);
5496 $parsed = str_replace("{text_natural_direction}", ($lang_direction == 'ltr' ? 'left' : 'right'), $parsed);
5497 $parsed = str_replace("{text_opposite_direction}", ($lang_direction == 'ltr' ? 'right' : 'left'), $parsed);
5498
5499 /**
5500 * Replace static floating styles depending on language direction (LTR or RTL).
5501 * For BC with older email template sources, we need to keep the inline style
5502 * for the floating of certain elements, which looks good only on LTR.
5503 *
5504 * @since 1.16.4 (J) - 1.6.4 (WP)
5505 */
5506 if ($lang_direction === 'rtl') {
5507 // get rid of inline static style declaration because the template uses CSS classes
5508 $parsed = str_replace("float: right;", '', $parsed);
5509 }
5510
5511 return $parsed;
5512 }
5513
5514 /**
5515 * New method for sending booking email messages
5516 * to the guest or to the administrator(s).
5517 *
5518 * @param int $bid the booking ID.
5519 * @param array $for guest, admin or a custom email address.
5520 * @param bool $send whether to send or return the HTML message.
5521 * @param bool $no_config if true, no configuration settings will be used for the status.
5522 *
5523 * @return mixed True or False depending on the result or HTML string for the preview.
5524 *
5525 * @since 1.13 (J) - 1.3 (WP)
5526 * @since 1.16.3 (J) - 1.6.3 (WP) added fourth argument $no_config to send the email even if
5527 * the reservation status is pending and against the configuration.
5528 */
5529 public static function sendBookingEmail($bid, $for = array(), $send = true, $no_config = false)
5530 {
5531 $dbo = JFactory::getDbo();
5532 $app = JFactory::getApplication();
5533
5534 $vbo_tn = self::getTranslator();
5535 $av_helper = self::getAvailabilityInstance();
5536
5537 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
5538 $dbo->setQuery($q);
5539 $booking = $dbo->loadAssoc();
5540 if (!$booking) {
5541 return false;
5542 }
5543
5544 $result = false;
5545
5546 // check if the language in use is the same as the one used during the checkout
5547 $lang = JFactory::getLanguage();
5548 if (!empty($booking['lang'])) {
5549 if ($lang->getTag() != $booking['lang']) {
5550 $lang->load('com_vikbooking', (defined('VIKBOOKING_LANG') ? VIKBOOKING_LANG : JPATH_SITE), $booking['lang'], true);
5551 if (VBOPlatformDetection::isJoomla()) {
5552 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
5553 }
5554 }
5555 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
5556 // force the translation to start because contents should be translated
5557 $vbo_tn::$force_tolang = $booking['lang'];
5558 }
5559 }
5560
5561 // load rooms booked
5562 $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;";
5563 $dbo->setQuery($q);
5564 $ordersrooms = $dbo->loadAssocList();
5565 if (!$ordersrooms) {
5566 return false;
5567 }
5568 $vbo_tn->translateContents($ordersrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
5569
5570 /**
5571 * Split stay reservation.
5572 *
5573 * @since 1.16.0 (J) - 1.6.0 (WP)
5574 */
5575 $room_stay_dates = [];
5576 if ($booking['split_stay']) {
5577 if ($booking['status'] == 'confirmed') {
5578 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($booking['id']);
5579 } else {
5580 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
5581 }
5582 // immediately count the number of nights of stay for each split room
5583 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
5584 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
5585 // overwrite values for compatibility with non-confirmed bookings
5586 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
5587 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
5588 }
5589 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
5590 // overwrite the whole array
5591 $room_stay_dates[$sps_r_k] = $sps_r_v;
5592 }
5593 }
5594
5595 $rooms = array();
5596 $tars = array();
5597 $is_package = !empty($booking['pkg']) ? true : false;
5598 $ftitle = self::getFrontTitle();
5599 $currencyname = self::getCurrencyName();
5600 foreach ($ordersrooms as $kor => $or) {
5601 $num = $kor + 1;
5602 $rooms[$num] = $or;
5603 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5604 // package or custom cost set from the back-end
5605 continue;
5606 }
5607
5608 // determine the days to consider for the count of the availability
5609 $room_nights = $booking['days'];
5610 $room_checkin = $booking['checkin'];
5611 $room_checkout = $booking['checkout'];
5612 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5613 $room_nights = $room_stay_dates[$kor]['nights'];
5614 $room_checkin = $room_stay_dates[$kor]['checkin'];
5615 $room_checkout = $room_stay_dates[$kor]['checkout'];
5616 }
5617
5618 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
5619 $dbo->setQuery($q);
5620 $tar = $dbo->loadAssocList();
5621 if (!$tar) {
5622 // tariff not found
5623 if (self::isAdmin()) {
5624 VikError::raiseWarning('', JText::translate('VBERRNOFAREFOUND'));
5625 }
5626 continue;
5627 }
5628
5629 // apply seasonal rates
5630 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
5631
5632 // different usage
5633 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
5634 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
5635 // Occupancy Override
5636 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
5637 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
5638 //
5639 if (is_array($diffusageprice)) {
5640 // set a charge or discount to the price(s) for the different usage of the room
5641 foreach ($tar as $kpr => $vpr) {
5642 $tar[$kpr]['diffusage'] = $or['adults'];
5643 if ($diffusageprice['chdisc'] == 1) {
5644 // charge
5645 if ($diffusageprice['valpcent'] == 1) {
5646 // fixed value
5647 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5648 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5649 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
5650 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5651 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
5652 } else {
5653 // percentage value
5654 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5655 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
5656 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
5657 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5658 $tar[$kpr]['cost'] = $aduseval;
5659 }
5660 } else {
5661 // discount
5662 if ($diffusageprice['valpcent'] == 1) {
5663 // fixed value
5664 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
5665 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
5666 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
5667 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5668 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
5669 } else {
5670 // percentage value
5671 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
5672 $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);
5673 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
5674 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
5675 $tar[$kpr]['cost'] = $aduseval;
5676 }
5677 }
5678 }
5679 }
5680 }
5681 //
5682 $tars[$num] = $tar[0];
5683 }
5684
5685 $secdiff = $booking['checkout'] - $booking['checkin'];
5686 $daysdiff = $secdiff / 86400;
5687 if (is_int($daysdiff)) {
5688 if ($daysdiff < 1) {
5689 $daysdiff = 1;
5690 }
5691 } else {
5692 if ($daysdiff < 1) {
5693 $daysdiff = 1;
5694 } else {
5695 $sum = floor($daysdiff) * 86400;
5696 $newdiff = $secdiff - $sum;
5697 $maxhmore = self::getHoursMoreRb() * 3600;
5698 if ($maxhmore >= $newdiff) {
5699 $daysdiff = floor($daysdiff);
5700 } else {
5701 $daysdiff = ceil($daysdiff);
5702 }
5703 }
5704 }
5705
5706 $isdue = 0;
5707 $pricestr = array();
5708 $optstr = array();
5709 foreach ($ordersrooms as $kor => $or) {
5710 $num = $kor + 1;
5711
5712 // determine the days to consider for the count of the availability
5713 $room_nights = $booking['days'];
5714 $room_checkin = $booking['checkin'];
5715 $room_checkout = $booking['checkout'];
5716 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
5717 $room_nights = $room_stay_dates[$kor]['nights'];
5718 $room_checkin = $room_stay_dates[$kor]['checkin'];
5719 $room_checkout = $room_stay_dates[$kor]['checkout'];
5720 }
5721
5722 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5723 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
5724 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
5725 $isdue += $calctar;
5726 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
5727 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
5728 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5729 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
5730 $tars[$num]['calctar'] = $calctar;
5731 $isdue += $calctar;
5732 $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'] : "");
5733 }
5734
5735 if (!empty($or['optionals'])) {
5736 $stepo = explode(";", $or['optionals']);
5737 foreach ($stepo as $roptkey => $oo) {
5738 if (empty($oo)) {
5739 continue;
5740 }
5741 $stept = explode(":", $oo);
5742 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
5743 $dbo->setQuery($q);
5744 $dbo->execute();
5745 if ($dbo->getNumRows() == 1) {
5746 $actopt = $dbo->loadAssocList();
5747 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals', array(), array(), (!empty($booking['lang']) ? $booking['lang'] : null));
5748 $chvar = '';
5749 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
5750 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
5751 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
5752 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
5753 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
5754 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
5755 $agestept = explode('-', $stept[1]);
5756 $stept[1] = $agestept[0];
5757 $chvar = $agestept[1];
5758 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
5759 // percentage value of the adults tariff
5760 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5761 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5762 } else {
5763 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5764 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5765 }
5766 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
5767 // VBO 1.10 - percentage value of room base cost
5768 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5769 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
5770 } else {
5771 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
5772 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
5773 }
5774 }
5775 $actopt[0]['chageintv'] = $chvar;
5776 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
5777 $actopt[0]['quan'] = $stept[1];
5778 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
5779 } else {
5780 $actopt[0]['quan'] = $stept[1];
5781 // VBO 1.11 - options percentage cost of the room total fee
5782 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
5783 $deftar_basecosts = $or['cust_cost'];
5784 } else {
5785 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
5786 }
5787 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
5788 //
5789 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
5790 }
5791 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
5792 $realcost = $actopt[0]['maxprice'];
5793 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
5794 $realcost = $actopt[0]['maxprice'] * $stept[1];
5795 }
5796 }
5797 if ($actopt[0]['perperson'] == 1) {
5798 $realcost = $realcost * $or['adults'];
5799 }
5800 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
5801 $isdue += $tmpopr;
5802 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
5803 }
5804 }
5805 }
5806
5807 // custom extra costs
5808 if (!empty($or['extracosts'])) {
5809 $cur_extra_costs = json_decode($or['extracosts'], true);
5810 foreach ($cur_extra_costs as $eck => $ecv) {
5811 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
5812 $isdue += $ecplustax;
5813 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
5814 }
5815 }
5816 }
5817
5818 // force the original total amount if rates have changed
5819 if (number_format($isdue, 2) != number_format($booking['total'], 2)) {
5820 $isdue = $booking['total'];
5821 }
5822
5823 // mail subject
5824 $subject = JText::sprintf('VBOMAILSUBJECT', strip_tags($ftitle));
5825 // $subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
5826
5827 // inject the recipient of the message for the template
5828 $booking['for'] = $for;
5829
5830 // load template file that will get $booking as variable
5831 $tmpl = self::loadEmailTemplate($booking);
5832
5833 // parse email template
5834 $hmess = self::parseEmailTemplate($tmpl, $booking, $rooms, $pricestr, $optstr, $isdue);
5835 $hmess = '<html>'."\n".'<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>'."\n".'<body>'.$hmess.'</body>'."\n".'</html>';
5836
5837 if ($send !== true) {
5838 // return the content of the email message parsed
5839 return $hmess;
5840 }
5841
5842 // when the message can be sent
5843 $sendwhen = self::getSendEmailWhen();
5844
5845 if ($no_config) {
5846 // force the configuration setting to send the message no matter what's the booking status
5847 $sendwhen = 1;
5848 }
5849
5850 // send the message
5851 foreach ($for as $who) {
5852 $use_subject = $subject;
5853 $recipients = array();
5854 $attachments = self::addEmailAttachment(null);
5855 $attach_ical = false;
5856 $force_replyto = null;
5857 if (strpos($who, '@') !== false) {
5858 // send email to custom email address
5859 array_push($recipients, trim($who));
5860 } elseif (stripos($who, 'guest') !== false || stripos($who, 'customer') !== false) {
5861 // send email to the customer
5862 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5863 continue;
5864 }
5865 array_push($recipients, $booking['custmail']);
5866 /**
5867 * Check whether an iCal should be attached for the customer.
5868 *
5869 * @since 1.12.0
5870 */
5871 $attach_ical = self::getEmailIcal('customer', $booking);
5872 } elseif (stripos($who, 'admin') !== false) {
5873 // send email to the administrator(s)
5874 if ($sendwhen > 1 && $booking['status'] == 'standby') {
5875 continue;
5876 }
5877 $use_subject = $subject . ' #' . $booking['id'];
5878 $adminemail = self::getAdminMail();
5879 $extra_admin_recipients = self::addAdminEmailRecipient(null);
5880 if (empty($adminemail) && empty($extra_admin_recipients)) {
5881 // Prevent Joomla Exceptions that would stop the script execution
5882 VikError::raiseWarning('', 'The administrator email address is empty. Email message could not be sent.');
5883 continue;
5884 }
5885 if (strpos($adminemail, ',') !== false) {
5886 // multiple addresses
5887 $adminemails = explode(',', $adminemail);
5888 foreach ($adminemails as $am) {
5889 if (strpos($am, '@') !== false) {
5890 array_push($recipients, trim($am));
5891 }
5892 }
5893 } else {
5894 // single address
5895 array_push($recipients, trim($adminemail));
5896 }
5897
5898 // merge extra recipients
5899 $recipients = array_merge($recipients, $extra_admin_recipients);
5900
5901 // admin should reply to the customer
5902 $force_replyto = !empty($booking['custmail']) ? $booking['custmail'] : $force_replyto;
5903
5904 /**
5905 * Check whether an iCal should be attached for the admin.
5906 *
5907 * @since 1.2.0
5908 */
5909 $attach_ical = self::getEmailIcal('admin', array(
5910 'ts' => $booking['ts'],
5911 'custdata' => $booking['custdata'],
5912 'checkin' => $booking['checkin'],
5913 'checkout' => $booking['checkout'],
5914 'subject' => JText::sprintf('VBNEWORDER', $booking['id']),
5915 ));
5916 }
5917
5918 // send the message, recipients should always be an array to support multiple admin addresses
5919
5920 // get sender e-mail
5921 $adsendermail = VBOFactory::getConfig()->get('senderemail');
5922
5923 // init mail data
5924 $mail = new VBOMailWrapper([
5925 'sender' => [$adsendermail, $ftitle],
5926 'recipient' => $recipients,
5927 'bcc' => self::addAdminEmailRecipient(null, true),
5928 'reply' => !empty($force_replyto) ? $force_replyto : $adsendermail,
5929 'subject' => $use_subject,
5930 'content' => $hmess,
5931 'attachments' => $attachments,
5932 ]);
5933
5934 if ($attach_ical !== false && $booking['status'] != 'standby') {
5935 // attach iCal file
5936 $mail->addAttachment($attach_ical);
5937 }
5938
5939 /**
5940 * Trigger event to allow third party plugins to overwrite any aspect of the mail message.
5941 *
5942 * @see VBOMailWrapper is the $mail object and its setter methods can modify the mail data.
5943 *
5944 * @since 1.15.4 (J) - 1.5.10 (WP)
5945 */
5946 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSendBookingMailVikBooking', [$who, $booking, $mail]);
5947
5948 // send e-mail
5949 $result = VBOFactory::getPlatform()->getMailer()->send($mail) || $result;
5950
5951 // unlink iCal file
5952 if ($attach_ical !== false) {
5953 @unlink($attach_ical);
5954 }
5955 }
5956
5957 return $result;
5958 }
5959
5960 /**
5961 * This method allows to add one or more recipient email
5962 * addresses for the next queue of email sending for the admin.
5963 * This method can be used in the template file for the customer
5964 * email to register an additional email address, maybe when a
5965 * specific room-type is booked.
5966 * The methods sending the email messages are supposed to call this
5967 * method by passing no arguments to obtain the extra addresses set.
5968 *
5969 * @param mixed $email null, string or array of email address(es).
5970 * @param bool $bcc if true, addresses will be used as bcc.
5971 * @param bool $reset if true, the queues will be emptied.
5972 *
5973 * @return array the current extra recipients or bcc addresses set.
5974 *
5975 * @since 1.13 (J) - 1.3.0 (WP)
5976 * @since 1.14 (J) - 1.4.0 (WP) added argument $bcc.
5977 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
5978 */
5979 public static function addAdminEmailRecipient($email, $bcc = false, $reset = false)
5980 {
5981 static $extra_recipients = [];
5982 static $extra_bcc = [];
5983
5984 if ($reset) {
5985 $extra_recipients = [];
5986 $extra_bcc = [];
5987 }
5988
5989 if (!empty($email)) {
5990 if (is_scalar($email)) {
5991 if ($bcc) {
5992 array_push($extra_bcc, $email);
5993 } else {
5994 array_push($extra_recipients, $email);
5995 }
5996 } else {
5997 if ($bcc) {
5998 $extra_bcc = array_merge($extra_bcc, $email);
5999 } else {
6000 $extra_recipients = array_merge($extra_recipients, $email);
6001 }
6002 }
6003 }
6004
6005 return $bcc ? array_unique($extra_bcc) : array_unique($extra_recipients);
6006 }
6007
6008 /**
6009 * This method serves to add one or more attachments to the
6010 * next queue of email sending for the admin.
6011 * The methods sending the email messages are supposed to call this
6012 * method by passing a null argument to obtain the attachments set.
6013 *
6014 * @param mixed $file null or string with path to file to attach.
6015 * @param bool $reset if true, the queue will be emptied.
6016 *
6017 * @return array the current attachments set.
6018 *
6019 * @since 1.14
6020 * @since 1.16 (J) - 1.6.0 (WP) added argument $reset.
6021 */
6022 public static function addEmailAttachment($file, $reset = false)
6023 {
6024 static $extra_attachments = [];
6025
6026 if ($reset) {
6027 $extra_attachments = [];
6028 }
6029
6030 if (!empty($file)) {
6031 if (is_scalar($file)) {
6032 array_push($extra_attachments, $file);
6033 } else {
6034 $extra_attachments = array_merge($extra_attachments, $file);
6035 }
6036 }
6037
6038 return array_unique($extra_attachments);
6039 }
6040
6041 /**
6042 * This method is called whenever some rooms get booked.
6043 * It checks whether the rooms involved have shared calendars.
6044 * If some are found, then also such rooms will be occupied.
6045 *
6046 * @param int $bid the booking ID.
6047 * @param array $roomids the list of rooms booked.
6048 * @param int $checkin checkin timestamp.
6049 * @param int $checkout checkout timestamp.
6050 *
6051 * @return boolean true if some other cals were occupied, false otherwise.
6052 *
6053 * @since 1.13 (J) - 1.3.0 (WP)
6054 *
6055 * @see SynchVikBooking::getRoomsSharedCalsInvolved() in VCM that checks if this method exists.
6056 */
6057 public static function updateSharedCalendars($bid, $roomids = array(), $checkin = 0, $checkout = 0)
6058 {
6059 $dbo = JFactory::getDbo();
6060 $bid = (int)$bid;
6061
6062 // availability helper
6063 $av_helper = self::getAvailabilityInstance();
6064
6065 // check for split stay rooms reservation
6066 $known_split_stay = false;
6067 $is_split_stay = 0;
6068
6069 if (empty($checkin) || empty($checkout)) {
6070 // get checkin and checkout timestamps from booking
6071 $q = "SELECT `checkin`, `checkout`, `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid};";
6072 $dbo->setQuery($q);
6073 $dbo->execute();
6074 if (!$dbo->getNumRows()) {
6075 // booking not found
6076 return false;
6077 }
6078 $bdata = $dbo->loadAssoc();
6079 $checkin = $bdata['checkin'];
6080 $checkout = $bdata['checkout'];
6081 $is_split_stay = $bdata['split_stay'];
6082 // turn flag on
6083 $known_split_stay = true;
6084 }
6085
6086 if (!$known_split_stay) {
6087 // we need to query the db to access this information
6088 $q = "SELECT `split_stay` FROM `#__vikbooking_orders` WHERE `id`={$bid}";
6089 $dbo->setQuery($q, 0, 1);
6090 $dbo->execute();
6091 if ($dbo->getNumRows()) {
6092 $is_split_stay = (int)$dbo->loadResult();
6093 }
6094 // turn flag on
6095 $known_split_stay = true;
6096 }
6097
6098 // check split stay room records
6099 $room_stay_dates = [];
6100 if ($is_split_stay) {
6101 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($bid);
6102 // immediately count the number of nights of stay for each split room
6103 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
6104 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
6105 // overwrite values for compatibility with non-confirmed bookings
6106 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
6107 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
6108 }
6109 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
6110 // overwrite the whole array
6111 $room_stay_dates[$sps_r_k] = $sps_r_v;
6112 }
6113 }
6114
6115 // get the rooms booked
6116 if (!count($roomids)) {
6117 // get the IDs of all rooms booked
6118 $q = "SELECT `idroom` FROM `#__vikbooking_ordersrooms` WHERE `idorder`={$bid};";
6119 $dbo->setQuery($q);
6120 $orr = $dbo->loadAssocList();
6121 if ($orr) {
6122 foreach ($orr as $or) {
6123 array_push($roomids, $or['idroom']);
6124 }
6125 }
6126 }
6127 if (!count($roomids) || empty($bid)) {
6128 // unable to proceed
6129 return false;
6130 }
6131 $roomids = array_unique($roomids);
6132
6133 // build room stay dates map in case of split stay
6134 $room_split_stay_map = [];
6135 if ($is_split_stay && !empty($room_stay_dates) && count($room_stay_dates) == count($roomids) && count($roomids) > 1) {
6136 // we have unique room IDs in a split stay reservation for more than one room
6137 foreach ($room_stay_dates as $room_stay_date) {
6138 $room_split_stay_map[$room_stay_date['idroom']] = $room_stay_date;
6139 }
6140 }
6141
6142 // get rooms involved
6143 $involved = [];
6144 $involved_stay_map = [];
6145 $q = "SELECT * FROM `#__vikbooking_calendars_xref` WHERE `mainroom` IN (" . implode(', ', $roomids) . ") OR `childroom` IN (" . implode(', ', $roomids) . ");";
6146 $dbo->setQuery($q);
6147 $rooms_found = $dbo->loadAssocList();
6148 if (!$rooms_found) {
6149 // no rooms involved that need their calendars updated
6150 return false;
6151 }
6152 foreach ($rooms_found as $rf) {
6153 if (!in_array($rf['mainroom'], $roomids)) {
6154 // push room ID
6155 array_push($involved, $rf['mainroom']);
6156 // check for specific stay dates in connected room
6157 if (!empty($rf['childroom']) && isset($room_split_stay_map[$rf['childroom']])) {
6158 // split stay information available
6159 $involved_stay_map[$rf['mainroom']] = $room_split_stay_map[$rf['childroom']];
6160 }
6161 }
6162 if (!in_array($rf['childroom'], $roomids)) {
6163 // push room ID
6164 array_push($involved, $rf['childroom']);
6165 // check for specific stay dates in connected room
6166 if (!empty($rf['mainroom']) && isset($room_split_stay_map[$rf['mainroom']])) {
6167 // split stay information available
6168 $involved_stay_map[$rf['childroom']] = $room_split_stay_map[$rf['mainroom']];
6169 }
6170 }
6171 }
6172 $involved = array_unique($involved);
6173 if (!count($involved)) {
6174 // no rooms involved
6175 return false;
6176 }
6177
6178 // turnover seconds
6179 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
6180
6181 // occupy the calendars for the involved rooms found
6182 $bids_generated = [];
6183 foreach ($involved as $rid) {
6184 // determine the values to use
6185 $room_checkin = (int)$checkin;
6186 $room_checkout = (int)$checkout;
6187 $room_realback = ($checkout + $turnover_secs);
6188 if (isset($involved_stay_map[$rid])) {
6189 // split stay dates available
6190 $room_checkin = (int)$involved_stay_map[$rid]['checkin'];
6191 $room_checkout = (int)$involved_stay_map[$rid]['checkout'];
6192 $room_realback = ($room_checkout + $turnover_secs);
6193 }
6194
6195 // occupy record in room with shared calendar
6196 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`,`sharedcal`) VALUES(" . (int)$rid . ", " . $room_checkin . ", " . $room_checkout . ", " . $room_realback . ", 1);";
6197 $dbo->setQuery($q);
6198 $dbo->execute();
6199 array_push($bids_generated, $dbo->insertid());
6200 }
6201
6202 // store busy relations created
6203 foreach ($bids_generated as $busyid) {
6204 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . $bid . ", " . (int)$busyid . ");";
6205 $dbo->setQuery($q);
6206 $dbo->execute();
6207 }
6208
6209 return true;
6210 }
6211
6212 /**
6213 * This method is needed whenever a booking gets modified by
6214 * adding or removing rooms. This way we reset (remove) all
6215 * busy records that were previously stored due to a shared
6216 * calendar. The correct relations should then be re-created
6217 * by calling updateSharedCalendars().
6218 *
6219 * @param int $bid the booking ID.
6220 *
6221 * @return boolean true if some records were cleaned.
6222 *
6223 * @see updateSharedCalendars() should be called after.
6224 *
6225 * @since 1.3.0
6226 */
6227 public static function cleanSharedCalendarsBusy($bid) {
6228 $dbo = JFactory::getDbo();
6229 $bid = (int)$bid;
6230 // get all the occupied records due to shared calendars for this booking
6231 $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;";
6232 $dbo->setQuery($q);
6233 $allbusy = $dbo->loadAssocList();
6234 if (!$allbusy) {
6235 return false;
6236 }
6237
6238 $busy_ids = array();
6239 foreach ($allbusy as $b) {
6240 // push busy ID to be removed later
6241 array_push($busy_ids, $b['id']);
6242 // delete the current busy ID-booking ID relation for this shared calendar
6243 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`={$bid} AND `idbusy`={$b['id']};";
6244 $dbo->setQuery($q);
6245 $dbo->execute();
6246 }
6247 if (count($busy_ids)) {
6248 // delete all busy records due to shared calendars
6249 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (" . implode(', ', $busy_ids) . ");";
6250 $dbo->setQuery($q);
6251 $dbo->execute();
6252 }
6253
6254 return true;
6255 }
6256
6257 public static function sendJutility() {
6258 //deprecated in VBO 1.10
6259 $dbo = JFactory::getDbo();
6260 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='sendjutility';";
6261 $dbo->setQuery($q);
6262 $dbo->execute();
6263 $s = $dbo->loadAssocList();
6264 return (intval($s[0]['setting']) == 1 ? true : false);
6265 }
6266
6267 public static function getCategoryName($idcat, $vbo_tn = null) {
6268 $dbo = JFactory::getDbo();
6269 $q = "SELECT `id`,`name` FROM `#__vikbooking_categories` WHERE `id`=" . $dbo->quote($idcat) . ";";
6270 $dbo->setQuery($q);
6271 $dbo->execute();
6272 $p = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : array();
6273 if (is_object($vbo_tn) && count($p) > 0) {
6274 $vbo_tn->translateContents($p, '#__vikbooking_categories');
6275 }
6276 return count($p) > 0 ? $p[0]['name'] : '';
6277 }
6278
6279 public static function loadAdultsDiff($idroom, $adults)
6280 {
6281 static $obp_map = [];
6282
6283 $map_sign = "{$idroom}:{$adults}";
6284
6285 if (array_key_exists($map_sign, $obp_map)) {
6286 return $obp_map[$map_sign];
6287 }
6288
6289 $dbo = JFactory::getDbo();
6290
6291 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " AND `adults`=" . (int)$adults;
6292 $dbo->setQuery($q, 0, 1);
6293 $obp_diff = $dbo->loadAssoc();
6294 if ($obp_diff) {
6295 // cache value and return it
6296 $obp_map[$map_sign] = $obp_diff;
6297
6298 return $obp_diff;
6299 }
6300
6301 // cache value and return it
6302 $obp_map[$map_sign] = null;
6303
6304 return null;
6305 }
6306
6307 public static function loadRoomAdultsDiff($idroom)
6308 {
6309 $dbo = JFactory::getDbo();
6310
6311 $q = "SELECT * FROM `#__vikbooking_adultsdiff` WHERE `idroom`=" . (int)$idroom . " ORDER BY `adults` ASC;";
6312 $dbo->setQuery($q);
6313 $diff = $dbo->loadAssocList();
6314 if ($diff) {
6315 $roomdiff = [];
6316 foreach ($diff as $v) {
6317 $roomdiff[$v['adults']] = $v;
6318 }
6319
6320 return $roomdiff;
6321 }
6322
6323 return [];
6324 }
6325
6326 public static function occupancyOverrideExists($tar, $adults) {
6327 foreach ($tar as $k => $v) {
6328 if (is_array($v) && array_key_exists('occupancy_ovr', $v)) {
6329 if (array_key_exists($adults, $v['occupancy_ovr'])) {
6330 return $v['occupancy_ovr'][$adults];
6331 }
6332 }
6333 }
6334 return false;
6335 }
6336
6337 public static function getChildrenCharges($idroom, $children, $ages, $num_nights) {
6338 /* charges as percentage amounts of the adults tariff not supported for third parties (only VBO 1.8) */
6339 $charges = array();
6340 if (!($children > 0) || !(count($ages) > 0)) {
6341 return $charges;
6342 }
6343 $dbo = JFactory::getDbo();
6344 $id_options = array();
6345 $q = "SELECT `id`,`idopt` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$idroom.";";
6346 $dbo->setQuery($q);
6347 $dbo->execute();
6348 if ($dbo->getNumRows() > 0) {
6349 $assocs = $dbo->loadAssocList();
6350 foreach ($assocs as $opts) {
6351 if (!empty($opts['idopt'])) {
6352 $r_ido = explode(';', rtrim($opts['idopt']));
6353 foreach ($r_ido as $ido) {
6354 if (!empty($ido) && !in_array($ido, $id_options)) {
6355 $id_options[] = $ido;
6356 }
6357 }
6358 }
6359 }
6360 }
6361 if (count($id_options) > 0) {
6362 $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;";
6363 $dbo->setQuery($q);
6364 $dbo->execute();
6365 if ($dbo->getNumRows() > 0) {
6366 $ageintervals = $dbo->loadAssocList();
6367 $split_ages = explode(';;', $ageintervals[0]['ageintervals']);
6368 $age_range = array();
6369 foreach ($split_ages as $kg => $spage) {
6370 if (empty($spage)) {
6371 continue;
6372 }
6373 $parts = explode('_', $spage);
6374 if (strlen($parts[0]) > 0 && intval($parts[1]) > 0 && floatval($parts[2]) > 0) {
6375 $ind = count($age_range);
6376 $age_range[$ind]['from'] = intval($parts[0]);
6377 $age_range[$ind]['to'] = intval($parts[1]);
6378 //taxes are calculated later in VCM
6379 //$age_range[$ind]['cost'] = self::sayOptionalsPlusIva((floatval($parts[2]) * $num_nights), $ageintervals[0]['idiva']);
6380 $age_range[$ind]['cost'] = floatval($parts[2]) * $num_nights;
6381 $age_range[$ind]['option_str'] = $ageintervals[0]['id'].':1-'.($kg + 1);
6382 }
6383 }
6384 if (count($age_range) > 0) {
6385 $tot_charge = 0;
6386 $affected = array();
6387 $option_str = '';
6388 foreach ($ages as $age) {
6389 if (strlen($age) == 0) {
6390 continue;
6391 }
6392 foreach ($age_range as $range) {
6393 if (intval($age) >= $range['from'] && intval($age) <= $range['to']) {
6394 $tot_charge += $range['cost'];
6395 $affected[] = $age;
6396 $option_str .= $range['option_str'].';';
6397 break;
6398 }
6399 }
6400 }
6401 if ($tot_charge > 0) {
6402 $charges['total'] = $tot_charge;
6403 $charges['affected'] = $affected;
6404 $charges['options'] = $option_str;
6405 }
6406 }
6407 }
6408 }
6409
6410 return $charges;
6411 }
6412
6413 public static function sortRoomPrices($arr) {
6414 $newarr = array();
6415 foreach ($arr as $k => $v) {
6416 $newarr[$k] = $v['cost'];
6417 }
6418 asort($newarr);
6419 $sorted = array();
6420 foreach ($newarr as $k => $v) {
6421 $sorted[$k] = $arr[$k];
6422 }
6423 return $sorted;
6424 }
6425
6426 public static function sortResults($arr) {
6427 $newarr = array();
6428 foreach ($arr as $k => $v) {
6429 $newarr[$k] = $v[0]['cost'];
6430 }
6431 asort($newarr);
6432 $sorted = array();
6433 foreach ($newarr as $k => $v) {
6434 $sorted[$k] = $arr[$k];
6435 }
6436 return $sorted;
6437 }
6438
6439 public static function sortMultipleResults($arr) {
6440 foreach ($arr as $k => $v) {
6441 $newarr = array();
6442 foreach ($v as $subk => $subv) {
6443 $newarr[$subk] = $subv[0]['cost'];
6444 }
6445 asort($newarr);
6446 $sorted = array();
6447 foreach ($newarr as $nk => $v) {
6448 $sorted[$nk] = $arr[$k][$nk];
6449 }
6450 $arr[$k] = $sorted;
6451 }
6452 return $arr;
6453 }
6454
6455 /**
6456 * Caches a list of promotion IDs that will be skipped when applying the special rates. This is
6457 * useful for the VCM's Bulk Actions to register a skip of the promotions for the various OTAs
6458 * to avoid duplicate discounts to be applied (room-rate-day level and promotion).
6459 *
6460 * @param array $promos optional list of promotion IDs to register in the execution flow.
6461 *
6462 * @return array list of cached promotion IDs (if any).
6463 *
6464 * @since 1.16.4 (J) - 1.6.4 (WP)
6465 */
6466 public static function registerPromotionIds(array $promos = null)
6467 {
6468 static $flagged_promos = [];
6469
6470 if ($promos) {
6471 // set cached value
6472 $flagged_promos = $promos;
6473 }
6474
6475 return $flagged_promos;
6476 }
6477
6478 /**
6479 * Getter and setter for seasons cache allowed.
6480 *
6481 * @param bool $enabled true or false for setter, null for getter.
6482 *
6483 * @return bool|null
6484 *
6485 * @since 1.16.5 (J) - 1.6.5 (WP)
6486 */
6487 public static function setSeasonsCache($enabled = null)
6488 {
6489 static $seasons_cache = null;
6490
6491 if ($enabled !== null) {
6492 // set value
6493 $seasons_cache = (bool)$enabled;
6494 }
6495
6496 return $seasons_cache;
6497 }
6498
6499 /**
6500 * Applies the seasonal rates over a list of room rate records.
6501 *
6502 * @param array $arr list of room rate records.
6503 * @param int $from unix timestamp for start date.
6504 * @param int $to unix timestamp for end date.
6505 *
6506 * @return array list of manipulated room rate records.
6507 */
6508 public static function applySeasonalPrices(array $arr, $from, $to)
6509 {
6510 $dbo = JFactory::getDbo();
6511 $vbo_tn = self::getTranslator();
6512
6513 $roomschange = [];
6514 $one = getdate($from);
6515
6516 // leap years
6517 if (($one['year'] % 4) == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
6518 $isleap = true;
6519 } else {
6520 $isleap = false;
6521 }
6522
6523 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
6524 $tomidnightone = intval($one['hours']) * 3600;
6525 $tomidnightone += intval($one['minutes']) * 60;
6526 $sfrom = $from - $baseone - $tomidnightone;
6527 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
6528 $two = getdate($to);
6529 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
6530 $tomidnighttwo = intval($two['hours']) * 3600;
6531 $tomidnighttwo += intval($two['minutes']) * 60;
6532 $sto = $to - $basetwo - $tomidnighttwo;
6533
6534 // leap years, last day of the month of the season
6535 if ($isleap) {
6536 $leapts = mktime(0, 0, 0, 2, 29, $two['year']);
6537 if ($two[0] > $leapts) {
6538 /**
6539 * Timestamp must be greater than the leap-day of Feb 29th.
6540 * It used to be checked for >= $leapts.
6541 *
6542 * @since July 3rd 2019
6543 */
6544 $sfrom -= 86400;
6545 $sto -= 86400;
6546 } elseif ($sto < $sfrom && $one['year'] < $two['year']) {
6547 //lower checkin date when in leap year but not for checkout
6548 $sfrom -= 86400;
6549 }
6550 }
6551
6552 // check for DST changes to adjust the query values to fetch
6553 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
6554 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
6555 // we are parsing the day when the DST changed (probably a Sunday)
6556 if (!date('I', $from)) {
6557 // DST was just turned off
6558 $sfrom -= 3600;
6559 }
6560 }
6561 }
6562
6563 // count nights requested
6564 $booking_nights = 1;
6565 foreach ($arr as $k => $a) {
6566 if (!isset($a[0])) {
6567 continue;
6568 }
6569 if (isset($a[0]['booking_nights'])) {
6570 // this value may be set when displaying pricing calendars
6571 $booking_nights = $a[0]['booking_nights'];
6572 break;
6573 }
6574 if (isset($a[0]['days'])) {
6575 $booking_nights = $a[0]['days'];
6576 break;
6577 }
6578 }
6579
6580 /**
6581 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
6582 *
6583 * @since 1.16.4 (J) - 1.6.4 (WP)
6584 */
6585 $skip_promo_ids = self::registerPromotionIds();
6586
6587 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
6588 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
6589 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
6590 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
6591 ($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`)") .
6592 ($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`)") .
6593 ($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`)") .
6594 ($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`)") .
6595 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
6596 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
6597
6598 $dbo->setQuery($q);
6599 $seasons = $dbo->loadAssocList();
6600
6601 if ($seasons) {
6602 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
6603 $applyseasons = false;
6604 $mem = [];
6605 foreach ($arr as $k => $a) {
6606 $mem[$k]['daysused'] = 0;
6607 $mem[$k]['sum'] = [];
6608 /**
6609 * The keys below are all needed to apply the promotions on the room's final cost.
6610 *
6611 * @since 1.13.5
6612 */
6613 $mem[$k]['diffs'] = [];
6614 $mem[$k]['trans_keys'] = [];
6615 $mem[$k]['trans_factors'] = [];
6616 $mem[$k]['trans_affdays'] = [];
6617 }
6618 foreach ($seasons as $s) {
6619 // check if this is a promotion registered for skipping
6620 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
6621 continue;
6622 }
6623
6624 // Special Price tied to the year
6625 if (!empty($s['year']) && $s['year'] > 0) {
6626 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
6627 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
6628 //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
6629 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
6630 continue;
6631 }
6632 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
6633 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
6634 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
6635 if ($calc_ends < ($from - $tomidnightone)) {
6636 continue;
6637 }
6638 }
6639 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
6640 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
6641 if ($s['from'] > $s['to']) {
6642 continue;
6643 }
6644 if (($basetwo + $s['from'] + 86399) > $to) {
6645 /**
6646 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
6647 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
6648 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
6649 *
6650 * @since 1.14.3 (J) - 1.4.3 (WP)
6651 */
6652 continue;
6653 }
6654 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
6655 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
6656 /**
6657 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
6658 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
6659 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
6660 *
6661 * @since 1.14.3 (J) - 1.4.3 (WP)
6662 */
6663 continue;
6664 }
6665 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
6666 // 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)
6667 if (($baseone + $s['from']) > $to) {
6668 continue;
6669 }
6670 }
6671 }
6672 //
6673 $allrooms = explode(",", $s['idrooms']);
6674 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
6675 $inits = $baseone + $s['from'];
6676 if ($s['from'] < $s['to']) {
6677 $ends = $basetwo + $s['to'];
6678 // check if the inits must be set to the year after
6679 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
6680 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
6681 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6682 $inits = $tmpbase + $s['from'];
6683 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6684 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
6685 $ends = $baseone + $s['to'];
6686 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
6687 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
6688 $ends = $baseone + $s['to'];
6689 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
6690 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
6691 $inits = $basetwo + $s['from'];
6692 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
6693 // booking dates across two years for a season tied to a specific year
6694 if ($one['year'] == $s['year']) {
6695 $inits = $baseone + $s['from'];
6696 } else {
6697 $inits = $basetwo + $s['from'];
6698 }
6699 if ($two['year'] == $s['year']) {
6700 $ends = $basetwo + $s['to'];
6701 } else {
6702 $ends = $baseone + $s['to'];
6703 }
6704 }
6705 } else {
6706 // between 2 years
6707 if ($baseone < $basetwo) {
6708 // ex. 29/12/2012 - 14/01/2013
6709 $ends = $basetwo + $s['to'];
6710 } else {
6711 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
6712 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
6713 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
6714 $ends = $tmpbase + $s['to'];
6715 } else {
6716 // ex. 03/01 - 09/01
6717 $ends = $basetwo + $s['to'];
6718 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
6719 $inits = $tmpbase + $s['from'];
6720 }
6721 }
6722 }
6723
6724 // leap years
6725 if ($isleap == true) {
6726 $infoseason = getdate($inits);
6727 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
6728 // VikBooking 1.6 added below && $infoseason['year'] == $one['year']
6729 // for those seasons like 2015 Dec 14 to 2016 Jan 5 and booking dates like 2016 Jan 1 to Jan 6 where 2015 is not leap
6730 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
6731 /**
6732 * Timestamp must be greater than the leap-day of Feb 29th.
6733 * It used to be checked for >= $leapts.
6734 *
6735 * @since July 3rd 2019
6736 */
6737 $inits += 86400;
6738 $ends += 86400;
6739 }
6740 }
6741
6742 // promotions
6743 $promotion = [];
6744 if ($s['promo'] == 1) {
6745 $daysadv = (($inits - time()) / 86400);
6746 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
6747 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
6748 continue;
6749 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
6750 $secstocheckin = ($from - time());
6751 if ($s['promolastmin'] < $secstocheckin) {
6752 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
6753 continue;
6754 }
6755 }
6756 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
6757 /**
6758 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
6759 *
6760 * @since 1.13.5
6761 */
6762 continue;
6763 }
6764 $promotion['todaydaysadv'] = $daysadv;
6765 $promotion['promodaysadv'] = $s['promodaysadv'];
6766 $promotion['promotxt'] = $s['promotxt'];
6767 }
6768
6769 // occupancy override
6770 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
6771
6772 // week days
6773 $filterwdays = !empty($s['wdays']) ? true : false;
6774 $wdays = $filterwdays ? explode(';', $s['wdays']) : '';
6775 if (is_array($wdays) && $wdays) {
6776 foreach ($wdays as $kw => $wd) {
6777 if (strlen($wd) == 0) {
6778 unset($wdays[$kw]);
6779 }
6780 }
6781 }
6782
6783 // checkin must be after the beginning of the season
6784 $checkininclok = true;
6785 if ($s['checkinincl'] == 1) {
6786 $checkininclok = false;
6787 if ($s['from'] < $s['to']) {
6788 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
6789 $checkininclok = true;
6790 }
6791 } else {
6792 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
6793 $checkininclok = true;
6794 }
6795 }
6796 }
6797 if ($checkininclok !== true) {
6798 continue;
6799 }
6800
6801 foreach ($arr as $k => $a) {
6802 // applied only to some types of price
6803 if ($allprices && !empty($allprices[0])) {
6804 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
6805 continue;
6806 }
6807 }
6808 // applied only to some room types
6809 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
6810 continue;
6811 }
6812
6813 // count affected nights of stay
6814 $affdays = 0;
6815 $season_fromdayts = $fromdayts;
6816 $is_dst = date('I', $season_fromdayts);
6817 for ($i = 0; $i < $a[0]['days']; $i++) {
6818 $todayts = $season_fromdayts + ($i * 86400);
6819 $is_now_dst = date('I', $todayts);
6820 if ($is_dst != $is_now_dst) {
6821 // Daylight Saving Time has changed, check how
6822 if ((bool)$is_dst === true) {
6823 $todayts += 3600;
6824 $season_fromdayts += 3600;
6825 } else {
6826 $todayts -= 3600;
6827 $season_fromdayts -= 3600;
6828 }
6829 $is_dst = $is_now_dst;
6830 }
6831 if ($todayts >= $inits && $todayts <= $ends) {
6832 // week days
6833 if ($filterwdays == true) {
6834 $checkwday = getdate($todayts);
6835 if (in_array($checkwday['wday'], $wdays)) {
6836 $affdays++;
6837 }
6838 } else {
6839 $affdays++;
6840 }
6841 //
6842 }
6843 }
6844 if (!($affdays > 0)) {
6845 // no nights affected
6846 continue;
6847 }
6848
6849 // apply the rule
6850 $applyseasons = true;
6851 $dailyprice = $a[0]['cost'] / $a[0]['days'];
6852
6853 // modification factor object
6854 $factor = new stdClass;
6855
6856 // calculate new price progressively
6857 if (intval($s['val_pcent']) == 2) {
6858 // percentage value
6859 $factor->pcent = 1;
6860 $pctval = $s['diffcost'];
6861 if (strlen((string)$s['losoverride'])) {
6862 // values overrides
6863 $arrvaloverrides = [];
6864 $valovrparts = explode('_', $s['losoverride']);
6865 foreach ($valovrparts as $valovr) {
6866 if (!empty($valovr)) {
6867 $ovrinfo = explode(':', $valovr);
6868 if (strstr($ovrinfo[0], '-i') != false) {
6869 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6870 if ((int)$ovrinfo[0] < $a[0]['days']) {
6871 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6872 }
6873 }
6874 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6875 }
6876 }
6877 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6878 $pctval = $arrvaloverrides[$a[0]['days']];
6879 }
6880 }
6881 if (intval($s['type']) == 1) {
6882 // charge
6883 $factor->type = '+';
6884 $cpercent = 100 + $pctval;
6885 } else {
6886 // discount
6887 $factor->type = '-';
6888 $cpercent = 100 - $pctval;
6889 }
6890 $factor->amount = $pctval;
6891 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
6892 } else {
6893 // absolute value
6894 $factor->pcent = 0;
6895 $absval = $s['diffcost'];
6896 if (strlen((string)$s['losoverride'])) {
6897 // values overrides
6898 $arrvaloverrides = [];
6899 $valovrparts = explode('_', $s['losoverride']);
6900 foreach ($valovrparts as $valovr) {
6901 if (!empty($valovr)) {
6902 $ovrinfo = explode(':', $valovr);
6903 if (strstr($ovrinfo[0], '-i') != false) {
6904 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
6905 if ((int)$ovrinfo[0] < $a[0]['days']) {
6906 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
6907 }
6908 }
6909 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
6910 }
6911 }
6912 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
6913 $absval = $arrvaloverrides[$a[0]['days']];
6914 }
6915 }
6916 if (intval($s['type']) == 1) {
6917 // charge
6918 $factor->type = '+';
6919 $newprice = ($dailyprice + $absval) * $affdays;
6920 } else {
6921 // discount
6922 $factor->type = '-';
6923 $newprice = ($dailyprice - $absval) * $affdays;
6924 }
6925 $factor->amount = $absval;
6926 }
6927
6928 // apply rounding
6929 $factor->roundmode = $s['roundmode'];
6930 if (!empty($s['roundmode'])) {
6931 $newprice = round($newprice, 0, constant($s['roundmode']));
6932 } else {
6933 $newprice = round($newprice, 2);
6934 }
6935
6936 // define the promotion (only if no value overrides set the amount to 0)
6937 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
6938 /**
6939 * Include the discount information (if any). The cost re-calculated may not be
6940 * precise if multiple special prices were applied over the same dates.
6941 *
6942 * @since 1.13
6943 */
6944 if ($s['type'] == 2 && $s['diffcost'] > 0) {
6945 $promotion['discount'] = [
6946 'amount' => $s['diffcost'],
6947 'pcent' => (int)($s['val_pcent'] == 2),
6948 ];
6949 }
6950 //
6951 $mem[$k]['promotion'] = $promotion;
6952 }
6953
6954 // define the occupancy override
6955 if (array_key_exists($a[0]['idroom'], $occupancy_ovr) && $occupancy_ovr[$a[0]['idroom']]) {
6956 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a[0]['idroom']];
6957 }
6958
6959 // push difference generated only if to be applied progressively
6960 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
6961 /**
6962 * Push the difference generated by this special price for later transliteration of final price,
6963 * only if the special price is calculated progressively and not on the final price.
6964 *
6965 * @since 1.13.5
6966 */
6967 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
6968 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
6969 /**
6970 * This is a % promotion to be applied on the final price, so we need to save that this memory key
6971 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
6972 * all differences applied by the previous special pricing rules.
6973 *
6974 * @since 1.13.5
6975 */
6976 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
6977 array_push($mem[$k]['trans_factors'], $factor);
6978 array_push($mem[$k]['trans_affdays'], $affdays);
6979 }
6980
6981 // push values in memory array
6982 array_push($mem[$k]['sum'], $newprice);
6983 $mem[$k]['daysused'] += $affdays;
6984 array_push($roomschange, $a[0]['idroom']);
6985 }
6986 }
6987 if ($applyseasons) {
6988 foreach ($mem as $k => $v) {
6989 if ($v['daysused'] > 0 && $v['sum']) {
6990 $newprice = 0;
6991 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
6992 $restdays = $arr[$k][0]['days'] - $v['daysused'];
6993 $addrest = $restdays * $dailyprice;
6994 $newprice += $addrest;
6995
6996 // calculate new final cost
6997 $redo_rounding = null;
6998 foreach ($v['sum'] as $sum_index => $add) {
6999 /**
7000 * The application of the various special pricing rules is made in a progressive and cumulative way
7001 * by always starting from the room base cost or its average daily cost. However, promotions may need
7002 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7003 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7004 * onto the differences generated by all the regular and progressively applied special pricing rules.
7005 *
7006 * @since 1.13.5
7007 */
7008 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7009 /**
7010 * This progressive price difference must be applied on the room final cost, so we need to
7011 * apply the transliteration over the other differences applied by other special prices.
7012 */
7013 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7014 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7015 // this is the % promotion we are looking for applying it on the final cost
7016 $factor = $v['trans_factors'][$transliterate_key];
7017 if (is_object($factor) && $factor->pcent) {
7018 $final_factor = 0;
7019 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7020 $final_factor += $prog_diff * $factor->amount / 100;
7021 }
7022 // update rounding
7023 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7024
7025 /**
7026 * Leverage the final factor depending on how many nights this affected.
7027 *
7028 * @since 1.15.0 (J) - 1.5.0 (WP)
7029 */
7030 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k][0]['days']) {
7031 $avg_final_factor = $final_factor / $arr[$k][0]['days'];
7032 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7033 }
7034
7035 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7036 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7037 }
7038 }
7039 }
7040
7041 // apply new price progressively
7042 $newprice += $add;
7043 }
7044
7045 // apply rounding from factor
7046 if (!empty($redo_rounding)) {
7047 $newprice = round($newprice, 0, constant($redo_rounding));
7048 }
7049
7050 // set promotion (if any)
7051 if (isset($v['promotion'])) {
7052 $arr[$k][0]['promotion'] = $v['promotion'];
7053 }
7054
7055 // set occupancy overrides (if any)
7056 if (isset($v['occupancy_ovr'])) {
7057 $arr[$k][0]['occupancy_ovr'] = $v['occupancy_ovr'];
7058 }
7059
7060 // set new final cost and update nights affected
7061 $arr[$k][0]['cost'] = $newprice;
7062 $arr[$k][0]['affdays'] = $v['daysused'];
7063 }
7064 }
7065 }
7066 }
7067
7068 // week days with no season
7069 $roomschange = array_unique($roomschange);
7070 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7071 $dbo->setQuery($q);
7072 $specials = $dbo->loadAssocList();
7073 if ($specials) {
7074 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7075 $applyseasons = false;
7076 unset($mem);
7077 $mem = [];
7078 foreach ($arr as $k => $a) {
7079 $mem[$k]['daysused'] = 0;
7080 $mem[$k]['sum'] = [];
7081 }
7082 foreach ($specials as $s) {
7083 // check if this is a promotion registered for skipping
7084 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7085 continue;
7086 }
7087
7088 // Special Price tied to the year
7089 if (!empty($s['year']) && $s['year'] > 0) {
7090 if ($one['year'] != $s['year']) {
7091 continue;
7092 }
7093 }
7094
7095 $allrooms = explode(",", $s['idrooms']);
7096 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7097 // week days
7098 $filterwdays = !empty($s['wdays']) ? true : false;
7099 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7100 if (is_array($wdays) && $wdays) {
7101 foreach ($wdays as $kw => $wd) {
7102 if (strlen($wd) == 0) {
7103 unset($wdays[$kw]);
7104 }
7105 }
7106 }
7107
7108 foreach ($arr as $k => $a) {
7109 // only rooms with no price modifications from seasons
7110
7111 // applied only to some types of price
7112 if ($allprices && !empty($allprices[0])) {
7113 if (!in_array("-" . $a[0]['idprice'] . "-", $allprices)) {
7114 continue;
7115 }
7116 }
7117
7118 /**
7119 * We should not exclude the rooms that already had a modification of the price through a season
7120 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
7121 * The control " || in_array($a[0]['idroom'], $roomschange)" was removed from the IF below.
7122 *
7123 * @since 1.11
7124 */
7125 if (!in_array("-" . $a[0]['idroom'] . "-", $allrooms)) {
7126 continue;
7127 }
7128
7129 $affdays = 0;
7130 $season_fromdayts = $fromdayts;
7131 $is_dst = date('I', $season_fromdayts);
7132 for ($i = 0; $i < $a[0]['days']; $i++) {
7133 $todayts = $season_fromdayts + ($i * 86400);
7134 $is_now_dst = date('I', $todayts);
7135 if ($is_dst != $is_now_dst) {
7136 // Daylight Saving Time has changed, check how
7137 if ((bool)$is_dst === true) {
7138 $todayts += 3600;
7139 $season_fromdayts += 3600;
7140 } else {
7141 $todayts -= 3600;
7142 $season_fromdayts -= 3600;
7143 }
7144 $is_dst = $is_now_dst;
7145 }
7146 // week days
7147 if ($filterwdays == true) {
7148 $checkwday = getdate($todayts);
7149 if (in_array($checkwday['wday'], $wdays)) {
7150 $affdays++;
7151 }
7152 }
7153 }
7154 if (!($affdays > 0)) {
7155 // no nights affected
7156 continue;
7157 }
7158
7159 // apply the rule
7160 $applyseasons = true;
7161 $dailyprice = $a[0]['cost'] / $a[0]['days'];
7162
7163 if (intval($s['val_pcent']) == 2) {
7164 // percentage value
7165 $pctval = $s['diffcost'];
7166 if (strlen((string)$s['losoverride'])) {
7167 // values overrides
7168 $arrvaloverrides = [];
7169 $valovrparts = explode('_', $s['losoverride']);
7170 foreach ($valovrparts as $valovr) {
7171 if (!empty($valovr)) {
7172 $ovrinfo = explode(':', $valovr);
7173 if (strstr($ovrinfo[0], '-i') != false) {
7174 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7175 if ((int)$ovrinfo[0] < $a[0]['days']) {
7176 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7177 }
7178 }
7179 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7180 }
7181 }
7182 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7183 $pctval = $arrvaloverrides[$a[0]['days']];
7184 }
7185 }
7186 if (intval($s['type']) == 1) {
7187 // charge
7188 $cpercent = 100 + $pctval;
7189 } else {
7190 // discount
7191 $cpercent = 100 - $pctval;
7192 }
7193 $newprice = ($dailyprice * $cpercent / 100) * $affdays;
7194 } else {
7195 // absolute value
7196 $absval = $s['diffcost'];
7197 if (strlen((string)$s['losoverride'])) {
7198 // values overrides
7199 $arrvaloverrides = [];
7200 $valovrparts = explode('_', $s['losoverride']);
7201 foreach ($valovrparts as $valovr) {
7202 if (!empty($valovr)) {
7203 $ovrinfo = explode(':', $valovr);
7204 if (strstr($ovrinfo[0], '-i') != false) {
7205 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7206 if ((int)$ovrinfo[0] < $a[0]['days']) {
7207 $arrvaloverrides[$a[0]['days']] = $ovrinfo[1];
7208 }
7209 }
7210 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7211 }
7212 }
7213 if (array_key_exists($a[0]['days'], $arrvaloverrides)) {
7214 $absval = $arrvaloverrides[$a[0]['days']];
7215 }
7216 }
7217 if (intval($s['type']) == 1) {
7218 // charge
7219 $newprice = ($dailyprice + $absval) * $affdays;
7220 } else {
7221 // discount
7222 $newprice = ($dailyprice - $absval) * $affdays;
7223 }
7224 }
7225
7226 // apply rounding
7227 if (!empty($s['roundmode'])) {
7228 $newprice = round($newprice, 0, constant($s['roundmode']));
7229 } else {
7230 $newprice = round($newprice, 2);
7231 }
7232
7233 // push values in memory array
7234 array_push($mem[$k]['sum'], $newprice);
7235 $mem[$k]['daysused'] += $affdays;
7236 }
7237 }
7238 if ($applyseasons) {
7239 foreach ($mem as $k => $v) {
7240 if ($v['daysused'] > 0 && $v['sum']) {
7241 $newprice = 0;
7242 $dailyprice = $arr[$k][0]['cost'] / $arr[$k][0]['days'];
7243 $restdays = $arr[$k][0]['days'] - $v['daysused'];
7244 $addrest = $restdays * $dailyprice;
7245 $newprice += $addrest;
7246 foreach ($v['sum'] as $add) {
7247 $newprice += $add;
7248 }
7249 $arr[$k][0]['cost'] = $newprice;
7250 $arr[$k][0]['affdays'] = $v['daysused'];
7251 }
7252 }
7253 }
7254 }
7255 // end week days with no season
7256
7257 return $arr;
7258 }
7259
7260 /**
7261 * Applies the special prices over an array of tariffs.
7262 * The function is also used by VCM (>= 1.6.5) with specific arguments.
7263 *
7264 * @param array $arr array of tariffs taken from the DB
7265 * @param int $from start timestamp
7266 * @param int $to end timestamp
7267 * @param array $parsed_season array of a season to parse (used to render the seasons calendars in back-end and front-end)
7268 * @param array $seasons_dates (VBO 1.10) array of seasons with dates filter taken from the DB to avoid multiple queries (VCM)
7269 * @param array $seasons_wdays (VBO 1.10) array of seasons with weekdays filter (only) taken from the DB to avoid multiple queries (VCM)
7270 *
7271 * @return array
7272 */
7273 public static function applySeasonsRoom(array $arr, $from, $to, array $parsed_season = [], array $seasons_dates = [], array $seasons_wdays = [])
7274 {
7275 static $cached_seasons = [];
7276
7277 $cache_signature = '';
7278 $cache_signature_wdays = '';
7279 if (self::setSeasonsCache() !== false && !$parsed_season && !$seasons_dates && !$seasons_wdays) {
7280 // enable season records caching
7281 $cache_signature = "{$from}_{$to}";
7282 $cache_signature_wdays = "wdays";
7283 }
7284
7285 $dbo = JFactory::getDbo();
7286 $vbo_tn = self::getTranslator();
7287
7288 $roomschange = [];
7289 $one = getdate($from);
7290
7291 // leap years
7292 if ($one['year'] % 4 == 0 && ($one['year'] % 100 != 0 || $one['year'] % 400 == 0)) {
7293 $isleap = true;
7294 } else {
7295 $isleap = false;
7296 }
7297
7298 $baseone = mktime(0, 0, 0, 1, 1, $one['year']);
7299 $tomidnightone = intval($one['hours']) * 3600;
7300 $tomidnightone += intval($one['minutes']) * 60;
7301 $sfrom = $from - $baseone - $tomidnightone;
7302 $fromdayts = mktime(0, 0, 0, $one['mon'], $one['mday'], $one['year']);
7303 $two = getdate($to);
7304 $basetwo = mktime(0, 0, 0, 1, 1, $two['year']);
7305 $tomidnighttwo = intval($two['hours']) * 3600;
7306 $tomidnighttwo += intval($two['minutes']) * 60;
7307 $sto = $to - $basetwo - $tomidnighttwo;
7308
7309 // leap years, last day of the month of the season
7310 if ($isleap) {
7311 $leapts = mktime(0, 0, 0, 2, 29, $two['year']);
7312 if ($two[0] > $leapts) {
7313 /**
7314 * Timestamp must be greater than the leap-day of Feb 29th.
7315 * It used to be checked for >= $leapts.
7316 *
7317 * @since July 3rd 2019
7318 */
7319 $sfrom -= 86400;
7320 $sto -= 86400;
7321 } elseif ($sto < $sfrom && $one['year'] < $two['year']) {
7322 // lower checkin date when in leap year but not for checkout
7323 $sfrom -= 86400;
7324 }
7325 }
7326
7327 // check for DST changes to adjust the query values to fetch
7328 if (date('I', $from) != date('I', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] - 1), $one['year']))) {
7329 if (date('Y-m-d', $to) == date('Y-m-d', mktime($one['hours'], $one['minutes'], $one['seconds'], $one['mon'], ($one['mday'] + 1), $one['year']))) {
7330 // we are parsing the day when the DST changed (probably a Sunday)
7331 if (!date('I', $from)) {
7332 // DST was just turned off
7333 $sfrom -= 3600;
7334 }
7335 }
7336 }
7337
7338 // count nights requested
7339 $booking_nights = 1;
7340 foreach ($arr as $k => $a) {
7341 if (isset($a['booking_nights'])) {
7342 // this value may be set when displaying pricing calendars
7343 $booking_nights = $a['booking_nights'];
7344 break;
7345 }
7346 if (isset($a['days'])) {
7347 $booking_nights = $a['days'];
7348 break;
7349 }
7350 }
7351
7352 /**
7353 * Get a list of promotion IDs that may have been set to avoid duplicate discounts on OTAs.
7354 *
7355 * @since 1.16.4 (J) - 1.6.4 (WP)
7356 */
7357 $skip_promo_ids = self::registerPromotionIds();
7358
7359 $totseasons = 0;
7360 if (!$parsed_season && !$seasons_dates) {
7361 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE (" .
7362 ($sto > $sfrom ? "(`from` <= " . $sfrom . " AND `to` >= " . $sto . ") " : "") .
7363 ($sto > $sfrom ? "OR (`from` <= " . $sfrom . " AND `to` >= " . $sfrom . ") " : "(`from` <= " . $sfrom . " AND `to` <= " . $sfrom . " AND `from` > `to`) ") .
7364 ($sto > $sfrom ? "OR (`from` <= " . $sto . " AND `to` >= " . $sto . ") " : "OR (`from` >= " . $sto . " AND `to` >= " . $sto . " AND `from` > `to`) ") .
7365 ($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`)") .
7366 ($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`)") .
7367 ($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`)") .
7368 ($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`)") .
7369 ($sto < $sfrom ? " OR (`from` = 0 AND `to` >= " . $sto . " AND `to` >= " . $sfrom . ")" : '') .
7370 ") ORDER BY `#__vikbooking_seasons`.`promo` ASC;";
7371
7372 if ($cache_signature && isset($cached_seasons[$cache_signature])) {
7373 // avoid making a query
7374 $seasons = $cached_seasons[$cache_signature];
7375 } else {
7376 // get the season records by running the query
7377 $dbo->setQuery($q);
7378 $seasons = $dbo->loadAssocList();
7379 }
7380
7381 // count total seasons
7382 $totseasons = $seasons ? count($seasons) : 0;
7383
7384 if ($cache_signature) {
7385 // cache season records
7386 $cached_seasons[$cache_signature] = $seasons;
7387 }
7388 }
7389
7390 if ($totseasons > 0 || $parsed_season || $seasons_dates) {
7391 if ($totseasons > 0) {
7392 $seasons = $seasons;
7393 } elseif ($parsed_season) {
7394 $seasons = [$parsed_season];
7395 } else {
7396 $seasons = $seasons_dates;
7397 }
7398 $vbo_tn->translateContents($seasons, '#__vikbooking_seasons');
7399 $applyseasons = false;
7400 $mem = [];
7401 foreach ($arr as $k => $a) {
7402 $mem[$k]['daysused'] = 0;
7403 $mem[$k]['sum'] = [];
7404 $mem[$k]['spids'] = [];
7405 /**
7406 * The keys below are all needed to apply the promotions on the room's final cost.
7407 *
7408 * @since 1.13.5
7409 */
7410 $mem[$k]['diffs'] = [];
7411 $mem[$k]['trans_keys'] = [];
7412 $mem[$k]['trans_factors'] = [];
7413 $mem[$k]['trans_affdays'] = [];
7414 }
7415 $affdayslistless = [];
7416 foreach ($seasons as $s) {
7417 // 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
7418 if (empty($s['from']) && empty($s['to']) && !empty($s['wdays'])) {
7419 // a season for Jan 1st to Jan 1st (1 day), with NO week-days filter is still accepted
7420 continue;
7421 }
7422
7423 /**
7424 * VCM may build a fake season as a "restriction placeholder" if no special prices found.
7425 * We need to skip such fake seasons as they do not need any parsing.
7426 *
7427 * @since 1.13
7428 */
7429 if (empty($s['from']) && empty($s['to']) && empty($s['diffcost']) && !isset($s['from'])) {
7430 continue;
7431 }
7432
7433 // check if this is a promotion registered for skipping
7434 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7435 continue;
7436 }
7437
7438 // Special Price tied to the year
7439 if (!empty($s['year']) && $s['year'] > 0) {
7440 //VBO 1.7 - do not skip seasons tied to the year for bookings between two years
7441 if ($one['year'] != $s['year'] && $two['year'] != $s['year']) {
7442 //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
7443 if (($one['year'] - $s['year']) != 1 || $s['from'] < $s['to']) {
7444 continue;
7445 }
7446 //VBO 1.9 - tied to 2016 going through Jan 2017: dates of December 2017 should skip this speacial price
7447 if (($one['year'] - $s['year']) == 1 && $s['from'] > $s['to']) {
7448 $calc_ends = mktime(0, 0, 0, 1, 1, ($s['year'] + 1)) + $s['to'];
7449 if ($calc_ends < ($from - $tomidnightone)) {
7450 continue;
7451 }
7452 }
7453 } elseif ($one['year'] < $s['year'] && $two['year'] == $s['year']) {
7454 //VBO 1.9 - season tied to the year 2017 accross 2018 and we are parsing dates accross prev year 2016-2017
7455 if ($s['from'] > $s['to']) {
7456 continue;
7457 }
7458 if (($basetwo + $s['from'] + 86399) > $to) {
7459 /**
7460 * Assuming that we are on 2021, and we are booking a 2-night stay from 30/12 to 01/01. This statement involves
7461 * a special price tied to the year 2022 for the night of 31/12 (or near dates), but we are booking the night of
7462 * New Year's Eve of 2021, and so the special price pre-prepared for the year after (2022) should be ignored.
7463 *
7464 * @since 1.14.3 (J) - 1.4.3 (WP)
7465 */
7466 continue;
7467 }
7468 } elseif ($one['year'] == $s['year'] && $two['year'] > $s['year']) {
7469 if (($baseone + $s['to'] + 86399) < $from && $s['from'] < $s['to']) {
7470 /**
7471 * Assuming that we are on 2021, and we are booking a 4-night stay from 29/12 to 02/01. This statement involves
7472 * a special price tied to the year 2021 for the night of 01/01 (or near dates), but we are booking the night of
7473 * First of the Year of 2022, and so the old special price for the year before (2021) should be ignored.
7474 *
7475 * @since 1.14.3 (J) - 1.4.3 (WP)
7476 */
7477 continue;
7478 }
7479 } elseif ($one['year'] == $s['year'] && $two['year'] == $s['year'] && $s['from'] > $s['to']) {
7480 // 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)
7481 if (($baseone + $s['from']) > $to) {
7482 continue;
7483 }
7484 }
7485 }
7486 //
7487 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7488 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7489 $inits = $baseone + $s['from'];
7490 if ($s['from'] < $s['to']) {
7491 $ends = $basetwo + $s['to'];
7492 // check if the inits must be set to the year after
7493 // ex. Season Jan 6 to Feb 12 - Booking Dec 31 to Jan 8 to charge Jan 6,7
7494 if ($sfrom > $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto <= $s['to'] && $s['from'] < $s['to'] && $sfrom > $sto) {
7495 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7496 $inits = $tmpbase + $s['from'];
7497 } elseif ($sfrom >= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7498 // Season Dec 23 to Dec 29 - Booking Dec 29 to Jan 5
7499 $ends = $baseone + $s['to'];
7500 } elseif ($sfrom <= $s['from'] && $sfrom <= $s['to'] && $sto < $s['from'] && $sto < $s['to'] && $sfrom > $sto) {
7501 // Season Dec 30 to Dec 31 - Booking Dec 29 to Jan 5
7502 $ends = $baseone + $s['to'];
7503 } elseif ($sfrom > $s['from'] && $sfrom > $s['to'] && $sto >= $s['from'] && ($sto >= $s['to'] || $sto <= $s['to']) && $sfrom > $sto) {
7504 // Season Jan 1 to Jan 2 - Booking Dec 29 to Jan 5
7505 $inits = $basetwo + $s['from'];
7506 } elseif ($sfrom > $sto && !empty($s['year']) && ($one['year'] != $s['year'] || $two['year'] != $s['year']) && !($one['year'] == $s['year'] && $two['year'] == $s['year'])) {
7507 // booking dates across two years for a season tied to a specific year
7508 if ($one['year'] == $s['year']) {
7509 $inits = $baseone + $s['from'];
7510 } else {
7511 $inits = $basetwo + $s['from'];
7512 }
7513 if ($two['year'] == $s['year']) {
7514 $ends = $basetwo + $s['to'];
7515 } else {
7516 $ends = $baseone + $s['to'];
7517 }
7518 }
7519 } else {
7520 // between 2 years
7521 if ($baseone < $basetwo) {
7522 // ex. 29/12/2012 - 14/01/2013
7523 $ends = $basetwo + $s['to'];
7524 } else {
7525 if (($sfrom >= $s['from'] && $sto >= $s['from']) || ($sfrom < $s['from'] && $sto >= $s['from'] && $sfrom > $s['to'] && $sto > $s['to'])) {
7526 // ex. 25/12 - 30/12 with init season on 20/12 OR 27/12 for counting 28,29,30/12
7527 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] + 1));
7528 $ends = $tmpbase + $s['to'];
7529 } else {
7530 // ex. 03/01 - 09/01
7531 $ends = $basetwo + $s['to'];
7532 $tmpbase = mktime(0, 0, 0, 1, 1, ($one['year'] - 1));
7533 $inits = $tmpbase + $s['from'];
7534 }
7535 }
7536 }
7537 // leap years
7538 if ($isleap == true) {
7539 $infoseason = getdate($inits);
7540 $leapts = mktime(0, 0, 0, 2, 29, $infoseason['year']);
7541 // VikBooking 1.6 added below && $infoseason['year'] == $one['year']
7542 // for those seasons like 2015 Dec 14 to 2016 Jan 5 and booking dates like 2016 Jan 1 to Jan 6 where 2015 is not leap
7543 if ($infoseason[0] > $leapts && $infoseason['year'] == $one['year']) {
7544 /**
7545 * Timestamp must be greater than the leap-day of Feb 29th.
7546 * It used to be checked for >= $leapts.
7547 *
7548 * @since July 3rd 2019
7549 */
7550 $inits += 86400;
7551 $ends += 86400;
7552 }
7553 }
7554
7555 // promotions
7556 $promotion = [];
7557 if ($s['promo'] == 1) {
7558 $daysadv = (($inits - time()) / 86400);
7559 $daysadv = $daysadv > 0 ? (int)ceil($daysadv) : 0;
7560 if (!empty($s['promodaysadv']) && $s['promodaysadv'] > $daysadv) {
7561 continue;
7562 } elseif (!empty($s['promolastmin']) && $s['promolastmin'] > 0) {
7563 $secstocheckin = ($from - time());
7564 if ($s['promolastmin'] < $secstocheckin) {
7565 // VBO 1.11 - too many seconds to the check-in date, skip this last minute promotion
7566 continue;
7567 }
7568 }
7569 if ($s['promominlos'] > 1 && $booking_nights < $s['promominlos']) {
7570 /**
7571 * The minimum length of stay parameter is also taken to exclude the promotion from the calculation.
7572 *
7573 * @since 1.13.5
7574 */
7575 continue;
7576 }
7577 $promotion['todaydaysadv'] = $daysadv;
7578 $promotion['promodaysadv'] = $s['promodaysadv'];
7579 $promotion['promotxt'] = $s['promotxt'];
7580 }
7581
7582 // occupancy override
7583 $occupancy_ovr = !empty($s['occupancy_ovr']) ? json_decode($s['occupancy_ovr'], true) : [];
7584
7585 //week days
7586 $filterwdays = !empty($s['wdays']) ? true : false;
7587 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7588 if (is_array($wdays) && $wdays) {
7589 foreach ($wdays as $kw => $wd) {
7590 if (strlen($wd) == 0) {
7591 unset($wdays[$kw]);
7592 }
7593 }
7594 }
7595
7596 // checkin must be after the begin of the season
7597 $checkininclok = true;
7598 if ($s['checkinincl'] == 1) {
7599 $checkininclok = false;
7600 if ($s['from'] < $s['to']) {
7601 if ($sfrom >= $s['from'] && $sfrom <= $s['to']) {
7602 $checkininclok = true;
7603 }
7604 } else {
7605 if (($sfrom >= $s['from'] && $sfrom > $s['to']) || ($sfrom < $s['from'] && $sfrom <= $s['to'])) {
7606 $checkininclok = true;
7607 }
7608 }
7609 }
7610 if ($checkininclok !== true) {
7611 continue;
7612 }
7613
7614 foreach ($arr as $k => $a) {
7615 // applied only to some types of price
7616 if ($allprices && !empty($allprices[0])) {
7617 // VikBooking 1.6: Price Calendar sets the idprice to -1
7618 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
7619 continue;
7620 }
7621 }
7622 // applied only to some room types
7623 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
7624 continue;
7625 }
7626
7627 if (isset($a['days']) && is_float($a['days'])) {
7628 // prevent any possible warning with array_key_exists()
7629 $a['days'] = (int)$a['days'];
7630 }
7631
7632 $affdays = 0;
7633 $season_fromdayts = $fromdayts;
7634 $is_dst = date('I', $season_fromdayts);
7635 for ($i = 0; $i < $a['days']; $i++) {
7636 $todayts = $season_fromdayts + ($i * 86400);
7637 $is_now_dst = date('I', $todayts);
7638 if ($is_dst != $is_now_dst) {
7639 // Daylight Saving Time has changed, check how
7640 if ((bool)$is_dst === true) {
7641 $todayts += 3600;
7642 $season_fromdayts += 3600;
7643 } else {
7644 $todayts -= 3600;
7645 $season_fromdayts -= 3600;
7646 }
7647 $is_dst = $is_now_dst;
7648 }
7649 if ($todayts >= $inits && $todayts <= $ends) {
7650 $checkwday = getdate($todayts);
7651 // week days
7652 if ($filterwdays == true) {
7653 if (in_array($checkwday['wday'], $wdays)) {
7654 if (!isset($arr[$k]['affdayslist'])) {
7655 $arr[$k]['affdayslist'] = [];
7656 }
7657 $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;
7658 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7659 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7660 $affdays++;
7661 }
7662 } else {
7663 if (!isset($arr[$k]['affdayslist'])) {
7664 $arr[$k]['affdayslist'] = [];
7665 }
7666 $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;
7667 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
7668 $affdayslistless[$s['id']][] = $checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon'];
7669 $affdays++;
7670 }
7671 //
7672 }
7673 }
7674
7675 if (!($affdays > 0)) {
7676 // no nights affected
7677 continue;
7678 }
7679
7680 // apply the rule
7681 $applyseasons = true;
7682 $dailyprice = $a['cost'] / $a['days'];
7683
7684 // modification factor object
7685 $factor = new stdClass;
7686
7687 // calculate new price progressively
7688 if (intval($s['val_pcent']) == 2) {
7689 // percentage value
7690 $factor->pcent = 1;
7691 $pctval = $s['diffcost'];
7692 if (strlen((string)$s['losoverride'])) {
7693 // values overrides
7694 $arrvaloverrides = [];
7695 $valovrparts = explode('_', $s['losoverride']);
7696 foreach ($valovrparts as $valovr) {
7697 if (!empty($valovr)) {
7698 $ovrinfo = explode(':', $valovr);
7699 if (strstr($ovrinfo[0], '-i') != false) {
7700 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7701 if ((int)$ovrinfo[0] < $a['days']) {
7702 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7703 }
7704 }
7705 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7706 }
7707 }
7708 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7709 $pctval = $arrvaloverrides[$a['days']];
7710 }
7711 }
7712 if (intval($s['type']) == 1) {
7713 // charge
7714 $factor->type = '+';
7715 $cpercent = 100 + $pctval;
7716 } else {
7717 // discount
7718 $factor->type = '-';
7719 $cpercent = 100 - $pctval;
7720 }
7721 $factor->amount = $pctval;
7722 $dailysum = ($dailyprice * $cpercent / 100);
7723 $newprice = $dailysum * $affdays;
7724 } else {
7725 // absolute value
7726 $factor->pcent = 0;
7727 $absval = $s['diffcost'];
7728 if (strlen((string)$s['losoverride'])) {
7729 // values overrides
7730 $arrvaloverrides = [];
7731 $valovrparts = explode('_', $s['losoverride']);
7732 foreach ($valovrparts as $valovr) {
7733 if (!empty($valovr)) {
7734 $ovrinfo = explode(':', $valovr);
7735 if (strstr($ovrinfo[0], '-i') != false) {
7736 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
7737 if ((int)$ovrinfo[0] < $a['days']) {
7738 $arrvaloverrides[$a['days']] = $ovrinfo[1];
7739 }
7740 }
7741 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
7742 }
7743 }
7744 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
7745 $absval = $arrvaloverrides[$a['days']];
7746 }
7747 }
7748 if (intval($s['type']) == 1) {
7749 // charge
7750 $factor->type = '+';
7751 $dailysum = ($dailyprice + $absval);
7752 $newprice = $dailysum * $affdays;
7753 } else {
7754 // discount
7755 $factor->type = '-';
7756 $dailysum = ($dailyprice - $absval);
7757 $newprice = $dailysum * $affdays;
7758 }
7759 $factor->amount = $absval;
7760 }
7761
7762 // apply rounding
7763 $factor->roundmode = $s['roundmode'];
7764 if (!empty($s['roundmode'])) {
7765 $newprice = round($newprice, 0, constant($s['roundmode']));
7766 } else {
7767 $newprice = round($newprice, 2);
7768 }
7769
7770 // define the promotion (only if no value overrides set the amount to 0)
7771 if ($promotion && ((isset($absval) && $absval > 0) || $pctval > 0)) {
7772 /**
7773 * Include the discount information (if any). The cost re-calculated may not be
7774 * precise if multiple special prices were applied over the same dates.
7775 *
7776 * @since 1.13
7777 */
7778 if ($s['type'] == 2 && $s['diffcost'] > 0) {
7779 $promotion['discount'] = [
7780 'amount' => $s['diffcost'],
7781 'pcent' => (int)($s['val_pcent'] == 2),
7782 ];
7783 }
7784 //
7785 $mem[$k]['promotion'] = $promotion;
7786 }
7787
7788 // define the occupancy override
7789 if (array_key_exists($a['idroom'], $occupancy_ovr) && $occupancy_ovr[$a['idroom']]) {
7790 $mem[$k]['occupancy_ovr'] = $occupancy_ovr[$a['idroom']];
7791 }
7792
7793 // affected days list
7794 if (isset($arr[$k]['affdayslist']) && is_array($arr[$k]['affdayslist'])) {
7795 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
7796 if (isset($affdayslistless[$s['id']]) && in_array($affk, $affdayslistless[$s['id']])) {
7797 $arr[$k]['affdayslist'][$affk] = !empty($arr[$k]['affdayslist'][$affk]) && $arr[$k]['affdayslist'][$affk] > 0 ? ($arr[$k]['affdayslist'][$affk] - $arr[$k]['origdailycost'] + $dailysum) : ($affv + $dailysum);
7798 }
7799 }
7800 }
7801
7802 // push special price ID
7803 if (!in_array($s['id'], $mem[$k]['spids'])) {
7804 array_push($mem[$k]['spids'], $s['id']);
7805 }
7806
7807 // push difference generated only if to be applied progressively
7808 if (!$s['promo'] || ($s['promo'] && !$s['promofinalprice'])) {
7809 /**
7810 * Push the difference generated by this special price for later transliteration of final price,
7811 * only if the special price is calculated progressively and not on the final price.
7812 *
7813 * @since 1.13.5
7814 */
7815 array_push($mem[$k]['diffs'], ($newprice - ($dailyprice * $affdays)));
7816 } elseif ($s['promo'] && $s['promofinalprice'] && $factor->pcent) {
7817 /**
7818 * This is a % promotion to be applied on the final price, so we need to save that this memory key
7819 * will need the transliteration, aka adjusting this new price by applying the charge/discount on
7820 * all differences applied by the previous special pricing rules.
7821 *
7822 * @since 1.13.5
7823 */
7824 array_push($mem[$k]['trans_keys'], count($mem[$k]['sum']));
7825 array_push($mem[$k]['trans_factors'], $factor);
7826 array_push($mem[$k]['trans_affdays'], $affdays);
7827 }
7828
7829 // push values in memory array
7830 array_push($mem[$k]['sum'], $newprice);
7831 $mem[$k]['daysused'] += $affdays;
7832 array_push($roomschange, $a['idroom']);
7833 }
7834 }
7835 if ($applyseasons) {
7836 foreach ($mem as $k => $v) {
7837 if ($v['daysused'] > 0 && $v['sum']) {
7838 $newprice = 0;
7839 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
7840 $restdays = $arr[$k]['days'] - $v['daysused'];
7841 $addrest = $restdays * $dailyprice;
7842 $newprice += $addrest;
7843
7844 // calculate new final cost
7845 $redo_rounding = null;
7846 foreach ($v['sum'] as $sum_index => $add) {
7847 /**
7848 * The application of the various special pricing rules is made in a progressive and cumulative way
7849 * by always starting from the room base cost or its average daily cost. However, promotions may need
7850 * to be applied on the room final cost, and not in a progresive way. In order to keep the progressive
7851 * algorithm, for applying the special prices on the room final cost we need to apply the same promotion
7852 * onto the differences generated by all the regular and progressively applied special pricing rules.
7853 *
7854 * @since 1.13.5
7855 */
7856 if (in_array($sum_index, $v['trans_keys']) && $v['diffs']) {
7857 /**
7858 * This progressive price difference must be applied on the room final cost, so we need to
7859 * apply the transliteration over the other differences applied by other special prices.
7860 */
7861 $transliterate_key = array_search($sum_index, $v['trans_keys']);
7862 if ($transliterate_key !== false && isset($v['trans_factors'][$transliterate_key])) {
7863 // this is the % promotion we are looking for applying it on the final cost
7864 $factor = $v['trans_factors'][$transliterate_key];
7865 if (is_object($factor) && $factor->pcent) {
7866 $final_factor = 0;
7867 foreach ($v['diffs'] as $diff_index => $prog_diff) {
7868 $final_factor += $prog_diff * $factor->amount / 100;
7869 }
7870 // update rounding
7871 $redo_rounding = !empty($factor->roundmode) ? $factor->roundmode : $redo_rounding;
7872
7873 /**
7874 * Leverage the final factor depending on how many nights this affected.
7875 *
7876 * @since 1.15.0 (J) - 1.5.0 (WP)
7877 */
7878 if (isset($v['trans_affdays'][$transliterate_key]) && $v['trans_affdays'][$transliterate_key] < $arr[$k]['days']) {
7879 $avg_final_factor = $final_factor / $arr[$k]['days'];
7880 $final_factor = $avg_final_factor * $v['trans_affdays'][$transliterate_key];
7881 }
7882
7883 // apply the final transliteration to obtain a value like if it was applied on the room's final cost
7884 $add = $factor->type == '+' ? ($add + $final_factor) : ($add - $final_factor);
7885 }
7886 }
7887 }
7888
7889 // apply new price progressively
7890 $newprice += $add;
7891 }
7892
7893 // apply rounding from factor
7894 if (!empty($redo_rounding)) {
7895 $newprice = round($newprice, 0, constant($redo_rounding));
7896 }
7897
7898 // set promotion (if any)
7899 if (isset($v['promotion'])) {
7900 $arr[$k]['promotion'] = $v['promotion'];
7901 }
7902
7903 // set occupancy overrides (if any)
7904 if (isset($v['occupancy_ovr'])) {
7905 $arr[$k]['occupancy_ovr'] = $v['occupancy_ovr'];
7906 }
7907
7908 // set new final cost and update nights affected
7909 $arr[$k]['cost'] = $newprice;
7910 $arr[$k]['affdays'] = $v['daysused'];
7911 if (array_key_exists('spids', $v) && $v['spids']) {
7912 $arr[$k]['spids'] = $v['spids'];
7913 }
7914 }
7915 }
7916 }
7917 }
7918
7919 // week days with no season
7920 $roomschange = array_unique($roomschange);
7921 $totspecials = 0;
7922 if (!$seasons_wdays) {
7923 $q = "SELECT * FROM `#__vikbooking_seasons` WHERE ((`from` = 0 AND `to` = 0) OR (`from` IS NULL AND `to` IS NULL));";
7924
7925 if ($cache_signature_wdays && isset($cached_seasons[$cache_signature_wdays])) {
7926 // avoid making a query
7927 $specials = $cached_seasons[$cache_signature_wdays];
7928 } else {
7929 // get the week-days-with-no-season records by running the query
7930 $dbo->setQuery($q);
7931 $specials = $dbo->loadAssocList();
7932 }
7933
7934 // count records
7935 $totspecials = $specials ? count($specials) : 0;
7936
7937 if ($cache_signature_wdays) {
7938 // cache week-days-with-no-season records
7939 $cached_seasons[$cache_signature_wdays] = $specials;
7940 }
7941 }
7942 if ($totspecials > 0 || $seasons_wdays) {
7943 $specials = $totspecials > 0 ? $specials : $seasons_wdays;
7944 $vbo_tn->translateContents($specials, '#__vikbooking_seasons');
7945 $applyseasons = false;
7946 /**
7947 * We no longer unset the previous memory of the seasons with dates filters
7948 * because we need the responses to be merged. We do it only if not set.
7949 * We only keep the property 'spids' but the others should be unset.
7950 *
7951 * @since 1.11
7952 */
7953 if (!isset($mem)) {
7954 $mem = [];
7955 foreach ($arr as $k => $a) {
7956 $mem[$k]['daysused'] = 0;
7957 $mem[$k]['sum'] = [];
7958 $mem[$k]['spids'] = [];
7959 }
7960 } else {
7961 foreach ($arr as $k => $a) {
7962 $mem[$k]['daysused'] = 0;
7963 $mem[$k]['sum'] = [];
7964 }
7965 }
7966 //
7967 foreach ($specials as $s) {
7968 // 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
7969 if (!empty($s['from']) || !empty($s['to'])) {
7970 continue;
7971 }
7972
7973 // check if this is a promotion registered for skipping
7974 if (isset($s['promo']) && $s['promo'] && in_array($s['id'], $skip_promo_ids)) {
7975 continue;
7976 }
7977
7978 // Special Price tied to the year
7979 if (!empty($s['year']) && $s['year'] > 0) {
7980 if ($one['year'] != $s['year']) {
7981 continue;
7982 }
7983 }
7984
7985 $allrooms = !empty($s['idrooms']) ? explode(",", $s['idrooms']) : [];
7986 $allprices = !empty($s['idprices']) ? explode(",", $s['idprices']) : [];
7987 // week days
7988 $filterwdays = !empty($s['wdays']) ? true : false;
7989 $wdays = $filterwdays == true ? explode(';', $s['wdays']) : '';
7990 if (is_array($wdays) && $wdays) {
7991 foreach ($wdays as $kw => $wd) {
7992 if (strlen($wd) == 0) {
7993 unset($wdays[$kw]);
7994 }
7995 }
7996 }
7997
7998 foreach ($arr as $k => $a) {
7999 // only rooms with no price modifications from seasons
8000
8001 // applied only to some types of price
8002 if ($allprices && !empty($allprices[0])) {
8003 // VikBooking 1.6: Price Calendar sets the idprice to -1
8004 if (!in_array("-" . $a['idprice'] . "-", $allprices) && $a['idprice'] > 0) {
8005 continue;
8006 }
8007 }
8008
8009 /**
8010 * We should not exclude the rooms that already had a modification of the price through a season
8011 * with a dates filter or we risk to get invalid prices by skipping a rule for just some weekdays.
8012 * The control " || in_array($a['idroom'], $roomschange)" was removed from the IF below.
8013 *
8014 * @since 1.11
8015 */
8016 if (!in_array("-" . $a['idroom'] . "-", $allrooms)) {
8017 continue;
8018 }
8019
8020 if (isset($a['days']) && is_float($a['days'])) {
8021 // prevent any possible warning with array_key_exists()
8022 $a['days'] = (int)$a['days'];
8023 }
8024
8025 $affdays = 0;
8026 $season_fromdayts = $fromdayts;
8027 $is_dst = date('I', $season_fromdayts);
8028 for ($i = 0; $i < $a['days']; $i++) {
8029 $todayts = $season_fromdayts + ($i * 86400);
8030 $is_now_dst = date('I', $todayts);
8031 if ($is_dst != $is_now_dst) {
8032 // Daylight Saving Time has changed, check how
8033 if ((bool)$is_dst === true) {
8034 $todayts += 3600;
8035 $season_fromdayts += 3600;
8036 } else {
8037 $todayts -= 3600;
8038 $season_fromdayts -= 3600;
8039 }
8040 $is_dst = $is_now_dst;
8041 }
8042 // week days
8043 if ($filterwdays == true) {
8044 $checkwday = getdate($todayts);
8045 if (in_array($checkwday['wday'], $wdays)) {
8046 $arr[$k]['affdayslist'][$checkwday['wday'].'-'.$checkwday['mday'].'-'.$checkwday['mon']] = 0;
8047 $arr[$k]['origdailycost'] = $a['cost'] / $a['days'];
8048 $affdays++;
8049 }
8050 }
8051 //
8052 }
8053
8054 if (!($affdays > 0)) {
8055 // no nights affected
8056 continue;
8057 }
8058
8059 // apply the rule
8060 $applyseasons = true;
8061 $dailyprice = $a['cost'] / $a['days'];
8062
8063 if (intval($s['val_pcent']) == 2) {
8064 // percentage value
8065 $pctval = $s['diffcost'];
8066 if (strlen((string)$s['losoverride'])) {
8067 // values overrides
8068 $arrvaloverrides = [];
8069 $valovrparts = explode('_', $s['losoverride']);
8070 foreach ($valovrparts as $valovr) {
8071 if (!empty($valovr)) {
8072 $ovrinfo = explode(':', $valovr);
8073 if (strstr($ovrinfo[0], '-i') != false) {
8074 $ovrinfo[0] = str_replace('-i', '', $ovrinfo[0]);
8075 if ((int)$ovrinfo[0] < $a['days']) {
8076 $arrvaloverrides[$a['days']] = $ovrinfo[1];
8077 }
8078 }
8079 $arrvaloverrides[$ovrinfo[0]] = $ovrinfo[1];
8080 }
8081 }
8082 if (isset($a['days']) && array_key_exists($a['days'], $arrvaloverrides)) {
8083 $pctval = $arrvaloverrides[$a['days']];
8084 }
8085 }
8086 if (intval($s['type']) == 1) {
8087 // charge
8088 $cpercent = 100 + $pctval;
8089 } else {
8090 // discount
8091 $cpercent = 100 - $pctval;
8092 }
8093 $dailysum = ($dailyprice * $cpercent / 100);
8094 $newprice = $dailysum * $affdays;
8095 } else {
8096 // absolute value
8097 $absval = $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 $absval = $arrvaloverrides[$a['days']];
8116 }
8117 }
8118 if (intval($s['type']) == 1) {
8119 // charge
8120 $dailysum = ($dailyprice + $absval);
8121 $newprice = $dailysum * $affdays;
8122 } else {
8123 // discount
8124 $dailysum = ($dailyprice - $absval);
8125 $newprice = $dailysum * $affdays;
8126 }
8127 }
8128
8129 // apply rounding
8130 if (!empty($s['roundmode'])) {
8131 $newprice = round($newprice, 0, constant($s['roundmode']));
8132 } else {
8133 $newprice = round($newprice, 2);
8134 }
8135
8136 foreach ($arr[$k]['affdayslist'] as $affk => $affv) {
8137 $arr[$k]['affdayslist'][$affk] = $affv + $dailysum;
8138 }
8139 if (!in_array($s['id'], $mem[$k]['spids'])) {
8140 $mem[$k]['spids'][] = $s['id'];
8141 }
8142 $mem[$k]['sum'][] = $newprice;
8143 $mem[$k]['daysused'] += $affdays;
8144 }
8145 }
8146 if ($applyseasons) {
8147 foreach ($mem as $k => $v) {
8148 if ($v['daysused'] > 0 && $v['sum']) {
8149 $newprice = 0;
8150 $dailyprice = $arr[$k]['cost'] / $arr[$k]['days'];
8151 $restdays = $arr[$k]['days'] - $v['daysused'];
8152 $addrest = $restdays * $dailyprice;
8153 $newprice += $addrest;
8154 foreach ($v['sum'] as $add) {
8155 $newprice += $add;
8156 }
8157 $arr[$k]['cost'] = $newprice;
8158 $arr[$k]['affdays'] = $v['daysused'];
8159 if (array_key_exists('spids', $v) && $v['spids']) {
8160 $arr[$k]['spids'] = $v['spids'];
8161 }
8162 }
8163 }
8164 }
8165 }
8166 // end week days with no season
8167
8168 return $arr;
8169 }
8170
8171 public static function getRoomRplansClosingDates($idroom)
8172 {
8173 $dbo = JFactory::getDbo();
8174
8175 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8176 $dbo->setQuery($q);
8177 $price_records = $dbo->loadAssocList();
8178 if (!$price_records) {
8179 return [];
8180 }
8181
8182 $closingd = [];
8183 foreach ($price_records as $prec) {
8184 if (empty($prec['closingd'])) {
8185 continue;
8186 }
8187 $price_closing = json_decode($prec['closingd'], true);
8188 if (!$price_closing || !isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8189 continue;
8190 }
8191 // check expired dates and clean up
8192 $today_midnight = mktime(0, 0, 0);
8193 $cleaned = false;
8194 foreach ($price_closing[$idroom] as $k => $v) {
8195 if (strtotime($v) < $today_midnight) {
8196 $cleaned = true;
8197 unset($price_closing[$idroom][$k]);
8198 }
8199 }
8200 if (!$price_closing[$idroom]) {
8201 unset($price_closing[$idroom]);
8202 } elseif ($cleaned === true) {
8203 // reset array keys for smaller JSON size
8204 $price_closing[$idroom] = array_values($price_closing[$idroom]);
8205 }
8206 if ($cleaned === true) {
8207 $q = "UPDATE `#__vikbooking_prices` SET `closingd`=".(count($price_closing) > 0 ? $dbo->quote(json_encode($price_closing)) : "NULL")." WHERE `id`=".$prec['id'].";";
8208 $dbo->setQuery($q);
8209 $dbo->execute();
8210 }
8211 if (!isset($price_closing[$idroom]) || !$price_closing[$idroom]) {
8212 continue;
8213 }
8214 $closingd[$prec['id']] = $price_closing[$idroom];
8215 }
8216
8217 return $closingd;
8218 }
8219
8220 public static function getRoomRplansClosedInDates($roomids, $checkints, $numnights)
8221 {
8222 $dbo = JFactory::getDbo();
8223
8224 $closingd = [];
8225
8226 $q = "SELECT * FROM `#__vikbooking_prices` WHERE `closingd` IS NOT NULL;";
8227 $dbo->setQuery($q);
8228 $price_records = $dbo->loadAssocList();
8229 if ($price_records && $roomids) {
8230 $info_start = getdate($checkints);
8231 $checkin_midnight = mktime(0, 0, 0, $info_start['mon'], $info_start['mday'], $info_start['year']);
8232 $all_nights = [];
8233 for ($i = 0; $i < (int)$numnights; $i++) {
8234 $next_midnight = mktime(0, 0, 0, $info_start['mon'], ($info_start['mday'] + $i), $info_start['year']);
8235 $all_nights[] = date('Y-m-d', $next_midnight);
8236 }
8237 foreach ($price_records as $prec) {
8238 if (empty($prec['closingd'])) {
8239 continue;
8240 }
8241 $price_closing = json_decode($prec['closingd'], true);
8242 if (!$price_closing) {
8243 continue;
8244 }
8245 foreach ($price_closing as $idroom => $rclosedd) {
8246 if (!in_array($idroom, $roomids) || !is_array($rclosedd)) {
8247 continue;
8248 }
8249 if (!array_key_exists($idroom, $closingd)) {
8250 $closingd[$idroom] = [];
8251 }
8252 foreach ($all_nights as $night) {
8253 if (in_array($night, $rclosedd)) {
8254 if (array_key_exists($prec['id'], $closingd[$idroom])) {
8255 $closingd[$idroom][$prec['id']][] = $night;
8256 } else {
8257 $closingd[$idroom][$prec['id']] = [$night];
8258 }
8259 }
8260 }
8261 }
8262 }
8263 }
8264
8265 return $closingd;
8266 }
8267
8268 /**
8269 * Tells whether at least one payment method must be selected to check-out.
8270 *
8271 * @param array $rooms_involved optional list of room IDs being booked.
8272 *
8273 * @return bool true if at least one payment method is applicable.
8274 *
8275 * @since 1.16.0 (J) - 1.6.0 (WP) added 1st argument to check the room associations.
8276 */
8277 public static function areTherePayments($rooms_involved = [])
8278 {
8279 $dbo = JFactory::getDbo();
8280
8281 $q = "SELECT `id`, `idrooms` FROM `#__vikbooking_gpayments` WHERE `published`=1;";
8282 $dbo->setQuery($q);
8283 $dbo->execute();
8284
8285 if (!$dbo->getNumRows()) {
8286 // no published payment options found
8287 return false;
8288 }
8289
8290 $pay_options = $dbo->loadAssocList();
8291
8292 if (!count($rooms_involved)) {
8293 // if no filters given, return true
8294 return true;
8295 }
8296
8297 // validate room associations
8298 foreach ($pay_options as $pay_k => $pay_v) {
8299 if (empty($pay_v['idrooms'])) {
8300 continue;
8301 }
8302 $rooms_supported = json_decode($pay_v['idrooms']);
8303 if (!is_array($rooms_supported) || !count($rooms_supported)) {
8304 continue;
8305 }
8306 // make sure all rooms being booked are supported
8307 foreach ($rooms_involved as $room_id_booked) {
8308 if (!in_array($room_id_booked, $rooms_supported)) {
8309 // we need to unset this payment option as soon as we find one non-matching room ID
8310 unset($pay_options[$pay_k]);
8311 break;
8312 }
8313 }
8314 }
8315
8316 return (bool)count($pay_options);
8317 }
8318
8319 public static function getPayment($idp, $vbo_tn = null)
8320 {
8321 if (empty($idp)) {
8322 return false;
8323 }
8324
8325 if (strstr((string)$idp, '=') !== false) {
8326 $parts = explode('=', $idp);
8327 $idp = $parts[0];
8328 }
8329
8330 $dbo = JFactory::getDbo();
8331
8332 $q = "SELECT * FROM `#__vikbooking_gpayments` WHERE `id`=" . $dbo->quote($idp) . ";";
8333 $dbo->setQuery($q);
8334 $payment = $dbo->loadAssocList();
8335 if (!$payment) {
8336 return false;
8337 }
8338
8339 if (is_object($vbo_tn)) {
8340 $vbo_tn->translateContents($payment, '#__vikbooking_gpayments');
8341 }
8342
8343 return $payment[0];
8344 }
8345
8346 public static function getCronKey() {
8347 $dbo = JFactory::getDbo();
8348 $ckey = '';
8349 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='cronkey';";
8350 $dbo->setQuery($q);
8351 $dbo->execute();
8352 if ($dbo->getNumRows() > 0) {
8353 $cval = $dbo->loadAssocList();
8354 $ckey = $cval[0]['setting'];
8355 }
8356 return $ckey;
8357 }
8358
8359 public static function getNextInvoiceNumber () {
8360 $dbo = JFactory::getDbo();
8361 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='invoiceinum';";
8362 $dbo->setQuery($q);
8363 $dbo->execute();
8364 $s = $dbo->loadAssocList();
8365 return (intval($s[0]['setting']) + 1);
8366 }
8367
8368 public static function getInvoiceNumberSuffix () {
8369 $dbo = JFactory::getDbo();
8370 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='invoicesuffix';";
8371 $dbo->setQuery($q);
8372 $dbo->execute();
8373 $s = $dbo->loadAssocList();
8374 return $s[0]['setting'];
8375 }
8376
8377 public static function getInvoiceCompanyInfo () {
8378 $dbo = JFactory::getDbo();
8379 $q="SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='invcompanyinfo';";
8380 $dbo->setQuery($q);
8381 $dbo->execute();
8382 $s=$dbo->loadAssocList();
8383 return $s[0]['setting'];
8384 }
8385
8386 /**
8387 * Gets the number for the next booking receipt generation,
8388 * updates the last receipt number used for the later iterations,
8389 * stores a new receipt record to keep track of the receipts issued.
8390 *
8391 * @param int $bid the Booking ID for which we are/want to generating the receipt.
8392 * @param [int] $newnum the last number used for generating the receipt.
8393 *
8394 * @return int
8395 */
8396 public static function getNextReceiptNumber ($bid, $newnum = false) {
8397 $dbo = JFactory::getDbo();
8398 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptinum';";
8399 $dbo->setQuery($q);
8400 $dbo->execute();
8401 if ($dbo->getNumRows() > 0) {
8402 $s = $dbo->loadResult();
8403 //check if this booking has already a receipt, and return that number
8404 $prev_receipt = array();
8405 $q = "SELECT * FROM `#__vikbooking_receipts` WHERE `idorder`=".(int)$bid.";";
8406 $dbo->setQuery($q);
8407 $dbo->execute();
8408 if ($dbo->getNumRows() > 0) {
8409 $prev_receipt = $dbo->loadAssoc();
8410 }
8411 //update value (receipt generated)
8412 if ($newnum !== false && $newnum > 0) {
8413 $s = (int)$newnum;
8414 if (!(count($prev_receipt) > 0)) {
8415 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$s." WHERE `param`='receiptinum';";
8416 $dbo->setQuery($q);
8417 $dbo->execute();
8418 //insert the new receipt record
8419 $q = "INSERT INTO `#__vikbooking_receipts` (`number`,`idorder`,`created_on`) VALUES (".(int)$newnum.", ".(int)$bid.", ".time().");";
8420 $dbo->setQuery($q);
8421 $dbo->execute();
8422 } else {
8423 //update receipt record
8424 $q = "UPDATE `#__vikbooking_receipts` SET `number`=".(int)$newnum.",`created_on`=".time()." WHERE `idorder`=".(int)$bid.";";
8425 $dbo->setQuery($q);
8426 $dbo->execute();
8427 }
8428 }
8429 //
8430 return count($prev_receipt) > 0 ? (int)$prev_receipt['number'] : ((int)$s + 1);
8431 }
8432 //first execution of the method should create the configuration record
8433 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptinum', '0');";
8434 $dbo->setQuery($q);
8435 $dbo->execute();
8436 return 1;
8437 }
8438
8439 public static function getReceiptNotes ($newnotes = false) {
8440 $dbo = JFactory::getDbo();
8441 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='receiptnotes';";
8442 $dbo->setQuery($q);
8443 $dbo->execute();
8444 if ($dbo->getNumRows() > 0) {
8445 $s = $dbo->loadResult();
8446 //update value
8447 if ($newnotes !== false) {
8448 $s = $newnotes;
8449 $q = "UPDATE `#__vikbooking_config` SET `setting`=".$dbo->quote($s)." WHERE `param`='receiptnotes';";
8450 $dbo->setQuery($q);
8451 $dbo->execute();
8452 }
8453 //
8454 return $s;
8455 }
8456 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('receiptnotes', '');";
8457 $dbo->setQuery($q);
8458 $dbo->execute();
8459 return "";
8460 }
8461
8462 public static function loadColorTagsRules()
8463 {
8464 $color_tag_rules = [
8465 0 => 'VBOCOLORTAGRULECUSTOMCOLOR',
8466 'pend1' => 'VBWAITINGFORPAYMENT',
8467 'conf1' => 'VBDBTEXTROOMCLOSED',
8468 'conf2' => 'VBOCOLORTAGRULECONFTWO',
8469 'conf3' => 'VBOCOLORTAGRULECONFTHREE',
8470 'inv1' => 'VBOCOLORTAGRULEINVONE',
8471 'rcp1' => 'VBOCOLORTAGRULERCPONE',
8472 'conf4' => 'VBOCOLORTAGRULECONFFOUR',
8473 'conf5' => 'VBOCOLORTAGRULECONFFIVE',
8474 'inv2' => 'VBOCOLORTAGRULEINVTWO',
8475 ];
8476
8477 /**
8478 * Trigger event to let third-party plugins override the default booking color tag rules.
8479 *
8480 * @since 1.16.4 (J) - 1.6.4 (WP)
8481 */
8482 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadColorTagRules', [&$color_tag_rules]);
8483
8484 return $color_tag_rules;
8485 }
8486
8487 public static function loadDefaultColorTags()
8488 {
8489 $default_color_tags = [
8490 [
8491 'color' => '#9b9b9b',
8492 'name' => 'VBWAITINGFORPAYMENT',
8493 'rule' => 'pend1'
8494 ],
8495 [
8496 'color' => '#333333',
8497 'name' => 'VBDBTEXTROOMCLOSED',
8498 'rule' => 'conf1'
8499 ],
8500 [
8501 'color' => '#ff8606',
8502 'name' => 'VBOCOLORTAGRULECONFTWO',
8503 'rule' => 'conf2'
8504 ],
8505 [
8506 'color' => '#0418c9',
8507 'name' => 'VBOCOLORTAGRULECONFTHREE',
8508 'rule' => 'conf3'
8509 ],
8510 [
8511 'color' => '#bed953',
8512 'name' => 'VBOCOLORTAGRULEINVONE',
8513 'rule' => 'inv1'
8514 ],
8515 [
8516 'color' => '#67f5b5',
8517 'name' => 'VBOCOLORTAGRULERCPONE',
8518 'rule' => 'rcp1'
8519 ],
8520 [
8521 'color' => '#04d2c2',
8522 'name' => 'VBOCOLORTAGRULECONFFOUR',
8523 'rule' => 'conf4'
8524 ],
8525 [
8526 'color' => '#00b316',
8527 'name' => 'VBOCOLORTAGRULECONFFIVE',
8528 'rule' => 'conf5'
8529 ],
8530 [
8531 'color' => '#00f323',
8532 'name' => 'VBOCOLORTAGRULEINVTWO',
8533 'rule' => 'inv2'
8534 ],
8535 ];
8536
8537 /**
8538 * Trigger event to let third-party plugins override the default color tags.
8539 *
8540 * @since 1.16.4 (J) - 1.6.4 (WP)
8541 */
8542 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeLoadDefaultColorTags', [&$default_color_tags]);
8543
8544 return $default_color_tags;
8545 }
8546
8547 public static function loadBookingsColorTags()
8548 {
8549 $bookings_ctags = VBOFactory::getConfig()->getArray('bookingsctags', []);
8550
8551 return is_array($bookings_ctags) && $bookings_ctags ? $bookings_ctags : self::loadDefaultColorTags();
8552 }
8553
8554 public static function getBestColorContrast($hexcolor)
8555 {
8556 $hexcolor = str_replace('#', '', $hexcolor);
8557
8558 if (empty($hexcolor) || strlen($hexcolor) != 6) {
8559 return '#000000';
8560 }
8561
8562 $r = hexdec(substr($hexcolor, 0, 2));
8563 $g = hexdec(substr($hexcolor, 2, 2));
8564 $b = hexdec(substr($hexcolor, 4, 2));
8565
8566 // Counting the perceptive luminance - human eye favors green color
8567 // < 0.5 bright colors
8568 // > 0.5 dark colors
8569
8570 return (1 - ( 0.299 * $r + 0.587 * $g + 0.114 * $b ) / 255) < 0.5 ? '#000000' : '#ffffff';
8571 }
8572
8573 public static function applyBookingColorTag($booking, $tags = [])
8574 {
8575 if (!is_array($tags) || !$tags) {
8576 $tags = self::loadBookingsColorTags();
8577 }
8578
8579 if (!empty($booking['colortag'])) {
8580 $color_tag_arr = json_decode($booking['colortag'], true);
8581 if (is_array($color_tag_arr) && array_key_exists('color', $color_tag_arr)) {
8582 $color_tag_arr['fontcolor'] = self::getBestColorContrast($color_tag_arr['color']);
8583 return $color_tag_arr;
8584 }
8585 }
8586
8587 $dbo = JFactory::getDbo();
8588
8589 $bid = array_key_exists('idorder', $booking) ? $booking['idorder'] : $booking['id'];
8590 $docs_data = [];
8591 $invoice_numb = false;
8592 $receipt_numb = false;
8593
8594 if ($booking['status'] == 'confirmed') {
8595 $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`
8596 FROM `#__vikbooking_orders` AS `b`
8597 LEFT JOIN `#__vikbooking_invoices` AS `i` ON `b`.`id`=`i`.`idorder`
8598 LEFT JOIN `#__vikbooking_receipts` AS `r` ON `b`.`id`=`r`.`idorder`
8599 WHERE `b`.`id`=" . (int)$bid . ";";
8600 $dbo->setQuery($q);
8601 $docs_data = $dbo->loadAssoc();
8602 if ($docs_data) {
8603 $invoice_numb = (!empty($docs_data['inv_id']));
8604 $receipt_numb = (!empty($docs_data['rcp_id']));
8605
8606 /**
8607 * We inject the invoice number or receipt number into the color tag rules.
8608 * This is useful for the Tableaux page to show the invoice/receipt number.
8609 *
8610 * @since 1.12 (J) - 1.1.7 (WP)
8611 */
8612 if ($invoice_numb || $receipt_numb) {
8613 foreach ($tags as &$tval) {
8614 if ($invoice_numb) {
8615 $tval['invoice_number'] = $docs_data['inv_number'];
8616 $tval['invoice_file_name'] = $docs_data['inv_file_name'];
8617 }
8618 if ($receipt_numb) {
8619 $tval['receipt_number'] = $docs_data['rcp_number'];
8620 }
8621 }
8622 // unset last reference
8623 unset($tval);
8624 }
8625 }
8626 }
8627
8628 /**
8629 * Some OTA bookings using an OTA-Collect business model may look as not entirely
8630 * paid when they actually are, due to commissions and pass through taxes. We also
8631 * take care of those OTA reservations that include a Virtual Credit Card (Hotel Collect).
8632 *
8633 * @since 1.16.3 (J) - 1.6.3 (WP)
8634 */
8635 $ota_collected_booking = false;
8636 $hotel_collect_vcc_unpaid = false;
8637 if ($booking['status'] == 'confirmed' && !empty($booking['idorderota']) && !empty($booking['channel']) && $booking['total'] > 0 && $booking['totpaid'] > 0) {
8638 if (stripos($booking['channel'], 'airbnb') !== false && ($booking['checkout'] < time() || date('Y-m-d', $booking['checkout']) == date('Y-m-d'))) {
8639 $ota_collected_booking = true;
8640 }
8641 if (!empty($booking['paymentlog']) && stripos($booking['paymentlog'], 'virtual credit card') !== false) {
8642 // VCC detected, check the history for a payment event
8643 $history_obj = self::getBookingHistoryInstance()->setBid($bid);
8644 // list of history types to fetch
8645 $h_payment_types = [];
8646 $h_payment_group = $history_obj->getTypeGroups($group = 'GPM');
8647 if ($h_payment_group && $h_payment_group['types']) {
8648 // merge default types with 'PU' for the manually set "new amount paid" and with 'RU' (refunded amount updated)
8649 $h_payment_types = array_unique(array_merge($h_payment_group['types'], ['PU', 'RU']));
8650 }
8651 $payment_ev_datas = $history_obj->getEventsWithData($h_payment_types, $callvalid = null, $onlydata = false);
8652 if (!$payment_ev_datas) {
8653 // this booking should be flagged as "unpaid" because a payment (manual/online) has never been recorded
8654 $hotel_collect_vcc_unpaid = true;
8655 }
8656 }
8657 }
8658
8659 foreach ($tags as $tkey => $tval) {
8660 if (empty($tval['rule'])) {
8661 continue;
8662 }
8663
8664 /**
8665 * Trigger event to let third-party plugins validate the custom rule.
8666 *
8667 * @since 1.16.4 (J) - 1.6.4 (WP)
8668 */
8669 $custom_rule = VBOFactory::getPlatform()->getDispatcher()->filter('onApplyBookingColorTag', [$ota_collected_booking, $hotel_collect_vcc_unpaid, $booking, $docs_data, $tval]);
8670 if (!empty($custom_rule)) {
8671 return (array) $custom_rule[0];
8672 }
8673
8674 switch ($tval['rule']) {
8675 case 'pend1':
8676 // Room is waiting for the payment (locked record)
8677 if ($booking['status'] == 'standby') {
8678 $q = "SELECT `id` FROM `#__vikbooking_tmplock` WHERE `idorder`=" . (int)$bid . " AND `until`>=" . time() . ";";
8679 $dbo->setQuery($q);
8680 $tmp_lock = $dbo->loadAssoc();
8681 if ($tmp_lock) {
8682 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8683 return $tval;
8684 }
8685 }
8686 break;
8687 case 'conf1':
8688 // Confirmed (Room Closed)
8689 if ($booking['status'] == 'confirmed' && $booking['custdata'] == JText::translate('VBDBTEXTROOMCLOSED')) {
8690 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8691 return $tval;
8692 }
8693 break;
8694 case 'conf2':
8695 // Confirmed (No Rate 0.00/NULL Total)
8696 if ($booking['status'] == 'confirmed' && (empty($booking['total']) || $booking['total'] <= 0.00 || $booking['total'] === null)) {
8697 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8698 return $tval;
8699 }
8700 break;
8701 case 'conf3':
8702 // Confirmed (Total > 0 && Total Paid = 0 && No Invoice && No Receipt)
8703 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) {
8704 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8705 return $tval;
8706 }
8707 break;
8708 case 'inv1':
8709 // Confirmed + Invoice (Total > 0 && Total Paid = 0 && Invoice Exists)
8710 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $invoice_numb !== false) {
8711 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8712 return $tval;
8713 }
8714 break;
8715 case 'rcp1':
8716 // Confirmed + Receipt Issued (Total > 0 && Total Paid = 0 && Receipt Issued)
8717 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && (empty($booking['totpaid']) || $booking['totpaid'] <= 0.00 || $booking['totpaid'] === null) && $receipt_numb !== false) {
8718 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8719 return $tval;
8720 }
8721 break;
8722 case 'conf4':
8723 // Confirmed (Total > 0 && Total Paid > 0 && Total Paid < Total)
8724 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && $booking['totpaid'] < $booking['total'] && !$ota_collected_booking) {
8725 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8726 return $tval;
8727 }
8728 break;
8729 case 'conf5':
8730 // Confirmed (Total > 0 && Total Paid >= Total)
8731 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && !$hotel_collect_vcc_unpaid) {
8732 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8733 return $tval;
8734 }
8735 break;
8736 case 'inv2':
8737 // Confirmed + Invoice + Paid (Total > 0 && Total Paid >= Total && Invoice Exists)
8738 if ($booking['status'] == 'confirmed' && $booking['total'] > 0 && $booking['totpaid'] > 0 && ($booking['totpaid'] >= $booking['total'] || $ota_collected_booking) && $invoice_numb !== false) {
8739 $tval['fontcolor'] = self::getBestColorContrast($tval['color']);
8740 return $tval;
8741 }
8742 break;
8743 default:
8744 // unsupported rule
8745 break;
8746 }
8747 }
8748
8749 return [];
8750 }
8751
8752 public static function getBookingInfoFromID($bid)
8753 {
8754 $dbo = JFactory::getDbo();
8755
8756 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . (int)$bid . ";";
8757 $dbo->setQuery($q);
8758 $booking_info = $dbo->loadAssoc();
8759 if ($booking_info) {
8760 return $booking_info;
8761 }
8762
8763 return [];
8764 }
8765
8766 /**
8767 * Load booking details for a given list of IDs.
8768 *
8769 * @param int $roomid the room integer number.
8770 * @param array $room_bids_pool list of booking IDs involved.
8771 *
8772 * @return array
8773 */
8774 public static function loadRoomIndexesBookings($roomid, array $room_bids_pool)
8775 {
8776 $dbo = JFactory::getDbo();
8777
8778 if (empty($roomid) || !$room_bids_pool) {
8779 return [];
8780 }
8781
8782 $room_features_bookings = [];
8783 $roomid = (int)$roomid;
8784
8785 $all_bids = [];
8786 foreach ($room_bids_pool as $day => $bids) {
8787 $all_bids = array_merge($all_bids, $bids);
8788 }
8789 $all_bids = array_map('intval', array_unique($all_bids));
8790 $all_bids_str = implode(', ', $all_bids);
8791
8792 $q = "SELECT `or`.`id`,`or`.`idorder`,`or`.`roomindex` FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idroom`={$roomid} AND `or`.`idorder` IN ({$all_bids_str})";
8793 $dbo->setQuery($q);
8794 $rbookings = $dbo->loadAssocList();
8795 if (!$rbookings) {
8796 return [];
8797 }
8798
8799 foreach ($rbookings as $k => $v) {
8800 if (empty($v['roomindex'])) {
8801 continue;
8802 }
8803 if (!isset($room_features_bookings[$v['roomindex']])) {
8804 $room_features_bookings[$v['roomindex']] = [];
8805 }
8806 $room_features_bookings[$v['roomindex']][] = $v['idorder'];
8807 }
8808
8809 return $room_features_bookings;
8810 }
8811
8812 public static function getSendEmailWhen() {
8813 $dbo = JFactory::getDbo();
8814 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='emailsendwhen';";
8815 $dbo->setQuery($q);
8816 $dbo->execute();
8817 if ($dbo->getNumRows() > 0) {
8818 $cval = $dbo->loadAssocList();
8819 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8820 } else {
8821 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('emailsendwhen','1');";
8822 $dbo->setQuery($q);
8823 $dbo->execute();
8824 }
8825 return 1;
8826 }
8827
8828 public static function getMinutesAutoRemove() {
8829 $dbo = JFactory::getDbo();
8830 $minar = 0;
8831 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='minautoremove';";
8832 $dbo->setQuery($q);
8833 $dbo->execute();
8834 if ($dbo->getNumRows() > 0) {
8835 $minar = (int)$dbo->loadResult();
8836 }
8837 return $minar;
8838 }
8839
8840 public static function getSMSAPIClass() {
8841 $dbo = JFactory::getDbo();
8842 $cfile = '';
8843 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsapi';";
8844 $dbo->setQuery($q);
8845 $dbo->execute();
8846 if ($dbo->getNumRows() > 0) {
8847 $cval = $dbo->loadAssocList();
8848 $cfile = $cval[0]['setting'];
8849 }
8850 return $cfile;
8851 }
8852
8853 public static function autoSendSMSEnabled() {
8854 $dbo = JFactory::getDbo();
8855 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsautosend';";
8856 $dbo->setQuery($q);
8857 $dbo->execute();
8858 if ($dbo->getNumRows() > 0) {
8859 $cval = $dbo->loadAssocList();
8860 return intval($cval[0]['setting']) > 0 ? true : false;
8861 }
8862 return false;
8863 }
8864
8865 public static function getSendSMSTo() {
8866 $dbo = JFactory::getDbo();
8867 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendto';";
8868 $dbo->setQuery($q);
8869 $dbo->execute();
8870 if ($dbo->getNumRows() > 0) {
8871 $cval = $dbo->loadAssocList();
8872 if (!empty($cval[0]['setting'])) {
8873 $sto = json_decode($cval[0]['setting'], true);
8874 if (is_array($sto)) {
8875 return $sto;
8876 }
8877 }
8878 }
8879 return array();
8880 }
8881
8882 public static function getSendSMSWhen() {
8883 $dbo = JFactory::getDbo();
8884 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smssendwhen';";
8885 $dbo->setQuery($q);
8886 $dbo->execute();
8887 if ($dbo->getNumRows() > 0) {
8888 $cval = $dbo->loadAssocList();
8889 return intval($cval[0]['setting']) > 1 ? 2 : 1;
8890 }
8891 return 1;
8892 }
8893
8894 public static function getSMSAdminPhone() {
8895 $dbo = JFactory::getDbo();
8896 $pnum = '';
8897 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsadminphone';";
8898 $dbo->setQuery($q);
8899 $dbo->execute();
8900 if ($dbo->getNumRows() > 0) {
8901 $cval = $dbo->loadAssocList();
8902 $pnum = $cval[0]['setting'];
8903 }
8904 return $pnum;
8905 }
8906
8907 public static function getSMSParams($as_array = true) {
8908 $dbo = JFactory::getDbo();
8909 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='smsparams';";
8910 $dbo->setQuery($q);
8911 $dbo->execute();
8912 if ($dbo->getNumRows() > 0) {
8913 $cval = $dbo->loadAssocList();
8914 if (!empty($cval[0]['setting'])) {
8915 if (!$as_array) {
8916 return $cval[0]['setting'];
8917 }
8918 $sparams = json_decode($cval[0]['setting'], true);
8919 if (is_array($sparams)) {
8920 return $sparams;
8921 }
8922 }
8923 }
8924 return array();
8925 }
8926
8927 public static function getSMSTemplate($vbo_tn = null, $booking_status = 'confirmed', $type = 'admin') {
8928 $dbo = JFactory::getDbo();
8929 switch (strtolower($booking_status)) {
8930 case 'standby':
8931 $status = 'pend';
8932 break;
8933 case 'cancelled':
8934 $status = 'canc';
8935 break;
8936 default:
8937 $status = '';
8938 break;
8939 }
8940 $paramtype = 'sms'.$type.'tpl'.$status;
8941 $q = "SELECT `id`,`setting` FROM `#__vikbooking_texts` WHERE `param`='".$paramtype."';";
8942 $dbo->setQuery($q);
8943 $dbo->execute();
8944 if ($dbo->getNumRows() < 1) {
8945 if ($status == 'canc') {
8946 //Type cancelled is used by VCM since v1.6.6
8947 $q = "INSERT INTO `#__vikbooking_texts` (`param`,`exp`,`setting`) VALUES ('".$paramtype."','".($type == 'admin' ? 'Administrator' : 'Customer')." SMS Template (Cancelled)','');";
8948 $dbo->setQuery($q);
8949 $dbo->execute();
8950 }
8951 return '';
8952 }
8953 $ft = $dbo->loadAssocList();
8954 if (is_object($vbo_tn)) {
8955 $vbo_tn->translateContents($ft, '#__vikbooking_texts');
8956 }
8957 return $ft[0]['setting'];
8958 }
8959
8960 public static function getSMSAdminTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8961 return self::getSMSTemplate($vbo_tn, $booking_status, 'admin');
8962 }
8963
8964 public static function getSMSCustomerTemplate($vbo_tn = null, $booking_status = 'confirmed') {
8965 return self::getSMSTemplate($vbo_tn, $booking_status, 'customer');
8966 }
8967
8968 public static function checkPhonePrefixCountry($phone, $country_threecode) {
8969 $dbo = JFactory::getDbo();
8970 $phone = str_replace(" ", '', trim($phone));
8971 $cprefix = '';
8972 if (!empty($country_threecode)) {
8973 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_3_code`=".$dbo->quote($country_threecode).";";
8974 $dbo->setQuery($q);
8975 $dbo->execute();
8976 if ($dbo->getNumRows() > 0) {
8977 $cprefix = $dbo->loadResult();
8978 $cprefix = str_replace(" ", '', trim($cprefix));
8979 }
8980 }
8981 if (!empty($cprefix)) {
8982 if (substr($phone, 0, 1) != '+') {
8983 if (substr($phone, 0, 2) == '00') {
8984 $phone = '+'.substr($phone, 2);
8985 } else {
8986 $phone = $cprefix.$phone;
8987 }
8988 }
8989 }
8990 return $phone;
8991 }
8992
8993 public static function parseAdminSMSTemplate($booking, $booking_rooms, $vbo_tn = null) {
8994 $tpl = self::getSMSAdminTemplate($vbo_tn, $booking['status']);
8995
8996 /**
8997 * Parse all conditional text rules.
8998 *
8999 * @since 1.14 (J) - 1.4.0 (WP)
9000 */
9001 self::getConditionalRulesInstance()
9002 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9003 ->parseTokens($tpl);
9004 //
9005
9006 $vbo_df = self::getDateFormat();
9007 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9008 $datesep = self::getDateSeparator();
9009
9010 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9011 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9012 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9013 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9014 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9015 if (!empty($booking['confirmnumber'])) {
9016 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9017 } else {
9018 $tpl = str_replace("{confirmnumb}", '', $tpl);
9019 }
9020 if (!empty($booking['idorderota'])) {
9021 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9022 } else {
9023 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9024 }
9025
9026 $rooms_booked = array();
9027 $rooms_names = array();
9028 $tot_adults = 0;
9029 $tot_children = 0;
9030 $tot_guests = 0;
9031 foreach ($booking_rooms as $broom) {
9032 $rooms_names[] = $broom['room_name'];
9033 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9034 $rooms_booked[$broom['room_name']] += 1;
9035 } else {
9036 $rooms_booked[$broom['room_name']] = 1;
9037 }
9038 $tot_adults += (int)$broom['adults'];
9039 $tot_children += (int)$broom['children'];
9040 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9041 }
9042 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9043 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9044 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9045 $rooms_booked_quant = array();
9046 foreach ($rooms_booked as $rname => $quant) {
9047 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9048 }
9049 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9050 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9051 $tpl = str_replace('{customer_country}', $booking['country_name'], $tpl);
9052 $tpl = str_replace('{customer_email}', $booking['custmail'], $tpl);
9053 $tpl = str_replace('{customer_phone}', $booking['phone'], $tpl);
9054 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9055 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9056 $remaining_bal = $booking['total'] - $booking['totpaid'];
9057 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9058
9059 return $tpl;
9060 }
9061
9062 public static function parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn = null, $force_text = null) {
9063 $tpl = !empty($force_text) ? $force_text : self::getSMSCustomerTemplate($vbo_tn, $booking['status']);
9064
9065 /**
9066 * Parse all conditional text rules.
9067 *
9068 * @since 1.14 (J) - 1.4.0 (WP)
9069 */
9070 self::getConditionalRulesInstance()
9071 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9072 ->parseTokens($tpl);
9073 //
9074
9075 $vbo_df = self::getDateFormat();
9076 $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y-m-d');
9077 $datesep = self::getDateSeparator();
9078
9079 $tpl = str_replace('{customer_name}', $booking['customer_name'], $tpl);
9080 $tpl = str_replace('{booking_id}', $booking['id'], $tpl);
9081 $tpl = str_replace('{checkin_date}', date(str_replace("/", $datesep, $df), $booking['checkin']), $tpl);
9082 $tpl = str_replace('{checkout_date}', date(str_replace("/", $datesep, $df), $booking['checkout']), $tpl);
9083 $tpl = str_replace('{num_nights}', $booking['days'], $tpl);
9084 if (!empty($booking['confirmnumber'])) {
9085 $tpl = str_replace("{confirmnumb}", $booking['confirmnumber'], $tpl);
9086 } else {
9087 $tpl = str_replace("{confirmnumb}", '', $tpl);
9088 }
9089 if (!empty($booking['idorderota'])) {
9090 $tpl = str_replace("{ota_booking_id}", $booking['idorderota'], $tpl);
9091 } else {
9092 $tpl = str_replace("{ota_booking_id}", '', $tpl);
9093 }
9094
9095 $rooms_booked = array();
9096 $rooms_names = array();
9097 $tot_adults = 0;
9098 $tot_children = 0;
9099 $tot_guests = 0;
9100 foreach ($booking_rooms as $broom) {
9101 $rooms_names[] = $broom['room_name'];
9102 if (array_key_exists($broom['room_name'], $rooms_booked)) {
9103 $rooms_booked[$broom['room_name']] += 1;
9104 } else {
9105 $rooms_booked[$broom['room_name']] = 1;
9106 }
9107 $tot_adults += (int)$broom['adults'];
9108 $tot_children += (int)$broom['children'];
9109 $tot_guests += ((int)$broom['adults'] + (int)$broom['children']);
9110 }
9111 $tpl = str_replace('{tot_adults}', $tot_adults, $tpl);
9112 $tpl = str_replace('{tot_children}', $tot_children, $tpl);
9113 $tpl = str_replace('{tot_guests}', $tot_guests, $tpl);
9114 $rooms_booked_quant = array();
9115 foreach ($rooms_booked as $rname => $quant) {
9116 $rooms_booked_quant[] = ($quant > 1 ? $quant.' ' : '').$rname;
9117 }
9118 $tpl = str_replace('{rooms_booked}', implode(', ', $rooms_booked_quant), $tpl);
9119 $tpl = str_replace('{rooms_names}', implode(', ', $rooms_names), $tpl);
9120 $tpl = str_replace('{total}', self::numberFormat($booking['total']), $tpl);
9121 $tpl = str_replace('{total_paid}', self::numberFormat($booking['totpaid']), $tpl);
9122 $remaining_bal = $booking['total'] - $booking['totpaid'];
9123 $tpl = str_replace('{remaining_balance}', self::numberFormat($remaining_bal), $tpl);
9124 $tpl = str_replace('{customer_pin}', $booking['customer_pin'], $tpl);
9125
9126 $use_sid = empty($booking['sid']) && !empty($booking['idorderota']) ? $booking['idorderota'] : $booking['sid'];
9127 $book_link = JUri::root() . 'index.php?option=com_vikbooking&view=booking&sid=' . $use_sid . '&ts=' . $booking['ts'];
9128
9129 if (VBOPlatformDetection::isWordPress()) {
9130 /**
9131 * @wponly Rewrite URI for front-end
9132 */
9133 $book_link = str_replace(JUri::root(), '', $book_link);
9134 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
9135 $itemid = $model->best(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9136 if ($itemid) {
9137 $book_link = JRoute::rewrite($book_link . "&Itemid={$itemid}", false);
9138 }
9139 } else {
9140 /**
9141 * @joomlaonly Rewrite URI for front-end
9142 */
9143 $bestitemid = self::findProperItemIdType(['booking'], (!empty($booking['lang']) ? $booking['lang'] : null));
9144 $uri_parts = [
9145 'option' => 'com_vikbooking',
9146 'view' => 'booking',
9147 'sid' => $use_sid,
9148 'ts' => $booking['ts'],
9149 ];
9150 if (!empty($bestitemid) && !empty($booking['lang']) && JFactory::getApplication()->isClient('administrator')) {
9151 // inject lang in query string for proper link routing
9152 $uri_parts['lang'] = $booking['lang'];
9153 }
9154 $book_link = self::externalroute('index.php?' . http_build_query($uri_parts), false, (!empty($bestitemid) ? $bestitemid : null));
9155 }
9156
9157 $tpl = str_replace('{booking_link}', $book_link, $tpl);
9158 // we use the alias tag {booking_url} to support just the URI unlike {order_link} that includes HTML
9159 $tpl = str_replace('{booking_url}', $book_link, $tpl);
9160
9161 return $tpl;
9162 }
9163
9164 public static function sendBookingSMS($oid, $skip_send_to = array(), $force_send_to = array(), $force_text = null) {
9165 $dbo = JFactory::getDbo();
9166 if (!class_exists('VboApplication')) {
9167 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
9168 }
9169 $vbo_app = new VboApplication;
9170 if (empty($oid)) {
9171 return false;
9172 }
9173 $sms_api = self::getSMSAPIClass();
9174 if (empty($sms_api)) {
9175 return false;
9176 }
9177 if (!is_file(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api)) {
9178 return false;
9179 }
9180 $sms_api_params = self::getSMSParams();
9181 if (!is_array($sms_api_params) || !(count($sms_api_params) > 0)) {
9182 return false;
9183 }
9184 if (!self::autoSendSMSEnabled() && !(count($force_send_to) > 0)) {
9185 return false;
9186 }
9187 $send_sms_to = self::getSendSMSTo();
9188 if (!(count($send_sms_to) > 0) && !(count($force_send_to) > 0)) {
9189 return false;
9190 }
9191 $booking = array();
9192 $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.";";
9193 $dbo->setQuery($q);
9194 $dbo->execute();
9195 if ($dbo->getNumRows() > 0) {
9196 $booking = $dbo->loadAssoc();
9197 }
9198 if (!(count($booking) > 0)) {
9199 return false;
9200 }
9201 if (strtolower($booking['status']) == 'standby' && self::getSendSMSWhen() < 2) {
9202 return false;
9203 }
9204 $booking_rooms = array();
9205 $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'].";";
9206 $dbo->setQuery($q);
9207 $dbo->execute();
9208 if ($dbo->getNumRows() > 0) {
9209 $booking_rooms = $dbo->loadAssocList();
9210 }
9211 $admin_phone = self::getSMSAdminPhone();
9212 $admin_sendermail = self::getSenderMail();
9213 $admin_email = self::getAdminMail();
9214 $f_result = false;
9215 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'smsapi'.DIRECTORY_SEPARATOR.$sms_api);
9216 if ((in_array('admin', $send_sms_to) && !empty($admin_phone) && !in_array('admin', $skip_send_to)) || in_array('admin', $force_send_to)) {
9217 //SMS for the administrator
9218 $sms_text = self::parseAdminSMSTemplate($booking, $booking_rooms);
9219 if (!empty($sms_text)) {
9220 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9221 //administrator phone can contain multiple numbers separated by comma or semicolon
9222 $admin_phones = array();
9223 if (strpos($admin_phone, ',') !== false) {
9224 $all_phones = explode(',', $admin_phone);
9225 foreach ($all_phones as $aph) {
9226 if (!empty($aph)) {
9227 $admin_phones[] = trim($aph);
9228 }
9229 }
9230 } elseif (strpos($admin_phone, ';') !== false) {
9231 $all_phones = explode(';', $admin_phone);
9232 foreach ($all_phones as $aph) {
9233 if (!empty($aph)) {
9234 $admin_phones[] = trim($aph);
9235 }
9236 }
9237 } else {
9238 $admin_phones[] = $admin_phone;
9239 }
9240 foreach ($admin_phones as $admphone) {
9241 $response_obj = $sms_obj->sendMessage($admphone, $sms_text);
9242 if ( !$sms_obj->validateResponse($response_obj) ) {
9243 //notify the administrator via email with the error of the SMS sending
9244 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDADMINSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9245 } else {
9246 $f_result = true;
9247 }
9248 }
9249 }
9250 }
9251 if ((in_array('customer', $send_sms_to) && !empty($booking['phone']) && !in_array('customer', $skip_send_to)) || in_array('customer', $force_send_to)) {
9252 //SMS for the Customer
9253 $vbo_tn = self::getTranslator();
9254 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'));
9255 $sms_text = self::parseCustomerSMSTemplate($booking, $booking_rooms, $vbo_tn, $force_text);
9256 if (!empty($sms_text)) {
9257 $sms_obj = new VikSmsApi($booking, $sms_api_params);
9258 $response_obj = $sms_obj->sendMessage($booking['phone'], $sms_text);
9259 if ( !$sms_obj->validateResponse($response_obj) ) {
9260 //notify the administrator via email with the error of the SMS sending
9261 $vbo_app->sendMail($admin_sendermail, $admin_sendermail, $admin_email, $admin_sendermail, JText::translate('VBOSENDSMSERRMAILSUBJ'), JText::translate('VBOSENDCUSTOMERSMSERRMAILTXT')."<br />".$sms_obj->getLog(), true);
9262 } else {
9263 $f_result = true;
9264 }
9265 }
9266 }
9267 return $f_result;
9268 }
9269
9270 public static function loadInvoiceTmpl($booking_info = array(), $booking_rooms = array())
9271 {
9272 if (!defined('_VIKBOOKINGEXEC')) {
9273 define('_VIKBOOKINGEXEC', '1');
9274 }
9275
9276 ob_start();
9277 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php';
9278 $content = ob_get_contents();
9279 ob_end_clean();
9280
9281 $default_params = array(
9282 'show_header' => 0,
9283 'header_data' => array(),
9284 'show_footer' => 0,
9285 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9286 'pdf_unit' => 'PDF_UNIT',
9287 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9288 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9289 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9290 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9291 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9292 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9293 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9294 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9295 'header_font_size' => '10',
9296 'body_font_size' => '10',
9297 'footer_font_size' => '8',
9298 'show_lines_taxrate_col' => 0,
9299 );
9300
9301 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && is_array($invoice_params) && count($invoice_params)) {
9302 $default_params = array_merge($default_params, $invoice_params);
9303 }
9304
9305 return array($content, $default_params);
9306 }
9307
9308 /**
9309 * Includes within a buffer the template file for the custom (manual) invoice.
9310 *
9311 * @param array $invoice the record of the custom invoice
9312 * @param array $customer the customer record
9313 *
9314 * @return string the HTML content of the template file
9315 *
9316 * @since 1.11.1
9317 */
9318 public static function loadCustomInvoiceTmpl($invoice, $customer) {
9319 if (!defined('_VIKBOOKINGEXEC')) {
9320 define('_VIKBOOKINGEXEC', '1');
9321 }
9322 ob_start();
9323 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "custom_invoice_tmpl.php";
9324 $content = ob_get_contents();
9325 ob_end_clean();
9326 $default_params = array(
9327 'show_header' => 0,
9328 'header_data' => array(),
9329 'show_footer' => 0,
9330 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
9331 'pdf_unit' => 'PDF_UNIT',
9332 'pdf_page_format' => 'PDF_PAGE_FORMAT',
9333 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
9334 'pdf_margin_top' => 'PDF_MARGIN_TOP',
9335 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
9336 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
9337 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
9338 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
9339 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
9340 'header_font_size' => '10',
9341 'body_font_size' => '10',
9342 'footer_font_size' => '8'
9343 );
9344 if (defined('_VIKBOOKING_INVOICE_PARAMS') && isset($invoice_params) && @count($invoice_params) > 0) {
9345 $default_params = array_merge($default_params, $invoice_params);
9346 }
9347 return array($content, $default_params);
9348 }
9349
9350 public static function parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, $vbo_tn = null, $pdfparams = [])
9351 {
9352 // clone the original template file source
9353 $parsed = $invoicetpl;
9354
9355 /**
9356 * Parse all conditional text rules.
9357 *
9358 * @since 1.14 (J) - 1.4.0 (WP)
9359 */
9360 self::getConditionalRulesInstance()
9361 ->set(array('booking', 'rooms'), array($booking, $booking_rooms))
9362 ->parseTokens($parsed);
9363 //
9364
9365 $dbo = JFactory::getDbo();
9366 if (is_null($vbo_tn)) {
9367 $vbo_tn = self::getTranslator();
9368 }
9369 // availability helper
9370 $av_helper = self::getAvailabilityInstance();
9371
9372 $nowdf = self::getDateFormat();
9373 if ($nowdf == "%d/%m/%Y") {
9374 $df = 'd/m/Y';
9375 } elseif ($nowdf == "%m/%d/%Y") {
9376 $df = 'm/d/Y';
9377 } else {
9378 $df = 'Y/m/d';
9379 }
9380 $datesep = self::getDateSeparator();
9381
9382 /**
9383 * Split stay reservation.
9384 *
9385 * @since 1.16.0 (J) - 1.6.0 (WP)
9386 */
9387 $room_stay_dates = [];
9388 if ($order_info['split_stay']) {
9389 if ($order_info['status'] == 'confirmed') {
9390 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order_info['id']);
9391 } else {
9392 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order_info['id'], []);
9393 }
9394 // immediately count the number of nights of stay for each split room
9395 foreach ($room_stay_dates as $sps_r_k => $sps_r_v) {
9396 if (!empty($sps_r_v['checkin_ts']) && !empty($sps_r_v['checkout_ts'])) {
9397 // overwrite values for compatibility with non-confirmed bookings
9398 $sps_r_v['checkin'] = $sps_r_v['checkin_ts'];
9399 $sps_r_v['checkout'] = $sps_r_v['checkout_ts'];
9400 }
9401 $sps_r_v['nights'] = $av_helper->countNightsOfStay($sps_r_v['checkin'], $sps_r_v['checkout']);
9402 // overwrite the whole array
9403 $room_stay_dates[$sps_r_k] = $sps_r_v;
9404 }
9405 }
9406
9407 $companylogo = self::getSiteLogo();
9408 $uselogo = '';
9409 if (!empty($companylogo)) {
9410 /**
9411 * Let's try to prevent TCPDF errors.
9412 *
9413 * @since August 2nd 2019
9414 */
9415 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9416 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9417 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9418 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9419 }
9420 }
9421 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
9422 $parsed = str_replace("{company_info}", $company_info, $parsed);
9423 $parsed = str_replace("{invoice_number}", $invoice_num, $parsed);
9424 $parsed = str_replace("{invoice_suffix}", $invoice_suff, $parsed);
9425 $parsed = str_replace("{invoice_date}", $invoice_date, $parsed);
9426 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
9427
9428 // custom fields replacement
9429 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
9430 if (is_array($cmatches[1]) && @count($cmatches[1]) > 0) {
9431 $cfids = array();
9432 foreach ($cmatches[1] as $cfid ){
9433 $cfids[] = $cfid;
9434 }
9435 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
9436 $dbo->setQuery($q);
9437 $dbo->execute();
9438 $cfields = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : "";
9439 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
9440 $cfmap = array();
9441 if (is_array($cfields)) {
9442 foreach ($cfields as $cf) {
9443 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
9444 }
9445 }
9446 $cfmapreplace = array();
9447 $partsreceived = explode("\n", $booking['custdata']);
9448 if (count($partsreceived) > 0) {
9449 foreach ($partsreceived as $pst) {
9450 if (!empty($pst)) {
9451 $tmpdata = explode(":", $pst);
9452 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
9453 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
9454 }
9455 }
9456 }
9457 }
9458 foreach ($cmatches[1] as $cfid ){
9459 if (array_key_exists($cfid, $cfmapreplace)) {
9460 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
9461 } else {
9462 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
9463 }
9464 }
9465 }
9466
9467 // invoice price description - Start
9468 $rooms = array();
9469 $tars = array();
9470 $arrpeople = array();
9471 $is_package = !empty($booking['pkg']) ? true : false;
9472 $tot_adults = 0;
9473 $tot_children = 0;
9474 $tot_guests = 0;
9475 foreach ($booking_rooms as $kor => $or) {
9476 $num = $kor + 1;
9477 $rooms[$num] = $or;
9478
9479 // determine the days to consider for the count of the availability
9480 $room_nights = $booking['days'];
9481 $room_checkin = $booking['checkin'];
9482 $room_checkout = $booking['checkout'];
9483 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9484 $room_nights = $room_stay_dates[$kor]['nights'];
9485 $room_checkin = $room_stay_dates[$kor]['checkin'];
9486 $room_checkout = $room_stay_dates[$kor]['checkout'];
9487 }
9488
9489 $arrpeople[$num]['adults'] = $or['adults'];
9490 $arrpeople[$num]['children'] = $or['children'];
9491 $tot_adults += $or['adults'];
9492 $tot_children += $or['children'];
9493 $tot_guests += ($or['adults'] + $or['children']);
9494 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9495 //package or custom cost set from the back-end
9496 continue;
9497 }
9498 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'] . ";";
9499 $dbo->setQuery($q);
9500 $dbo->execute();
9501 if ($dbo->getNumRows() > 0) {
9502 $tar = $dbo->loadAssocList();
9503 $tar = self::applySeasonsRoom($tar, $room_checkin, $room_checkout);
9504 //different usage
9505 if ($or['fromadult'] <= $or['adults'] && $or['toadult'] >= $or['adults']) {
9506 $diffusageprice = self::loadAdultsDiff($or['idroom'], $or['adults']);
9507 //Occupancy Override
9508 $occ_ovr = self::occupancyOverrideExists($tar, $or['adults']);
9509 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
9510 //
9511 if (is_array($diffusageprice)) {
9512 //set a charge or discount to the price(s) for the different usage of the room
9513 foreach ($tar as $kpr => $vpr) {
9514 $tar[$kpr]['diffusage'] = $or['adults'];
9515 if ($diffusageprice['chdisc'] == 1) {
9516 //charge
9517 if ($diffusageprice['valpcent'] == 1) {
9518 //fixed value
9519 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9520 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9521 $tar[$kpr]['diffusagecost'] = "+".$aduseval;
9522 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9523 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
9524 } else {
9525 //percentage value
9526 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9527 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
9528 $tar[$kpr]['diffusagecost'] = "+".$diffusageprice['value']."%";
9529 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9530 $tar[$kpr]['cost'] = $aduseval;
9531 }
9532 } else {
9533 //discount
9534 if ($diffusageprice['valpcent'] == 1) {
9535 //fixed value
9536 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
9537 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
9538 $tar[$kpr]['diffusagecost'] = "-".$aduseval;
9539 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9540 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
9541 } else {
9542 //percentage value
9543 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
9544 $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);
9545 $tar[$kpr]['diffusagecost'] = "-".$diffusageprice['value']."%";
9546 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
9547 $tar[$kpr]['cost'] = $aduseval;
9548 }
9549 }
9550 }
9551 }
9552 }
9553 //
9554 $tars[$num] = $tar[0];
9555 }
9556 }
9557 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
9558 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
9559 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
9560 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
9561 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
9562 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
9563
9564 $isdue = 0;
9565 $tot_taxes = 0;
9566 $tot_city_taxes = 0;
9567 $tot_fees = 0;
9568 $pricestr = array();
9569 $optstr = array();
9570 // start building the tax summary
9571 VBOTaxonomySummary::start();
9572 foreach ($booking_rooms as $kor => $or) {
9573 $num = $kor + 1;
9574
9575 // determine the days to consider for the count of the availability
9576 $room_nights = $booking['days'];
9577 $room_checkin = $booking['checkin'];
9578 $room_checkout = $booking['checkout'];
9579 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
9580 $room_nights = $room_stay_dates[$kor]['nights'];
9581 $room_checkin = $room_stay_dates[$kor]['checkin'];
9582 $room_checkout = $room_stay_dates[$kor]['checkout'];
9583 }
9584
9585 $pricestr[$num] = array();
9586 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9587 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
9588 $calctar = self::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
9589 $cost_minus_tax = self::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
9590 $pricestr[$num]['name'] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN')));
9591 $pricestr[$num]['tot'] = $calctar;
9592 $pricestr[$num]['tax'] = ($calctar - $cost_minus_tax);
9593 $tot_taxes += ($calctar - $cost_minus_tax);
9594 $isdue += $calctar;
9595 $tax_rate = VBOTaxonomySummary::addOptionTax($or['cust_idiva'], ($calctar - $cost_minus_tax));
9596 $pricestr[$num]['tax_rate'] = $tax_rate;
9597 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
9598 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9599 $calctar = self::sayCostPlusIva($display_rate, $tars[$num]['idprice']);
9600 $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'] : "");
9601 $pricestr[$num]['tot'] = $calctar;
9602 $tars[$num]['calctar'] = $calctar;
9603 $isdue += $calctar;
9604 if ($calctar == $display_rate) {
9605 $cost_minus_tax = self::sayCostMinusIva($display_rate, $tars[$num]['idprice']);
9606 $tot_taxes += ($display_rate - $cost_minus_tax);
9607 $pricestr[$num]['tax'] = ($display_rate - $cost_minus_tax);
9608 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($display_rate - $cost_minus_tax));
9609 $pricestr[$num]['tax_rate'] = $tax_rate;
9610 } else {
9611 $tot_taxes += ($calctar - $display_rate);
9612 $pricestr[$num]['tax'] = ($calctar - $display_rate);
9613 $tax_rate = VBOTaxonomySummary::addRatePlanTax($tars[$num]['idprice'], ($calctar - $display_rate));
9614 $pricestr[$num]['tax_rate'] = $tax_rate;
9615 }
9616 }
9617 $optstr[$num] = array();
9618 $opt_ind = 0;
9619 if (!empty($or['optionals'])) {
9620 $stepo = explode(";", $or['optionals']);
9621 foreach ($stepo as $roptkey => $oo) {
9622 if (!empty($oo)) {
9623 $stept = explode(":", $oo);
9624 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
9625 $dbo->setQuery($q);
9626 $dbo->execute();
9627 if ($dbo->getNumRows() == 1) {
9628 $actopt = $dbo->loadAssocList();
9629 if (is_object($vbo_tn)) {
9630 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
9631 }
9632 $optstr[$num][$opt_ind] = array();
9633 $chvar = '';
9634 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
9635 $optagenames = self::getOptionIntervalsAges($actopt[0]['ageintervals']);
9636 $optagepcent = self::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
9637 $optageovrct = self::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
9638 $child_num = self::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
9639 $optagecosts = self::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
9640 $agestept = explode('-', $stept[1]);
9641 $stept[1] = $agestept[0];
9642 $chvar = $agestept[1];
9643 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
9644 //percentage value of the adults tariff
9645 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9646 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9647 } else {
9648 $display_rate = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9649 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9650 }
9651 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
9652 //VBO 1.10 - percentage value of room base cost
9653 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9654 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
9655 } else {
9656 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : (!empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost']);
9657 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
9658 }
9659 }
9660 $actopt[0]['chageintv'] = $chvar;
9661 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
9662 $actopt[0]['quan'] = $stept[1];
9663 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $room_nights * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
9664 } else {
9665 $actopt[0]['quan'] = $stept[1];
9666 // VBO 1.11 - options percentage cost of the room total fee
9667 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
9668 $deftar_basecosts = $or['cust_cost'];
9669 } else {
9670 $deftar_basecosts = !empty($or['room_cost']) ? $or['room_cost'] : $tars[$num]['cost'];
9671 }
9672 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
9673 //
9674 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $room_nights * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
9675 }
9676 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
9677 $realcost = $actopt[0]['maxprice'];
9678 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
9679 $realcost = $actopt[0]['maxprice'] * $stept[1];
9680 }
9681 }
9682 if ($actopt[0]['perperson'] == 1) {
9683 $realcost = $realcost * $or['adults'];
9684 }
9685 $tmpopr = self::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
9686 $optstr[$num][$opt_ind]['name'] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'];
9687 $optstr[$num][$opt_ind]['tot'] = $tmpopr;
9688 $optstr[$num][$opt_ind]['tax'] = 0;
9689 if ($actopt[0]['is_citytax'] == 1) {
9690 $tot_city_taxes += $tmpopr;
9691 } elseif ($actopt[0]['is_fee'] == 1) {
9692 $tot_fees += $tmpopr;
9693 }
9694 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
9695 if ($tmpopr == $realcost) {
9696 $opt_minus_tax = self::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
9697 $tot_taxes += ($realcost - $opt_minus_tax);
9698 $optstr[$num][$opt_ind]['tax'] = ($realcost - $opt_minus_tax);
9699 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($realcost - $opt_minus_tax));
9700 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9701 } else {
9702 $tot_taxes += ($tmpopr - $realcost);
9703 $optstr[$num][$opt_ind]['tax'] = ($tmpopr - $realcost);
9704 $tax_rate = VBOTaxonomySummary::addOptionTax($actopt[0]['idiva'], ($tmpopr - $realcost));
9705 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9706 }
9707 //
9708 $opt_ind++;
9709 $isdue += $tmpopr;
9710 }
9711 }
9712 }
9713 }
9714
9715 // custom extra costs
9716 if (!empty($or['extracosts'])) {
9717 $cur_extra_costs = json_decode($or['extracosts'], true);
9718 foreach ($cur_extra_costs as $eck => $ecv) {
9719 $ecplustax = !empty($ecv['idtax']) ? self::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9720 $isdue += $ecplustax;
9721 $optstr[$num][$opt_ind]['name'] = $ecv['name'];
9722 $optstr[$num][$opt_ind]['tot'] = $ecplustax;
9723 $optstr[$num][$opt_ind]['tax'] = 0;
9724 if ($ecplustax == $ecv['cost']) {
9725 $ec_minus_tax = !empty($ecv['idtax']) ? self::sayOptionalsMinusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
9726 $tot_taxes += ($ecv['cost'] - $ec_minus_tax);
9727 $optstr[$num][$opt_ind]['tax'] = ($ecv['cost'] - $ec_minus_tax);
9728 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecv['cost'] - $ec_minus_tax));
9729 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9730 } else {
9731 $tot_taxes += ($ecplustax - $ecv['cost']);
9732 $optstr[$num][$opt_ind]['tax'] = ($ecplustax - $ecv['cost']);
9733 $tax_rate = VBOTaxonomySummary::addOptionTax($ecv['idtax'], ($ecplustax - $ecv['cost']));
9734 $optstr[$num][$opt_ind]['tax_rate'] = $tax_rate;
9735 }
9736 $opt_ind++;
9737 }
9738 }
9739 }
9740
9741 $usedcoupon = false;
9742 if (strlen($booking['coupon']) > 0) {
9743 $orig_isdue = $isdue;
9744 $expcoupon = explode(";", $booking['coupon']);
9745 $usedcoupon = $expcoupon;
9746 $isdue = $isdue - (float)$expcoupon[1];
9747 if ($isdue != $orig_isdue) {
9748 //lower taxes proportionally
9749 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9750 }
9751 }
9752 if ($booking['refund'] > 0) {
9753 $orig_isdue = $isdue;
9754 $isdue -= $booking['refund'];
9755 if ($isdue != $orig_isdue) {
9756 //lower taxes proportionally
9757 $tot_taxes = $isdue * $tot_taxes / $orig_isdue;
9758 }
9759 }
9760 $rows_written = 0;
9761 $inv_rows = '';
9762 foreach ($pricestr as $num => $price_descr) {
9763 // add room split stay information, if any
9764 $split_stay_str = '';
9765 $kor = $num - 1;
9766 if ($booking['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor])) {
9767 $split_stay_str .= $room_stay_dates[$kor]['nights'] . ' ' . ($room_stay_dates[$kor]['nights'] > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
9768 $split_stay_str .= date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkin']) . ' - ' . date(str_replace("/", $datesep, $df), $room_stay_dates[$kor]['checkout']);
9769 }
9770
9771 $inv_rows .= '<tr>'."\n";
9772 $inv_rows .= '<td>' . $rooms[$num]['room_name'] . (!empty($split_stay_str) ? '<br/>' . $split_stay_str : '') . '<br/>' . nl2br(rtrim($price_descr['name'], "\n")) . '</td>' . "\n";
9773 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($price_descr['tot'] - $price_descr['tax'])).'</td>'."\n";
9774 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tax']).'</td>'."\n";
9775 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($price_descr['tot']).'</td>'."\n";
9776 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9777 $inv_rows .= '<td>' . (isset($price_descr['tax_rate']) ? $price_descr['tax_rate'] : '0') . '%</td>'."\n";
9778 }
9779 $inv_rows .= '</tr>'."\n";
9780 $rows_written++;
9781 if (array_key_exists($num, $optstr) && count($optstr[$num]) > 0) {
9782 foreach ($optstr[$num] as $optk => $optv) {
9783 $inv_rows .= '<tr>'."\n";
9784 $inv_rows .= '<td>'.$optv['name'].'</td>'."\n";
9785 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat(($optv['tot'] - $optv['tax'])).'</td>'."\n";
9786 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tax']).'</td>'."\n";
9787 $inv_rows .= '<td>'.$booking['currencyname'].' '.self::numberFormat($optv['tot']).'</td>'."\n";
9788 if (!empty($pdfparams['show_lines_taxrate_col'])) {
9789 $inv_rows .= '<td>' . (isset($optv['tax_rate']) ? $optv['tax_rate'] : '0') . '%</td>'."\n";
9790 }
9791 $inv_rows .= '</tr>'."\n";
9792 $rows_written++;
9793 }
9794 }
9795 }
9796
9797 // if discount print row
9798 if ($usedcoupon !== false) {
9799 $inv_rows .= '<tr>'."\n";
9800 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9801 $inv_rows .= '</tr>'."\n";
9802 $inv_rows .= '<tr>'."\n";
9803 $inv_rows .= '<td>'.$usedcoupon[2].'</td>'."\n";
9804 $inv_rows .= '<td></td>'."\n";
9805 $inv_rows .= '<td></td>'."\n";
9806 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($usedcoupon[1]).'</td>'."\n";
9807 $inv_rows .= '</tr>'."\n";
9808 $rows_written += 2;
9809 }
9810 // if refunded amount, print row
9811 if ($booking['refund'] > 0) {
9812 $inv_rows .= '<tr>'."\n";
9813 $inv_rows .= '<td></td><td></td><td></td><td></td>'."\n";
9814 $inv_rows .= '</tr>'."\n";
9815 $inv_rows .= '<tr>'."\n";
9816 $inv_rows .= '<td>' . JText::translate('VBO_AMOUNT_REFUNDED') . '</td>'."\n";
9817 $inv_rows .= '<td></td>'."\n";
9818 $inv_rows .= '<td></td>'."\n";
9819 $inv_rows .= '<td>- '.$booking['currencyname'].' '.self::numberFormat($booking['refund']).'</td>'."\n";
9820 $inv_rows .= '</tr>'."\n";
9821 $rows_written += 2;
9822 }
9823 //
9824 $min_records = 8;
9825 if ($rows_written < $min_records) {
9826 for ($i = 1; $i <= ($min_records - $rows_written); $i++) {
9827 $inv_rows .= '<tr>'."\n";
9828 $inv_rows .= '<td></td>'."\n";
9829 $inv_rows .= '<td></td>'."\n";
9830 $inv_rows .= '<td></td>'."\n";
9831 $inv_rows .= '</tr>'."\n";
9832 }
9833 }
9834 //invoice price description - End
9835 $parsed = str_replace("{invoice_products_descriptions}", $inv_rows, $parsed);
9836 $parsed = str_replace("{invoice_totalnet}", $booking['currencyname'].' '.self::numberFormat(($isdue - $tot_taxes)), $parsed);
9837 $parsed = str_replace("{invoice_totaltax}", $booking['currencyname'].' '.self::numberFormat($tot_taxes), $parsed);
9838 $parsed = str_replace("{invoice_grandtotal}", $booking['currencyname'].' '.self::numberFormat($isdue), $parsed);
9839 $parsed = str_replace("{inv_notes}", $booking['inv_notes'], $parsed);
9840
9841 // invoice tax summary
9842 $tax_summary = VBOTaxonomySummary::get();
9843 $tax_names = VBOTaxonomySummary::getNames();
9844 $taxsum_rows = "";
9845 foreach ($tax_summary as $tax_rate => $tax_amount) {
9846 if (!$tax_rate && !$tax_amount) {
9847 continue;
9848 }
9849 $tax_name = isset($tax_names[$tax_rate]) ? $tax_names[$tax_rate] : JText::translate('VBO_INV_VATGST');
9850 $taxsum_rows .= '<tr>' . "\n";
9851 $taxsum_rows .= '<td>' . $tax_name . '</td>' . "\n";
9852 $taxsum_rows .= '<td>' . $tax_rate . '%</td>' . "\n";
9853 $taxsum_rows .= '<td>' . $booking['currencyname'] . ' ' . self::numberFormat($tax_amount) . '</td>' . "\n";
9854 $taxsum_rows .= '</tr>' . "\n";
9855 }
9856 $parsed = str_replace("{invoice_tax_summary}", $taxsum_rows, $parsed);
9857
9858 return $parsed;
9859 }
9860
9861 /**
9862 * Specific method for parsing the template file for the custom (manual) invoices.
9863 *
9864 * @param string $invoicetpl the plain custom invoice template file before parsing
9865 * @param array $invoice the invoice record
9866 * @param array $customer the customer record
9867 *
9868 * @return string the HTML content of the parsed custom invoice template
9869 *
9870 * @since 1.11.1
9871 */
9872 public static function parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer) {
9873 $nowdf = self::getDateFormat(true);
9874 if ($nowdf == "%d/%m/%Y") {
9875 $df = 'd/m/Y';
9876 } elseif ($nowdf == "%m/%d/%Y") {
9877 $df = 'm/d/Y';
9878 } else {
9879 $df = 'Y/m/d';
9880 }
9881 $datesep = self::getDateSeparator(true);
9882 $companylogo = self::getSiteLogo();
9883 $uselogo = '';
9884 if (!empty($companylogo)) {
9885 /**
9886 * Let's try to prevent TCPDF errors by checking if the file exists.
9887 *
9888 * @since August 2nd 2019
9889 */
9890 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9891 $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
9892 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
9893 $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
9894 }
9895 }
9896 $invoicetpl = str_replace("{company_logo}", $uselogo, $invoicetpl);
9897 $invoicetpl = str_replace("{company_info}", self::getInvoiceCompanyInfo(), $invoicetpl);
9898 $invoicetpl = str_replace("{invoice_number}", $invoice['number'], $invoicetpl);
9899 $invoicetpl = str_replace("{invoice_date}", date(str_replace("/", $datesep, $df), $invoice['for_date']), $invoicetpl);
9900 // customer information
9901 $custinfo = '';
9902 $custinfo .= $customer['first_name'] . ' ' . $customer['last_name'] . "\n";
9903 $custinfo .= $customer['email'] . "\n";
9904 $custinfo .= !empty($customer['company']) ? $customer['company'] . "\n" : '';
9905 $custinfo .= !empty($customer['vat']) ? $customer['vat'] . "\n" : '';
9906 $custinfo .= !empty($customer['address']) ? $customer['address'] . "\n" : '';
9907 $custinfo .= (!empty($customer['zip']) ? $customer['zip'] . " " : '') . (!empty($customer['city']) ? $customer['city'] . "\n" : '');
9908 $custinfo .= (!empty($customer['country_name']) ? $customer['country_name'] . "\n" : (!empty($customer['country']) ? $customer['country'] . "\n" : ''));
9909 $custinfo .= !empty($customer['fisccode']) ? $customer['fisccode'] . "\n" : '';
9910 $invoicetpl = str_replace("{customer_info}", nl2br($custinfo), $invoicetpl);
9911 // invoice notes
9912 $invoicetpl = str_replace("{invoice_notes}", (isset($invoice['rawcont']) && isset($invoice['rawcont']['notes']) ? $invoice['rawcont']['notes'] : ''), $invoicetpl);
9913
9914 return $invoicetpl;
9915 }
9916
9917 public static function generateBookingInvoice($booking, $invoice_num = 0, $invoice_suff = '', $invoice_date = '', $company_info = '', $translate = false)
9918 {
9919 $invoice_num = empty($invoice_num) ? self::getNextInvoiceNumber() : $invoice_num;
9920 $invoice_suff = empty($invoice_suff) ? self::getInvoiceNumberSuffix() : $invoice_suff;
9921 $company_info = empty($company_info) ? self::getInvoiceCompanyInfo() : $company_info;
9922
9923 if (!is_array($booking) || !$booking || !($booking['total'] > 0)) {
9924 return false;
9925 }
9926
9927 /**
9928 * Trigger event to allow third party plugins to override some values for the invoice generation.
9929 *
9930 * @since 1.16.3 (J) - 1.6.3 (WP)
9931 */
9932 $event_args = [
9933 'invoice_num' => $invoice_num,
9934 'invoice_suff' => $invoice_suff,
9935 'invoice_date' => $invoice_date,
9936 'company_info' => $company_info,
9937 'translate' => $translate,
9938 ];
9939 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeGenerateInvoiceVikBooking', [$booking, &$event_args]);
9940 if (is_array($event_args) && $event_args) {
9941 // extract the new values to be used
9942 extract($event_args);
9943 }
9944
9945 $dbo = JFactory::getDbo();
9946 $vbo_tn = self::getTranslator();
9947 $currencyname = self::getCurrencyName();
9948 $booking['currencyname'] = $currencyname;
9949 $nowdf = self::getDateFormat(true);
9950 if ($nowdf == "%d/%m/%Y") {
9951 $df = 'd/m/Y';
9952 } elseif ($nowdf == "%m/%d/%Y") {
9953 $df = 'm/d/Y';
9954 } else {
9955 $df = 'Y/m/d';
9956 }
9957 $datesep = self::getDateSeparator(true);
9958 if (empty($invoice_date)) {
9959 $invoice_date = date(str_replace("/", $datesep, $df), $booking['ts']);
9960 $used_date = $booking['ts'];
9961 } else {
9962 /**
9963 * We could be re-generating an invoice for a booking that already had a invoice.
9964 * In order to modify some entries in the invoice, the whole PDF is re-generated.
9965 * It is now possible to keep the same invoice date as the previous one, so check
9966 * what value contains $invoice_date to see if it's different from today's date.
9967 * The cron jobs may be calling this method with a $invoice_date = 1, so we need
9968 * to also check the length of the string $invoice_date before using that date.
9969 *
9970 * @since 1.10 - August 2018
9971 */
9972 $base_ts = time();
9973 if (date($df, $base_ts) != $invoice_date && strlen($invoice_date) >= 6) {
9974 $base_ts = self::getDateTimestamp($invoice_date, 0, 0);
9975 }
9976 $invoice_date = date(str_replace("/", $datesep, $df), $base_ts);
9977 $used_date = $base_ts;
9978 //
9979 }
9980 $booking_rooms = array();
9981 $q = "SELECT `or`.*,`r`.`name` AS `room_name`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_rooms` `r` ON `r`.`id`=`or`.`idroom` WHERE `or`.`idorder`=".(int)$booking['id'].";";
9982 $dbo->setQuery($q);
9983 $dbo->execute();
9984 if ($dbo->getNumRows() > 0) {
9985 $booking_rooms = $dbo->loadAssocList();
9986 }
9987 if (!(count($booking_rooms) > 0)) {
9988 return false;
9989 }
9990 //Translations for the invoices are disabled by default as well as the language definitions for the customer language
9991 if ($translate === true) {
9992 if (!empty($booking['lang'])) {
9993 $lang = JFactory::getLanguage();
9994 if ($lang->getTag() != $booking['lang']) {
9995 if (VBOPlatformDetection::isWordPress()) {
9996 $lang->load('com_vikbooking', VIKBOOKING_LANG, $booking['lang'], true);
9997 } else {
9998 $lang->load('com_vikbooking', JPATH_SITE, $booking['lang'], true);
9999 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $booking['lang'], true);
10000 $lang->load('joomla', JPATH_SITE, $booking['lang'], true);
10001 $lang->load('joomla', JPATH_ADMINISTRATOR, $booking['lang'], true);
10002 }
10003 }
10004 if ($vbo_tn->getDefaultLang() != $booking['lang']) {
10005 // force the translation to start because contents should be translated
10006 $vbo_tn::$force_tolang = $booking['lang'];
10007 }
10008 $vbo_tn->translateContents($booking_rooms, '#__vikbooking_rooms', array('id' => 'idroom', 'room_name' => 'name'), array(), $booking['lang']);
10009 }
10010 }
10011 //
10012 if (!class_exists('TCPDF')) {
10013 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10014 }
10015 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10016
10017 /**
10018 * Trigger event to allow third party plugins to return a specific font name.
10019 *
10020 * @since 1.16.0 (J) - 1.6.0 (WP)
10021 */
10022 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10023 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10024 $usepdffont = $custom_pdf_font[0];
10025 }
10026
10027 //vikbooking 1.8 - set array variable to the template file
10028 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`='".(int)$booking['id']."';";
10029 $dbo->setQuery($q);
10030 $dbo->execute();
10031 $booking_info = $dbo->loadAssoc();
10032 list($invoicetpl, $pdfparams) = self::loadInvoiceTmpl($booking_info, $booking_rooms);
10033 //
10034 $invoice_body = self::parseInvoiceTemplate($invoicetpl, $booking, $booking_rooms, $invoice_num, $invoice_suff, $invoice_date, $company_info, ($translate === true ? $vbo_tn : null), $pdfparams);
10035 $pdffname = $booking['id'] . '_' . $booking['sid'] . '.pdf';
10036 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10037 if (file_exists($pathpdf)) @unlink($pathpdf);
10038 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10039 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10040 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice_num);
10041 //Header for each page of the pdf
10042 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10043 $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]);
10044 }
10045 //Change some currencies to their unicode (decimal) value
10046 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10047 if (array_key_exists($booking['currencyname'], $unichr_map)) {
10048 $invoice_body = str_replace($booking['currencyname'], TCPDF_FONTS::unichr($unichr_map[$booking['currencyname']]), $invoice_body);
10049 }
10050 //header and footer fonts
10051 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10052 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10053 //margins
10054 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10055 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10056 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10057 //
10058 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10059 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10060 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10061 if ($pdfparams['show_header'] == 0 || !(count($pdfparams['header_data']) > 0)) {
10062 $pdf->SetPrintHeader(false);
10063 }
10064 if ($pdfparams['show_footer'] == 0) {
10065 $pdf->SetPrintFooter(false);
10066 }
10067 $pdf->AddPage();
10068 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10069 $pdf->lastPage();
10070 $pdf->Output($pathpdf, 'F');
10071 if (!file_exists($pathpdf)) {
10072 return false;
10073 } else {
10074 /**
10075 * @wponly - trigger files mirroring
10076 */
10077 VikBookingLoader::import('update.manager');
10078 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10079 //
10080 }
10081 //insert or update record for this invoice
10082 $invoice_id = 0;
10083 $q = "SELECT `id` FROM `#__vikbooking_invoices` WHERE `idorder`=".(int)$booking['id'].";";
10084 $dbo->setQuery($q);
10085 $dbo->execute();
10086 if ($dbo->getNumRows() > 0) {
10087 $invoice_data = $dbo->loadAssocList();
10088 $invoice_id = $invoice_data[0]['id'];
10089 }
10090 //Booking History
10091 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', '#'.$invoice_num.$invoice_suff);
10092 //
10093 if ($invoice_id > 0) {
10094 //update
10095 $q = "UPDATE `#__vikbooking_invoices` SET `number`=".$dbo->quote($invoice_num.$invoice_suff).", `file_name`=".$dbo->quote($pdffname).", `created_on`=".time().", `for_date`=".(int)$used_date." WHERE `id`=".(int)$invoice_id.";";
10096 $dbo->setQuery($q);
10097 $dbo->execute();
10098 $retval = $invoice_id;
10099 } else {
10100 //insert
10101 $q = "INSERT INTO `#__vikbooking_invoices` (`number`,`file_name`,`idorder`,`idcustomer`,`created_on`,`for_date`) VALUES(".$dbo->quote($invoice_num.$invoice_suff).", ".$dbo->quote($pdffname).", ".(int)$booking['id'].", ".(int)$booking['idcustomer'].", ".time().", ".(int)$used_date.");";
10102 $dbo->setQuery($q);
10103 $dbo->execute();
10104 $lid = $dbo->insertid();
10105 $retval = $lid > 0 ? $lid : false;
10106 }
10107
10108 /**
10109 * The generation of the analogic invoices can trigger the drivers for the
10110 * generation of the e-Invoices if they are set to automatically run.
10111 * However, the e-Invoicing classes may be calling this method, so we need
10112 * to make sure the eInvoicing class is not running before proceeding.
10113 * This method could be called within a loop, and so the second iterations
10114 * may already have loaded the eInvocing class. For this we use a static variable.
10115 * This piece of code should run after updating the information of the PDF invoice.
10116 *
10117 * @since 1.11
10118 */
10119 static $einvocing_can_run = false;
10120 if (!defined('VBO_EINVOICING_RUN') && !$einvocing_can_run) {
10121 // allow second iterations calling this method to run
10122 $einvocing_can_run = true;
10123 }
10124 if ($einvocing_can_run === true) {
10125 $q = "SELECT * FROM `#__vikbooking_einvoicing_config` WHERE `automatic`=1;";
10126 $dbo->setQuery($q);
10127 $dbo->execute();
10128 if ($dbo->getNumRows()) {
10129 $drivers = $dbo->loadAssocList();
10130 // require the parent abstract class
10131 $driver_base = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'einvoicing' . DIRECTORY_SEPARATOR;
10132 require_once $driver_base . 'einvoicing.php';
10133
10134 /**
10135 * Trigger event to let other plugins register additional drivers.
10136 *
10137 * @return array A list of supported drivers.
10138 */
10139 $list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadEinvoicingDrivers');
10140
10141 // invoke all drivers that should run automatically
10142 $driver_base .= 'drivers' . DIRECTORY_SEPARATOR;
10143 foreach ($drivers as $driver) {
10144 $driver_file = $driver['driver'].'.php';
10145 $driver_path = $driver_base . $driver_file;
10146 if (is_file($driver_path)) {
10147 // require the driver sub-class
10148 require_once $driver_path;
10149 }
10150 // invoke the class
10151 $classname = 'VikBookingEInvoicing'.str_replace(' ', '', ucwords(str_replace('.php', '', str_replace('_', ' ', $driver_file))));
10152 if (!class_exists($classname)) {
10153 continue;
10154 }
10155 $driver_obj = new $classname;
10156 // set the flag of the external call
10157 $driver_obj->externalCall = __METHOD__;
10158 // inject invoice number to avoid discrepanices between analogic and electronic, maybe due to missing information for the e-Invoices
10159 $driver_obj->externalData['einvnum'] = $invoice_num;
10160 // generate the e-Invoice
10161 $driver_err = '';
10162 if (!$driver_obj->generateEInvoice((int)$booking['id'])) {
10163 $driver_err = $driver_obj->getError();
10164 VikError::raiseWarning('', $driver_err);
10165 }
10166 // Booking History - store log for the e-Invoice result
10167 self::getBookingHistoryInstance()->setBid($booking['id'])->store('BI', $driver_obj->getName().(!empty($driver_err) ? ': '.$driver_err : ''));
10168 }
10169 }
10170 }
10171 //
10172
10173 // return the result of the generation of the PDF invoice
10174 return $retval;
10175 }
10176
10177 /**
10178 * Generates an analogic invoice in PDF format for a custom list of services.
10179 * No bookings are assigned to this custom invoice. The method parses the same
10180 * invoice template as for the regular process with real booking IDs.
10181 * The invoice number must be stored before calling this method.
10182 *
10183 * @param int $invoice_id the ID of the custom invoice record
10184 *
10185 * @return boolean
10186 *
10187 * @since 1.11.1
10188 */
10189 public static function generateCustomInvoice($invoice_id) {
10190 $dbo = JFactory::getDbo();
10191 $mainframe = JFactory::getApplication();
10192 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id." AND `idorder` < 0;";
10193 $dbo->setQuery($q);
10194 $dbo->execute();
10195 if (!$dbo->getNumRows()) {
10196 return false;
10197 }
10198 $invoice = $dbo->loadAssoc();
10199 $rawcont = !empty($invoice['rawcont']) ? json_decode($invoice['rawcont'], true) : array();
10200 $rawcont = is_array($rawcont) ? $rawcont : array();
10201 $rows = isset($rawcont['rows']) ? $rawcont['rows'] : array();
10202 $invoice['rawcont'] = $rawcont;
10203 $customer = self::getCPinIstance()->getCustomerByID($invoice['idcustomer']);
10204 if (!count($rawcont) || !count($rows) || !count($customer)) {
10205 // at least one invoice raw is mandatory as well as the customer
10206 return false;
10207 }
10208 $company_info = self::getInvoiceCompanyInfo();
10209 // load invoice template file
10210 list($invoicetpl, $pdfparams) = self::loadCustomInvoiceTmpl($invoice, $customer);
10211 // parse invoice template file
10212 $invoice_body = self::parseCustomInvoiceTemplate($invoicetpl, $invoice, $customer);
10213 //
10214 if (!class_exists('TCPDF')) {
10215 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . 'tcpdf.php');
10216 }
10217 $usepdffont = is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "tcpdf" . DIRECTORY_SEPARATOR . "fonts" . DIRECTORY_SEPARATOR . "dejavusans.php") ? 'dejavusans' : 'helvetica';
10218
10219 /**
10220 * Trigger event to allow third party plugins to return a specific font name.
10221 *
10222 * @since 1.16.0 (J) - 1.6.0 (WP)
10223 */
10224 $custom_pdf_font = VBOFactory::getPlatform()->getDispatcher()->filter('onGetPdfFontNameVikBooking', [$usepdffont]);
10225 if (is_array($custom_pdf_font) && !empty($custom_pdf_font[0])) {
10226 $usepdffont = $custom_pdf_font[0];
10227 }
10228
10229 $pdffname = $invoice['file_name'];
10230 $pathpdf = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $pdffname;
10231 if (file_exists($pathpdf)) @unlink($pathpdf);
10232 $pdf_page_format = is_array($pdfparams['pdf_page_format']) ? $pdfparams['pdf_page_format'] : constant($pdfparams['pdf_page_format']);
10233 $pdf = new TCPDF(constant($pdfparams['pdf_page_orientation']), constant($pdfparams['pdf_unit']), $pdf_page_format, true, 'UTF-8', false);
10234 $pdf->SetTitle(JText::translate('VBOINVNUM').' '.$invoice['number']);
10235 //Header for each page of the pdf
10236 if ($pdfparams['show_header'] == 1 && count($pdfparams['header_data']) > 0) {
10237 $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]);
10238 }
10239 //Change some currencies to their unicode (decimal) value
10240 $currencyname = self::getCurrencyName();
10241 $unichr_map = array('EUR' => 8364, 'USD' => 36, 'AUD' => 36, 'CAD' => 36, 'GBP' => 163);
10242 if (array_key_exists($currencyname, $unichr_map)) {
10243 $invoice_body = str_replace($currencyname, TCPDF_FONTS::unichr($unichr_map[$currencyname]), $invoice_body);
10244 }
10245 //header and footer fonts
10246 $pdf->setHeaderFont(array($usepdffont, '', $pdfparams['header_font_size']));
10247 $pdf->setFooterFont(array($usepdffont, '', $pdfparams['footer_font_size']));
10248 //margins
10249 $pdf->SetMargins(constant($pdfparams['pdf_margin_left']), constant($pdfparams['pdf_margin_top']), constant($pdfparams['pdf_margin_right']));
10250 $pdf->SetHeaderMargin(constant($pdfparams['pdf_margin_header']));
10251 $pdf->SetFooterMargin(constant($pdfparams['pdf_margin_footer']));
10252 //
10253 $pdf->SetAutoPageBreak(true, constant($pdfparams['pdf_margin_bottom']));
10254 $pdf->setImageScale(constant($pdfparams['pdf_image_scale_ratio']));
10255 $pdf->SetFont($usepdffont, '', (int)$pdfparams['body_font_size']);
10256 if ($pdfparams['show_header'] == 0 || !(count($pdfparams['header_data']) > 0)) {
10257 $pdf->SetPrintHeader(false);
10258 }
10259 if ($pdfparams['show_footer'] == 0) {
10260 $pdf->SetPrintFooter(false);
10261 }
10262 $pdf->AddPage();
10263 $pdf->writeHTML($invoice_body, true, false, true, false, '');
10264 $pdf->lastPage();
10265 $pdf->Output($pathpdf, 'F');
10266 if (is_file($pathpdf)) {
10267 /**
10268 * @wponly - trigger files mirroring
10269 */
10270 VikBookingLoader::import('update.manager');
10271 VikBookingUpdateManager::triggerUploadBackup($pathpdf);
10272 //
10273 }
10274
10275 /**
10276 * The generation of the analogic invoices can trigger the drivers for the
10277 * generation of the e-Invoices if they are set to automatically run.
10278 * However, the e-Invoicing classes may be calling this method, so we need
10279 * to make sure the eInvoicing class is not running before proceeding.
10280 * This method could be called within a loop, and so the second iterations
10281 * may already have loaded the eInvocing class. For this we use a static variable.
10282 * This piece of code should run after updating the information of the PDF invoice.
10283 *
10284 * @since 1.11.1
10285 */
10286 $q = "SELECT * FROM `#__vikbooking_einvoicing_config` WHERE `automatic`=1;";
10287 $dbo->setQuery($q);
10288 $dbo->execute();
10289 if ($dbo->getNumRows()) {
10290 $drivers = $dbo->loadAssocList();
10291 // make sure the invoice number is just a number
10292 $invoice['number'] = str_replace(self::getInvoiceNumberSuffix(), '', $invoice['number']);
10293 // require the parent abstract class
10294 $driver_base = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'einvoicing' . DIRECTORY_SEPARATOR;
10295 require_once $driver_base . 'einvoicing.php';
10296
10297 /**
10298 * Trigger event to let other plugins register additional drivers.
10299 *
10300 * @return array A list of supported drivers.
10301 */
10302 $list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadEinvoicingDrivers');
10303
10304 // invoke all drivers that should run automatically
10305 $driver_base .= 'drivers' . DIRECTORY_SEPARATOR;
10306 foreach ($drivers as $driver) {
10307 $driver_file = $driver['driver'].'.php';
10308 $driver_path = $driver_base . $driver_file;
10309 if (is_file($driver_path)) {
10310 // require the driver sub-class
10311 require_once $driver_path;
10312 }
10313 // invoke the class
10314 $classname = 'VikBookingEInvoicing'.str_replace(' ', '', ucwords(str_replace('.php', '', str_replace('_', ' ', $driver_file))));
10315 if (!class_exists($classname)) {
10316 continue;
10317 }
10318 $driver_obj = new $classname;
10319 // set the flag of the external call
10320 $driver_obj->externalCall = __METHOD__;
10321 // inject custom invoice number, date and details to avoid discrepanices between analogic and electronic
10322 $driver_obj->externalData['einvnum'] = $invoice['number'];
10323 $driver_obj->externalData['einvdate'] = (int)$invoice['for_date'];
10324 $driver_obj->externalData['einvcustom'] = $invoice;
10325 // prepare data array for the generation of the e-Invoice
10326 $einvdata = $driver_obj->prepareCustomInvoiceData($invoice, $customer);
10327 // before generating the e-invoice, make sure to obliterate it if exists already (case of update custom invoice)
10328 $preveinv = $driver_obj->eInvoiceExists(array('idorder' => $einvdata[0]['id']));
10329 if ($preveinv !== false) {
10330 $driver_obj->obliterateEInvoice(array('id' => $preveinv));
10331 }
10332 // generate the e-Invoice
10333 $driver_err = '';
10334 if (!$driver_obj->generateEInvoice($einvdata)) {
10335 $driver_err = $driver_obj->getError();
10336 VikError::raiseWarning('', $driver_err);
10337 }
10338 }
10339 }
10340 //
10341
10342 // return the result of the generation of the PDF invoice
10343 return is_file($pathpdf);
10344 }
10345
10346 public static function sendBookingInvoice($invoice_id, $booking, $text = '', $subject = '') {
10347 if (!(count($booking) > 0) || empty($invoice_id) || empty($booking['custmail'])) {
10348 return false;
10349 }
10350 $dbo = JFactory::getDbo();
10351 $invoice_data = array();
10352 $q = "SELECT * FROM `#__vikbooking_invoices` WHERE `id`=".(int)$invoice_id.";";
10353 $dbo->setQuery($q);
10354 $dbo->execute();
10355 if ($dbo->getNumRows() == 1) {
10356 $invoice_data = $dbo->loadAssoc();
10357 }
10358 if (!(count($invoice_data) > 0)) {
10359 return false;
10360 }
10361 $mail_text = empty($text) ? JText::translate('VBOEMAILINVOICEATTACHTXT') : $text;
10362 $mail_subject = empty($subject) ? JText::translate('VBOEMAILINVOICEATTACHSUBJ') : $subject;
10363 $invoice_file_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "invoices" . DIRECTORY_SEPARATOR . "generated" . DIRECTORY_SEPARATOR . $invoice_data['file_name'];
10364 if (!file_exists($invoice_file_path)) {
10365 return false;
10366 }
10367 if (!class_exists('VboApplication')) {
10368 require_once(VBO_ADMIN_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'jv_helper.php');
10369 }
10370 $vbo_app = new VboApplication;
10371 $admin_sendermail = self::getSenderMail();
10372 $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);
10373 //update record
10374 $q = "UPDATE `#__vikbooking_invoices` SET `emailed`=1, `emailed_to`=".$dbo->quote($booking['custmail'])." WHERE `id`=".(int)$invoice_id.";";
10375 $dbo->setQuery($q);
10376 $dbo->execute();
10377 //
10378 return true;
10379 }
10380
10381 public static function loadCheckinDocTmpl($booking_info = array(), $booking_rooms = array(), $customer = array()) {
10382 if (!defined('_VIKBOOKINGEXEC')) {
10383 define('_VIKBOOKINGEXEC', '1');
10384 }
10385 ob_start();
10386 include VBO_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "checkins" . DIRECTORY_SEPARATOR . "checkin_tmpl.php";
10387 $content = ob_get_contents();
10388 ob_end_clean();
10389 $default_params = array(
10390 'show_header' => 0,
10391 'header_data' => array(),
10392 'show_footer' => 0,
10393 'pdf_page_orientation' => 'PDF_PAGE_ORIENTATION',
10394 'pdf_unit' => 'PDF_UNIT',
10395 'pdf_page_format' => 'PDF_PAGE_FORMAT',
10396 'pdf_margin_left' => 'PDF_MARGIN_LEFT',
10397 'pdf_margin_top' => 'PDF_MARGIN_TOP',
10398 'pdf_margin_right' => 'PDF_MARGIN_RIGHT',
10399 'pdf_margin_header' => 'PDF_MARGIN_HEADER',
10400 'pdf_margin_footer' => 'PDF_MARGIN_FOOTER',
10401 'pdf_margin_bottom' => 'PDF_MARGIN_BOTTOM',
10402 'pdf_image_scale_ratio' => 'PDF_IMAGE_SCALE_RATIO',
10403 'header_font_size' => '10',
10404 'body_font_size' => '10',
10405 'footer_font_size' => '8'
10406 );
10407 if (defined('_VIKBOOKING_CHECKIN_PARAMS') && isset($checkin_params) && @count($checkin_params) > 0) {
10408 $default_params = array_merge($default_params, $checkin_params);
10409 }
10410 return array($content, $default_params);
10411 }
10412
10413 public static function parseCheckinDocTemplate($checkintpl, $booking, $booking_rooms, $customer) {
10414 $parsed = $checkintpl;
10415 $dbo = JFactory::getDbo();
10416 $app = JFactory::getApplication();
10417 $nowdf = self::getDateFormat();
10418 if ($nowdf == "%d/%m/%Y") {
10419 $df='d/m/Y';
10420 } elseif ($nowdf == "%m/%d/%Y") {
10421 $df='m/d/Y';
10422 } else {
10423 $df='Y/m/d';
10424 }
10425 $datesep = self::getDateSeparator();
10426 $companylogo = self::getSiteLogo();
10427 $uselogo = '';
10428 if (!empty($companylogo)) {
10429 /**
10430 * Let's try to prevent TCPDF errors, as custom logos are always uploaded in /admin
10431 *
10432 * @since March 16th 2021
10433 */
10434 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10435 // $uselogo = '<img src="' . VBO_ADMIN_URI_REL . 'resources/' . $companylogo . '"/>';
10436 $uselogo = '<img src="' . VBO_ADMIN_URI . 'resources/' . $companylogo . '"/>';
10437 } elseif (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . $companylogo)) {
10438 // $uselogo = '<img src="' . VBO_SITE_URI_REL . 'resources/' . $companylogo . '"/>';
10439 $uselogo = '<img src="' . VBO_SITE_URI . 'resources/' . $companylogo . '"/>';
10440 }
10441 }
10442 $company_name = self::getFrontTitle();
10443 $company_info = self::getInvoiceCompanyInfo();
10444 $parsed = str_replace("{company_name}", $company_name, $parsed);
10445 $parsed = str_replace("{company_logo}", $uselogo, $parsed);
10446 $parsed = str_replace("{company_info}", $company_info, $parsed);
10447 $parsed = str_replace("{customer_info}", nl2br(rtrim($booking['custdata'], "\n")), $parsed);
10448 $parsed = str_replace("{checkin_date}", date(str_replace("/", $datesep, $df), $booking['checkin']), $parsed);
10449 $parsed = str_replace("{checkout_date}", date(str_replace("/", $datesep, $df), $booking['checkout']), $parsed);
10450 $parsed = str_replace("{num_nights}", $booking['days'], $parsed);
10451 $tot_guests = 0;
10452 $tot_adults = 0;
10453 $tot_children = 0;
10454 foreach ($booking_rooms as $kor => $or) {
10455 $tot_guests += ($or['adults'] + $or['children']);
10456 $tot_adults += $or['adults'];
10457 $tot_children += $or['children'];
10458 }
10459 $parsed = str_replace("{tot_guests}", $tot_guests, $parsed);
10460 $parsed = str_replace("{tot_adults}", $tot_adults, $parsed);
10461 $parsed = str_replace("{tot_children}", $tot_children, $parsed);
10462 if (count($customer) && isset($customer['comments'])) {
10463 $parsed = str_replace("{checkin_comments}", $customer['comments'], $parsed);
10464 }
10465 $termsconds = self::getTermsConditions();
10466 $parsed = str_replace("{terms_and_conditions}", $termsconds, $parsed);
10467 //custom fields replace
10468 preg_match_all('/\{customfield ([0-9]+)\}/U', $parsed, $cmatches);
10469 if (is_array($cmatches[1]) && @count($cmatches[1]) > 0) {
10470 $cfids = array();
10471 foreach ($cmatches[1] as $cfid ){
10472 $cfids[] = $cfid;
10473 }
10474 $q = "SELECT * FROM `#__vikbooking_custfields` WHERE `id` IN (".implode(", ", $cfids).");";
10475 $dbo->setQuery($q);
10476 $dbo->execute();
10477 $cfields = $dbo->getNumRows() > 0 ? $dbo->loadAssocList() : "";
10478 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
10479 $cfmap = array();
10480 if (is_array($cfields)) {
10481 foreach ($cfields as $cf) {
10482 $cfmap[trim(JText::translate($cf['name']))] = $cf['id'];
10483 }
10484 }
10485 $cfmapreplace = array();
10486 $partsreceived = explode("\n", $booking['custdata']);
10487 if (count($partsreceived) > 0) {
10488 foreach ($partsreceived as $pst) {
10489 if (!empty($pst)) {
10490 $tmpdata = explode(":", $pst);
10491 if (array_key_exists(trim($tmpdata[0]), $cfmap)) {
10492 $cfmapreplace[$cfmap[trim($tmpdata[0])]] = trim($tmpdata[1]);
10493 }
10494 }
10495 }
10496 }
10497 foreach ($cmatches[1] as $cfid ){
10498 if (array_key_exists($cfid, $cfmapreplace)) {
10499 $parsed = str_replace("{customfield ".$cfid."}", $cfmapreplace[$cfid], $parsed);
10500 } else {
10501 $parsed = str_replace("{customfield ".$cfid."}", "", $parsed);
10502 }
10503 }
10504 }
10505 //end custom fields replace
10506
10507 return $parsed;
10508 }
10509
10510 /**
10511 * Returns an array of key-value pairs to be used for building the Guest Details
10512 * in the Check-in process. The keys will be compared to the fields of the table
10513 * #__customers to see if some values already exist. The values can use lang defs
10514 * of both front-end or back-end. To be called as list(fields, attributes).
10515 *
10516 * @param boolean $precheckin true if requested for front-end pre check-in.
10517 * @param string $type optional type of custom pax fields to force.
10518 *
10519 * @return array key-value pairs for showing and collecting guest details.
10520 *
10521 * @since 1.15.0 (J) - 1.5.0 (WP) introduced custom pax data collection type.
10522 * @since 1.16.3 (J) - 1.6.3 (WP) implemented hook to override the pre-checkin custom fields.
10523 */
10524 public static function getPaxFields($precheckin = false, $type = null)
10525 {
10526 if (!$precheckin) {
10527 // back-end check-in ("registration") key-value pairs
10528
10529 // check the type of pax fields collection data
10530 $collection_type = !empty($type) ? $type : VBOFactory::getConfig()->getString('checkindata', 'basic');
10531
10532 // return the requested pax fields collection list
10533 $custom_pax_fields = VBOCheckinPax::getFields($collection_type);
10534
10535 if (is_array($custom_pax_fields) && count($custom_pax_fields)) {
10536 // requested driver returned a list of fields
10537 return $custom_pax_fields;
10538 }
10539
10540 // in case the driver is unsupported, fallback to "basic" (default) driver
10541 return VBOCheckinPax::getFields('basic');
10542 }
10543
10544 // front-end key-value pairs for pre check-in
10545 $precheckin_pax_fields = [
10546 [
10547 'first_name' => JText::translate('VBCCFIRSTNAME'),
10548 'last_name' => JText::translate('VBCCLASTNAME'),
10549 'date_birth' => JText::translate('ORDER_DBIRTH'),
10550 'place_birth' => JText::translate('VBOCUSTPLACEBIRTH'),
10551 'country' => JText::translate('ORDER_STATE'),
10552 'city' => JText::translate('ORDER_CITY'),
10553 'zip' => JText::translate('ORDER_ZIP'),
10554 'nationality' => JText::translate('VBOCUSTNATIONALITY'),
10555 'gender' => JText::translate('VBOCUSTGENDER'),
10556 'doctype' => JText::translate('VBOCUSTDOCTYPE'),
10557 'docnum' => JText::translate('VBOCUSTDOCNUM'),
10558 'documents' => JText::translate('VBO_CUSTOMER_UPLOAD_DOCS'),
10559 ],
10560 [
10561 'first_name' => 'text',
10562 'last_name' => 'text',
10563 'date_birth' => 'calendar',
10564 'place_birth' => 'text',
10565 'country' => 'country',
10566 'city' => 'text',
10567 'zip' => 'text',
10568 'nationality' => 'country',
10569 'gender' => [
10570 'M' => JText::translate('VBOCUSTGENDERM'),
10571 'F' => JText::translate('VBOCUSTGENDERF'),
10572 ],
10573 'doctype' => 'text',
10574 'docnum' => 'text',
10575 'documents' => 'file',
10576 ],
10577 ];
10578
10579 // make a safe copy of the default precheckin fields
10580 $def_precheckin_pax_fields = $precheckin_pax_fields;
10581
10582 /**
10583 * Trigger event to allow third party plugins to override the default pre-checkin pax fields.
10584 *
10585 * @since 1.16.3 (J) - 1.6.3 (WP)
10586 */
10587 VBOFactory::getPlatform()->getDispatcher()->trigger('onDisplayPrecheckinPaxFieldsVikBooking', [&$precheckin_pax_fields]);
10588
10589 if (is_array($precheckin_pax_fields) && count($precheckin_pax_fields) > 1 && !empty($precheckin_pax_fields[0]) && !empty($precheckin_pax_fields[1])) {
10590 // use the filtered pax fields
10591 $def_precheckin_pax_fields = $precheckin_pax_fields;
10592 }
10593
10594 // return the front-end key-value pairs for pre check-in
10595 return $def_precheckin_pax_fields;
10596 }
10597
10598 /**
10599 * Returns the associative list of countries from the DB.
10600 *
10601 * @param bool $tn whether to translate the country name.
10602 * @param bool $no_id whether to unset the ID of the country.
10603 *
10604 * @return array associative or empty array.
10605 *
10606 * @since 1.15.0 (J) - 1.5.0 (WP) translations supported and applied by default.
10607 * @since 1.16.0 (J) - 1.6.0 (WP) added second argument.
10608 */
10609 public static function getCountriesArray($tn = true, $no_id = true)
10610 {
10611 $dbo = JFactory::getDbo();
10612 $all_countries = [];
10613
10614 $q = "SELECT " . ($no_id ? '`id`, `country_name`, `country_3_code`' : '*') . " FROM `#__vikbooking_countries` ORDER BY `country_name` ASC;";
10615 $dbo->setQuery($q);
10616 $dbo->execute();
10617 if (!$dbo->getNumRows()) {
10618 return [];
10619 }
10620 $countries = $dbo->loadAssocList();
10621
10622 if ($tn === true) {
10623 $vbo_tn = self::getTranslator();
10624 $vbo_tn->translateContents($countries, '#__vikbooking_countries');
10625 // re-apply sorting by country name
10626 $sorting = [];
10627 foreach ($countries as $country) {
10628 $sorting[$country['country_name']] = $country;
10629 }
10630 ksort($sorting);
10631 $sorted = [];
10632 foreach ($sorting as $country) {
10633 $sorted[] = $country;
10634 }
10635 $countries = $sorted;
10636 unset($sorting, $sorted);
10637 }
10638
10639 foreach ($countries as $v) {
10640 if ($no_id) {
10641 // keep the original structure by unsetting the ID only needed for translation
10642 unset($v['id']);
10643 }
10644 $all_countries[$v['country_3_code']] = $v;
10645 }
10646
10647 return $all_countries;
10648 }
10649
10650 public static function getCountriesSelect($name, $all_countries = array(), $current_value = '', $empty_value = ' ')
10651 {
10652 if (!count($all_countries)) {
10653 $all_countries = self::getCountriesArray();
10654 }
10655
10656 $countries = '<select name="'.$name.'">'."\n";
10657 if (strlen($empty_value)) {
10658 $countries .= '<option value="">'.$empty_value.'</option>'."\n";
10659 }
10660 foreach ($all_countries as $v) {
10661 $countries .= '<option value="'.$v['country_3_code'].'"'.($v['country_3_code'] == $current_value ? ' selected="selected"' : '').'>'.$v['country_name'].'</option>'."\n";
10662 }
10663 $countries .= '</select>';
10664
10665 return $countries;
10666 }
10667
10668 public static function getThumbSize($skipsession = false)
10669 {
10670 static $thumb_size = null;
10671
10672 if ($thumb_size) {
10673 return $thumb_size;
10674 }
10675
10676 $thumb_size = VBOFactory::getConfig()->get('thumbsize', 500);
10677
10678 return $thumb_size;
10679 }
10680
10681 /**
10682 * Checks whether an iCal file for the reservation should be
10683 * attached to the confirmation email for customer and/or admin.
10684 *
10685 * @return int 1=admin+customer, 2=admin, 3=customer, 0=no
10686 *
10687 * @since 1.12.0 (J) - 1.2.0 (WP)
10688 */
10689 public static function attachIcal()
10690 {
10691 return VBOFactory::getConfig()->getInt('attachical', 1);
10692 }
10693
10694 /**
10695 * How the calculation of the orphan dates should take place.
10696 *
10697 * @return string "next" for only checking the bookings ahead, "prevnext" if
10698 * also the previous bookings should be checked.
10699 *
10700 * @since 1.13 (J) - 1.3.0 (WP)
10701 */
10702 public static function orphansCalculation()
10703 {
10704 return VBOFactory::getConfig()->get('orphanscalculation', 'next');
10705 }
10706
10707 /**
10708 * Returns the name of the template file to use for the front-end View "search".
10709 *
10710 * @return string the name of the template file. New and upgraded user will both use the classic file.
10711 *
10712 * @since 1.13 (J) - 1.3.0 (WP)
10713 */
10714 public static function searchResultsTmpl()
10715 {
10716 return VBOFactory::getConfig()->get('searchrestmpl', 'classic');
10717 }
10718
10719 /**
10720 * Returns a string without any new-line characters
10721 * to be used for JavaScript values without facing
10722 * errors like 'unterminated string literal'.
10723 * By passing nl2br($str) as argument, we can keep
10724 * the wanted new-line HTML tags for PRE tags.
10725 * We use implode() with just one argument to
10726 * not use an empty string as glue for the string.
10727 *
10728 * @param $str string
10729 *
10730 * @return string
10731 */
10732 public static function strTrimLiteral($str) {
10733 $str = str_replace(array("\r\n", "\r"), "\n", $str);
10734 $lines = explode("\n", $str);
10735 $new_lines = array();
10736 foreach ($lines as $i => $line) {
10737 if (strlen($line)) {
10738 $new_lines[] = trim($line);
10739 }
10740 }
10741 return implode($new_lines);
10742 }
10743
10744 /**
10745 * Returns an instance of the VboApplication class.
10746 *
10747 * @return VboApplication
10748 */
10749 public static function getVboApplication()
10750 {
10751 // require library
10752 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'jv_helper.php';
10753
10754 // return a new instance
10755 return new VboApplication();
10756 }
10757
10758 /**
10759 * Returns the translation of the given 0-indexed week day.
10760 *
10761 * @param int $wd the 0-indexed day of the week.
10762 * @param bool $short whether to get the short version of the weekday.
10763 *
10764 * @return string the name of the week day.
10765 *
10766 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10767 */
10768 public static function sayWeekDay($wd, $short = false)
10769 {
10770 switch ($wd) {
10771 case '6' :
10772 $ret = $short ? JText::translate('VBSAT') : JText::translate('VBWEEKDAYSIX');
10773 break;
10774 case '5' :
10775 $ret = $short ? JText::translate('VBFRI') : JText::translate('VBWEEKDAYFIVE');
10776 break;
10777 case '4' :
10778 $ret = $short ? JText::translate('VBTHU') : JText::translate('VBWEEKDAYFOUR');
10779 break;
10780 case '3' :
10781 $ret = $short ? JText::translate('VBWED') : JText::translate('VBWEEKDAYTHREE');
10782 break;
10783 case '2' :
10784 $ret = $short ? JText::translate('VBTUE') : JText::translate('VBWEEKDAYTWO');
10785 break;
10786 case '1' :
10787 $ret = $short ? JText::translate('VBMON') : JText::translate('VBWEEKDAYONE');
10788 break;
10789 default :
10790 $ret = $short ? JText::translate('VBSUN') : JText::translate('VBWEEKDAYZERO');
10791 break;
10792 }
10793 return $ret;
10794 }
10795
10796 /**
10797 * Returns the translation of the given 1-indexed month of the year.
10798 *
10799 * @param int $idm the 1-indexed month of the year.
10800 * @param bool $short whether to get the short version of the month.
10801 *
10802 * @return string the name of the month.
10803 *
10804 * @since 1.15.0 (J) - 1.5.0 (WP) added support for short translation.
10805 */
10806 public static function sayMonth($idm, $short = false)
10807 {
10808 switch ($idm) {
10809 case '12' :
10810 $ret = $short ? JText::translate('VBSHORTMONTHTWELVE') : JText::translate('VBMONTHTWELVE');
10811 break;
10812 case '11' :
10813 $ret = $short ? JText::translate('VBSHORTMONTHELEVEN') : JText::translate('VBMONTHELEVEN');
10814 break;
10815 case '10' :
10816 $ret = $short ? JText::translate('VBSHORTMONTHTEN') : JText::translate('VBMONTHTEN');
10817 break;
10818 case '9' :
10819 $ret = $short ? JText::translate('VBSHORTMONTHNINE') : JText::translate('VBMONTHNINE');
10820 break;
10821 case '8' :
10822 $ret = $short ? JText::translate('VBSHORTMONTHEIGHT') : JText::translate('VBMONTHEIGHT');
10823 break;
10824 case '7' :
10825 $ret = $short ? JText::translate('VBSHORTMONTHSEVEN') : JText::translate('VBMONTHSEVEN');
10826 break;
10827 case '6' :
10828 $ret = $short ? JText::translate('VBSHORTMONTHSIX') : JText::translate('VBMONTHSIX');
10829 break;
10830 case '5' :
10831 $ret = $short ? JText::translate('VBSHORTMONTHFIVE') : JText::translate('VBMONTHFIVE');
10832 break;
10833 case '4' :
10834 $ret = $short ? JText::translate('VBSHORTMONTHFOUR') : JText::translate('VBMONTHFOUR');
10835 break;
10836 case '3' :
10837 $ret = $short ? JText::translate('VBSHORTMONTHTHREE') : JText::translate('VBMONTHTHREE');
10838 break;
10839 case '2' :
10840 $ret = $short ? JText::translate('VBSHORTMONTHTWO') : JText::translate('VBMONTHTWO');
10841 break;
10842 default :
10843 $ret = $short ? JText::translate('VBSHORTMONTHONE') : JText::translate('VBMONTHONE');
10844 break;
10845 }
10846 return $ret;
10847 }
10848
10849 public static function sayDayMonth($d) {
10850 switch ($d) {
10851 case '31' :
10852 $ret = JText::translate('VBDAYMONTHTHIRTYONE');
10853 break;
10854 case '30' :
10855 $ret = JText::translate('VBDAYMONTHTHIRTY');
10856 break;
10857 case '29' :
10858 $ret = JText::translate('VBDAYMONTHTWENTYNINE');
10859 break;
10860 case '28' :
10861 $ret = JText::translate('VBDAYMONTHTWENTYEIGHT');
10862 break;
10863 case '27' :
10864 $ret = JText::translate('VBDAYMONTHTWENTYSEVEN');
10865 break;
10866 case '26' :
10867 $ret = JText::translate('VBDAYMONTHTWENTYSIX');
10868 break;
10869 case '25' :
10870 $ret = JText::translate('VBDAYMONTHTWENTYFIVE');
10871 break;
10872 case '24' :
10873 $ret = JText::translate('VBDAYMONTHTWENTYFOUR');
10874 break;
10875 case '23' :
10876 $ret = JText::translate('VBDAYMONTHTWENTYTHREE');
10877 break;
10878 case '22' :
10879 $ret = JText::translate('VBDAYMONTHTWENTYTWO');
10880 break;
10881 case '21' :
10882 $ret = JText::translate('VBDAYMONTHTWENTYONE');
10883 break;
10884 case '20' :
10885 $ret = JText::translate('VBDAYMONTHTWENTY');
10886 break;
10887 case '19' :
10888 $ret = JText::translate('VBDAYMONTHNINETEEN');
10889 break;
10890 case '18' :
10891 $ret = JText::translate('VBDAYMONTHEIGHTEEN');
10892 break;
10893 case '17' :
10894 $ret = JText::translate('VBDAYMONTHSEVENTEEN');
10895 break;
10896 case '16' :
10897 $ret = JText::translate('VBDAYMONTHSIXTEEN');
10898 break;
10899 case '15' :
10900 $ret = JText::translate('VBDAYMONTHFIFTEEN');
10901 break;
10902 case '14' :
10903 $ret = JText::translate('VBDAYMONTHFOURTEEN');
10904 break;
10905 case '13' :
10906 $ret = JText::translate('VBDAYMONTHTHIRTEEN');
10907 break;
10908 case '12' :
10909 $ret = JText::translate('VBDAYMONTHTWELVE');
10910 break;
10911 case '11' :
10912 $ret = JText::translate('VBDAYMONTHELEVEN');
10913 break;
10914 case '10' :
10915 $ret = JText::translate('VBDAYMONTHTEN');
10916 break;
10917 case '9' :
10918 $ret = JText::translate('VBDAYMONTHNINE');
10919 break;
10920 case '8' :
10921 $ret = JText::translate('VBDAYMONTHEIGHT');
10922 break;
10923 case '7' :
10924 $ret = JText::translate('VBDAYMONTHSEVEN');
10925 break;
10926 case '6' :
10927 $ret = JText::translate('VBDAYMONTHSIX');
10928 break;
10929 case '5' :
10930 $ret = JText::translate('VBDAYMONTHFIVE');
10931 break;
10932 case '4' :
10933 $ret = JText::translate('VBDAYMONTHFOUR');
10934 break;
10935 case '3' :
10936 $ret = JText::translate('VBDAYMONTHTHREE');
10937 break;
10938 case '2' :
10939 $ret = JText::translate('VBDAYMONTHTWO');
10940 break;
10941 default :
10942 $ret = JText::translate('VBDAYMONTHONE');
10943 break;
10944 }
10945 return $ret;
10946 }
10947
10948 public static function totElements($arr) {
10949 $n = 0;
10950 if (is_array($arr)) {
10951 foreach ($arr as $a) {
10952 if (!empty($a)) {
10953 $n++;
10954 }
10955 }
10956 return $n;
10957 }
10958 return false;
10959 }
10960
10961 /**
10962 * Returns a list of documents that were uploaded
10963 * for the specified customer.
10964 *
10965 * @param integer $id The customer ID.
10966 *
10967 * @return array A list of documents.
10968 *
10969 * @since 1.3.0
10970 */
10971 public static function getCustomerDocuments($id)
10972 {
10973 $dbo = JFactory::getDbo();
10974
10975 $q = $dbo->getQuery(true)
10976 ->select($dbo->qn('docsfolder'))
10977 ->from($dbo->qn('#__vikbooking_customers'))
10978 ->where($dbo->qn('id') . ' = ' . (int) $id);
10979
10980 $dbo->setQuery($q, 0, 1);
10981 $dbo->execute();
10982
10983 if (!$dbo->getNumRows())
10984 {
10985 // customer not found
10986 return array();
10987 }
10988
10989 // retrieve customer documents directory name
10990 $dirname = $dbo->loadResult();
10991
10992 if (empty($dirname))
10993 {
10994 // no available directory
10995 return array();
10996 }
10997
10998 // build documents folder path
10999 $dirname = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR . $dirname;
11000
11001 if (!is_dir($dirname))
11002 {
11003 // the customer directory doesn't exist
11004 return array();
11005 }
11006
11007 // read all files from customer directory
11008 $glob = glob($dirname . DIRECTORY_SEPARATOR . '*');
11009
11010 $files = array();
11011
11012 foreach ($glob as $path)
11013 {
11014 // skip "index.html"
11015 if (!preg_match("/[\/\\\\]index\.html$/i", $path))
11016 {
11017 // extract name and extension from file path
11018 if (preg_match("/(.*)\.([a-z0-9]{2,})$/i", basename($path), $match))
11019 {
11020 $name = $match[1];
11021 $ext = $match[2];
11022 }
11023 else
11024 {
11025 $name = basename($path);
11026 $ext = '';
11027 }
11028
11029 $file = new stdClass;
11030 $file->path = $path;
11031 $file->name = $name;
11032 $file->ext = $ext;
11033 $file->basename = $file->name . '.' . $file->ext;
11034 $file->size = filesize($path);
11035 $file->date = filemtime($path);
11036 $file->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $file->path));
11037
11038 $files[] = $file;
11039 }
11040 }
11041
11042 // sort files by creation date
11043 usort($files, function($a, $b)
11044 {
11045 return $b->date - $a->date;
11046 });
11047
11048 return $files;
11049 }
11050
11051 public static function displayPaymentParameters($pfile, $pparams = '') {
11052 $html = '<p>---------</p>';
11053
11054 /**
11055 * @wponly The payment gateway is now loaded
11056 * using the apposite dispatcher.
11057 *
11058 * @since 1.0.5
11059 */
11060 JLoader::import('adapter.payment.dispatcher');
11061
11062 try
11063 {
11064 $payment = JPaymentDispatcher::getInstance('vikbooking', $pfile);
11065 }
11066 catch (Exception $e)
11067 {
11068 // payment not found
11069 $html = $e->getMessage();
11070
11071 if ($code = $e->getCode())
11072 {
11073 $html = '<b>' . $code . '</b> : ' . $html;
11074 }
11075
11076 return $html;
11077 }
11078 //
11079
11080 $arrparams = !empty($pparams) ? json_decode($pparams, true) : array();
11081 $arrparams = !is_array($arrparams) ? array() : $arrparams;
11082
11083 // get admin parameters
11084 $pconfig = $payment->getAdminParameters();
11085
11086 if (count($pconfig) > 0) {
11087 $html = '';
11088 foreach ($pconfig as $value => $cont) {
11089 if (empty($value)) {
11090 continue;
11091 }
11092 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11093 $label = $labelparts[0];
11094 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11095 if (!empty($cont['help'])) {
11096 $labelhelp = $cont['help'];
11097 }
11098 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11099 $html .= '<div class="vbo-param-container">';
11100 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11101 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11102 }
11103 $html .= '<div class="vbo-param-setting">';
11104 switch ($cont['type']) {
11105 case 'custom':
11106 $html .= $cont['html'];
11107 break;
11108 case 'select':
11109 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11110 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11111 if (isset($cont['multiple']) && $cont['multiple']) {
11112 $html .= '<select name="vikpaymentparams['.$value.'][]" multiple="multiple">';
11113 } else {
11114 $html .= '<select name="vikpaymentparams['.$value.']">';
11115 }
11116 foreach ($options as $optkey => $poption) {
11117 $checkval = $is_assoc ? $optkey : $poption;
11118 $selected = false;
11119 if (isset($arrparams[$value])) {
11120 if (is_array($arrparams[$value])) {
11121 $selected = in_array($checkval, $arrparams[$value]);
11122 } else {
11123 $selected = ($checkval == $arrparams[$value]);
11124 }
11125 } elseif (isset($default_paramv)) {
11126 if (is_array($default_paramv)) {
11127 $selected = in_array($checkval, $default_paramv);
11128 } else {
11129 $selected = ($checkval == $default_paramv);
11130 }
11131 }
11132 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11133 }
11134 $html .= '</select>';
11135 break;
11136 case 'password':
11137 $html .= '<div class="btn-wrapper input-append">';
11138 $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"/>';
11139 $html .= '<button type="button" class="btn btn-primary" onclick="vikPaymentParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11140 $html .= '</div>';
11141 break;
11142 case 'number':
11143 $number_attr = array();
11144 if (isset($cont['min'])) {
11145 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11146 }
11147 if (isset($cont['max'])) {
11148 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11149 }
11150 if (isset($cont['step'])) {
11151 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11152 }
11153 $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) . '/>';
11154 break;
11155 case 'textarea':
11156 $html .= '<textarea name="vikpaymentparams['.$value.']">'.(isset($arrparams[$value]) ? JHtml::fetch('esc_textarea', $arrparams[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11157 break;
11158 case 'hidden':
11159 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="'.(isset($arrparams[$value]) ? JHtml::fetch('esc_attr', $arrparams[$value]) : JHtml::fetch('esc_attr', $default_paramv)).'"/>';
11160 break;
11161 case 'checkbox':
11162 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11163 $html .= '<input type="hidden" name="vikpaymentparams['.$value.']" value="0" />';
11164 $html .= self::getVboApplication()->printYesNoButtons('vikpaymentparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($arrparams[$value]) ? (int)$arrparams[$value] : (int)$default_paramv), 1, 0);
11165 break;
11166 default:
11167 $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"/>';
11168 break;
11169 }
11170 if (strlen($labelhelp) > 0) {
11171 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11172 }
11173 $html .= '</div>';
11174 $html .= '</div>';
11175 }
11176 // JS helper function to toggle the password fields
11177 $html .= "\n" . '<script>' . "\n";
11178 $html .= 'function vikPaymentParamTogglePwd(elem) {' . "\n";
11179 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11180 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11181 $html .= ' var inp_type = inp.attr("type");' . "\n";
11182 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11183 $html .= '}' . "\n";
11184 $html .= "\n" . '</script>' . "\n";
11185 }
11186
11187 return $html;
11188 }
11189
11190 public static function displaySMSParameters($pfile, $params = null)
11191 {
11192 $html = '---------';
11193
11194 $params = is_string($params) ? json_decode($params, true) : $params;
11195 $params = is_array($params) ? $params : [];
11196
11197 if (empty($pfile) || !is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile)) {
11198 return $html;
11199 }
11200
11201 // attempt to load the class file
11202 require_once(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'smsapi' . DIRECTORY_SEPARATOR . $pfile);
11203
11204 if (!class_exists('VikSmsApi') || !method_exists('VikSmsApi', 'getAdminParameters')) {
11205 return $html;
11206 }
11207
11208 // load the gateway parameters
11209 $config = VikSmsApi::getAdminParameters();
11210
11211 if (!is_array($config) || !count($config)) {
11212 return $html;
11213 }
11214
11215 // flags for JS helpers
11216 $js_helpers = array();
11217
11218 $html = '';
11219 foreach ($config as $value => $cont) {
11220 if (empty($value)) {
11221 continue;
11222 }
11223 $inp_attr = '';
11224 if (isset($cont['attributes'])) {
11225 foreach ($cont['attributes'] as $inpk => $inpv) {
11226 $inp_attr .= $inpk.'="'.$inpv.'" ';
11227 }
11228 $inp_attr = ' ' . rtrim($inp_attr);
11229 }
11230 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11231 $label = $labelparts[0];
11232 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11233 if (!empty($cont['help'])) {
11234 $labelhelp = $cont['help'];
11235 }
11236 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11237 $html .= '<div class="vbo-param-container' . (in_array($cont['type'], array('textarea', 'visual_html')) ? ' vbo-param-container-full' : '') . '">';
11238 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11239 $html .= '<div class="vbo-param-label">'.$label.'</div>';
11240 }
11241 $html .= '<div class="vbo-param-setting">';
11242 switch ($cont['type']) {
11243 case 'custom':
11244 $html .= $cont['html'];
11245 break;
11246 case 'select':
11247 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : array();
11248 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11249 if (isset($cont['multiple']) && $cont['multiple']) {
11250 $html .= '<select name="viksmsparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11251 } else {
11252 $html .= '<select name="viksmsparams['.$value.']"' . $inp_attr . '>';
11253 }
11254 foreach ($options as $optkey => $poption) {
11255 $checkval = $is_assoc ? $optkey : $poption;
11256 $selected = false;
11257 if (isset($params[$value])) {
11258 if (is_array($params[$value])) {
11259 $selected = in_array($checkval, $params[$value]);
11260 } else {
11261 $selected = ($checkval == $params[$value]);
11262 }
11263 } elseif (isset($default_paramv)) {
11264 if (is_array($default_paramv)) {
11265 $selected = in_array($checkval, $default_paramv);
11266 } else {
11267 $selected = ($checkval == $default_paramv);
11268 }
11269 }
11270 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11271 }
11272 $html .= '</select>';
11273 break;
11274 case 'password':
11275 $html .= '<div class="btn-wrapper input-append">';
11276 $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 . '/>';
11277 $html .= '<button type="button" class="btn btn-primary" onclick="vikSMSParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11278 $html .= '</div>';
11279 // set flag for JS helper
11280 $js_helpers[] = $cont['type'];
11281 break;
11282 case 'number':
11283 $number_attr = array();
11284 if (isset($cont['min'])) {
11285 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11286 }
11287 if (isset($cont['max'])) {
11288 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11289 }
11290 if (isset($cont['step'])) {
11291 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11292 }
11293 $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 . '/>';
11294 break;
11295 case 'textarea':
11296 $html .= '<textarea name="viksmsparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11297 break;
11298 case 'visual_html':
11299 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11300 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : array();
11301 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : array();
11302 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : array();
11303 $html .= self::getVboApplication()->renderVisualEditor('viksmsparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11304 break;
11305 case 'hidden':
11306 $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 . '/>';
11307 break;
11308 case 'checkbox':
11309 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11310 $html .= '<input type="hidden" name="viksmsparams['.$value.']" value="0" />';
11311 $html .= self::getVboApplication()->printYesNoButtons('viksmsparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11312 break;
11313 default:
11314 $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 . '/>';
11315 break;
11316 }
11317 if (strlen($labelhelp) > 0) {
11318 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11319 }
11320 $html .= '</div>';
11321 $html .= '</div>';
11322 }
11323
11324 // JS helper functions
11325 if (in_array('password', $js_helpers)) {
11326 // toggle the password fields
11327 $html .= "\n" . '<script>' . "\n";
11328 $html .= 'function vikSMSParamTogglePwd(elem) {' . "\n";
11329 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11330 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11331 $html .= ' var inp_type = inp.attr("type");' . "\n";
11332 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11333 $html .= '}' . "\n";
11334 $html .= "\n" . '</script>' . "\n";
11335 }
11336
11337 return $html;
11338 }
11339
11340 /**
11341 * Renders the params of the requested cron job file-class.
11342 *
11343 * @param string $file the name of the cron job driver file.
11344 * @param array $params the parameters array.
11345 *
11346 * @return string the necessary HTML content to render.
11347 *
11348 * @since 1.15.0 (J) - 1.5.0 (WP) - support for new fields added.
11349 */
11350 public static function displayCronParameters($file, $params = [])
11351 {
11352 try
11353 {
11354 // attempt to create a new instance
11355 $job = VBOFactory::getCronFactory()->createInstance($file);
11356 }
11357 catch (Exception $e)
11358 {
11359 // something went wrong, display error message
11360 return '<p>' . $e->getMessage() . '<p>';
11361 }
11362
11363 // get admin parameters
11364 $config = $job->getForm();
11365
11366 if (!is_array($config) || !count($config))
11367 {
11368 return '<p>---------</p>';
11369 }
11370
11371 // flags for JS helpers
11372 $js_helpers = array();
11373
11374 $html = '';
11375 foreach ($config as $value => $cont) {
11376 if (empty($value)) {
11377 continue;
11378 }
11379
11380 $inp_attr = '';
11381 if (isset($cont['attributes']) && is_array($cont['attributes'])) {
11382 foreach ($cont['attributes'] as $inpk => $inpv) {
11383 $inp_attr .= $inpk . '="' . $inpv . '" ';
11384 }
11385 $inp_attr = ' ' . rtrim($inp_attr);
11386 }
11387
11388 $labelparts = explode('//', (isset($cont['label']) ? $cont['label'] : ''));
11389 $label = $labelparts[0];
11390 $labelhelp = isset($labelparts[1]) ? $labelparts[1] : '';
11391 if (!empty($cont['help'])) {
11392 $labelhelp = $cont['help'];
11393 }
11394
11395 $default_paramv = isset($cont['default']) ? $cont['default'] : null;
11396 $nested_style = (isset($cont['nested']) && $cont['nested']);
11397 $hidden_wrapper = false;
11398 if (isset($cont['conditional']) && isset($config[$cont['conditional']])) {
11399 $check_cond = isset($config[$cont['conditional']]['default']) ? $config[$cont['conditional']]['default'] : null;
11400 $check_cond = isset($params[$cont['conditional']]) ? $params[$cont['conditional']] : $check_cond;
11401 if (!is_null($check_cond) && (!$check_cond || !strcasecmp($check_cond, 'off'))) {
11402 // hide current field because the field to who this is dependant is "off" or disabled (i.e. 0)
11403 $hidden_wrapper = true;
11404 }
11405 }
11406
11407 $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;"' : '') . '>';
11408 if (strlen($label) > 0 && (!isset($cont['hidden']) || $cont['hidden'] != true)) {
11409 $html .= '<div class="vbo-param-label">' . $label . '</div>';
11410 }
11411 $html .= '<div class="vbo-param-setting">';
11412
11413 switch ($cont['type']) {
11414 case 'custom':
11415 $html .= $cont['html'];
11416 break;
11417 case 'select':
11418 $options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11419 $is_assoc = (array_keys($options) !== range(0, count($options) - 1));
11420 if (isset($cont['multiple']) && $cont['multiple']) {
11421 $html .= '<select name="vikcronparams['.$value.'][]" multiple="multiple"' . $inp_attr . '>';
11422 } else {
11423 $html .= '<select name="vikcronparams['.$value.']"' . $inp_attr . '>';
11424 }
11425 foreach ($options as $optkey => $poption) {
11426 $checkval = $is_assoc ? $optkey : $poption;
11427 $selected = false;
11428 if (isset($params[$value])) {
11429 if (is_array($params[$value])) {
11430 $selected = in_array($checkval, $params[$value]);
11431 } else {
11432 $selected = ($checkval == $params[$value]);
11433 }
11434 } elseif (isset($default_paramv)) {
11435 if (is_array($default_paramv)) {
11436 $selected = in_array($checkval, $default_paramv);
11437 } else {
11438 $selected = ($checkval == $default_paramv);
11439 }
11440 }
11441 $html .= '<option value="' . ($is_assoc ? $optkey : $poption) . '"'.($selected ? ' selected="selected"' : '').'>'.$poption.'</option>';
11442 }
11443 $html .= '</select>';
11444 break;
11445 case 'password':
11446 $html .= '<div class="btn-wrapper input-append">';
11447 $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 . '/>';
11448 $html .= '<button type="button" class="btn btn-primary" onclick="vikCronParamTogglePwd(this);"><i class="' . VikBookingIcons::i('eye') . '"></i></button>';
11449 $html .= '</div>';
11450 // set flag for JS helper
11451 $js_helpers[] = $cont['type'];
11452 break;
11453 case 'number':
11454 $number_attr = [];
11455 if (isset($cont['min'])) {
11456 $number_attr[] = 'min="' . JHtml::fetch('esc_attr', $cont['min']) . '"';
11457 }
11458 if (isset($cont['max'])) {
11459 $number_attr[] = 'max="' . JHtml::fetch('esc_attr', $cont['max']) . '"';
11460 }
11461 if (isset($cont['step'])) {
11462 $number_attr[] = 'step="' . JHtml::fetch('esc_attr', $cont['step']) . '"';
11463 }
11464 $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 . '/>';
11465 break;
11466 case 'textarea':
11467 $html .= '<textarea name="vikcronparams['.$value.']"' . $inp_attr . '>'.(isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)).'</textarea>';
11468 break;
11469 case 'visual_html':
11470 $tarea_cont = isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv);
11471 $tarea_attr = isset($cont['attributes']) && is_array($cont['attributes']) ? $cont['attributes'] : [];
11472 $editor_opts = isset($cont['editor_opts']) && is_array($cont['editor_opts']) ? $cont['editor_opts'] : [];
11473 $editor_btns = isset($cont['editor_btns']) && is_array($cont['editor_btns']) ? $cont['editor_btns'] : [];
11474 $html .= self::getVboApplication()->renderVisualEditor('vikcronparams[' . $value . ']', $tarea_cont, $tarea_attr, $editor_opts, $editor_btns);
11475 break;
11476 case 'codemirror':
11477 $editor = JEditor::getInstance('codemirror');
11478 $e_options = isset($cont['options']) && is_array($cont['options']) ? $cont['options'] : [];
11479 $e_name = 'vikcronparams[' . $value . ']';
11480 $e_value = isset($params[$value]) ? $params[$value] : $default_paramv;
11481 $e_width = isset($e_options['width']) ? $e_options['width'] : '100%';
11482 $e_height = isset($e_options['height']) ? $e_options['height'] : 300;
11483 $e_col = isset($e_options['col']) ? $e_options['col'] : 70;
11484 $e_row = isset($e_options['row']) ? $e_options['row'] : 20;
11485 $e_buttons = isset($e_options['buttons']) ? (bool)$e_options['buttons'] : true;
11486 $e_id = isset($e_options['id']) ? $e_options['id'] : 'vikcronparams_' . $value;
11487 $e_params = isset($e_options['params']) && is_array($e_options['params']) ? $e_options['params'] : [];
11488 if (interface_exists('Throwable')) {
11489 /**
11490 * With PHP >= 7 supporting throwable exceptions for Fatal Errors
11491 * we try to avoid issues with third party plugins that make use
11492 * of the WP native function get_current_screen().
11493 *
11494 * @wponly
11495 */
11496 try {
11497 $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);
11498 } catch (Throwable $t) {
11499 $html .= $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine() . '<br/>';
11500 $html .= '<textarea name="vikcronparams[' . $value . ']"' . $inp_attr . '>' . (isset($params[$value]) ? JHtml::fetch('esc_textarea', $params[$value]) : JHtml::fetch('esc_textarea', $default_paramv)) . '</textarea>';
11501 }
11502 } else {
11503 $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);
11504 }
11505 break;
11506 case 'hidden':
11507 $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 . '/>';
11508 break;
11509 case 'checkbox':
11510 // always display a hidden input value turned off before the actual checkbox to support the "off" (0) status
11511 $html .= '<input type="hidden" name="vikcronparams['.$value.']" value="0" />';
11512 $html .= self::getVboApplication()->printYesNoButtons('vikcronparams['.$value.']', JText::translate('VBYES'), JText::translate('VBNO'), (isset($params[$value]) ? (int)$params[$value] : (int)$default_paramv), 1, 0);
11513 break;
11514 default:
11515 $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 . '/>';
11516 break;
11517 }
11518
11519 if (strlen($labelhelp)) {
11520 $html .= '<span class="vbo-param-setting-comment">'.$labelhelp.'</span>';
11521 }
11522
11523 $html .= '</div>';
11524 $html .= '</div>';
11525 }
11526
11527 // JS helper functions
11528 if (in_array('password', $js_helpers)) {
11529 // toggle the password fields
11530 $html .= "\n" . '<script>' . "\n";
11531 $html .= 'function vikCronParamTogglePwd(elem) {' . "\n";
11532 $html .= ' var btn = jQuery(elem), inp = btn.parent().find("input").first();' . "\n";
11533 $html .= ' if (!inp || !inp.length) {return false;}' . "\n";
11534 $html .= ' var inp_type = inp.attr("type");' . "\n";
11535 $html .= ' inp.attr("type", (inp_type == "password" ? "text" : "password"));' . "\n";
11536 $html .= '}' . "\n";
11537 $html .= "\n" . '</script>' . "\n";
11538 }
11539
11540 return $html;
11541 }
11542
11543 public static function invokeChannelManager($skiporder = true, $order = array()) {
11544 $task = VikRequest::getString('task', '', 'request');
11545 $view = VikRequest::getString('view', '', 'request');
11546 $tmpl = VikRequest::getString('tmpl', '', 'request');
11547 $noimpression = array('vieworder', 'booking');
11548 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')) {
11549 //VCM Channel Impression
11550 if (!class_exists('VikChannelManagerConfig')) {
11551 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11552 }
11553 if (!class_exists('VikChannelManager')) {
11554 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11555 }
11556 VikChannelManager::invokeChannelImpression();
11557 } elseif ($tmpl != 'component' && count($order) > 0 && file_exists(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php')) {
11558 //VCM Channel Conversion-Impression
11559 if (!class_exists('VikChannelManagerConfig')) {
11560 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'vcm_config.php');
11561 }
11562 if (!class_exists('VikChannelManager')) {
11563 require_once(VCM_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikchannelmanager.php');
11564 }
11565 VikChannelManager::invokeChannelConversionImpression($order);
11566 }
11567 }
11568
11569 public static function validEmail($email) {
11570 $isValid = true;
11571 $atIndex = strrpos($email, "@");
11572 if (is_bool($atIndex) && !$atIndex) {
11573 $isValid = false;
11574 } else {
11575 $domain = substr($email, $atIndex +1);
11576 $local = substr($email, 0, $atIndex);
11577 $localLen = strlen($local);
11578 $domainLen = strlen($domain);
11579 if ($localLen < 1 || $localLen > 64) {
11580 // local part length exceeded
11581 $isValid = false;
11582 } else
11583 if ($domainLen < 1 || $domainLen > 255) {
11584 // domain part length exceeded
11585 $isValid = false;
11586 } else
11587 if ($local[0] == '.' || $local[$localLen -1] == '.') {
11588 // local part starts or ends with '.'
11589 $isValid = false;
11590 } else
11591 if (preg_match('/\\.\\./', $local)) {
11592 // local part has two consecutive dots
11593 $isValid = false;
11594 } else
11595 if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) {
11596 // character not valid in domain part
11597 $isValid = false;
11598 } else
11599 if (preg_match('/\\.\\./', $domain)) {
11600 // domain part has two consecutive dots
11601 $isValid = false;
11602 } else
11603 if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) {
11604 // character not valid in local part unless
11605 // local part is quoted
11606 if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) {
11607 $isValid = false;
11608 }
11609 }
11610 if ($isValid && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) {
11611 // domain not found in DNS
11612 $isValid = false;
11613 }
11614 }
11615 return $isValid;
11616 }
11617
11618 public static function caniWrite($path) {
11619 if ($path[strlen($path) - 1] == '/') {
11620 // ricorsivo return a temporary file path
11621 return self::caniWrite($path . uniqid(mt_rand()) . '.tmp');
11622 }
11623 if (is_dir($path)) {
11624 return self::caniWrite($path . '/' . uniqid(mt_rand()) . '.tmp');
11625 }
11626 // check tmp file for read/write capabilities
11627 $rm = file_exists($path);
11628 $f = @fopen($path, 'a');
11629 if ($f === false) {
11630 return false;
11631 }
11632 fclose($f);
11633 if (!$rm) {
11634 unlink($path);
11635 }
11636 return true;
11637 }
11638
11639 /**
11640 * Alias method of JFile::upload to unify any
11641 * upload function into one.
11642 *
11643 * @param string $src The name of the php (temporary) uploaded file.
11644 * @param string $dest The path (including filename) to move the uploaded file to.
11645 * @param boolean [$copy_only] Whether to skip the file upload and just copy the file.
11646 *
11647 * @return boolean True on success.
11648 *
11649 * @since 1.10 - Revision April 24th 2018 for compatibility with the VikWP Framework.
11650 * @wponly 1.0.7 added the third $copy_only argument to remove the use of copy()
11651 */
11652 public static function uploadFile($src, $dest, $copy_only = false) {
11653 // always attempt to include the File class
11654 jimport('joomla.filesystem.file');
11655
11656 // upload the file
11657 if (!$copy_only) {
11658 $result = JFile::upload($src, $dest);
11659 } else {
11660 // this is to avoid the use of the PHP function copy() and allow files mirroring in WP (triggerUploadBackup)
11661 $result = JFile::copy($src, $dest);
11662 }
11663
11664 /**
11665 * @wponly in order to not lose uploaded files after installing an update,
11666 * we need to move any uploaded file onto a recovery folder.
11667 */
11668 if ($result) {
11669 VikBookingLoader::import('update.manager');
11670 VikBookingUpdateManager::triggerUploadBackup($dest);
11671 }
11672 //
11673
11674 // return upload result
11675 return $result;
11676 }
11677
11678 /**
11679 * Helper method used to upload the given file (retrieved from $_FILES)
11680 * into the specified destination.
11681 *
11682 * @param array $file An associative array with the file details.
11683 * @param string $dest The destination path.
11684 * @param string $filters A string (or a regex) containing the allowed extensions.
11685 *
11686 * @return array The uploading result.
11687 *
11688 * @throws RuntimeException
11689 *
11690 * @since 1.3.0
11691 */
11692 public static function uploadFileFromRequest($file, $dest, $filters = '*')
11693 {
11694 jimport('joomla.filesystem.file');
11695
11696 $dest = rtrim($dest, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
11697
11698 if (empty($file['name']))
11699 {
11700 throw new RuntimeException('Missing file', 400);
11701 }
11702
11703 /**
11704 * Make sure the upload of the file didn't raise any errors.
11705 *
11706 * @link https://www.php.net/manual/en/features.file-upload.errors.php
11707 */
11708 if ((int) $file['error'])
11709 {
11710 if ($file['error'] == UPLOAD_ERR_INI_SIZE)
11711 {
11712 $error = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.';
11713 }
11714 else if ($file['error'] == UPLOAD_ERR_FORM_SIZE)
11715 {
11716 $error = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.';
11717 }
11718 else if ($file['error'] == UPLOAD_ERR_PARTIAL)
11719 {
11720 $error = 'The uploaded file was only partially uploaded.';
11721 }
11722 else if ($file['error'] == UPLOAD_ERR_NO_FILE)
11723 {
11724 $error = 'No file was uploaded.';
11725 }
11726 else if ($file['error'] == UPLOAD_ERR_NO_TMP_DIR)
11727 {
11728 $error = 'Missing a temporary folder.';
11729 }
11730 else if ($file['error'] == UPLOAD_ERR_CANT_WRITE)
11731 {
11732 $error = 'Failed to write file to disk.';
11733 }
11734 else if ($file['error'] == UPLOAD_ERR_EXTENSION)
11735 {
11736 $error = 'A PHP extension stopped the file upload.';
11737 }
11738 else
11739 {
11740 $error = 'Unknown error.';
11741 }
11742
11743 throw new RuntimeException($error, 500);
11744 }
11745
11746 $src = $file['tmp_name'];
11747
11748 // extract file name and extension
11749 if (preg_match("/(.*?)(\.[0-9a-z]{2,})$/i", basename($file['name']), $match))
11750 {
11751 $filename = $match[1];
11752 $fileext = $match[2];
11753 }
11754 else
11755 {
11756 // probably no extension provided
11757 $filename = basename($file['name']);
11758 $fileext = '';
11759 }
11760
11761 $j = '';
11762
11763 if (file_exists($dest . $filename . $fileext))
11764 {
11765 $j = 2;
11766
11767 while (file_exists($dest . $filename . '-' . $j . $fileext))
11768 {
11769 $j++;
11770 }
11771
11772 $j = '-' . $j;
11773 }
11774
11775 $finaldest = $dest . $filename . $j . $fileext;
11776
11777 if ($filters !== '*')
11778 {
11779 $ext = $file['type'];
11780
11781 // check if we have a regex
11782 if (preg_match("/^[#\/]/", $filters) && preg_match("/[#\/][a-z]*$/", $filters))
11783 {
11784 if (!preg_match($filters, $ext))
11785 {
11786 // extension not supported
11787 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11788 }
11789 }
11790 else
11791 {
11792 // get all supported types
11793 $types = array_map('strtolower', array_filter(explode(',', $filters)));
11794
11795 if (!in_array($ext, $types))
11796 {
11797 // extension not supported
11798 throw new RuntimeException(sprintf('Extension [%s] is not supported', $ext), 400);
11799 }
11800 }
11801 }
11802
11803 // try to upload the file
11804 if (!JFile::upload($src, $finaldest, $use_streams = false, $allow_unsafe = true))
11805 {
11806 throw new RuntimeException(sprintf('Unable to upload the file [%s] to [%s]', $src, $finaldest), 500);
11807 }
11808
11809 $file = new stdClass;
11810 $file->name = $filename . $j;
11811 $file->ext = ltrim($fileext, '.');
11812 $file->filename = basename($finaldest);
11813 $file->path = $finaldest;
11814
11815 return $file;
11816 }
11817
11818 /**
11819 * Gets the instance of a specific report class.
11820 *
11821 * @param string $report the name of the report to load.
11822 *
11823 * @return mixed false or report object instance.
11824 *
11825 * @since 1.3.0
11826 */
11827 public static function getReportInstance($report)
11828 {
11829 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'report' . DIRECTORY_SEPARATOR . 'report.php';
11830
11831 return VikBookingReport::getInstanceOf($report);
11832 }
11833
11834 /**
11835 * Checks whether the guest reviews are enabled and VCM is installed.
11836 *
11837 * @return boolean true if enabled, false otherwise.
11838 *
11839 * @since 1.3.0
11840 */
11841 public static function allowGuestReviews()
11842 {
11843 $dbo = JFactory::getDbo();
11844 $vcm_installed = is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
11845 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grenabled';";
11846 $dbo->setQuery($q);
11847 $dbo->execute();
11848 if ($dbo->getNumRows()) {
11849 $s = $dbo->loadResult();
11850 return ((int)$s === 1 && $vcm_installed);
11851 }
11852 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grenabled', '1');";
11853 $dbo->setQuery($q);
11854 $dbo->execute();
11855 return $vcm_installed;
11856 }
11857
11858 /**
11859 * Gets the minimum chars for the guest review message.
11860 *
11861 * @return int minimum number of chars for the comment (0 = no limits, -1 = disabled).
11862 *
11863 * @since 1.3.0
11864 */
11865 public static function guestReviewMinChars()
11866 {
11867 $dbo = JFactory::getDbo();
11868 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grminchars';";
11869 $dbo->setQuery($q);
11870 $dbo->execute();
11871 if ($dbo->getNumRows() > 0) {
11872 return (int)$dbo->loadResult();
11873 }
11874 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grminchars', '15');";
11875 $dbo->setQuery($q);
11876 $dbo->execute();
11877 return 15;
11878 }
11879
11880 /**
11881 * The approval type for new guest reviews.
11882 *
11883 * @return string auto or manual.
11884 *
11885 * @since 1.3.0
11886 */
11887 public static function guestReviewsApproval()
11888 {
11889 $dbo = JFactory::getDbo();
11890 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grappr';";
11891 $dbo->setQuery($q);
11892 $dbo->execute();
11893 if ($dbo->getNumRows() > 0) {
11894 return $dbo->loadResult() == 'manual' ? 'manual' : 'auto';
11895 }
11896 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grappr', 'auto');";
11897 $dbo->setQuery($q);
11898 $dbo->execute();
11899 return 'auto';
11900 }
11901
11902 /**
11903 * The type of reviews guests should leave.
11904 *
11905 * @return string global or service.
11906 *
11907 * @since 1.3.0
11908 */
11909 public static function guestReviewsType()
11910 {
11911 $dbo = JFactory::getDbo();
11912 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='grtype';";
11913 $dbo->setQuery($q);
11914 $dbo->execute();
11915 if ($dbo->getNumRows() > 0) {
11916 return $dbo->loadResult() == 'global' ? 'global' : 'service';
11917 }
11918 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('grtype', 'service');";
11919 $dbo->setQuery($q);
11920 $dbo->execute();
11921 return 'service';
11922 }
11923
11924 /**
11925 * The services to be reviewed by the guests.
11926 *
11927 * @return array list, empty or not, of the services to be reviewed.
11928 *
11929 * @since 1.3.0
11930 */
11931 public static function guestReviewsServices()
11932 {
11933 $dbo = JFactory::getDbo();
11934 $q = "SELECT `id`,`service_name` FROM `#__vikbooking_greview_service` GROUP BY `service_name` ORDER BY `id` ASC;";
11935 $dbo->setQuery($q);
11936 $dbo->execute();
11937 if ($dbo->getNumRows()) {
11938 return $dbo->loadAssocList();
11939 }
11940 // insert the default services
11941 $default_services = array(
11942 JText::translate('VBOGREVVALUE'),
11943 JText::translate('VBOGREVLOCATION'),
11944 JText::translate('VBOGREVSTAFF'),
11945 JText::translate('VBOGREVCLEAN'),
11946 JText::translate('VBOGREVCOMFORT'),
11947 JText::translate('VBOGREVFACILITIES'),
11948 );
11949 foreach ($default_services as $def_service) {
11950 $q = "INSERT INTO `#__vikbooking_greview_service` (`service_name`) VALUES (" . $dbo->quote($def_service) . ");";
11951 $dbo->setQuery($q);
11952 $dbo->execute();
11953 }
11954 return array();
11955 }
11956
11957 /**
11958 * Checks whether the guest reviews should be downloaded (max 1 per day).
11959 * VCM must be installed in order to download the new reviews.
11960 *
11961 * @return int -1 if VCM is not installed, 0 if already downloaded, 1 for download.
11962 *
11963 * @since 1.3.0
11964 */
11965 public static function shouldDownloadReviews()
11966 {
11967 if (!is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php')) {
11968 return -1;
11969 }
11970
11971 $today = date('Y-m-d');
11972
11973 $dbo = JFactory::getDbo();
11974 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gr_last_download' LIMIT 1;";
11975 $dbo->setQuery($q);
11976 $dbo->execute();
11977 if ($dbo->getNumRows()) {
11978 $last_download = $dbo->loadResult();
11979 // update last download day
11980 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote($today) . " WHERE `param`='gr_last_download';";
11981 $dbo->setQuery($q);
11982 $dbo->execute();
11983 //
11984 return ($last_download != $today ? 1 : 0);
11985 }
11986 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gr_last_download', " . $dbo->quote($today) . ");";
11987 $dbo->setQuery($q);
11988 $dbo->execute();
11989 return 1;
11990 }
11991
11992 /**
11993 * Tells whether a booking can be reviewed.
11994 *
11995 * @param array $booking booking array details.
11996 *
11997 * @return boolean true if a review can be left, false otherwise.
11998 *
11999 * @since 1.3.0
12000 */
12001 public static function canBookingBeReviewed($booking)
12002 {
12003 // reviews must be enabled and supported
12004 if (!self::allowGuestReviews()) {
12005 return false;
12006 }
12007
12008 // booking status must be confirmed
12009 if ($booking['status'] != 'confirmed') {
12010 return false;
12011 }
12012
12013 // review can be left starting from the check-out day
12014 $checkout_info = getdate($booking['checkout']);
12015 $checkout_midn = mktime(0, 0, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
12016 if (time() < $checkout_midn) {
12017 return false;
12018 }
12019
12020 // make sure a review for this booking does not exist
12021 $noreview = true;
12022 $dbo = JFactory::getDbo();
12023 try {
12024 $q = "SELECT `id` FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . ";";
12025 $dbo->setQuery($q);
12026 $dbo->execute();
12027 $noreview = ($dbo->getNumRows() < 1);
12028 } catch (Exception $e) {
12029 // if the query fails, we do not allow the review to be left
12030 $noreview = false;
12031 }
12032
12033 return $noreview;
12034 }
12035
12036 /**
12037 * Gets the review for a booking, no matter if it's published or not.
12038 * Review will be returned only if reviews are enabled and supported.
12039 * Only reviews left through the website, no OTA reviews as this method
12040 * should be used in the front-end to display the review to the guest.
12041 *
12042 * @param array $booking booking array details.
12043 *
12044 * @return mixed associative array or false.
12045 *
12046 * @since 1.3.0
12047 */
12048 public static function getBookingReview($booking)
12049 {
12050 // reviews must be enabled and supported
12051 if (!self::allowGuestReviews()) {
12052 return false;
12053 }
12054
12055 if (empty($booking['id'])) {
12056 return false;
12057 }
12058
12059 // make sure a review for this booking exists
12060 $dbo = JFactory::getDbo();
12061 $review = array();
12062 try {
12063 $q = "SELECT * FROM `#__vikchannelmanager_otareviews` WHERE `idorder`=" . $dbo->quote($booking['id']) . " LIMIT 1;";
12064 $dbo->setQuery($q);
12065 $dbo->execute();
12066 $review = $dbo->getNumRows() ? $dbo->loadAssoc() : $review;
12067 } catch (Exception $e) {
12068 // query has failed
12069 $review = array();
12070 }
12071
12072 if (count($review)) {
12073 // make sure the review was left from the website
12074 $review['uniquekey'] = (int)$review['uniquekey'];
12075 }
12076
12077 if (count($review) && !empty($review['uniquekey'])) {
12078 // this is an OTA review
12079 return false;
12080 }
12081
12082 if (count($review) && !empty($review['content'])) {
12083 // decode content
12084 $review['content'] = json_decode($review['content'], true);
12085 }
12086
12087 return count($review) ? $review : false;
12088 }
12089
12090 /**
12091 * Attempts to find a translation for a raw customer data label.
12092 * Bookings downloaded from the OTAs will save a raw string of information
12093 * composed of pairs of label-value separated by new line feeds.
12094 * We try to translate the given label into the current language.
12095 *
12096 * @param string $label the raw string label to translate.
12097 *
12098 * @return string either the original or the translated label.
12099 *
12100 * @since 1.3.5
12101 * @since 1.4.0 back-end support added.
12102 */
12103 public static function tnCustomerRawDataLabel($label)
12104 {
12105 // this is a map of the known labels
12106 $known_lbls = array(
12107 'NAME' => 'VBOCUSTOMERNOMINATIVE',
12108 'LASTNAME' => 'ORDER_LNAME',
12109 'COUNTRY' => 'ORDER_STATE',
12110 'EMAIL' => 'VBMAIL',
12111 'TELEPHONE' => 'VBPHONE',
12112 'PHONE' => 'VBPHONE',
12113 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12114 'MEAL_PLAN' => 'VBOMEALPLAN',
12115 'CITY' => 'VBCITY',
12116 'BEDPREFERENCE' => 'VBOBEDPREFERENCE',
12117 'BOOKER_IS_GENIUS' => 'VBOBOOKERISGENIUS',
12118 );
12119
12120 if (self::isAdmin()) {
12121 // override the map of known translation strings
12122 $known_lbls = array(
12123 'NAME' => 'ORDER_NAME',
12124 'LASTNAME' => 'ORDER_LNAME',
12125 'COUNTRY' => 'ORDER_STATE',
12126 'ADDRESS' => 'ORDER_ADDRESS',
12127 'CITY' => 'ORDER_CITY',
12128 'LOCATION' => 'ORDER_CITY',
12129 'EMAIL' => 'ORDER_EMAIL',
12130 'TELEPHONE' => 'ORDER_PHONE',
12131 'PHONE' => 'ORDER_PHONE',
12132 'SPECIALREQUEST' => 'ORDER_SPREQUESTS',
12133 );
12134 }
12135
12136 // we get rid of any empty space by keeping the underscores
12137 $converted = str_replace(' ', '', strtoupper($label));
12138
12139 if (isset($known_lbls[$converted])) {
12140 // this language definition has been mapped
12141 return JText::translate($known_lbls[$converted]);
12142 }
12143
12144 // we try to guess the translation string by prepending VBO
12145 $guessed = JText::translate('VBO' . $converted);
12146 if ($guessed != 'VBO' . $converted) {
12147 // the label was translated correctly
12148 return $guessed;
12149 }
12150
12151 // this label could not be translated, so we return the plain string
12152 return $label;
12153 }
12154
12155 /**
12156 * We check whether some bookings are available for import from third party plugins.
12157 *
12158 * @return mixed array with list of plugins supported, false otherwise.
12159 *
12160 * @wponly this method is only useful for WordPress.
12161 *
12162 * @since 1.3.5
12163 */
12164 public static function canImportBookingsFromThirdPartyPlugins()
12165 {
12166 $dbo = JFactory::getDbo();
12167
12168 $plugins = array();
12169
12170 /**
12171 * As requested from hundreds of our clients, for the moment we check only the custom
12172 * post types of type "mphb_booking" to see if some bookings are available for import.
12173 */
12174 $q = "SELECT `post_type` FROM `#__posts` WHERE `post_type`=" . $dbo->quote('mphb_booking') . ";";
12175 $dbo->setQuery($q);
12176 $dbo->execute();
12177
12178 if (!$dbo->getNumRows()) {
12179 return false;
12180 }
12181
12182 // push this third party plugin
12183 $plugins['mphb'] = 'MotoPress Hotel Booking';
12184
12185 return count($plugins) ? $plugins : false;
12186 }
12187
12188 /**
12189 * This method returns a list of the known languages sorted by the
12190 * administrator custom preferences. Useful for the phone input fields.
12191 *
12192 * @param boolean $code_assoc whether to get an associative array with the lang name.
12193 *
12194 * @return array the sorted list of preferred countries.
12195 *
12196 * @since 1.3.11
12197 */
12198 public static function preferredCountriesOrdering($code_assoc = false)
12199 {
12200 $preferred_countries = array();
12201
12202 // try to get the preferred countries from db
12203 $dbo = JFactory::getDbo();
12204 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='preferred_countries';";
12205 $dbo->setQuery($q);
12206 $setting = $dbo->loadResult();
12207 if (!$setting) {
12208 // create empty configuration record
12209 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('preferred_countries', '[]');";
12210 $dbo->setQuery($q);
12211 $dbo->execute();
12212 } else {
12213 $preferred_countries = json_decode($setting);
12214 }
12215
12216 // get the default known languages
12217 $sorted_known_langs = self::getVboApplication()->getKnownLanguages();
12218
12219 if (!is_array($preferred_countries) || !count($preferred_countries)) {
12220 // sort the default known languages by country code alphabetically
12221 ksort($sorted_known_langs);
12222 foreach ($sorted_known_langs as $k => $v) {
12223 $langsep = strpos($k, '_') !== false ? '_' : '-';
12224 $langparts = explode($langsep, $k);
12225 array_push($preferred_countries, isset($langparts[1]) ? strtolower($langparts[1]) : strtolower($langparts[0]));
12226 }
12227 // update the database record
12228 $q = "UPDATE `#__vikbooking_config` SET `setting`=" . $dbo->quote(json_encode($preferred_countries)) . " WHERE `param`='preferred_countries';";
12229 $dbo->setQuery($q);
12230 $dbo->execute();
12231 }
12232
12233 if ($code_assoc) {
12234 // this is useful for displaying the preferred countries codes together with the language name
12235 $map = array();
12236 foreach ($preferred_countries as $ccode) {
12237 // look for the current country code in the keys of the known language tags
12238 $match_found = false;
12239 foreach ($sorted_known_langs as $langtag => $langinfo) {
12240 $langsep = strpos($langtag, '_') !== false ? '_' : '-';
12241 $langparts = explode($langsep, $langtag);
12242 if (isset($langparts[1]) && strtoupper($ccode) == strtoupper($langparts[1])) {
12243 // match found
12244 $match_found = true;
12245 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12246 } elseif (strtoupper($ccode) == strtoupper($langparts[0])) {
12247 // match found
12248 $match_found = true;
12249 $map[$ccode] = !empty($langinfo['nativeName']) ? $langinfo['nativeName'] : $langinfo['name'];
12250 }
12251 }
12252 if (!$match_found) {
12253 // in case someone would like to add a custom country code via DB, we allow to do so by returning the raw value
12254 $map[$ccode] = strtoupper($ccode);
12255 }
12256 }
12257 if (count($map)) {
12258 // set the associatve array to be returned
12259 $preferred_countries = $map;
12260 }
12261 }
12262
12263 return $preferred_countries;
12264 }
12265
12266 /**
12267 * Gets the instance of the admin widgets helper class.
12268 *
12269 * @return VikBookingHelperAdminWidgets
12270 *
12271 * @since 1.4.0
12272 */
12273 public static function getAdminWidgetsInstance()
12274 {
12275 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'admin_widgets.php';
12276
12277 return VikBookingHelperAdminWidgets::getInstance();
12278 }
12279
12280 /**
12281 * Gets the instance of the conditional rules helper class.
12282 *
12283 * @param bool $require_only whether to return the object.
12284 *
12285 * @return mixed VikBookingHelperConditionalRules or true.
12286 *
12287 * @since 1.4.0
12288 */
12289 public static function getConditionalRulesInstance($require_only = false)
12290 {
12291 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'conditional_rules.php';
12292
12293 return $require_only ? true : VikBookingHelperConditionalRules::getInstance();
12294 }
12295
12296 /**
12297 * Gets the instance of the geocoding helper class.
12298 *
12299 * @param bool $require_only whether to return the object.
12300 *
12301 * @return mixed VikBookingHelperGeocoding or true.
12302 *
12303 * @since 1.4.0
12304 */
12305 public static function getGeocodingInstance($require_only = false)
12306 {
12307 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'geocoding.php';
12308
12309 return $require_only ? true : VikBookingHelperGeocoding::getInstance();
12310 }
12311
12312 /**
12313 * Helper method to cope with the removal of the same method
12314 * in the JApplication class introduced with Joomla 4. Using
12315 * isClient() would break the compatibility with J < 3.7 so
12316 * we can rely on this helper method to avoid Fatal Errors.
12317 *
12318 * @return boolean
12319 *
12320 * @since October 2020
12321 */
12322 public static function isAdmin()
12323 {
12324 $app = JFactory::getApplication();
12325 if (method_exists($app, 'isClient')) {
12326 return $app->isClient('administrator');
12327 }
12328
12329 return $app->isAdmin();
12330 }
12331
12332 /**
12333 * Helper method to cope with the removal of the same method
12334 * in the JApplication class introduced with Joomla 4. Using
12335 * isClient() would break the compatibility with J < 3.7 so
12336 * we can rely on this helper method to avoid Fatal Errors.
12337 *
12338 * @return boolean
12339 *
12340 * @since October 2020
12341 */
12342 public static function isSite()
12343 {
12344 $app = JFactory::getApplication();
12345 if (method_exists($app, 'isClient')) {
12346 return $app->isClient('site');
12347 }
12348
12349 return $app->isSite();
12350 }
12351
12352 /**
12353 * Gets the Google Maps API Key.
12354 *
12355 * @return string
12356 *
12357 * @since 1.14 (J) - 1.4.0 (WP)
12358 */
12359 public static function getGoogleMapsKey()
12360 {
12361 $dbo = JFactory::getDbo();
12362 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='gmapskey';";
12363 $dbo->setQuery($q);
12364 $dbo->execute();
12365 if ($dbo->getNumRows()) {
12366 return $dbo->loadResult();
12367 }
12368 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('gmapskey', '');";
12369 $dbo->setQuery($q);
12370 $dbo->execute();
12371 return '';
12372 }
12373
12374 /**
12375 * Checks whether the interactive map booking is enabled.
12376 *
12377 * @return bool
12378 *
12379 * @since 1.14 (J) - 1.4.0 (WP)
12380 */
12381 public static function interactiveMapEnabled()
12382 {
12383 $dbo = JFactory::getDbo();
12384 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='interactive_map';";
12385 $dbo->setQuery($q);
12386 $dbo->execute();
12387 if ($dbo->getNumRows()) {
12388 return ((int)$dbo->loadResult() > 0);
12389 }
12390
12391 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('interactive_map', '0');";
12392 $dbo->setQuery($q);
12393 $dbo->execute();
12394
12395 return false;
12396 }
12397
12398 /**
12399 * Gets the preferred colors saved in the configuration, if any.
12400 *
12401 * @return array
12402 *
12403 * @since 1.14 (J) - 1.4.0 (WP)
12404 */
12405 public static function getPreferredColors()
12406 {
12407 $dbo = JFactory::getDbo();
12408 $pref_colors = array(
12409 'textcolor' => '',
12410 'bgcolor' => '',
12411 'fontcolor' => '',
12412 'bgcolorhov' => '',
12413 'fontcolorhov' => '',
12414 );
12415
12416 $q = "SELECT `setting` FROM `#__vikbooking_config` WHERE `param`='pref_colors';";
12417 $dbo->setQuery($q);
12418 $dbo->execute();
12419 if ($dbo->getNumRows()) {
12420 $colors = json_decode($dbo->loadResult(), true);
12421 if (!is_array($colors) || !isset($colors['textcolor'])) {
12422 return $pref_colors;
12423 }
12424 return $colors;
12425 }
12426 $q = "INSERT INTO `#__vikbooking_config` (`param`,`setting`) VALUES ('pref_colors', '{}');";
12427 $dbo->setQuery($q);
12428 $dbo->execute();
12429 return $pref_colors;
12430 }
12431
12432 /**
12433 * Adds to the document inline styles for the preferred colors, if any.
12434 *
12435 * @return void
12436 *
12437 * @since 1.14 (J) - 1.4.0 (WP)
12438 */
12439 public static function loadPreferredColorStyles()
12440 {
12441 $view = VikRequest::getString('view', '', 'request');
12442 $pref_colors = self::getPreferredColors();
12443
12444 $css_classes = array();
12445
12446 if (!empty($pref_colors['textcolor'])) {
12447 // titles and headings
12448 array_push($css_classes, '.vbo-pref-color-text { color: ' . $pref_colors['textcolor'] . ' !important; }');
12449 // stepbar, oconfirm
12450 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; }');
12451 // buttons secondary color
12452 array_push($css_classes, '.vbo-pref-color-btn-secondary { border: 2px solid ' . $pref_colors['textcolor'] . ' !important; color: ' . $pref_colors['textcolor'] . ' !important; background: transparent !important; }');
12453 if (!empty($pref_colors['fontcolor'])) {
12454 array_push($css_classes, '.vbo-pref-color-btn-secondary:hover { color: ' . $pref_colors['fontcolor'] . ' !important; background: ' . $pref_colors['textcolor'] . ' !important; }');
12455 }
12456 // datepicker
12457 array_push($css_classes, '.ui-datepicker .ui-datepicker-today {
12458 color: ' . $pref_colors['textcolor'] . ' !important;
12459 border-color: ' . $pref_colors['textcolor'] . ' !important;
12460 }');
12461 array_push($css_classes, '.ui-datepicker .ui-datepicker-today a {
12462 color: ' . $pref_colors['textcolor'] . ' !important;
12463 }');
12464 // operators tableaux
12465 if ($view == 'tableaux') {
12466 array_push($css_classes, '.vbo-roomdaynote-empty .vbo-roomdaynote-trigger i { color: ' . $pref_colors['textcolor'] . ' !important; }');
12467 }
12468 }
12469
12470 if (!empty($pref_colors['bgcolor']) && !empty($pref_colors['fontcolor'])) {
12471 // elements with backgrounds
12472 array_push($css_classes, '.vbo-pref-color-element { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12473 array_push($css_classes, '.vbo-pref-bordercolor { border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12474 array_push($css_classes, '.vbo-pref-bordertext { color: ' . $pref_colors['bgcolor'] . ' !important; border-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12475 // buttons with backgrounds
12476 array_push($css_classes, '.vbo-pref-color-btn { background-color: ' . $pref_colors['bgcolor'] . ' !important; color: ' . $pref_colors['fontcolor'] . ' !important; }');
12477 // stepbar
12478 array_push($css_classes, 'ol.vbo-stepbar li.vbo-step-complete:before { background-color: ' . $pref_colors['bgcolor'] . ' !important; }');
12479 // datepicker
12480 array_push($css_classes, '.ui-datepicker table td:hover {
12481 border-color: ' . $pref_colors['bgcolor'] . ' !important;
12482 }');
12483 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day {
12484 background: ' . $pref_colors['bgcolor'] . ' !important;
12485 color: ' . $pref_colors['fontcolor'] . ' !important;
12486 }');
12487 array_push($css_classes, '.ui-datepicker .ui-datepicker-current-day a {
12488 color: ' . $pref_colors['fontcolor'] . ' !important;
12489 }');
12490 // operators tableaux
12491 if ($view == 'tableaux') {
12492 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; }');
12493 }
12494 }
12495
12496 if (!empty($pref_colors['bgcolorhov']) && !empty($pref_colors['fontcolorhov'])) {
12497 // buttons with backgrounds during hover state
12498 array_push($css_classes, '.vbo-pref-color-btn:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12499 // operators tableaux
12500 if ($view == 'tableaux') {
12501 array_push($css_classes, '.vbo-tableaux-togglefullscreen:hover { background-color: ' . $pref_colors['bgcolorhov'] . ' !important; color: ' . $pref_colors['fontcolorhov'] . ' !important; }');
12502 }
12503 }
12504
12505 if (!count($css_classes)) {
12506 return;
12507 }
12508
12509 // add in-line style declaration
12510 JFactory::getDocument()->addStyleDeclaration(implode("\n", $css_classes));
12511 }
12512
12513 /**
12514 * Given the full endpoint URL for the AJAX request, it
12515 * returns an appropriate URI for the current platform.
12516 *
12517 * @param mixed $query The query string or a routed URL.
12518 * @param boolean $xhtml Replace & by &amp; for XML compliance.
12519 *
12520 * @return string The AJAX end-point URI.
12521 *
12522 * @since 1.14 (J) - 1.4.0 (WP)
12523 * @since 1.15.0 (J) - 1.5.0 (WP) we rely on the new platform libraries.
12524 */
12525 public static function ajaxUrl($query = '', $xhtml = false)
12526 {
12527 return VBOFactory::getPlatform()->getUri()->ajax($query, $xhtml);
12528 }
12529
12530 /**
12531 * Tells whether no tax rates have been defined so far.
12532 *
12533 * @return bool True if no tax rates defined, false otherwise.
12534 *
12535 * @since 1.14 (J) - 1.4.0 (WP)
12536 */
12537 public static function noTaxRates()
12538 {
12539 $dbo = JFactory::getDbo();
12540
12541 $q = "SELECT `id`, `aliq` FROM `#__vikbooking_iva` WHERE `aliq` > 0;";
12542 $dbo->setQuery($q);
12543 $dbo->execute();
12544
12545 return ($dbo->getNumRows() < 1);
12546 }
12547
12548 /**
12549 * Booking Type feature is strictly connected to VCM and OTAs. Updated
12550 * versions of VBO will always support it as long as VCM is installed.
12551 * Needed by VCM to understand whether certain SQL queries can be performed.
12552 *
12553 * @return bool true if VCM exists, false otherwise.
12554 *
12555 * @since 1.14 (J) - 1.4.0 (WP)
12556 */
12557 public static function isBookingTypeSupported()
12558 {
12559 return is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php');
12560 }
12561
12562 /**
12563 * Returns an instance of the Rates Flow helper class of VCM, if available.
12564 *
12565 * @return mixed VCMRatesFlow if VCM available and updated, or null.
12566 *
12567 * @since 1.15.0 (J) - 1.5.0 (WP)
12568 */
12569 public static function getRatesFlowInstance()
12570 {
12571 $vcm_fpath = VCM_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'rates_flow.php';
12572 if (!is_file($vcm_fpath)) {
12573 return null;
12574 }
12575
12576 // load dependencies
12577 require_once $vcm_fpath;
12578
12579 // return the instance of the class
12580 return VCMRatesFlow::getInstance();
12581 }
12582
12583 /**
12584 * Returns the current appearance preference.
12585 *
12586 * @return string light, auto or dark.
12587 *
12588 * @since 1.15.0 (J) - 1.5.0 (WP)
12589 */
12590 public static function getAppearancePref()
12591 {
12592 static $preference = null;
12593
12594 if ($preference !== null) {
12595 return $preference;
12596 }
12597
12598 // auto is the default appearance preference
12599 $default_pref = 'auto';
12600
12601 // accepted preferences
12602 $valid_pref = [
12603 'auto',
12604 'light',
12605 'dark',
12606 ];
12607
12608 $preference = VBOFactory::getConfig()->get('appearance_pref', $default_pref);
12609 $preference = in_array($preference, $valid_pref) ? $preference : $default_pref;
12610
12611 return $preference;
12612 }
12613
12614 /**
12615 * According to the appearance preferences, the apposite
12616 * CSS assets are loaded within the document.
12617 *
12618 * @param bool $get_info true to not load any assets.
12619 *
12620 * @return mixed string light, auto, dark, info array or false.
12621 *
12622 * @since 1.15.0 (J) - 1.5.0 (WP)
12623 * @since 1.16.0 (J) - 1.6.0 (WP) added argument $get_info.
12624 */
12625 public static function loadAppearancePreferenceAssets($get_info = false)
12626 {
12627 // get document object
12628 $document = JFactory::getDocument();
12629
12630 // load current color scheme preference
12631 $current_pref = self::getAppearancePref();
12632
12633 // define caching values
12634 $file_opt = array('version' => VIKBOOKING_SOFTWARE_VERSION);
12635
12636 // define file attributes
12637 $file_attr = array('id' => 'vbo-css-appearance-' . $current_pref);
12638
12639 // apposite file path and URI for back-end
12640 $css_path = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-' . $current_pref . '.css';
12641 $css_uri = VBO_ADMIN_URI . 'resources/vbo-appearance-' . $current_pref . '.css';
12642
12643 // check if this is for the front-end ("auto" file only)
12644 if (self::isSite() && in_array($current_pref, array('auto', 'dark'))) {
12645 $css_path = VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'vbo-appearance-auto.css';
12646 $css_uri = VBO_SITE_URI . 'resources/vbo-appearance-auto.css';
12647 // check if front-end appearance is disabled
12648 if (!VBOFactory::getConfig()->getInt('appearance_front', 0)) {
12649 // do nothing for the front-end by default
12650 return false;
12651 }
12652 }
12653
12654 if (VBOPlatformDetection::isJoomla()) {
12655 $css_path = str_replace('resources' . DIRECTORY_SEPARATOR, '', $css_path);
12656 $css_uri = str_replace('resources/', '', $css_uri);
12657 }
12658
12659 if (!is_file($css_path)) {
12660 // this preference does not require a specific stylesheet
12661 return false;
12662 }
12663
12664 if ($get_info) {
12665 // do not actually load any assets
12666 return [
12667 'id' => $file_attr['id'],
12668 'href' => $css_uri,
12669 ];
12670 }
12671
12672 // load the apposite CSS file
12673 $document->addStyleSheet($css_uri, $file_opt, $file_attr);
12674
12675 return $current_pref;
12676 }
12677
12678 /**
12679 * VBO trigger for VCM subscription status and updates. If called within the admin section
12680 * returns the expiration reminder array to display a modal window, and sets a warning
12681 * message. If called within the front-end, may trigger an email alert.
12682 *
12683 * @return mixed null, array or boolean depending on client position.
12684 *
12685 * @since 1.15.0 (J) - 1.5.0 (WP)
12686 */
12687 public static function getVCMSubscriptionStatus()
12688 {
12689 $vcm_lib_path = VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikchannelmanager.php';
12690 if (!is_file($vcm_lib_path)) {
12691 // do nothing
12692 return null;
12693 }
12694
12695 // determine client position
12696 $is_admin = self::isAdmin();
12697
12698 try {
12699 if (!class_exists('VikChannelManager') || !method_exists('VikChannelManager', 'checkSubscriptionReminder')) {
12700 // VCM is outdated
12701 return null;
12702 }
12703
12704 // always attempt to load the admin lang handler of VCM
12705 $lang = JFactory::getLanguage();
12706
12707 // load the VCM admin language file
12708 $vcm_admin_lang_path = '';
12709 if (VBOPlatformDetection::isJoomla()) {
12710 $vcm_admin_lang_path = JPATH_ADMINISTRATOR;
12711 } elseif (defined('VIKCHANNELMANAGER_ADMIN_LANG')) {
12712 $vcm_admin_lang_path = VIKCHANNELMANAGER_ADMIN_LANG;
12713 } elseif (VBOPlatformDetection::isWordPress() && defined('VIKBOOKING_ADMIN_LANG')) {
12714 /**
12715 * If running within Vik Booking, the constant VIKCHANNELMANAGER_ADMIN_LANG may not be available
12716 */
12717 $vcm_admin_lang_path = str_replace('vikbooking', 'vikchannelmanager', VIKBOOKING_ADMIN_LANG);
12718 }
12719 // load translation file
12720 $lang->load('com_vikchannelmanager', $vcm_admin_lang_path, $lang->getTag(), true);
12721 if (VBOPlatformDetection::isWordPress() && defined('VIKCHANNELMANAGER_LIBRARIES')) {
12722 /**
12723 * @wponly load language admin handler as well for WP.
12724 * We do this only because of WordPress, but in a way also compatible with Joomla as
12725 * the constant VIKCHANNELMANAGER_LIBRARIES and method attachHandler are not in Joomla.
12726 */
12727 $lang->attachHandler(VIKCHANNELMANAGER_LIBRARIES . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikchannelmanager');
12728 }
12729
12730 if ($is_admin) {
12731 // back-end section of the site: check status
12732 if (VikChannelManager::loadExpirationDetails() === false) {
12733 // download and update the expiration details
12734 VikChannelManager::downloadExpirationDetails();
12735 }
12736 $expiration_reminder = null;
12737 if (VikChannelManager::isSubscriptionExpired()) {
12738 VikError::raiseWarning('', JText::translate('VCM_WARN_SUBSCR_EXPIRED'));
12739 } else {
12740 $expiration_reminder = VikChannelManager::shouldRemindExpiration();
12741 // we also check if an update is available
12742 if (VikChannelManager::checkUpdates()) {
12743 // an update is more important
12744 JFactory::getApplication()->redirect('index.php?option=com_vikchannelmanager&task=update_program&forcecheck=1&force_check=1');
12745 exit;
12746 }
12747 }
12748 // return reminder status
12749 return $expiration_reminder;
12750 }
12751
12752 // front-end section of the site: trigger alert, if any (bool)
12753 return VikChannelManager::checkSubscriptionReminder();
12754
12755 } catch (Exception $e) {
12756 // do nothing
12757 }
12758
12759 return null;
12760 }
12761
12762 /**
12763 * Given a list of website rate plans, returns the sorted list.
12764 * Naming score is the highest sorting method, pricing scoring is also supported.
12765 *
12766 * @param array $rate_plans the list of rate plan arrays to sort.
12767 * @param bool $assoc whether to keep the original array keys.
12768 *
12769 * @return array the sorted list of rate plan arrays.
12770 *
12771 * @since 1.15.0 (J) - 1.5.0 (WP)
12772 */
12773 public static function sortRatePlans($rate_plans, $assoc = false)
12774 {
12775 if (!is_array($rate_plans) || count($rate_plans) < 2) {
12776 return $rate_plans;
12777 }
12778
12779 foreach ($rate_plans as $rp_ind => $rate_plan) {
12780 if (!is_array($rate_plan) || empty($rate_plan['name'])) {
12781 // do not proceed in case of a weird array structure
12782 return $rate_plans;
12783 }
12784 // count the sorting-score of this rate plan
12785 $sort_score = 0;
12786 if (stripos($rate_plan['name'], 'Standard') !== false || stripos($rate_plan['name'], 'Base') !== false) {
12787 // "standard rate" rate plans should go first
12788 $sort_score += 3;
12789 } else {
12790 $sort_score--;
12791 }
12792 if (stripos($rate_plan['name'], 'Non') === false && stripos($rate_plan['name'], 'Not') === false) {
12793 // this doesn't seem to be a "non refundable rate" so it should go first
12794 $sort_score += 2;
12795 }
12796
12797 // set sorting score for this rate plan ID
12798 $rate_plans[$rp_ind]['sort_score'] = $sort_score;
12799 }
12800
12801 // apply custom sorting and keep index association
12802 uasort($rate_plans, function($a, $b) {
12803 // sort by name-scoring DESC
12804 $sort_diff = $b['sort_score'] - $a['sort_score'];
12805 if (isset($b['cost'])) {
12806 // apply also sorting by price ASC
12807 $sort_diff += ($a['cost'] < $b['cost']) ? -1 : 1;
12808 }
12809 return $sort_diff;
12810 });
12811
12812 return !$assoc ? array_values($rate_plans) : $rate_plans;
12813 }
12814
12815 /**
12816 * Returns an instance of the Availability helper class.
12817 *
12818 * @return VikBookingAvailability
12819 *
12820 * @since 1.15.0 (J) - 1.5.0 (WP)
12821 */
12822 public static function getAvailabilityInstance()
12823 {
12824 // require library
12825 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'availability.php';
12826
12827 // return the instance of the class
12828 return VikBookingAvailability::getInstance();
12829 }
12830
12831 /**
12832 * Helper method to quickly guess the internal dependency to import.
12833 *
12834 * @param string $path_id either a full path to a file or its identifier.
12835 *
12836 * @return bool true if the requested library was imported or false.
12837 *
12838 * @since 1.15.0 (J) - 1.5.0 (WP)
12839 */
12840 public static function import($path_id)
12841 {
12842 if (empty($path_id)) {
12843 return false;
12844 }
12845
12846 // always append .php extension
12847 $path_id = basename($path_id, '.php') . '.php';
12848
12849 if (is_file($path_id)) {
12850 // full path given
12851 require_once $path_id;
12852
12853 return true;
12854 }
12855
12856 // try guessing where the file id is located
12857 if (is_file(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12858 // library was found
12859 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12860
12861 return true;
12862 }
12863
12864 if (is_file(VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id)) {
12865 // library was found
12866 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . $path_id;
12867
12868 return true;
12869 }
12870
12871 return false;
12872 }
12873 }
12874
12875 if (!class_exists('VikResizer'))
12876 {
12877 class VikResizer
12878 {
12879 public function __construct()
12880 {
12881 // objects of this class can also be instantiated without calling the methods statically.
12882 }
12883
12884 /**
12885 * Resizes an image proportionally. For PNG files it can optionally
12886 * trim the image to exclude the transparency, and add some padding to it.
12887 * All PNG files keep the alpha background in the resized version.
12888 *
12889 * @param string $fileimg path to original image file
12890 * @param string $dest path to destination image file
12891 * @param int $towidth
12892 * @param int $toheight
12893 * @param bool $trim_png remove empty background from image
12894 * @param string $trim_pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
12895 *
12896 * @return boolean
12897 */
12898 public static function proportionalImage($fileimg, $dest, $towidth, $toheight, $trim_png = false, $trim_pad = null)
12899 {
12900 if (!is_file($fileimg)) {
12901 return false;
12902 }
12903 if (empty($towidth) && empty($toheight)) {
12904 return copy($fileimg, $dest);
12905 }
12906
12907 $result = true;
12908
12909 list ($owid, $ohei, $type) = getimagesize($fileimg);
12910
12911 if ($owid > $towidth || $ohei > $toheight) {
12912 $xscale = $owid / $towidth;
12913 $yscale = $ohei / $toheight;
12914 if ($yscale > $xscale) {
12915 $new_width = round($owid * (1 / $yscale));
12916 $new_height = round($ohei * (1 / $yscale));
12917 } else {
12918 $new_width = round($owid * (1 / $xscale));
12919 $new_height = round($ohei * (1 / $xscale));
12920 }
12921
12922 $imageresized = imagecreatetruecolor($new_width, $new_height);
12923
12924 switch ($type) {
12925 case '1' :
12926 $imagetmp = imagecreatefromgif ($fileimg);
12927 break;
12928 case '2' :
12929 $imagetmp = imagecreatefromjpeg($fileimg);
12930 break;
12931 case '18' :
12932 $imagetmp = imagecreatefromwebp($fileimg);
12933 break;
12934 default :
12935 //keep alpha for PNG files
12936 $background = imagecolorallocate($imageresized, 0, 0, 0);
12937 imagecolortransparent($imageresized, $background);
12938 imagealphablending($imageresized, false);
12939 imagesavealpha($imageresized, true);
12940 //
12941 $imagetmp = imagecreatefrompng($fileimg);
12942 break;
12943 }
12944
12945 if (!$imagetmp) {
12946 return false;
12947 }
12948
12949 imagecopyresampled($imageresized, $imagetmp, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
12950
12951 switch ($type) {
12952 case '1' :
12953 imagegif($imageresized, $dest);
12954 break;
12955 case '2' :
12956 imagejpeg($imageresized, $dest);
12957 break;
12958 case '18' :
12959 imagewebp($imageresized, $dest);
12960 break;
12961 default :
12962 if ($trim_png) {
12963 self::imageTrim($imageresized, $background, $trim_pad);
12964 }
12965 imagepng($imageresized, $dest);
12966 break;
12967 }
12968
12969 imagedestroy($imageresized);
12970 } else {
12971 $result = copy($fileimg, $dest);
12972 }
12973
12974 if (VBOPlatformDetection::isWordPress()) {
12975 /**
12976 * @wponly in order to not lose resized files after installing an update,
12977 * we need to move any uploaded file onto a recovery folder.
12978 */
12979 VikBookingLoader::import('update.manager');
12980 VikBookingUpdateManager::triggerUploadBackup($dest);
12981 }
12982
12983 return $result;
12984 }
12985
12986 /**
12987 * (BETA) Resizes an image proportionally. For PNG files it can optionally
12988 * trim the image to exclude the transparency, and add some padding to it.
12989 * All PNG files keep the alpha background in the resized version.
12990 *
12991 * @param resource $im Image link resource (reference)
12992 * @param int $bg imagecolorallocate color identifier
12993 * @param string $pad CSS-style version of padding (top right bottom left) ex: '1 2 3 4'
12994 *
12995 * @return void
12996 */
12997 public static function imagetrim(&$im, $bg, $pad = null)
12998 {
12999 // Calculate padding for each side.
13000 if (isset($pad)) {
13001 $pp = explode(' ', $pad);
13002 if (isset($pp[3])) {
13003 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[3]);
13004 } elseif (isset($pp[2])) {
13005 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[2], (int) $pp[1]);
13006 } elseif (isset($pp[1])) {
13007 $p = array((int) $pp[0], (int) $pp[1], (int) $pp[0], (int) $pp[1]);
13008 } else {
13009 $p = array_fill(0, 4, (int) $pp[0]);
13010 }
13011 } else {
13012 $p = array_fill(0, 4, 0);
13013 }
13014
13015 // Get the image width and height.
13016 $imw = imagesx($im);
13017 $imh = imagesy($im);
13018
13019 // Set the X variables.
13020 $xmin = $imw;
13021 $xmax = 0;
13022
13023 // Start scanning for the edges.
13024 for ($iy=0; $iy<$imh; $iy++) {
13025 $first = true;
13026 for ($ix=0; $ix<$imw; $ix++) {
13027 $ndx = imagecolorat($im, $ix, $iy);
13028 if ($ndx != $bg) {
13029 if ($xmin > $ix) {
13030 $xmin = $ix;
13031 }
13032 if ($xmax < $ix) {
13033 $xmax = $ix;
13034 }
13035 if (!isset($ymin)) {
13036 $ymin = $iy;
13037 }
13038 $ymax = $iy;
13039 if ($first) {
13040 $ix = $xmax;
13041 $first = false;
13042 }
13043 }
13044 }
13045 }
13046
13047 // The new width and height of the image. (not including padding)
13048 $imw = 1+$xmax-$xmin; // Image width in pixels
13049 $imh = 1+$ymax-$ymin; // Image height in pixels
13050
13051 // Make another image to place the trimmed version in.
13052 $im2 = imagecreatetruecolor($imw+$p[1]+$p[3], $imh+$p[0]+$p[2]);
13053
13054 // Make the background of the new image the same as the background of the old one.
13055 $bg2 = imagecolorallocate($im2, ($bg >> 16) & 0xFF, ($bg >> 8) & 0xFF, $bg & 0xFF);
13056 imagefill($im2, 0, 0, $bg2);
13057
13058 // Copy it over to the new image.
13059 imagecopy($im2, $im, $p[3], $p[0], $xmin, $ymin, $imw, $imh);
13060
13061 // To finish up, we replace the old image which is referenced.
13062 $im = $im2;
13063 }
13064
13065 public static function bandedImage($fileimg, $dest, $towidth, $toheight, $rgb)
13066 {
13067 if (!file_exists($fileimg)) {
13068 return false;
13069 }
13070 if (empty($towidth) && empty($toheight)) {
13071 copy($fileimg, $dest);
13072 return true;
13073 }
13074
13075 $exp = explode(",", $rgb);
13076 if (count($exp) == 3) {
13077 $r = trim($exp[0]);
13078 $g = trim($exp[1]);
13079 $b = trim($exp[2]);
13080 } else {
13081 $r = 0;
13082 $g = 0;
13083 $b = 0;
13084 }
13085
13086 list ($owid, $ohei, $type) = getimagesize($fileimg);
13087
13088 if ($owid > $towidth || $ohei > $toheight) {
13089 $xscale = $owid / $towidth;
13090 $yscale = $ohei / $toheight;
13091 if ($yscale > $xscale) {
13092 $new_width = round($owid * (1 / $yscale));
13093 $new_height = round($ohei * (1 / $yscale));
13094 $ydest = 0;
13095 $diff = $towidth - $new_width;
13096 $xdest = ($diff > 0 ? round($diff / 2) : 0);
13097 } else {
13098 $new_width = round($owid * (1 / $xscale));
13099 $new_height = round($ohei * (1 / $xscale));
13100 $xdest = 0;
13101 $diff = $toheight - $new_height;
13102 $ydest = ($diff > 0 ? round($diff / 2) : 0);
13103 }
13104
13105 $imageresized = imagecreatetruecolor($towidth, $toheight);
13106
13107 $bgColor = imagecolorallocate($imageresized, (int) $r, (int) $g, (int) $b);
13108 imagefill($imageresized, 0, 0, $bgColor);
13109
13110 switch ($type) {
13111 case '1' :
13112 $imagetmp = imagecreatefromgif ($fileimg);
13113 break;
13114 case '2' :
13115 $imagetmp = imagecreatefromjpeg($fileimg);
13116 break;
13117 default :
13118 $imagetmp = imagecreatefrompng($fileimg);
13119 break;
13120 }
13121
13122 imagecopyresampled($imageresized, $imagetmp, $xdest, $ydest, 0, 0, $new_width, $new_height, $owid, $ohei);
13123
13124 switch ($type) {
13125 case '1' :
13126 imagegif ($imageresized, $dest);
13127 break;
13128 case '2' :
13129 imagejpeg($imageresized, $dest);
13130 break;
13131 default :
13132 imagepng($imageresized, $dest);
13133 break;
13134 }
13135
13136 imagedestroy($imageresized);
13137
13138 return true;
13139 } else {
13140 copy($fileimg, $dest);
13141 }
13142 return true;
13143 }
13144
13145 public static function croppedImage($fileimg, $dest, $towidth, $toheight)
13146 {
13147 if (!file_exists($fileimg)) {
13148 return false;
13149 }
13150 if (empty($towidth) && empty($toheight)) {
13151 copy($fileimg, $dest);
13152 return true;
13153 }
13154
13155 list ($owid, $ohei, $type) = getimagesize($fileimg);
13156
13157 if ($owid <= $ohei) {
13158 $new_width = $towidth;
13159 $new_height = ($towidth / $owid) * $ohei;
13160 } else {
13161 $new_height = $toheight;
13162 $new_width = ($new_height / $ohei) * $owid;
13163 }
13164
13165 switch ($type) {
13166 case '1' :
13167 $img_src = imagecreatefromgif ($fileimg);
13168 $img_dest = imagecreate($new_width, $new_height);
13169 break;
13170 case '2' :
13171 $img_src = imagecreatefromjpeg($fileimg);
13172 $img_dest = imagecreatetruecolor($new_width, $new_height);
13173 break;
13174 default :
13175 $img_src = imagecreatefrompng($fileimg);
13176 $img_dest = imagecreatetruecolor($new_width, $new_height);
13177 break;
13178 }
13179
13180 imagecopyresampled($img_dest, $img_src, 0, 0, 0, 0, $new_width, $new_height, $owid, $ohei);
13181
13182 switch ($type) {
13183 case '1' :
13184 $cropped = imagecreate($towidth, $toheight);
13185 break;
13186 case '2' :
13187 $cropped = imagecreatetruecolor($towidth, $toheight);
13188 break;
13189 default :
13190 $cropped = imagecreatetruecolor($towidth, $toheight);
13191 break;
13192 }
13193
13194 imagecopy($cropped, $img_dest, 0, 0, 0, 0, $owid, $ohei);
13195
13196 switch ($type) {
13197 case '1' :
13198 imagegif ($cropped, $dest);
13199 break;
13200 case '2' :
13201 imagejpeg($cropped, $dest);
13202 break;
13203 default :
13204 imagepng($cropped, $dest);
13205 break;
13206 }
13207
13208 imagedestroy($img_dest);
13209 imagedestroy($cropped);
13210
13211 return true;
13212 }
13213 }
13214 }
13215