Files
conservatorio-tomadini/plugins/system/tgeoip/helper/tgeoip.php
2024-12-31 11:07:09 +01:00

551 lines
12 KiB
PHP

<?php
/**
* @package Advanced Custom Fields
* @version 2.8.8 Pro
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2024 Tassos Marinos All Rights Reserved
* @license GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/
defined('_JEXEC') or die;
use Tassos\Vendor\GeoIp2\Database\Reader;
use Tassos\Vendor\splitbrain\PHPArchive\Tar;
use NRFramework\User;
use Joomla\CMS\Plugin\PluginHelper;
use Joomla\Registry\Registry;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Factory;
use Joomla\Filesystem\Path;
use Joomla\CMS\Http\HttpFactory;
class TGeoIP
{
/**
* The MaxMind GeoLite database reader
*
* @var Reader
*/
private $reader = null;
/**
* Records for IP addresses already looked up
*
* @var array
*
*/
private $lookups = array();
/**
* Max Age Database before it needs an update
*
* @var integer
*/
private $maxAge = 30;
/**
* Database File name
*
* @var string
*/
private $DBFileName = 'GeoLite2-City';
/**
* Database Remote URL
*
* @var string
*/
private $DBUpdateURL = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=USER_LICENSE_KEY&suffix=tar.gz';
/**
* GeoIP Enable Geolocations Documentation URL
*
* @var string
*/
private $TGeoIPEnableDocURL = 'https://www.tassos.gr/kb/general/how-to-enable-geolocation-features-in-tassos-gr-extensions';
/**
* The IP address to look up
*
* @var string
*/
private $ip;
/**
* The License Key
*
* @var string
*/
private $key;
/**
* Public constructor. Loads up the GeoLite2 database.
*/
public function __construct($ip = null)
{
if (!function_exists('bcadd') || !function_exists('bcmul') || !function_exists('bcpow'))
{
require_once __DIR__ . '/fakebcmath.php';
}
// Check we have a valid GeoLite2 database
$filePath = $this->getDBPath();
if (!file_exists($filePath))
{
$this->reader = null;
}
try
{
$this->reader = new Reader($filePath);
}
// If anything goes wrong, MaxMind will raise an exception, resulting in a WSOD. Let's be sure to catch everything.
catch(\Exception $e)
{
$this->reader = null;
}
// Setup IP
$this->ip = $ip ?: User::getIP();
if (in_array($this->ip, array('127.0.0.1', '::1')))
{
$this->ip = '';
}
}
/**
* Sets the license key
*
* @param string
*
* @return mixed
*/
public function setKey($key)
{
$this->key = $key;
}
/**
* Retrieves the key
*
* @return string
*/
private function getKey()
{
if ($this->key)
{
return $this->key;
}
$plugin = PluginHelper::getPlugin('system', 'tgeoip');
$params = new Registry($plugin->params);
return $params->get('license_key', '');
}
/**
* Set the IP to look up
*
* @param string $ip The IP to look up
*/
public function setIP($ip)
{
$this->ip = $ip;
return $this;
}
/**
* Gets the ISO country code from an IP address
*
* @return mixed A string with the country ISO code if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getCountryCode()
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
return $record->country->isoCode;
}
/**
* Gets the country name from an IP address
*
* @param string $locale The locale of the country name, e.g 'de' to return the country names in German. If not specified the English (US) names are returned.
*
* @return mixed A string with the country name if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getCountryName($locale = null)
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
if (empty($locale))
{
return $record->country->name;
}
return $record->country->names[$locale];
}
/**
* Gets the continent ISO code from an IP address
*
* @return mixed A string with the country name if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getContinentCode($locale = null)
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
return $record->continent->code;
}
/**
* Gets the continent name from an IP address
*
* @param string $locale The locale of the continent name, e.g 'de' to return the country names in German. If not specified the English (US) names are returned.
*
* @return mixed A string with the country name if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getContinentName($locale = null)
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
if (empty($locale))
{
return $record->continent;
}
return $record->continent->names[$locale];
}
/**
* Gets a raw record from an IP address
*
* @return mixed A \GeoIp2\Model\City record if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getRecord()
{
if (empty($this->ip))
{
return false;
}
$ip = $this->ip;
$needsToLoad = !array_key_exists($ip, $this->lookups);
if ($needsToLoad)
{
try
{
if (!is_null($this->reader))
{
$this->lookups[$ip] = $this->reader->city($ip);
}
else
{
$this->lookups[$ip] = null;
}
}
catch (Tassos\Vendor\GeoIp2\Exception\AddressNotFoundException $e)
{
$this->lookups[$ip] = false;
}
catch (\Exception $e)
{
// GeoIp2 could throw several different types of exceptions. Let's be sure that we're going to catch them all
$this->lookups[$ip] = null;
}
}
return $this->lookups[$ip];
}
/**
* Gets the city's name from an IP address
*
* @param string $locale The locale of the city's name, e.g 'de' to return the city names in German. If not specified the English (US) names are returned.
* @return mixed A string with the city name if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getCity($locale = null)
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
if (empty($locale))
{
return $record->city->name;
}
return $record->city->names[$locale];
}
/**
* Gets a geographical region's (i.e. a country's province/state) name from an IP address
*
* @param string $locale The locale of the regions's name, e.g 'de' to return region names in German. If not specified the English (US) names are returned.
* @return mixed A string with the region's name if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getRegionName($locale = null)
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
// MaxMind stores region information in a 'Subdivision' object (also found in $record->city->subdivision)
// http://maxmind.github.io/GeoIP2-php/doc/v2.9.0/class-GeoIp2.Record.Subdivision.html
if (empty($locale))
{
return $record->mostSpecificSubdivision->name;
}
return $record->mostSpecificSubdivision->names[$locale];
}
/**
* Gets a geographical region's (i.e. a country's province/state) ISO 3611-2 (alpha-2) code from an IP address
*
* @return mixed A string with the region's code if found, false if the IP address is not found, null if the db can't be loaded
*/
public function getRegionCode()
{
$record = $this->getRecord();
if ($record === false || is_null($record))
{
return false;
}
// MaxMind stores region information in a 'Subdivision' object
// http://maxmind.github.io/GeoIP2-php/doc/v2.9.0/class-GeoIp2.Record.Subdivision.html
return $record->mostSpecificSubdivision->isoCode;
}
/**
* Downloads and installs a fresh copy of the GeoLite2 City database
*
* @return mixed True on success, error string on failure
*/
public function updateDatabase()
{
// Try to download the package, if I get any exception I'll simply stop here and display the error
try
{
$compressed = $this->downloadDatabase();
}
catch (\Exception $e)
{
return $e->getMessage();
}
// Write the downloaded file to a temporary location
$target = $this->getTempFolder() . $this->DBFileName . '.tar.gz';
if (File::write($target, $compressed) === false)
{
return Text::_('PLG_SYSTEM_TGEOIP_ERR_WRITEFAILED');
}
// Unzip database to the same temporary location
$tar = new Tar;
$tar->open($target);
$extracted_files = $tar->extract($this->getTempFolder());
$database_file = '';
$extracted_folder = '';
// Loop through extracted files to find the name of the extracted folder and the name of the database file
foreach ($extracted_files as $key => $extracted_file)
{
if ($extracted_file->getIsdir())
{
$extracted_folder = $extracted_file->getPath();
}
if (strpos($extracted_file->getPath(), '.mmdb') === false)
{
continue;
}
$database_file = $extracted_file->getPath();
}
// Move database file to the correct location
if (!File::move($this->getTempFolder() . $database_file, $this->getDBPath()))
{
return Text::sprintf('PLG_SYSTEM_TGEOIP_ERR_CANTWRITE', $this->getDBPath());
}
// Make sure the database is readable
if (!$this->dbIsValid())
{
return Text::_('PLG_SYSTEM_TGEOIP_ERR_INVALIDDB');
}
// Delete leftovers
File::delete($target);
Folder::delete($this->getTempFolder() . $extracted_folder);
return true;
}
/**
* Double check if MaxMind can actually read and validate the downloaded database
*
* @return bool
*/
private function dbIsValid()
{
try
{
$reader = new Reader($this->getDBPath());
}
catch (\Exception $e)
{
return false;
}
return true;
}
/**
* Download the compressed database for the provider
*
* @return string The compressed data
*
* @throws Exception
*/
private function downloadDatabase()
{
// Make sure we have enough memory limit
ini_set('memory_limit', '-1');
$license_key = $this->getKey();
if (empty($license_key))
{
throw new \Exception(Text::_('PLG_SYSTEM_TGEOIP_LICENSE_KEY_EMPTY') . '&nbsp;<a href="' . $this->TGeoIPEnableDocURL . '" target="_blank">' . Text::_('PLG_SYSTEM_TGEOIP_ENABLE_DOC_LINK_LABEL') . '</a>');
}
$http = HttpFactory::getHttp();
$this->DBUpdateURL = str_replace('USER_LICENSE_KEY', $license_key, $this->DBUpdateURL);
// Let's bubble up the exception, we will take care in the caller
$response = $http->get($this->DBUpdateURL);
$compressed = $response->body;
// 401 is thrown if you have incorrect credentials or wrong license key
if ($response->code == 401)
{
throw new \Exception(Text::_('PLG_SYSTEM_TGEOIP_ERR_WRONG_LICENSE_KEY'));
}
// Generic check on valid HTTP code
if ($response->code > 299)
{
throw new \Exception(Text::_('PLG_SYSTEM_TGEOIP_ERR_MAXMIND_GENERIC'));
}
// An empty file indicates a problem with MaxMind's servers
if (empty($compressed))
{
throw new \Exception(Text::_('PLG_SYSTEM_TGEOIP_ERR_EMPTYDOWNLOAD'));
}
// Sometimes you get a rate limit exceeded
if (stristr($compressed, 'Rate limited exceeded') !== false)
{
throw new \Exception(Text::_('PLG_SYSTEM_TGEOIP_ERR_MAXMINDRATELIMIT'));
}
return $compressed;
}
/**
* Reads (and checks) the temp Joomla folder
*
* @return string
*/
private function getTempFolder()
{
$ds = DIRECTORY_SEPARATOR;
$tmpdir = Factory::getConfig()->get('tmp_path');
if (realpath($tmpdir) == $ds . 'tmp')
{
$tmpdir = JPATH_SITE . $ds . 'tmp';
}
elseif (!is_dir($tmpdir))
{
$tmpdir = JPATH_SITE . $ds . 'tmp';
}
return Path::clean(trim($tmpdir) . $ds);
}
/**
* Returns Database local file path
*
* @return string
*/
private function getDBPath()
{
return JPATH_ROOT . '/plugins/system/tgeoip/db/' . $this->DBFileName . '.mmdb';
}
/**
* Does the GeoIP database need update?
*
* @return boolean
*/
public function needsUpdate()
{
// Get the modification time of the database file
$modTime = @filemtime($this->getDBPath());
// This is now
$now = time();
// Minimum time difference
$threshold = $this->maxAge * 24 * 3600;
// Do we need an update?
$needsUpdate = ($now - $modTime) > $threshold;
return $needsUpdate;
}
}