PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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
← All changes | site/helpers/tracker.php +1467 -0 1.6.1 → 1.8.16 View file →
@@ -1,0 +1,1467 @@
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 +}