PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.11
VikBooking Hotel Booking Engine & PMS v1.8.11
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 / tracker.php

tracker.php in VikBooking Hotel Booking Engine & PMS 1.8.11, at site/helpers/tracker.php

1,468 lines 42.6 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 /**
14 * This class is used to store statistics about the requests made by the customers to
15 * produce Tracking and Conversion. May be used also by VCM for the booking links.
16 *
17 * @since 1.11 (J) - 1.1 (WP)
18 * @since 1.15.0 (J) - 1.5.0 (WP) implemented several improvements and new features.
19 */
20 class VikBookingTracker
21 {
22 /**
23 * The singleton instance of the class.
24 *
25 * @var VikBookingTracker
26 */
27 protected static $instance = null;
28
29 /**
30 * The fingerprint of this session.
31 *
32 * @var string
33 */
34 protected static $fingerprint = null;
35
36 /**
37 * The tracking fingerprint of a previous session. If provided,
38 * this tracking execution should override the previous session.
39 * Introduced to keep up with VCM and Google Hotel booking links.
40 *
41 * @var string
42 *
43 * @since 1.15.0 (J) - 1.5.0 (WP)
44 */
45 protected static $trk_fingerprint = null;
46
47 /**
48 * The tracking data object.
49 *
50 * @var object
51 */
52 protected static $trackdata;
53
54 /**
55 * Optional associative array of extra information.
56 *
57 * @var array
58 *
59 * @since 1.16.7 (J) - 1.6.7 (WP)
60 */
61 protected static $extra_info = [];
62
63 /**
64 * The tracking info identifier.
65 *
66 * @var int
67 */
68 protected static $identifier = 0;
69
70 /**
71 * The referrer string to which the visitor came from.
72 *
73 * @var string
74 */
75 protected static $referrer = '';
76
77 /**
78 * The database handler instance.
79 *
80 * @var object
81 */
82 protected $dbo;
83
84 /**
85 * List of common properties that should not be displayed as "extra" properties
86 * set by VCM during a tracking. Useful for the back-end "trackings" View of VBO.
87 *
88 * @var array
89 *
90 * @since 1.15.0 (J) - 1.5.0 (WP)
91 */
92 public static $common_trk_props = array(
93 'checkin',
94 'checkout',
95 'nights',
96 'rooms_num',
97 'party',
98 'rooms',
99 'rplans',
100 'idcustomer',
101 'idorder',
102 'rplans',
103 'referrer',
104 'test',
105 );
106
107 /**
108 * Class constructor is protected.
109 *
110 * @see getInstance()
111 */
112 protected function __construct()
113 {
114 $this->dbo = JFactory::getDbo();
115 $this->getFingerprint();
116 static::$trackdata = new stdClass;
117 $this->getIdentifier();
118 $this->getReferrer();
119 }
120
121 /**
122 * Returns the global Tracker object, either
123 * a new instance or the existing instance
124 * if the class was already instantiated.
125 *
126 * @return self A new instance of the class.
127 */
128 public static function getInstance()
129 {
130 if (is_null(static::$instance)) {
131 static::$instance = new static();
132 }
133
134 return static::$instance;
135 }
136
137 /**
138 * Starts a new fingerprint if it doesn't exist.
139 * Returns the existing fingerprint otherwise.
140 * Fingerprint is composed of: Session ID + IP + User Agent.
141 * The generated fingerprint is stored in the session
142 * as well as on a class variable for the execution.
143 * A cookie is sent to the visitor to memorize the fingerprint.
144 *
145 * @return string the md5 fingerprint of the current session.
146 *
147 * @see this public method may also be used by VCM.
148 */
149 public function getFingerprint()
150 {
151 // check if the fingerprint has been instantiated already
152 if (!is_null(static::$fingerprint)) {
153 // return the current fingerprint
154 return static::$fingerprint;
155 }
156
157 // abort if tracking is disabled
158 if (!VBOFactory::getConfig()->getBool('trkenabled')) {
159 return '';
160 }
161
162 /**
163 * To check for an existing or previous fingerprint, we give the highest priority
164 * to the reserved request var "vbo_tracking", which may have been passed after
165 * a previous tracking session for a booking link with VCM.
166 *
167 * @since 1.15.0 (J) - 1.5.0 (WP)
168 */
169 $vbo_tracking = VikRequest::getString('vbo_tracking', '', 'request');
170 $prev_trkdata = $this->loadFingerprintData($vbo_tracking);
171 if (is_array($prev_trkdata) && count($prev_trkdata)) {
172 // generate a new fingerprint for the real visitor
173 static::$fingerprint = $this->generateFingerprintHash();
174 // set the previous tracking fingerprint of the server
175 static::$trk_fingerprint = $prev_trkdata['fingerprint'];
176 // renew cookie for fingerprint
177 VikRequest::setCookie('vboTFP', static::$fingerprint, (time() + (86400 * 365)), '/');
178 return static::$fingerprint;
179 }
180
181 // rely on session and cookies
182 $session = JFactory::getSession();
183 $app = JFactory::getApplication();
184 $cookie = $app->input->cookie;
185
186 // check if the fingerprint was saved in the session
187 $sesstfp = $session->get('vboTFP', '');
188 if (!empty($sesstfp)) {
189 // set var and return the session fingerprint
190 static::$fingerprint = $sesstfp;
191 // renew cookie for fingerprint
192 VikRequest::setCookie('vboTFP', static::$fingerprint, (time() + (86400 * 365)), '/');
193 return $sesstfp;
194 }
195
196 // check if the fingerprint is available in a cookie
197 $cketfp = $cookie->get('vboTFP', '', 'string');
198 if (!empty($cketfp)) {
199 // set var, session and return the fingerprint cookie
200 static::$fingerprint = $cketfp;
201 $session->set('vboTFP', $cketfp);
202 // renew cookie for fingerprint
203 VikRequest::setCookie('vboTFP', static::$fingerprint, (time() + (86400 * 365)), '/');
204
205 return $cketfp;
206 }
207
208 // create a new fingerprint for the visitor
209 static::$fingerprint = $this->generateFingerprintHash();
210
211 // set the fingerprint session and cookie values
212 $session->set('vboTFP', static::$fingerprint);
213 VikRequest::setCookie('vboTFP', static::$fingerprint, (time() + (86400 * 365)), '/');
214
215 return static::$fingerprint;
216 }
217
218 /**
219 * Generates a fingerprint for the current visitor. The signature
220 * is composed of: PHP Session ID + IP address + User Agent.
221 *
222 * @return string the md5 hash of the visitor's fingerprint
223 *
224 * @since 1.15.0 (J) - 1.5.0 (WP)
225 */
226 protected function generateFingerprintHash()
227 {
228 // get the current Session ID
229 $sid = @session_id();
230 if (empty($sid)) {
231 if (class_exists('JSession') && method_exists('JSession', 'getFormToken')) {
232 $sid = JSession::getFormToken();
233 } else {
234 $sid = rand();
235 }
236 }
237
238 // get visitor IP
239 $client_ip = $this->getIpAddress();
240
241 // get visitor user agent
242 $visitorua = JFactory::getApplication()->input->server->getString('HTTP_USER_AGENT', '');
243
244 // return the md5 hash of the signature
245 return md5($sid . $client_ip . $visitorua);
246 }
247
248 /**
249 * Generates or updates and returns the
250 * tracking info identifier that will be
251 * cleared after the tracking conversion.
252 * This is useful to group later the various
253 * tracking info records into precise processes.
254 *
255 * @return int the tracking info identifier
256 */
257 protected function getIdentifier()
258 {
259 $session = JFactory::getSession();
260
261 $sess_identifier = $session->get('vboTidentifier', '');
262
263 if (!empty($sess_identifier)) {
264 // get the identifier from the session
265 static::$identifier = (int)$sess_identifier;
266 } else {
267 // generate a new tracking info identifier
268 static::$identifier = time();
269 // update the session
270 $session->set('vboTidentifier', static::$identifier);
271 }
272
273 return static::$identifier;
274 }
275
276 /**
277 * Gets and stores in the session the referrer string.
278 * This method is called when the object is instantiated,
279 * but the headers are only available after a redirect from
280 * another site, and they may not be available all the times.
281 *
282 * @return string the referrer string, empty string if none
283 */
284 protected function getReferrer()
285 {
286 if (!empty(static::$referrer)) {
287 // if previously registered, return it
288 return static::$referrer;
289 }
290
291 // rely on the session, cookies or headers
292 $session = JFactory::getSession();
293 $input = JFactory::getApplication()->input;
294 $cookie = $input->cookie;
295 $baseuri = JUri::root();
296
297 $sess_referrer = $session->get('vboTreferrer', '');
298 $ck_referrer = $cookie->get('vboTProv', '', 'string');
299 $sess_channel = $session->get('vcmChannelData', '');
300
301 if (!empty($sess_referrer)) {
302 // get the referrer from the session
303 static::$referrer = $sess_referrer;
304 } elseif (!empty($ck_referrer)) {
305 // get the referrer from the cookie
306 static::$referrer = $ck_referrer;
307 } elseif (is_array($sess_channel) && !empty($sess_channel['name'])) {
308 /**
309 * Get the referrer from VCM in case of meta-search booking links.
310 * VCM is invoked before the VBO Tracker, and so we can rely on it.
311 *
312 * @since 1.15.0 (J) - 1.5.0 (WP)
313 */
314 static::$referrer = $sess_channel['name'];
315 } else {
316 // try to get the referrer from the HTTP headers
317 $provenience = $input->server->getString('HTTP_REFERER', '');
318
319 if (!empty($provenience) && strpos($provenience, $baseuri) !== false) {
320 // this could be an internal redirect made by the CMS to set the language (Joomla) or update data
321 $provenience = '';
322 }
323
324 if (empty($provenience)) {
325 // try to get the provenience from the campaign requests
326 $rqdata = $input->getArray();
327 foreach (self::loadCampaigns() as $rkey => $cval) {
328 if (isset($rqdata[$rkey])) {
329 if (!empty($cval['value'])) {
330 if ($rqdata[$rkey] == $cval['value']) {
331 // request key is set and matches the value so we take this campaign as provenience
332 $provenience = $cval['name'];
333 break;
334 }
335 } else {
336 // request key is set and no value is needed so we take this campaign as provenience
337 $provenience = $cval['name'];
338 break;
339 }
340 }
341
342 }
343 }
344
345 if (!empty($provenience)) {
346 // store the provenience in the browser cookie
347 VikRequest::setCookie('vboTProv', $provenience, floor((time() + (86400 * (float)self::loadSettings('trkcookierfrdur')))), '/');
348 }
349
350 // register the variable
351 static::$referrer = $provenience;
352 }
353
354 // update the session
355 $session->set('vboTreferrer', static::$referrer);
356
357 return static::$referrer;
358 }
359
360 /**
361 * Inserts a new main tracking record onto the db.
362 *
363 * @return string the IP address of the visitor
364 */
365 protected function getIpAddress()
366 {
367 $client_ip = '';
368
369 // get server super global
370 $srv = JFactory::getApplication()->input->server;
371
372 // vars identifying the remote's IP address
373 $ipvars = array(
374 'REMOTE_ADDR',
375 'HTTP_CLIENT_IP',
376 'HTTP_X_FORWARDED_FOR',
377 'HTTP_X_FORWARDED',
378 'HTTP_FORWARDED_FOR',
379 'HTTP_FORWARDED'
380 );
381
382 // seek for the visitor's IP address from several vars
383 do {
384
385 // the var to look for in the super global
386 $ipvar = array_shift($ipvars);
387
388 // get the visitor IP address from the super global
389 $client_ip = $srv->getString($ipvar, '');
390
391 } while (empty($client_ip) && count($ipvars));
392
393 return $client_ip;
394 }
395
396 /**
397 * Attempts to find and return the record of the given fingerprint tracking.
398 * If tracking has been disabled for this ID, boolean false is returned.
399 *
400 * @param string $id the md5 hash of the fingerprint to look for
401 *
402 * @return mixed the array record for the found fingerprint tracking, or
403 * an empty array. False if this Tracking ID was unpublished.
404 */
405 protected function loadFingerprintData($id)
406 {
407 if (empty($id)) {
408 return array();
409 }
410
411 $q = "SELECT * FROM `#__vikbooking_trackings` WHERE `fingerprint`=".$this->dbo->quote($id).";";
412 $this->dbo->setQuery($q);
413 $this->dbo->execute();
414 if ($this->dbo->getNumRows()) {
415 $data = $this->dbo->loadAssoc();
416 return $data['published'] ? $data : false;
417 }
418
419 return array();
420 }
421
422 /**
423 * Returns a specific country data information, if anything was found.
424 *
425 * @param string $country the country to look for.
426 * @param int $type the type of data to get.
427 *
428 * @return string either the 2-char, 3-char or full country name, or null.
429 *
430 * @since 1.15.0 (J) - 1.5.0 (WP)
431 */
432 protected function getCountryData($country, $type = 3)
433 {
434 if (empty($country) || !is_string($country)) {
435 return null;
436 }
437 $country = trim($country);
438
439 if (strlen($country) == 2) {
440 $clause = '`country_2_code`=' . $this->dbo->quote($country);
441 } elseif (strlen($country) == 3) {
442 $clause = '`country_3_code`=' . $this->dbo->quote($country);
443 } elseif (strlen($country) == 3) {
444 $clause = '`country_name` LIKE ' . $this->dbo->quote("%{$country}%");
445 }
446
447 $q = "SELECT * FROM `#__vikbooking_countries` WHERE $clause";
448 $this->dbo->setQuery($q, 0, 1);
449 $this->dbo->execute();
450 if (!$this->dbo->getNumRows()) {
451 return null;
452 }
453 $country_data = $this->dbo->loadAssoc();
454
455 if ($type == 2) {
456 return $country_data['country_2_code'];
457 }
458
459 if ($type == 3) {
460 return $country_data['country_3_code'];
461 }
462
463 return $country_data['country_name'];
464 }
465
466 /**
467 * Gets the current datetime string for SQL, either in UTC or local timezone.
468 *
469 * @param bool $local true to use the current timezone, false for UTC datetime.
470 *
471 * @return string the datetime string in the desired timezone.
472 *
473 * @since 1.15.0 (J) - 1.5.0 (WP)
474 */
475 protected function getCurrentSqlDateTime($local = true)
476 {
477 try {
478 // get server's current datetime string
479 $date = JFactory::getDate();
480 $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
481 $now_date = $date->toSql($local);
482 } catch (Exception $e) {
483 $now_date = date('Y-m-d H:i:s');
484 }
485
486 return $now_date;
487 }
488
489 /**
490 * Inserts a new main tracking record onto the db.
491 *
492 * @return int the ID of the main tracking record created
493 *
494 * @since 1.15.0 (J) - 1.5.0 (WP) inserts object record and supports a given country, if set.
495 */
496 protected function storeMainTracking()
497 {
498 // get SQL NOW() equivalent
499 $now_date = $this->getCurrentSqlDateTime();
500
501 // build record object
502 $tracking_record = new stdClass;
503 $tracking_record->dt = $now_date;
504 $tracking_record->lastdt = $now_date;
505 $tracking_record->fingerprint = static::$fingerprint;
506 $tracking_record->ip = $this->getIpAddress();
507
508 if (isset(static::$trackdata->country)) {
509 $three_char_country = $this->getCountryData(static::$trackdata->country, 3);
510 if (!empty($three_char_country)) {
511 $tracking_record->country = $three_char_country;
512 }
513 }
514
515 $this->dbo->insertObject('#__vikbooking_trackings', $tracking_record, 'id');
516
517 return isset($tracking_record->id) ? (int)$tracking_record->id : 0;
518 }
519
520 /**
521 * Merges the current tracking data information to the previous ones.
522 * This is useful in case a step of the booking process does not push
523 * some information that were previously pushed, such as the room IDs,
524 * or the price IDs, but maybe it pushes other details, such as the new Booking ID.
525 *
526 * @param int $track_info_id the ID of the previous tracking info record
527 *
528 * @return object the merged tracking data object
529 */
530 protected function mergeTrackingData($track_info_id)
531 {
532 if (!is_object(static::$trackdata) || !count(get_object_vars(static::$trackdata))) {
533 return static::$trackdata;
534 }
535
536 $q = "SELECT `trkdata` FROM `#__vikbooking_tracking_infos` WHERE `id`=".(int)$track_info_id.";";
537 $this->dbo->setQuery($q);
538 $prev_trackdata = $this->dbo->loadResult();
539 if ($prev_trackdata) {
540 $prev_trackdata = json_decode($prev_trackdata);
541 if (!is_object($prev_trackdata)) {
542 return static::$trackdata;
543 }
544
545 // merge new properties onto previous properties, whether they are missing or different
546 foreach (static::$trackdata as $prop => $val) {
547 if (!property_exists($prev_trackdata, $prop) || $prev_trackdata->$prop != $val) {
548 $prev_trackdata->$prop = $val;
549 }
550 }
551
552 return $prev_trackdata;
553 }
554
555 return static::$trackdata;
556 }
557
558 /**
559 * Pushes the dates requested onto the tracking data object.
560 * It's preferred to pass the dates as unix timestamps.
561 *
562 * @param string $checkin the checkin date, either a string or an integer
563 * @param string $checkout the checkout date, either a string or an integer
564 * @param int $night optional, the number of nights for the stay
565 *
566 * @return self for chainability.
567 */
568 public function pushDates($checkin, $checkout, $nights = 0)
569 {
570 if (!is_numeric($checkin)) {
571 // get timestamp from date string
572 $checkin = VikBooking::getDateTimestamp($checkin, 0, 0);
573 }
574 if (!is_numeric($checkout)) {
575 // get timestamp from date string
576 $checkout = VikBooking::getDateTimestamp($checkout, 0, 0);
577 }
578
579 // prepare unix timestamps for sql format
580 $checkin = JDate::getInstance(date('Y-m-d H:i:s', $checkin))->toSql();
581 $checkout = JDate::getInstance(date('Y-m-d H:i:s', $checkout))->toSql();
582
583 // register variables
584 static::$trackdata->checkin = $checkin;
585 static::$trackdata->checkout = $checkout;
586 if ($nights > 0) {
587 static::$trackdata->nights = $nights;
588 }
589
590 return static::$instance;
591 }
592
593 /**
594 * Pushes the party requested onto the tracking data object.
595 * Includes the guests per room and number of rooms
596 *
597 * @param array $guests the array of guests per room
598 *
599 * @return self for chainability.
600 */
601 public function pushParty($guests)
602 {
603 if (is_array($guests) && count($guests)) {
604 // register variable
605 static::$trackdata->rooms_num = count($guests);
606 static::$trackdata->party = $guests;
607 }
608
609 return static::$instance;
610 }
611
612 /**
613 * Pushes the rooms selected onto the tracking data object.
614 * Sets an array of key-value pairs (key = ID, value = units).
615 *
616 * @param mixed $rooms integer or array of integers for the room IDs selected
617 * @param mixed [$rplans] integer or array of integers for the price IDs selected
618 * @param mixed [$rindex] integer or array of integers for the room indexes selected (geomap)
619 *
620 * @return self for chainability.
621 *
622 * @since 1.14 (J) - 1.4.0 (WP) 3rd argument $rindex introduced due to interactive geomap
623 */
624 public function pushRooms($rooms, $rplans = array(), $rindex = array())
625 {
626 if (is_scalar($rooms)) {
627 $rooms = array($rooms);
628 }
629
630 if (!empty($rplans) && is_scalar($rplans)) {
631 $rplans = array($rplans);
632 }
633
634 if (!empty($rindex) && is_scalar($rindex)) {
635 $rindex = array($rindex);
636 }
637
638 if (!property_exists(static::$trackdata, 'rooms')) {
639 static::$trackdata->rooms = array();
640 }
641
642 // group the rooms by units requested and id
643 $rooms_data = array();
644 foreach ($rooms as $id) {
645 if (!isset($rooms_data[$id])) {
646 $rooms_data[$id] = 0;
647 }
648 $rooms_data[$id]++;
649 }
650
651 // register variable
652 static::$trackdata->rooms = $rooms_data;
653
654 if (is_array($rplans) && count($rplans) == count($rooms)) {
655 // add also the information about the rate plans selected
656 if (!property_exists(static::$trackdata, 'rplans')) {
657 static::$trackdata->rplans = array();
658 }
659
660 // group the rate plans by units requested and id like the rooms
661 $rplans_data = array();
662 foreach ($rplans as $id) {
663 if (!isset($rplans_data[$id])) {
664 $rplans_data[$id] = 0;
665 }
666 $rplans_data[$id]++;
667 }
668
669 // register variable
670 static::$trackdata->rplans = $rplans_data;
671 }
672
673 if (is_array($rindex) && count($rindex) == count($rooms)) {
674 // register variable also for the information about the room indexes selected
675 static::$trackdata->rindex = $rindex;
676 }
677
678 return static::$instance;
679 }
680
681 /**
682 * Pushes the esit onto the tracking data object.
683 * Multiple messages allowed.
684 *
685 * @param string $msg the message log for the tracking
686 * @param string $type the type of the message
687 *
688 * @return self for chainability.
689 */
690 public function pushMessage($msg, $type = 'success')
691 {
692 if (!property_exists(static::$trackdata, 'msg')) {
693 static::$trackdata->msg = array();
694 }
695
696 // register variable
697 array_push(static::$trackdata->msg, array(
698 'text' => $msg,
699 'type' => $type
700 ));
701
702 return static::$instance;
703 }
704
705 /**
706 * Pushes custom data onto the tracking data object.
707 * Any previously set property will be overridden.
708 *
709 * @param string $prop the name of the property
710 * @param mixed $val the value for the property
711 *
712 * @return self for chainability.
713 */
714 public function pushData($prop, $val)
715 {
716 // list of reserved properties that won't be added to the $trackdata object
717 $reserved_props = array(
718 'referrer',
719 );
720
721 if (empty($prop)) {
722 // do not proceed
723 return static::$instance;
724 }
725
726 if ((is_string($val) && strlen($val)) || !empty($val)) {
727 // check if the property is reserved
728 if (in_array($prop, $reserved_props) && isset(static::${$prop})) {
729 // replace internal/reserved class property
730 static::${$prop} = $val;
731 } else {
732 // register variable
733 static::$trackdata->$prop = $val;
734 }
735 }
736
737 return static::$instance;
738 }
739
740 /**
741 * Closes the current tracking process by preventing multiple calls.
742 * This method should be called before the end of the execution process.
743 *
744 * @return int whether the track was closed
745 */
746 public function closeTrack()
747 {
748 static $track_closed = null;
749
750 if (!$track_closed) {
751 // prevent the track from being closed multiple times
752 $track_closed = 1;
753
754 // abort if tracking is disabled or framework is admin
755 if (!(int)self::loadSettings('trkenabled') || VikBooking::isAdmin()) {
756 return 0;
757 }
758
759 // abort if no tracking data
760 $arrtrack = (array)static::$trackdata;
761 if (empty($arrtrack)) {
762 return 0;
763 }
764
765 // close the track by storing the information
766 $id_tracking = null;
767
768 // get the tracking ID or abort (if tracking for this visitor is disabled)
769 $prev_tracking = $this->loadFingerprintData((!is_null(static::$trk_fingerprint) ? static::$trk_fingerprint : static::$fingerprint));
770 if ($prev_tracking === false) {
771 // abort, tracking for this ID is disabled
772 return 0;
773 }
774 if ($prev_tracking) {
775 $id_tracking = $prev_tracking['id'];
776 }
777 if (!$id_tracking) {
778 // store the main tracking record
779 $id_tracking = $this->storeMainTracking();
780 if (!$id_tracking) {
781 return 0;
782 }
783 }
784
785 // current dates requested
786 $cur_checkin = property_exists(static::$trackdata, 'checkin') ? static::$trackdata->checkin : '';
787 $cur_checkout = property_exists(static::$trackdata, 'checkout') ? static::$trackdata->checkout : '';
788
789 $session = JFactory::getSession();
790
791 // previous tracking info identifier
792 $prev_identifier = $session->get('vboTinfoId', '');
793 $prev_identifier_id = 0;
794 if (!empty($prev_identifier)) {
795 $prev_parts = explode(';', $prev_identifier);
796 if (count($prev_parts) > 2 && !empty($prev_parts[0]) && $prev_parts[1] == $cur_checkin && $prev_parts[2] == $cur_checkout) {
797 // an equal previous identifier is available, we should not create a new tracking info record as the dates have not changed
798 $prev_identifier_id = (int)$prev_parts[0];
799 }
800 }
801
802 // get SQL NOW() equivalent
803 $now_date = $this->getCurrentSqlDateTime();
804
805 // store or update the tracking info record
806 if (empty($prev_identifier_id)) {
807 // detect user agent type (device)
808 $device = isset(static::$trackdata->device) ? static::$trackdata->device : '';
809 if (!empty($device)) {
810 /**
811 * Validate device type: "mobile", "tablet", "desktop", "unknown" should be converted to either
812 * "smartphone", "tablet", "computer" or empty string for compliance with detectUserAgent().
813 *
814 * @since 1.15.0 (J) - 1.5.0 (WP)
815 */
816 if (!strcasecmp($device, 'desktop') || !strcasecmp($device, 'unknown')) {
817 $device = 'computer';
818 } elseif (!strcasecmp($device, 'mobile')) {
819 $device = 'smartphone';
820 }
821 }
822 $uatype = !empty($device) ? $device : VikBooking::detectUserAgent(true, false);
823 $uatype = !empty($uatype) ? strtoupper(substr($uatype, 0, 1)) : '';
824
825 // insert a new tracking info record
826 $trk_info_record = new stdClass;
827 $trk_info_record->idtracking = (int)$id_tracking;
828 $trk_info_record->identifier = $this->getIdentifier();
829 $trk_info_record->trackingdt = $now_date;
830 $trk_info_record->device = $uatype;
831 $trk_info_record->trkdata = json_encode(static::$trackdata);
832 $trk_info_record->checkin = !empty($cur_checkin) ? $cur_checkin : null;
833 $trk_info_record->checkout = !empty($cur_checkout) ? $cur_checkout : null;
834 $trk_info_record->idorder = isset(static::$trackdata->idorder) ? (int)static::$trackdata->idorder : 0;
835 $trk_info_record->referrer = !empty(static::$referrer) ? static::$referrer : null;
836
837 /**
838 * Trigger event for the tracking information being saved.
839 *
840 * @since 1.16.2 (J) - 1.6.2 (WP)
841 */
842 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSaveTrackingInformationVikBooking', [$trk_info_record, (int)$id_tracking, $this]);
843
844 // store record
845 $this->dbo->insertObject('#__vikbooking_tracking_infos', $trk_info_record, 'id');
846
847 $trkinfo_id = isset($trk_info_record->id) ? (int)$trk_info_record->id : 0;
848 if (!$trkinfo_id) {
849 return 0;
850 }
851
852 // store in the session the tracking info identifier
853 $identifier = array(
854 $trkinfo_id,
855 $cur_checkin,
856 $cur_checkout
857 );
858 $session->set('vboTinfoId', implode(';', $identifier));
859 } else {
860 // merge tracking data from previous tracking info with current info
861 $new_trackdata = $this->mergeTrackingData($prev_identifier_id);
862
863 // update an existing tracking info record
864 $trk_info_record = new stdClass;
865 $trk_info_record->id = $prev_identifier_id;
866 $trk_info_record->trackingdt = $now_date;
867 $trk_info_record->trkdata = json_encode($new_trackdata);
868 $trk_info_record->checkin = !empty($cur_checkin) ? $cur_checkin : null;
869 $trk_info_record->checkout = !empty($cur_checkout) ? $cur_checkout : null;
870 if (isset(static::$trackdata->idorder)) {
871 $trk_info_record->idorder = (int)static::$trackdata->idorder;
872 }
873
874 /**
875 * Trigger event for the tracking information being saved.
876 *
877 * @since 1.16.2 (J) - 1.6.2 (WP)
878 */
879 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeSaveTrackingInformationVikBooking', [$trk_info_record, (int)$id_tracking, $this]);
880
881 // update record
882 $this->dbo->updateObject('#__vikbooking_tracking_infos', $trk_info_record, 'id');
883 }
884
885 // update main tracking record
886 $main_record = new stdClass;
887 $main_record->id = (int)$id_tracking;
888 $main_record->lastdt = $now_date;
889 if (!is_null(static::$trk_fingerprint)) {
890 // the real visitor has received a new fingerprint, so we update it on the main tracking record
891 $main_record->fingerprint = static::$fingerprint;
892 // update the IP address for the actual visitor and replace a possible e4jConnect server IP address
893 $main_record->ip = $this->getIpAddress();
894 }
895 if (isset(static::$trackdata->idcustomer)) {
896 $main_record->idcustomer = (int)static::$trackdata->idcustomer;
897 }
898 $this->dbo->updateObject('#__vikbooking_trackings', $main_record, 'id');
899
900 }
901
902 return $track_closed;
903 }
904
905 /**
906 * Resets the class and session variables to allow a new tracking.
907 * This should be called after the conversion is reached and done.
908 *
909 * @return void
910 */
911 public function resetTrack()
912 {
913 static::$instance = null;
914 static::$fingerprint = null;
915 static::$trk_fingerprint = null;
916 static::$trackdata = new stdClass;
917
918 $session = JFactory::getSession();
919 $session->set('vboTinfoId', '');
920 $session->set('vboTidentifier', '');
921 $session->set('vboTreferrer', '');
922 }
923
924 /**
925 * Returns the current tracking data object.
926 *
927 * @return object the current track data.
928 *
929 * @since 1.16.3 (J) - 1.6.3 (WP)
930 */
931 public function getTrackData()
932 {
933 return static::$trackdata;
934 }
935
936 /**
937 * Returns a registry or array for the current extra information.
938 *
939 * @param bool $registry whether to get the registry object or array.
940 *
941 * @return JObject|array
942 *
943 * @since 1.16.7 (J) - 1.6.7 (WP)
944 */
945 public function getExtraInfo($registry = true)
946 {
947 return $registry ? (new JObject(static::$extra_info)) : static::$extra_info;
948 }
949
950 /**
951 * Sets a value for an extra information type.
952 *
953 * @param string $key the information key in the associative array.
954 * @param mixed $value the value to set for the given key.
955 *
956 * @return self
957 *
958 * @since 1.16.7 (J) - 1.6.7 (WP)
959 */
960 public function setExtraInfo($key, $value = null)
961 {
962 static::$extra_info[$key] = $value;
963
964 return $this;
965 }
966
967 /**
968 * Runs when visiting the booking details page in case conversion must be triggered.
969 *
970 * @param array $booking the booking record for conversion.
971 *
972 * @return self
973 *
974 * @since 1.16.3 (J) - 1.6.3 (WP)
975 */
976 public function triggerBookingConversion(array $booking)
977 {
978 $fingerprint = $this->getFingerprint();
979
980 if (!$fingerprint) {
981 // tracking may be disabled
982 return $this;
983 }
984
985 $q = $this->dbo->getQuery(true);
986
987 $q->select($this->dbo->qn([
988 'i.id',
989 'i.trkdata',
990 'i.checkin',
991 'i.checkout',
992 'i.idorder',
993 'i.referrer',
994 't.geo',
995 't.country',
996 't.idcustomer',
997 ]));
998 $q->from($this->dbo->qn('#__vikbooking_tracking_infos', 'i'));
999 $q->leftJoin($this->dbo->qn('#__vikbooking_trackings', 't') . ' ON ' . $this->dbo->qn('t.id') . ' = ' . $this->dbo->qn('i.idtracking'));
1000 $q->where($this->dbo->qn('t.fingerprint') . ' = ' . $this->dbo->q($fingerprint));
1001 $q->where($this->dbo->qn('i.idorder') . ' = ' . (int)$booking['id']);
1002 $q->order($this->dbo->qn('i.trackingdt') . ' DESC');
1003
1004 $this->dbo->setQuery($q, 0, 1);
1005
1006 $last_tracking_record = $this->dbo->loadObject();
1007
1008 if (!$last_tracking_record) {
1009 // tracking must be enabled and functioning
1010 return $this;
1011 }
1012
1013 $prev_trackdata = json_decode($last_tracking_record->trkdata);
1014 if (!is_object($prev_trackdata)) {
1015 // no previous tracking data available
1016 return $this;
1017 }
1018
1019 /**
1020 * Check if tracking was made for the current booking status.
1021 *
1022 * @since 1.16.7 (J) - 1.6.7 (WP)
1023 */
1024 $converted_status_prop = 'converted_' . strtolower($booking['status']);
1025
1026 if (!isset($prev_trackdata->{$converted_status_prop})) {
1027 // proceed with triggering the conversion event for this status
1028
1029 // ensure no more tracking events will be triggered for the current status
1030 $prev_trackdata->{$converted_status_prop} = 1;
1031
1032 $upd_record = new stdClass;
1033 $upd_record->id = $last_tracking_record->id;
1034 $upd_record->trkdata = json_encode($prev_trackdata);
1035
1036 $this->dbo->updateObject('#__vikbooking_tracking_infos', $upd_record, 'id');
1037
1038 // trigger event to allow third party plugins to perform a tracking of the booking conversion only once
1039 VBOFactory::getPlatform()->getDispatcher()->trigger('onBookingConversionTrackingVikBooking', [$booking, $last_tracking_record]);
1040 }
1041
1042 return $this;
1043 }
1044
1045 /**
1046 * Retrieves information about a given IP address.
1047 * This method does NOT require getInstance() to
1048 * be called to instantiate the object. It was made
1049 * to be used from the admin section of the site.
1050 * Returned array keys are maintained for mapping.
1051 *
1052 * @param mixed $ips the visitors IP address(es) as a string or array
1053 *
1054 * @return mixed array on success, false on failure
1055 */
1056 public static function getIpGeoInfo($ips)
1057 {
1058 if (is_scalar($ips)) {
1059 $ips = array($ips);
1060 }
1061
1062 if (empty($ips)) {
1063 return false;
1064 }
1065
1066 // pool of data to be returned
1067 $geo_info = array();
1068
1069 // buffer to cache IPs already parsed to avoid double queries for same IPs
1070 $buffer = array();
1071
1072 // request endpoint
1073 $endpoint = 'https://'.'ip'.'info'.'.'.'io'.'/'.'%s'.'/'.'geo';
1074
1075 // iterate through the IPs requested
1076 foreach ($ips as $k => $ip) {
1077 if (isset($buffer[$ip])) {
1078 // this IP address was already requested
1079 $geo_info[$k] = $buffer[$ip];
1080 continue;
1081 }
1082
1083 $geo_info[$k] = null;
1084 if (empty($ip)) {
1085 continue;
1086 }
1087 // make the curl request to obtain the information
1088 $try = 0;
1089 $curl_errno = 0;
1090 $curl_err = '';
1091 do {
1092 $ch = curl_init(sprintf($endpoint, $ip));
1093 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
1094 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
1095 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1096 curl_setopt($ch, CURLOPT_TIMEOUT, 20);
1097 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
1098 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
1099 $serverres = curl_exec($ch);
1100 if ($curl_errno = curl_errno($ch)) {
1101 $curl_err = curl_error($ch);
1102 } else {
1103 $curl_errno = 0;
1104 $curl_err = '';
1105 }
1106 curl_close($ch);
1107 $try++;
1108 } while ($try < 2 && $curl_errno > 0 && in_array($curl_errno, array(2, 6, 7, 28)));
1109
1110 if ($curl_errno) {
1111 $geo_info[$k] = false;
1112 continue;
1113 }
1114
1115 // decode response
1116 $resp = json_decode($serverres, true);
1117 if (!is_array($resp) || !isset($resp['city']) || !isset($resp['country'])) {
1118 // invalid response or missing required data
1119 $geo_info[$k] = false;
1120 continue;
1121 }
1122
1123 // cache result for this IP
1124 $buffer[$ip] = $resp;
1125
1126 // push result
1127 $geo_info[$k] = $resp;
1128 }
1129
1130 return $geo_info;
1131 }
1132
1133 /**
1134 * Returns information about the differences between two dates.
1135 * This method does NOT require getInstance() to
1136 * be called to instantiate the object. It was made
1137 * to be used from the admin section of the site.
1138 * The dates passed should be either Unix timestamps, or
1139 * strings in a format compatible with strtotime().
1140 * The first date is supposed to be greater than the second.
1141 *
1142 * @param mixed $first int unix timestamp or string formatted date
1143 * @param mixed $second int unix timestamp or string formatted date
1144 *
1145 * @return array the information about the differences (max type = hours)
1146 */
1147 public static function datesDiff($first, $second)
1148 {
1149 // make sure dates are converted to timestamps
1150 if (!is_numeric($first)) {
1151 $first = strtotime($first);
1152 } else {
1153 $first = (int)$first;
1154 }
1155 if (!is_numeric($second)) {
1156 $second = strtotime($second);
1157 } else {
1158 $second = (int)$second;
1159 }
1160
1161 // seconds of difference
1162 $diff = abs($first - $second);
1163
1164 if ($diff < 60) {
1165 // just some seconds of difference
1166 return array(
1167 'diff' => $diff,
1168 'type' => 'seconds'
1169 );
1170 }
1171
1172 if ($diff < 3600) {
1173 // minutes of difference
1174 return array(
1175 'diff' => round(($diff / 60), 0, PHP_ROUND_HALF_UP),
1176 'type' => 'minutes'
1177 );
1178 }
1179
1180 // hours of difference
1181 return array(
1182 'diff' => round(($diff / 3600), 0, PHP_ROUND_HALF_UP),
1183 'type' => 'hours'
1184 );
1185 }
1186
1187 /**
1188 * Loads the tracking settings or one specific setting
1189 *
1190 * @param string $key an optional key for the setting to load
1191 *
1192 * @return mixed tracking settings array or the value for the requested setting key
1193 */
1194 public static function loadSettings($key = '')
1195 {
1196 $dbo = JFactory::getDbo();
1197
1198 // configuration settings keys for tracking
1199 $validkeys = array(
1200 'trkenabled',
1201 'trkcookierfrdur',
1202 'trkcampaigns',
1203 );
1204
1205 if (!empty($key)) {
1206 $validkeys = array($key);
1207 }
1208
1209 // quote keys for query
1210 $query_keys = array_map(array($dbo, 'quote'), $validkeys);
1211
1212 // load settings from db
1213 $q = "SELECT `param`, `setting` FROM `#__vikbooking_config` WHERE `param` IN (".implode(', ', $query_keys).");";
1214 $dbo->setQuery($q);
1215 $dbo->execute();
1216 if (!$dbo->getNumRows()) {
1217 return false;
1218 }
1219 $res = $dbo->loadAssocList();
1220 $settings = array();
1221 foreach ($res as $s) {
1222 $settings[$s['param']] = $s['setting'];
1223 }
1224
1225 return !empty($key) && count($settings) < 2 ? $settings[$key] : $settings;
1226 }
1227
1228 /**
1229 * Loads all the current campaigns from the settings
1230 *
1231 * @return array all the campaigns decoded data
1232 *
1233 * @uses loadSettings()
1234 */
1235 public static function loadCampaigns()
1236 {
1237 $campaigns = array();
1238
1239 $data = self::loadSettings('trkcampaigns');
1240 if (!empty($data)) {
1241 $campaigns = json_decode($data, true);
1242 $campaigns = !is_array($campaigns) ? array() : $campaigns;
1243 }
1244
1245 return $campaigns;
1246 }
1247
1248 /**
1249 * Counts tracked records in a given time frame.
1250 * Firstly developed for the admin widget "visitors counter".
1251 *
1252 * @param string $from_date start date in Y-m-d H:i:s format.
1253 * @param string $to_date end date in Y-m-d H:i:s format.
1254 *
1255 * @return int the total number of tracking records found.
1256 *
1257 * @since 1.14 (J) - 1.4.0 (WP)
1258 */
1259 public static function countTrackedRecords($from_date = null, $to_date = null)
1260 {
1261 $dbo = JFactory::getDbo();
1262
1263 $total_records = 0;
1264
1265 $filters = array();
1266 if (!empty($from_date)) {
1267 array_push($filters, '`t`.`lastdt` >= '.$dbo->quote(JDate::getInstance($from_date)->toSql()));
1268 }
1269 if (!empty($to_date)) {
1270 array_push($filters, '`t`.`lastdt` <= '.$dbo->quote(JDate::getInstance($to_date)->toSql()));
1271 }
1272 // exclude records with tracking status disabled
1273 array_push($filters, '`t`.`published` = 1');
1274
1275 $q = "SELECT COUNT(*) FROM `#__vikbooking_trackings` AS `t` WHERE " . implode(' AND ', $filters);
1276 $dbo->setQuery($q);
1277 $dbo->execute();
1278 if ($dbo->getNumRows()) {
1279 $total_records = $dbo->loadResult();
1280 }
1281
1282 return $total_records;
1283 }
1284
1285 /**
1286 * Gets the global statistics for a given time frame. This includes
1287 * the most demanded nights, conversion rates and best referrers.
1288 * Firstly developed for the VCM framework of the e4jConnect App.
1289 *
1290 * @param string $from_date start date in Y-m-d format.
1291 * @param string $to_date end date in Y-m-d format.
1292 * @param int $dt_type type of dates identifier (tracking, booking etc..).
1293 *
1294 * @return array empty or associative array with statistics.
1295 *
1296 * @since 1.15.3 (J) - 1.5.5 (WP)
1297 */
1298 public static function getStatistics($from_date, $to_date, $dt_type = 1)
1299 {
1300 if (empty($from_date) || empty($to_date)) {
1301 return [];
1302 }
1303
1304 $dbo = JFactory::getDbo();
1305
1306 // dates information
1307 $from_info = getdate(strtotime($from_date));
1308 $to_info = getdate(strtotime($to_date));
1309 $from_start_ts = mktime(0, 0, 0, $from_info['mon'], $from_info['mday'], $from_info['year']);
1310 $from_end_ts = mktime(23, 59, 59, $from_info['mon'], $from_info['mday'], $from_info['year']);
1311 $to_start_ts = mktime(0, 0, 0, $to_info['mon'], $to_info['mday'], $to_info['year']);
1312 $to_end_ts = mktime(23, 59, 59, $to_info['mon'], $to_info['mday'], $to_info['year']);
1313
1314 // query filters
1315 $filters = [];
1316
1317 if ($dt_type == 2) {
1318 // booking dates
1319 $bookdatesfilt = [];
1320 // filter from-date inside a range of dates booked (we use 23:59:59 to avoid calculating the check-in time)
1321 array_push(
1322 $bookdatesfilt,
1323 '(' .
1324 '`i`.`checkin` <= ' . $dbo->quote(date('Y-m-d H:i:s', $from_end_ts)) .
1325 ' AND ' .
1326 '`i`.`checkout` >= ' . $dbo->quote(date('Y-m-d H:i:s', $from_start_ts)) .
1327 ')'
1328 );
1329 // filter to-date inside a range of dates booked (we use 00:00:00 to avoid calculating the check-out time)
1330 array_push(
1331 $bookdatesfilt,
1332 '(' .
1333 '`i`.`checkin` <= ' . $dbo->quote(date('Y-m-d H:i:s', $to_end_ts)) .
1334 ' AND ' .
1335 '`i`.`checkout` >= ' . $dbo->quote(date('Y-m-d H:i:s', $to_start_ts)) .
1336 ')'
1337 );
1338 // filter dates including booking dates (bigger than)
1339 array_push(
1340 $bookdatesfilt,
1341 '(' .
1342 '`i`.`checkin` >= ' . $dbo->quote(date('Y-m-d H:i:s', $from_start_ts)) .
1343 ' AND ' .
1344 '`i`.`checkout` <= ' . $dbo->quote(date('Y-m-d H:i:s', $to_end_ts)) .
1345 ')'
1346 );
1347 // push all clauses to filters
1348 array_push($filters, '(' . implode(' OR ', $bookdatesfilt) . ')');
1349 } elseif ($dt_type == 3) {
1350 // checkin date
1351 array_push($filters, '`i`.`checkin` >= ' . $dbo->quote(date('Y-m-d H:i:s', $from_start_ts)));
1352 array_push($filters, '`i`.`checkin` <= ' . $dbo->quote(date('Y-m-d H:i:s', $to_end_ts)));
1353 } elseif ($dt_type == 4) {
1354 // checkout date
1355 array_push($filters, '`i`.`checkout` >= ' . $dbo->quote(date('Y-m-d H:i:s', $from_start_ts)));
1356 array_push($filters, '`i`.`checkout` <= ' . $dbo->quote(date('Y-m-d H:i:s', $to_end_ts)));
1357 } else {
1358 // default (1) tracking dates
1359 array_push($filters, '`t`.`lastdt` >= ' . $dbo->quote(date('Y-m-d H:i:s', $from_start_ts)));
1360 array_push($filters, '`t`.`lastdt` <= ' . $dbo->quote(date('Y-m-d H:i:s', $to_end_ts)));
1361 }
1362
1363 // calculate most demanded nights, conversion rates, best referrers
1364 $tomorrowdt = JDate::getInstance(date('Y-m-d', strtotime('tomorrow')))->toSql();
1365 $q = "SELECT `i`.`id`, `i`.`idtracking`, `i`.`identifier`, `i`.`checkin`, `i`.`checkout`, `i`.`idorder`, `i`.`referrer`, `t`.`lastdt`, `t`.`published`
1366 FROM `#__vikbooking_tracking_infos` AS `i`
1367 LEFT JOIN `#__vikbooking_trackings` AS `t` ON `i`.`idtracking`=`t`.`id`
1368 WHERE `t`.`published`=1 AND " . (count($filters) ? implode(' AND ', $filters) : '`i`.`checkin` > ' . $dbo->quote($tomorrowdt)) . "
1369 ORDER BY `i`.`checkin` ASC, `i`.`id` DESC;";
1370 $dbo->setQuery($q);
1371 $dbo->execute();
1372 if (!$dbo->getNumRows()) {
1373 // no data found
1374 return [];
1375 }
1376
1377 // grab all records
1378 $stats_data = $dbo->loadAssocList();
1379
1380 // calculate statistics
1381 $demands_nights = [];
1382 $demands_count = [];
1383 $referrer_count = [];
1384 $totidentifiers = [];
1385 $totbookings = [];
1386 $los_pool = [];
1387 foreach ($stats_data as $stat) {
1388 if (!empty($stat['referrer'])) {
1389 if (!isset($referrer_count[$stat['referrer']])) {
1390 $referrer_count[$stat['referrer']] = 0;
1391 }
1392 $referrer_count[$stat['referrer']]++;
1393 }
1394 if (!isset($totidentifiers[$stat['identifier']])) {
1395 // total identifiers
1396 $totidentifiers[$stat['identifier']] = 1;
1397 }
1398 if (!empty($stat['idorder']) && !isset($totbookings[$stat['identifier']])) {
1399 // one conversion per tracking identifier
1400 $totbookings[$stat['identifier']] = $stat['idorder'];
1401 }
1402 // loop through the nights of this tracking info record
1403 $in_ts = strtotime($stat['checkin']);
1404 $in_info = getdate($in_ts);
1405 $out_ts = strtotime($stat['checkout']);
1406 $out_dt = date('Y-m-d', $out_ts);
1407 $in_dt = date('Y-m-d', $in_info[0]);
1408 $now_los = 0;
1409 while ($in_dt != $out_dt) {
1410 if (!($in_ts < $out_ts)) {
1411 // prevent any possible loop in case of records with invalid data
1412 break;
1413 }
1414 $now_los++;
1415 if (!isset($demands_nights[$in_dt])) {
1416 $demands_nights[$in_dt] = 0;
1417 }
1418 // increase the requests for this night
1419 $demands_nights[$in_dt]++;
1420 if (!isset($demands_count[$in_dt])) {
1421 $demands_count[$in_dt] = array();
1422 }
1423 if (!in_array($stat['idtracking'], $demands_count[$in_dt])) {
1424 // push this visitor (tracking) ID to the counter for this night
1425 array_push($demands_count[$in_dt], $stat['idtracking']);
1426 }
1427 // update next loop
1428 $in_info = getdate(mktime(0, 0, 0, $in_info['mon'], ($in_info['mday'] + 1), $in_info['year']));
1429 $in_dt = date('Y-m-d', $in_info[0]);
1430 }
1431 array_push($los_pool, $now_los);
1432 }
1433
1434 // sort most demanded nights and best referrers
1435 arsort($demands_nights);
1436 arsort($referrer_count);
1437
1438 // count values that could be 0
1439 $cnt_tot_idfs = count($totidentifiers);
1440 $cnt_tot_idfs = $cnt_tot_idfs > 0 ? $cnt_tot_idfs : 1;
1441 $cnt_los_pool = count($los_pool);
1442 $cnt_los_pool = $cnt_los_pool > 0 ? $cnt_los_pool : 1;
1443
1444 // average conversion rate: 100 : totidentifiers = x : totbookings
1445 $avg_conv_rate = 100 * count($totbookings) / $cnt_tot_idfs;
1446 $avg_conv_rate = round($avg_conv_rate, 2);
1447
1448 // average length of stay
1449 $avg_los = array_sum($los_pool) / $cnt_los_pool;
1450 $avg_los = round($avg_los, 1);
1451
1452 // compose associative values to be returned
1453 return [
1454 'from_date' => $from_date,
1455 'to_date' => $to_date,
1456 'date_type' => $dt_type,
1457 'demanded_nights' => $demands_nights,
1458 'demanded_ncount' => $demands_count,
1459 'referrers' => $referrer_count,
1460 'bookings' => $totbookings,
1461 'avg_los' => $avg_los,
1462 'avg_conv_rate' => $avg_conv_rate,
1463 'tot_visitors' => $cnt_tot_idfs,
1464 'tot_bookings' => count($totbookings),
1465 ];
1466 }
1467 }
1468