551 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			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') . ' <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;
 | |
| 	}
 | |
| } |