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

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

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