forked from rebillar/site-accueil-insa
318 lines
11 KiB
PHP
318 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Matomo - free/libre analytics platform
|
|
*
|
|
* @link https://matomo.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
namespace Piwik\Plugins\UserCountry;
|
|
|
|
use Matomo\Cache\Cache;
|
|
use Matomo\Cache\Transient;
|
|
use Piwik\Common;
|
|
use Piwik\Container\StaticContainer;
|
|
use Piwik\DataAccess\RawLogDao;
|
|
use Matomo\Network\IPUtils;
|
|
use Piwik\Plugins\UserCountry\LocationProvider\DisabledProvider;
|
|
use Piwik\Tracker\Visit;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
require_once PIWIK_INCLUDE_PATH . "/plugins/UserCountry/LocationProvider.php";
|
|
|
|
/**
|
|
* Service that determines a visitor's location using visitor information.
|
|
*
|
|
* Individual locations are provided by a LocationProvider instance. By default,
|
|
* the configured LocationProvider (as determined by
|
|
* `Common::getCurrentLocationProviderId()` is used.
|
|
*
|
|
* If the configured location provider cannot provide a location for the visitor,
|
|
* the default location provider (`DefaultProvider`) is used.
|
|
*
|
|
* A cache is used internally to speed up location retrieval. By default, an
|
|
* in-memory cache is used, but another type of cache can be supplied during
|
|
* construction.
|
|
*
|
|
* This service can be used from within the tracker.
|
|
*/
|
|
class VisitorGeolocator
|
|
{
|
|
const LAT_LONG_COMPARE_EPSILON = 0.0001;
|
|
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
public static $logVisitFieldsToUpdate = array(
|
|
'location_country' => LocationProvider::COUNTRY_CODE_KEY,
|
|
'location_region' => LocationProvider::REGION_CODE_KEY,
|
|
'location_city' => LocationProvider::CITY_NAME_KEY,
|
|
'location_latitude' => LocationProvider::LATITUDE_KEY,
|
|
'location_longitude' => LocationProvider::LONGITUDE_KEY
|
|
);
|
|
|
|
/**
|
|
* @var Cache
|
|
*/
|
|
protected static $defaultLocationCache = null;
|
|
|
|
/**
|
|
* @var LocationProvider
|
|
*/
|
|
private $provider;
|
|
|
|
/**
|
|
* @var LocationProvider
|
|
*/
|
|
private $backupProvider;
|
|
|
|
/**
|
|
* @var Cache
|
|
*/
|
|
private $locationCache;
|
|
|
|
/**
|
|
* @var RawLogDao
|
|
*/
|
|
protected $dao;
|
|
|
|
/**
|
|
* @var LoggerInterface
|
|
*/
|
|
protected $logger;
|
|
|
|
public function __construct(LocationProvider $provider = null, LocationProvider $backupProvider = null, Cache $locationCache = null,
|
|
RawLogDao $dao = null, LoggerInterface $logger = null)
|
|
{
|
|
if ($provider === null) {
|
|
// note: Common::getCurrentLocationProviderId() uses the tracker cache, which is why it's used here instead
|
|
// of accessing the option table
|
|
$provider = LocationProvider::getProviderById(Common::getCurrentLocationProviderId());
|
|
|
|
if (empty($provider)) {
|
|
Common::printDebug("GEO: no current location provider sent, falling back to '" . LocationProvider::getDefaultProviderId() . "' one.");
|
|
|
|
$provider = $this->getDefaultProvider();
|
|
}
|
|
}
|
|
$this->provider = $provider;
|
|
|
|
$this->backupProvider = $backupProvider ?: $this->getDefaultProvider();
|
|
$this->locationCache = $locationCache ?: self::getDefaultLocationCache();
|
|
$this->dao = $dao ?: new RawLogDao();
|
|
$this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
|
|
}
|
|
|
|
public function getLocation($userInfo, $useClassCache = true)
|
|
{
|
|
$userInfoKey = md5(implode(',', $userInfo));
|
|
if ($useClassCache
|
|
&& $this->locationCache->contains($userInfoKey)
|
|
) {
|
|
return $this->locationCache->fetch($userInfoKey);
|
|
}
|
|
|
|
$location = $this->getLocationObject($this->provider, $userInfo);
|
|
|
|
if (empty($location)) {
|
|
$providerId = $this->provider->getId();
|
|
Common::printDebug("GEO: couldn't find a location with Geo Module '$providerId'");
|
|
|
|
// Only use the default provider as fallback if the configured one isn't "disabled"
|
|
if ($providerId != DisabledProvider::ID && $providerId != $this->backupProvider->getId()) {
|
|
Common::printDebug("Using default provider as fallback...");
|
|
|
|
$location = $this->getLocationObject($this->backupProvider, $userInfo);
|
|
}
|
|
}
|
|
|
|
$location = $location ?: array();
|
|
if (empty($location['country_code'])) {
|
|
$location['country_code'] = Visit::UNKNOWN_CODE;
|
|
}
|
|
|
|
$this->locationCache->save($userInfoKey, $location);
|
|
|
|
return $location;
|
|
}
|
|
|
|
/**
|
|
* @param LocationProvider $provider
|
|
* @param array $userInfo
|
|
* @return array|false
|
|
*/
|
|
private function getLocationObject(LocationProvider $provider, $userInfo)
|
|
{
|
|
$location = $provider->getLocation($userInfo);
|
|
$providerId = $provider->getId();
|
|
$ipAddress = $userInfo['ip'];
|
|
|
|
if ($location === false) {
|
|
return false;
|
|
}
|
|
|
|
Common::printDebug("GEO: Found IP $ipAddress location (provider '" . $providerId . "'): " . var_export($location, true));
|
|
|
|
return $location;
|
|
}
|
|
|
|
/**
|
|
* Geolcates an existing visit and then updates it if it's current attributes are different than
|
|
* what was geolocated. Also updates all conversions of a visit.
|
|
*
|
|
* **This method should NOT be used from within the tracker.**
|
|
*
|
|
* @param array $visit The visit information. Must contain an `"idvisit"` element and `"location_ip"` element.
|
|
* @param bool $useClassCache
|
|
* @return array|null The visit properties that were updated in the DB mapped to the updated values. If null,
|
|
* required information was missing from `$visit`.
|
|
*/
|
|
public function attributeExistingVisit($visit, $useClassCache = true)
|
|
{
|
|
if (empty($visit['idvisit'])) {
|
|
$this->logger->debug('Empty idvisit field. Skipping re-attribution..');
|
|
return null;
|
|
}
|
|
|
|
$idVisit = $visit['idvisit'];
|
|
|
|
if (empty($visit['location_ip'])) {
|
|
$this->logger->debug('Empty location_ip field for idvisit = %s. Skipping re-attribution.', array('idvisit' => $idVisit));
|
|
return null;
|
|
}
|
|
|
|
$ip = IPUtils::binaryToStringIP($visit['location_ip']);
|
|
$location = $this->getLocation(array('ip' => $ip), $useClassCache);
|
|
|
|
$valuesToUpdate = $this->getVisitFieldsToUpdate($visit, $location);
|
|
|
|
if (!empty($valuesToUpdate)) {
|
|
$this->logger->debug('Updating visit with idvisit = {idVisit} (IP = {ip}). Changes: {changes}', array(
|
|
'idVisit' => $idVisit,
|
|
'ip' => $ip,
|
|
'changes' => json_encode($valuesToUpdate)
|
|
));
|
|
|
|
$this->dao->updateVisits($valuesToUpdate, $idVisit);
|
|
$this->dao->updateConversions($valuesToUpdate, $idVisit);
|
|
} else {
|
|
$this->logger->debug('Nothing to update for idvisit = %s (IP = {ip}). Existing location info is same as geolocated.', array(
|
|
'idVisit' => $idVisit,
|
|
'ip' => $ip
|
|
));
|
|
}
|
|
|
|
return $valuesToUpdate;
|
|
}
|
|
|
|
/**
|
|
* Returns location log values that are different than the values currently in a log row.
|
|
*
|
|
* @param array $row The visit row.
|
|
* @param array $location The location information.
|
|
* @return array The location properties to update.
|
|
*/
|
|
private function getVisitFieldsToUpdate(array $row, $location)
|
|
{
|
|
if (isset($location[LocationProvider::COUNTRY_CODE_KEY])) {
|
|
$location[LocationProvider::COUNTRY_CODE_KEY] = strtolower($location[LocationProvider::COUNTRY_CODE_KEY]);
|
|
}
|
|
|
|
$valuesToUpdate = array();
|
|
foreach (self::$logVisitFieldsToUpdate as $column => $locationKey) {
|
|
if (empty($location[$locationKey])) {
|
|
continue;
|
|
}
|
|
|
|
$locationPropertyValue = $location[$locationKey];
|
|
$existingPropertyValue = $row[$column];
|
|
|
|
if (!$this->areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue)) {
|
|
$valuesToUpdate[$column] = $locationPropertyValue;
|
|
}
|
|
}
|
|
return $valuesToUpdate;
|
|
}
|
|
|
|
/**
|
|
* Re-geolocate visits within a date range for a specified site (if any).
|
|
*
|
|
* @param string $from A datetime string to treat as the lower bound. Visits newer than this date are processed.
|
|
* @param string $to A datetime string to treat as the upper bound. Visits older than this date are processed.
|
|
* @param int|null $idSite If supplied, only visits for this site are re-attributed.
|
|
* @param int $iterationStep The number of visits to re-attribute at the same time.
|
|
* @param callable|null $onLogProcessed If supplied, this callback is called after every row is processed.
|
|
* The processed visit and the updated values are passed to the callback.
|
|
*/
|
|
public function reattributeVisitLogs($from, $to, $idSite = null, $iterationStep = 1000, $onLogProcessed = null)
|
|
{
|
|
$visitFieldsToSelect = array_merge(array('idvisit', 'location_ip'), array_keys(VisitorGeolocator::$logVisitFieldsToUpdate));
|
|
|
|
$conditions = array(
|
|
array('visit_last_action_time', '>=', $from),
|
|
array('visit_last_action_time', '<', $to)
|
|
);
|
|
|
|
if (!empty($idSite)) {
|
|
$conditions[] = array('idsite', '=', $idSite);
|
|
}
|
|
|
|
$self = $this;
|
|
$this->dao->forAllLogs('log_visit', $visitFieldsToSelect, $conditions, $iterationStep, function ($logs) use ($self, $onLogProcessed) {
|
|
foreach ($logs as $row) {
|
|
$updatedValues = $self->attributeExistingVisit($row);
|
|
|
|
if (!empty($onLogProcessed)) {
|
|
$onLogProcessed($row, $updatedValues);
|
|
}
|
|
}
|
|
}, $willDelete = false);
|
|
}
|
|
|
|
/**
|
|
* @return LocationProvider
|
|
*/
|
|
public function getProvider()
|
|
{
|
|
return $this->provider;
|
|
}
|
|
|
|
/**
|
|
* @return LocationProvider
|
|
*/
|
|
public function getBackupProvider()
|
|
{
|
|
return $this->backupProvider;
|
|
}
|
|
|
|
private function areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue)
|
|
{
|
|
if (($locationKey == LocationProvider::LATITUDE_KEY
|
|
|| $locationKey == LocationProvider::LONGITUDE_KEY)
|
|
&& $existingPropertyValue != 0
|
|
) {
|
|
// floating point comparison
|
|
return abs(($locationPropertyValue - $existingPropertyValue) / $existingPropertyValue) < self::LAT_LONG_COMPARE_EPSILON;
|
|
} else {
|
|
return $locationPropertyValue == $existingPropertyValue;
|
|
}
|
|
}
|
|
|
|
private function getDefaultProvider()
|
|
{
|
|
return LocationProvider::getProviderById(LocationProvider::getDefaultProviderId());
|
|
}
|
|
|
|
public static function getDefaultLocationCache()
|
|
{
|
|
if (self::$defaultLocationCache === null) {
|
|
if (class_exists('\Piwik\Cache\Transient')) {
|
|
// during the oneclickupdate from 3.x => greater, this class will be loaded, so we have to use it instead of the Matomo namespaced one
|
|
self::$defaultLocationCache = new \Piwik\Cache\Transient();
|
|
} else {
|
|
self::$defaultLocationCache = new Transient();
|
|
}
|
|
}
|
|
return self::$defaultLocationCache;
|
|
}
|
|
}
|