PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.4
VikBooking Hotel Booking Engine & PMS v1.6.4
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.4, at site/helpers/tracker.php

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