This commit is contained in:
2024-12-31 11:07:09 +01:00
parent df7915205d
commit e089172b15
1916 changed files with 165422 additions and 271 deletions

View File

@ -0,0 +1 @@
<!DOCTYPE html><title></title><!-- e4aNe339 -->

View File

@ -0,0 +1,39 @@
<?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
*/
// No direct access to this file
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Factory;
use Joomla\Filesystem\File;
class JFormFieldTG_LastUpdated extends FormField
{
/**
* Method to render the input field
*
* @return string
*/
public function getInput()
{
$file = JPATH_PLUGINS . '/system/tgeoip/db/GeoLite2-City.mmdb';
if (!file_exists($file))
{
return '';
}
return Factory::getDate(@filemtime($file))->format('d M Y H:m');
}
}

View File

@ -0,0 +1,108 @@
<?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
*/
// No direct access to this file
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use NRFramework\User;
class JFormFieldTG_Lookup extends FormField
{
/**
* GeoIP Class
*
* @var object
*/
private $geoIP;
/**
* Method to render the input field
*
* @return string
*/
public function getInput()
{
// JavaScript
$ajaxURL = Uri::base() . 'index.php?option=com_ajax&format=raw&plugin=tgeoip&task=get&' . Session::getFormToken() . '=1';
Factory::getDocument()->addScriptDeclaration('
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("click", function(e) {
var btn = e.target.closest(".tGeoIPtest button");
if (!btn) {
return;
}
e.preventDefault();
ip = document.querySelector(".tGeoIPtest input").value;
if (!ip) {
alert("Please enter a valid IP address");
return false;
}
var data = new FormData();
data.append("ip", ip);
fetch("' . $ajaxURL . '",
{
method: "POST",
body: data
})
.then(function(res){ return res.json(); })
.then(function(response){
if (response) {
if (response.continent) {
document.querySelector(".tGeoIPtest .continent").innerHTML = response.continent.names.en;
}
if (response.city) {
document.querySelector(".tGeoIPtest .city").innerHTML = response.city.names.en;
}
if (response.country) {
document.querySelector(".tGeoIPtest .country").innerHTML = response.country.names.en;
document.querySelector(".tGeoIPtest .country_code").innerHTML = response.country.iso_code;
}
document.querySelector(".tGeoIPtest .results").style.display = "block";
} else {
alert("Invalid IP address");
document.querySelector(".tGeoIPtest .results").style.display = "none";
}
})
return false;
})
});
');
// HTML
$ip = User::getIP();
return '<div class="tGeoIPtest">
<input class="form-control input-medium" type="text" value="' . $ip . '"/>
<button class="btn btn-outline-secondary">Lookup</button>
<ul class="results" style="margin-top:20px; display:none;">
<li>Continent: <span class="continent"></span></li>
<li>Country: <span class="country"></span></li>
<li>Country Code: <span class="country_code"></span></li>
<li>City: <span class="city"></span></li>
<ul>
</div>';
}
}

View File

@ -0,0 +1,129 @@
<?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
*/
// No direct access to this file
defined('_JEXEC') or die;
use Joomla\CMS\Form\FormField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
class JFormFieldTG_UpdateButton extends FormField
{
/**
* Method to render the input field
*
* @return string
*/
public function getInput()
{
if (!NRFramework\Extension::pluginIsEnabled('tgeoip'))
{
return '<span class="label label-warning" style="margin-top:4px;">' . Text::_('PLG_SYSTEM_TGEOIP_ENABLE_PLUGIN') . '</span>';
}
HTMLHelper::stylesheet('plg_system_nrframework/joomla4.css', ['relative' => true, 'version' => 'auto']);
$ajaxURL = Uri::base() . 'index.php?option=com_ajax&format=raw&plugin=tgeoip&task=update&license_key=USER_LICENSE_KEY&' . Session::getFormToken() . '=1';
Text::script('PLG_SYSTEM_TGEOIP_DATABASE_UPDATED');
Text::script('PLG_SYSTEM_TGEOIP_PLEASE_WAIT');
Factory::getDocument()->addScriptDeclaration('
document.addEventListener("DOMContentLoaded", function() {
document.addEventListener("click", function(e) {
var btn = e.target.closest(".geo button");
if (!btn) {
return;
}
e.preventDefault();
var license_key = e.target.closest("form").querySelector("#jform_params_license_key").value;
if (!license_key) {
return;
}
var alert = document.querySelector(".geo .alert");
var url = "' . $ajaxURL . '";
url = url.replace("USER_LICENSE_KEY", license_key);
// before request
alert.style.display = "none";
btn.querySelector("span").innerHTML = Joomla.Text._("PLG_SYSTEM_TGEOIP_PLEASE_WAIT");
btn.classList.add("btn-working");
fetch(url,
{
method: "POST"
})
.then(function(res){ return res.text(); })
.then(function(response){
if (response == "1") {
alert.innerHTML = Joomla.Text._("PLG_SYSTEM_TGEOIP_DATABASE_UPDATED");
alert.style.display = "block";
alert.classList.remove("alert-danger");
alert.classList.add("alert-success");
} else {
alert.innerHTML = response;
alert.style.display = "block";
alert.classList.remove("alert-success");
alert.classList.add("alert-danger");
}
btn.classList.remove("btn-working");
btn.querySelector("span").innerHTML = btn.dataset.label;
});
return false;
});
});
');
Factory::getDocument()->addStyleDeclaration('
.geo .btn-working {
pointer-events:none;
}
.geo .alert {
display:none;
margin-bottom: 10px;
}
.geo button {
outline:none !important;
width: auto;
height: auto;
line-height: inherit;
}
.geo button:before {
margin-right:5px;
position:relative;
top:1px;
}
#wrapper .geo .icon-refresh {
margin-right: 5px;
}
');
return '
<div class="geo">
<div class="alert alert-danger"></div>
<button class="btn btn-primary" data-label="' . Text::_('PLG_SYSTEM_TGEOIP_UPDATE_DATABASE') . '">
<em class="icon-refresh"></em>
<span>' . Text::_('PLG_SYSTEM_TGEOIP_UPDATE_DATABASE') . '</span>
</button>
</div>';
}
}

View File

@ -0,0 +1,132 @@
<?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;
if (!function_exists('bcadd'))
{
function bcadd($Num1,$Num2,$Scale=null)
{
// check if they're valid positive numbers, extract the whole numbers and decimals
if(!preg_match("/^\+?(\d+)(\.\d+)?$/",$Num1,$Tmp1)||
!preg_match("/^\+?(\d+)(\.\d+)?$/",$Num2,$Tmp2)) return('0');
// this is where the result is stored
$Output=array();
// remove ending zeroes from decimals and remove point
$Dec1=isset($Tmp1[2])?rtrim(substr($Tmp1[2],1),'0'):'';
$Dec2=isset($Tmp2[2])?rtrim(substr($Tmp2[2],1),'0'):'';
// calculate the longest length of decimals
$DLen=max(strlen($Dec1),strlen($Dec2));
// if $Scale is null, automatically set it to the amount of decimal places for accuracy
if($Scale==null) $Scale=$DLen;
// remove leading zeroes and reverse the whole numbers, then append padded decimals on the end
$Num1=strrev(ltrim($Tmp1[1],'0').str_pad($Dec1,$DLen,'0'));
$Num2=strrev(ltrim($Tmp2[1],'0').str_pad($Dec2,$DLen,'0'));
// calculate the longest length we need to process
$MLen=max(strlen($Num1),strlen($Num2));
// pad the two numbers so they are of equal length (both equal to $MLen)
$Num1=str_pad($Num1,$MLen,'0');
$Num2=str_pad($Num2,$MLen,'0');
// process each digit, keep the ones, carry the tens (remainders)
for($i=0;$i<$MLen;$i++) {
$Sum=((int)$Num1[$i]+(int)$Num2[$i]);
if(isset($Output[$i])) $Sum+=$Output[$i];
$Output[$i]=$Sum%10;
if($Sum>9) $Output[$i+1]=1;
}
// convert the array to string and reverse it
$Output=strrev(implode($Output));
// substring the decimal digits from the result, pad if necessary (if $Scale > amount of actual decimals)
// next, since actual zero values can cause a problem with the substring values, if so, just simply give '0'
// next, append the decimal value, if $Scale is defined, and return result
$Decimal=str_pad(substr($Output,-$DLen,$Scale),$Scale,'0');
$Output=(($MLen-$DLen<1)?'0':substr($Output,0,-$DLen));
$Output.=(($Scale>0)?".{$Decimal}":'');
return($Output);
}
}
if (!function_exists('bcmul'))
{
function bcmul($Num1='0',$Num2='0') {
// check if they're both plain numbers
if(!preg_match("/^\d+$/",$Num1)||!preg_match("/^\d+$/",$Num2)) return(0);
// remove zeroes from beginning of numbers
for($i=0;$i<strlen($Num1);$i++) if(@$Num1[$i]!='0') {$Num1=substr($Num1,$i);break;}
for($i=0;$i<strlen($Num2);$i++) if(@$Num2[$i]!='0') {$Num2=substr($Num2,$i);break;}
// get both number lengths
$Len1=strlen($Num1);
$Len2=strlen($Num2);
// $Rema is for storing the calculated numbers and $Rema2 is for carrying the remainders
$Rema=$Rema2=array();
// we start by making a $Len1 by $Len2 table (array)
for($y=$i=0;$y<$Len1;$y++)
for($x=0;$x<$Len2;$x++)
// we use the classic lattice method for calculating the multiplication..
// this will multiply each number in $Num1 with each number in $Num2 and store it accordingly
@$Rema[$i++%$Len2].=sprintf('%02d',(int)$Num1[$y]*(int)$Num2[$x]);
// cycle through each stored number
for($y=0;$y<$Len2;$y++)
for($x=0;$x<$Len1*2;$x++)
// add up the numbers in the diagonal fashion the lattice method uses
@$Rema2[Floor(($x-1)/2)+1+$y]+=(int)$Rema[$y][$x];
// reverse the results around
$Rema2=array_reverse($Rema2);
// cycle through all the results again
for($i=0;$i<count($Rema2);$i++) {
// reverse this item, split, keep the first digit, spread the other digits down the array
$Rema3=str_split(strrev($Rema2[$i]));
for($o=0;$o<count($Rema3);$o++)
if($o==0) @$Rema2[$i+$o]=$Rema3[$o];
else @$Rema2[$i+$o]+=$Rema3[$o];
}
// implode $Rema2 so it's a string and reverse it, this is the result!
$Rema2=strrev(implode($Rema2));
// just to make sure, we delete the zeros from the beginning of the result and return
while(strlen($Rema2)>1&&$Rema2[0]=='0') $Rema2=substr($Rema2,1);
return($Rema2);
}
}
if (!function_exists('bcpow'))
{
function bcpow($num, $power)
{
$answer = "1";
while ($power)
{
$answer = bcmul($answer, $num, 100);
$power--;
}
return rtrim($answer, '0.');
}
}

View File

@ -0,0 +1,551 @@
<?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;
}
}

View File

@ -0,0 +1,37 @@
; @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
TGEOIP="Tassos GeoIP"
PLG_SYSTEM_TGEOIP="System - Tassos GeoIP"
PLG_SYSTEM_TGEOIP_DESC="This plugin provides GeoIP features (finding out the country of an IP address) for Tassos.gr extensions. Without it the GeoIP features will not be available. This plugin includes GeoLite2 data created by MaxMind <a href='http://www.maxmind.com'>http://www.maxmind.com</a>."
PLG_SYSTEM_TGEOIP_ERR_NOGZSUPPORT="Your server does not support extraction of GZip (.gz) files. The GeoLite2 Country database update cannot proceed."
PLG_SYSTEM_TGEOIP_ERR_EMPTYDOWNLOAD="Downloading the GeoLite2 Country database failed: empty file retrieved from server. Please contact your host."
PLG_SYSTEM_TGEOIP_ERR_WRITEFAILED="Writing the temporary file failed. Please make sure that the temporary directory defined in your site's Global Configuration is writeable. The GeoLite2 Country database update cannot proceed."
PLG_SYSTEM_TGEOIP_ERR_CANTUNCOMPRESS="Cannot decompress the GeoLite2 Country database file. Probably a corrupt download? The GeoLite2 Country database update cannot proceed."
PLG_SYSTEM_TGEOIP_ERR_MAXMINDRATELIMIT="MaxMind's servers are busy. Please retry updating the GeoLite2 Country database in 24 hours."
PLG_SYSTEM_TGEOIP_ERR_MAXMIND_GENERIC="A connection error occurred. Please retry updating the GeoLite2 Country database in 24 hours."
PLG_SYSTEM_TGEOIP_ERR_INVALIDDB="Downloaded database seems to be invalid. Please retry updating the GeoLite2 Country database in 24 hours."
PLG_SYSTEM_TGEOIP_ERR_WRONG_LICENSE_KEY="Your MaxMind license key appears to be incorrect."
PLG_SYSTEM_TGEOIP_ERR_CANTWRITE="Moving the database file failed. Please make sure that the database directory is writeable: %s"
PLG_SYSTEM_TGEOIP_UPDATE_DATABASE="Update Database"
PLG_SYSTEM_TGEOIP_UPDATE_DATABASE_DESC="Update the GeoLite2 database from MaxMind servers. This might take several seconds to finish. Please be patient."
PLG_SYSTEM_TGEOIP_DATABASE="Database"
PLG_SYSTEM_TGEOIP_LAST_UPDATED="Last Updated"
PLG_SYSTEM_TGEOIP_LAST_UPDATED_DESC="Indicates the last datetime the database updated."
PLG_SYSTEM_TGEOIP_CHECK_IP="Lookup IP Address"
PLG_SYSTEM_TGEOIP_CHECK_IP_DESC="Test drive the GeoIP plugin by looking up an IP address."
PLG_SYSTEM_TGEOIP_MAINTENANCE="GeoIP Database Maintenance"
PLG_SYSTEM_TGEOIP_MAINTENANCE_DESC="%s finds the country of your visitors' IP addresses using the MaxMind GeoLite2 Country database. You are advised to update it at least once per month. On most servers you can perform the update by clicking the button below."
PLG_SYSTEM_TGEOIP_DATABASE_UPDATED="GeoIP database successfully updated!"
PLG_SYSTEM_TGEOIP_LICENSE_KEY="License Key"
PLG_SYSTEM_TGEOIP_LICENSE_KEY_DESC="Get your free License Key to download the latest MaxMind GeoLite2 Database."
PLG_SYSTEM_TGEOIP_LICENSE_KEY_GET="Get a free License Key"
PLG_SYSTEM_TGEOIP_LICENSE_KEY_EMPTY="Please enter a valid MaxMind License Key."
PLG_SYSTEM_TGEOIP_ENABLE_PLUGIN="To be able to update the database you will need to enable this plugin first"
PLG_SYSTEM_TGEOIP_ENABLE_DOC_LINK_LABEL="Click to learn how to enable Geolocation features in Tassos.gr extensions"
PLG_SYSTEM_TGEOIP_PLEASE_WAIT="Please wait..."

View File

@ -0,0 +1,11 @@
; @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
TGEOIP="Tassos GeoIP"
PLG_SYSTEM_TGEOIP="System - Tassos GeoIP"
PLG_SYSTEM_TGEOIP_DESC="This plugin provides GeoIP features (finding out the country of an IP address) for Tassos.gr extensions. Without it the GeoIP features will not be available. This plugin includes GeoLite2 data created by MaxMind <a href='http://www.maxmind.com'>http://www.maxmind.com</a>."

View File

@ -0,0 +1,691 @@
<?php
/**
* Installer Script Helper
*
* @author Tassos Marinos <info@tassos.gr>
* @link http://www.tassos.gr
* @copyright Copyright © 2016 Tassos Marinos All Rights Reserved
* @license http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
*/
defined('_JEXEC') or die;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
class PlgSystemTgeoipInstallerScriptHelper
{
public $name = '';
public $alias = '';
public $extname = '';
public $extension_type = '';
public $plugin_folder = 'system';
public $module_position = 'status';
public $client_id = 1;
public $install_type = 'install';
public $show_message = true;
public $autopublish = true;
public $db = null;
public $app = null;
public $installedVersion;
public function __construct(&$params)
{
$this->extname = $this->extname ?: $this->alias;
$this->db = Factory::getDbo();
$this->app = Factory::getApplication();
$this->installedVersion = $this->getVersion($this->getInstalledXMLFile());
}
/**
* Preflight event
*
* @param string
* @param JAdapterInstance
*
* @return boolean
*/
public function preflight($route, $adapter)
{
if (!in_array($route, array('install', 'update')))
{
return;
}
Factory::getLanguage()->load('plg_system_novaraininstaller', JPATH_PLUGINS . '/system/novaraininstaller');
if ($this->show_message && $this->isInstalled())
{
$this->install_type = 'update';
}
if ($this->onBeforeInstall() === false)
{
return false;
}
}
/**
* Preflight event
*
* @param string
* @param JAdapterInstance
*
* @return boolean
*/
public function postflight($route, $adapter)
{
Factory::getLanguage()->load($this->getPrefix() . '_' . $this->extname, $this->getMainFolder());
if (!in_array($route, array('install', 'update')))
{
return;
}
if ($this->onAfterInstall() === false)
{
return false;
}
if ($route == 'install' && $this->autopublish)
{
$this->publishExtension();
}
if ($this->show_message)
{
$this->addInstalledMessage();
}
Factory::getCache()->clean('com_plugins');
Factory::getCache()->clean('_system');
}
public function isInstalled()
{
if (!is_file($this->getInstalledXMLFile()))
{
return false;
}
$query = $this->db->getQuery(true)
->select('extension_id')
->from('#__extensions')
->where($this->db->quoteName('type') . ' = ' . $this->db->quote($this->extension_type))
->where($this->db->quoteName('element') . ' = ' . $this->db->quote($this->getElementName()));
$this->db->setQuery($query, 0, 1);
$result = $this->db->loadResult();
return empty($result) ? false : true;
}
public function getMainFolder()
{
switch ($this->extension_type)
{
case 'plugin' :
return JPATH_SITE . '/plugins/' . $this->plugin_folder . '/' . $this->extname;
case 'component' :
return JPATH_ADMINISTRATOR . '/components/com_' . $this->extname;
case 'module' :
return JPATH_ADMINISTRATOR . '/modules/mod_' . $this->extname;
case 'library' :
return JPATH_SITE . '/libraries/' . $this->extname;
}
}
public function getInstalledXMLFile()
{
return $this->getXMLFile($this->getMainFolder());
}
public function getCurrentXMLFile()
{
return $this->getXMLFile(__DIR__);
}
public function getXMLFile($folder)
{
switch ($this->extension_type)
{
case 'module' :
return $folder . '/mod_' . $this->extname . '.xml';
default :
return $folder . '/' . $this->extname . '.xml';
}
}
public function foldersExist($folders = array())
{
foreach ($folders as $folder)
{
if (is_dir($folder))
{
return true;
}
}
return false;
}
public function publishExtension()
{
switch ($this->extension_type)
{
case 'plugin' :
$this->publishPlugin();
case 'module' :
$this->publishModule();
}
}
public function publishPlugin()
{
$query = $this->db->getQuery(true)
->update('#__extensions')
->set($this->db->quoteName('enabled') . ' = 1')
->where($this->db->quoteName('type') . ' = ' . $this->db->quote('plugin'))
->where($this->db->quoteName('element') . ' = ' . $this->db->quote($this->extname))
->where($this->db->quoteName('folder') . ' = ' . $this->db->quote($this->plugin_folder));
$this->db->setQuery($query);
$this->db->execute();
}
public function publishModule()
{
// Get module id
$query = $this->db->getQuery(true)
->select('id')
->from('#__modules')
->where($this->db->quoteName('module') . ' = ' . $this->db->quote('mod_' . $this->extname))
->where($this->db->quoteName('client_id') . ' = ' . (int) $this->client_id);
$this->db->setQuery($query, 0, 1);
$id = $this->db->loadResult();
if (!$id)
{
return;
}
// check if module is already in the modules_menu table (meaning is is already saved)
$query->clear()
->select('moduleid')
->from('#__modules_menu')
->where($this->db->quoteName('moduleid') . ' = ' . (int) $id);
$this->db->setQuery($query, 0, 1);
$exists = $this->db->loadResult();
if ($exists)
{
return;
}
// Get highest ordering number in position
$query->clear()
->select('ordering')
->from('#__modules')
->where($this->db->quoteName('position') . ' = ' . $this->db->quote($this->module_position))
->where($this->db->quoteName('client_id') . ' = ' . (int) $this->client_id)
->order('ordering DESC');
$this->db->setQuery($query, 0, 1);
$ordering = $this->db->loadResult();
$ordering++;
// publish module and set ordering number
$query->clear()
->update('#__modules')
->set($this->db->quoteName('published') . ' = 1')
->set($this->db->quoteName('ordering') . ' = ' . (int) $ordering)
->set($this->db->quoteName('position') . ' = ' . $this->db->quote($this->module_position))
->where($this->db->quoteName('id') . ' = ' . (int) $id);
$this->db->setQuery($query);
$this->db->execute();
// add module to the modules_menu table
$query->clear()
->insert('#__modules_menu')
->columns(array($this->db->quoteName('moduleid'), $this->db->quoteName('menuid')))
->values((int) $id . ', 0');
$this->db->setQuery($query);
$this->db->execute();
}
public function addInstalledMessage()
{
Factory::getApplication()->enqueueMessage(
Text::sprintf(
Text::_($this->install_type == 'update' ? 'NRI_THE_EXTENSION_HAS_BEEN_UPDATED_SUCCESSFULLY' : 'NRI_THE_EXTENSION_HAS_BEEN_INSTALLED_SUCCESSFULLY'),
'<strong>' . Text::_($this->name) . '</strong>',
'<strong>' . $this->getVersion() . '</strong>',
$this->getFullType()
)
);
}
public function getPrefix()
{
switch ($this->extension_type)
{
case 'plugin';
return Text::_('plg_' . strtolower($this->plugin_folder));
case 'component':
return Text::_('com');
case 'module':
return Text::_('mod');
case 'library':
return Text::_('lib');
default:
return $this->extension_type;
}
}
public function getElementName($type = null, $extname = null)
{
$type = is_null($type) ? $this->extension_type : $type;
$extname = is_null($extname) ? $this->extname : $extname;
switch ($type)
{
case 'component' :
return 'com_' . $extname;
case 'module' :
return 'mod_' . $extname;
case 'plugin' :
default:
return $extname;
}
}
public function getFullType()
{
return Text::_('NRI_' . strtoupper($this->getPrefix()));
}
public function isPro()
{
$versionFile = __DIR__ . "/version.php";
// If version file does not exist we assume a PRO version
if (!is_file($versionFile))
{
return true;
}
// Load version file
require_once $versionFile;
return (bool) $NR_PRO;
}
public function getVersion($file = '')
{
$file = $file ?: $this->getCurrentXMLFile();
if (!is_file($file))
{
return '';
}
$xml = Installer::parseXMLInstallFile($file);
if (!$xml || !isset($xml['version']))
{
return '';
}
return $xml['version'];
}
/**
* Checks wether the extension can be installed or not
*
* @return boolean
*/
public function canInstall()
{
// The extension is not installed yet. Accept Install.
if (!$installed_version = $this->getVersion($this->getInstalledXMLFile()))
{
return true;
}
// Path to extension's version file
$versionFile = $this->getMainFolder() . "/version.php";
$NR_PRO = true;
// If version file does not exist we assume we have a PRO version installed
if (file_exists($versionFile))
{
require_once($versionFile);
}
// The free version is installed. Accept install.
if (!(bool)$NR_PRO)
{
return true;
}
// Current package is a PRO version. Accept install.
if ($this->isPro())
{
return true;
}
// User is trying to update from PRO version to FREE. Do not accept install.
Factory::getLanguage()->load($this->getPrefix() . '_' . $this->extname, __DIR__);
Factory::getApplication()->enqueueMessage(
Text::_('NRI_ERROR_PRO_TO_FREE'), 'error'
);
Factory::getApplication()->enqueueMessage(
html_entity_decode(
Text::sprintf(
'NRI_ERROR_UNINSTALL_FIRST',
'<a href="http://www.tassos.gr/joomla-extensions/' . $this->getUrlAlias() . '" target="_blank">',
'</a>',
Text::_($this->name)
)
), 'error'
);
return false;
}
/**
* Returns the URL alias of the extension.
*
* @return string
*/
private function getUrlAlias()
{
$alias = $this->alias;
switch ($alias)
{
case 'smilepack':
$alias = 'smile-pack';
break;
case 'convertforms':
$alias = 'convert-forms';
break;
case 'rstbox':
$alias = 'engagebox';
break;
case 'gsd':
$alias = 'google-structured-data';
break;
}
// ACF
if ($this->plugin_folder === 'fields' && ($alias === 'acf' || $this->startsWith($alias, 'acf')))
{
$alias = 'advanced-custom-fields';
}
return $alias;
}
/**
* Checks whether string starts with substring.
*
* @param string $string
* @param string $query
*
* @return bool
*/
public static function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
}
/**
* Checks if current version is newer than the installed one
* Used for Novarain Framework
*
* @return boolean [description]
*/
public function isNewer()
{
if (!$installed_version = $this->getVersion($this->getInstalledXMLFile()))
{
return true;
}
$package_version = $this->getVersion();
return version_compare($installed_version, $package_version, '<=');
}
/**
* Helper method triggered before installation
*
* @return bool
*/
public function onBeforeInstall()
{
if (!$this->canInstall())
{
return false;
}
}
/**
* Helper method triggered after installation
*/
public function onAfterInstall()
{
}
/**
* Delete files
*
* @param array $folders
*/
public function deleteFiles($files = array())
{
foreach ($files as $key => $file)
{
if (!is_file($file))
{
continue;
}
File::delete($file);
}
}
/**
* Deletes folders
*
* @param array $folders
*/
public function deleteFolders($folders = array())
{
foreach ($folders as $folder)
{
if (!is_dir($folder))
{
continue;
}
Folder::delete($folder);
}
}
public function dropIndex($table, $index)
{
$db = $this->db;
// Check if index exists first
$query = 'SHOW INDEX FROM ' . $db->quoteName('#__' . $table) . ' WHERE KEY_NAME = ' . $db->quote($index);
$db->setQuery($query);
$db->execute();
if (!$db->loadResult())
{
return;
}
// Remove index
$query = 'ALTER TABLE ' . $db->quoteName('#__' . $table) . ' DROP INDEX ' . $db->quoteName($index);
$db->setQuery($query);
$db->execute();
}
public function dropUnwantedTables($tables) {
if (!$tables) {
return;
}
foreach ($tables as $table) {
$query = "DROP TABLE IF EXISTS #__".$this->db->escape($table);
$this->db->setQuery($query);
$this->db->execute();
}
}
public function dropUnwantedColumns($table, $columns) {
if (!$columns || !$table) {
return;
}
$db = $this->db;
// Check if columns exists in database
function qt($n) {
return(Factory::getDBO()->quote($n));
}
$query = 'SHOW COLUMNS FROM #__'.$table.' WHERE Field IN ('.implode(",", array_map("qt", $columns)).')';
$db->setQuery($query);
$rows = $db->loadColumn(0);
// Abort if we don't have any rows
if (!$rows) {
return;
}
// Let's remove the columns
$q = "";
foreach ($rows as $key => $column) {
$comma = (($key+1) < count($rows)) ? "," : "";
$q .= "drop ".$this->db->escape($column).$comma;
}
$query = "alter table #__".$table." $q";
$db->setQuery($query);
$db->execute();
}
public function fetch($table, $columns = "*", $where = null, $singlerow = false) {
if (!$table) {
return;
}
$db = $this->db;
$query = $db->getQuery(true);
$query
->select($columns)
->from("#__$table");
if (isset($where)) {
$query->where("$where");
}
$db->setQuery($query);
return ($singlerow) ? $db->loadObject() : $db->loadObjectList();
}
/**
* Load the Novarain Framework
*
* @return boolean
*/
public function loadFramework()
{
if (is_file(JPATH_PLUGINS . '/system/nrframework/autoload.php'))
{
include_once JPATH_PLUGINS . '/system/nrframework/autoload.php';
}
}
/**
* Re-orders plugin after passed array of plugins
*
* @param string $plugin Plugin element name
* @param array $lowerPluginOrder Array of plugin element names
*
* @return boolean
*/
public function pluginOrderAfter($lowerPluginOrder)
{
if (!is_array($lowerPluginOrder) || !count($lowerPluginOrder))
{
return;
}
$db = $this->db;
// Get plugins max order
$query = $db->getQuery(true);
$query
->select($db->quoteName('b.ordering'))
->from($db->quoteName('#__extensions', 'b'))
->where($db->quoteName('b.element') . ' IN ("'.implode("\",\"",$lowerPluginOrder).'")')
->order('b.ordering desc');
$db->setQuery($query);
$maxOrder = $db->loadResult();
if (is_null($maxOrder))
{
return;
}
// Get plugin details
$query
->clear()
->select(array($db->quoteName('extension_id'), $db->quoteName('ordering')))
->from($db->quoteName('#__extensions'))
->where($db->quoteName('element') . ' = ' . $db->quote($this->alias));
$db->setQuery($query);
$pluginInfo = $db->loadObject();
if (!isset($pluginInfo->ordering) || $pluginInfo->ordering > $maxOrder)
{
return;
}
// Update the new plugin order
$object = new stdClass();
$object->extension_id = $pluginInfo->extension_id;
$object->ordering = ($maxOrder + 1);
try {
$db->updateObject('#__extensions', $object, 'extension_id');
} catch (Exception $e) {
return $e->getMessage();
}
}
}

View File

@ -0,0 +1,19 @@
<?php
/**
* @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;
require_once __DIR__ . '/script.install.helper.php';
class PlgSystemTGeoIPInstallerScript extends PlgSystemTGeoIPInstallerScriptHelper
{
public $name = 'TGEOIP';
public $alias = 'tgeoip';
public $extension_type = 'plugin';
public $show_message = false;
}

View File

@ -0,0 +1,121 @@
<?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( 'Restricted access' );
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Session\Session;
use Joomla\CMS\Language\Text;
class plgSystemTGeoIP extends CMSPlugin
{
/**
* Joomla Application Object
*
* @var object
*/
protected $app;
/**
* Auto load plugin language
*
* @var boolean
*/
protected $autoloadLanguage = true;
/**
* GeoIP Class
*
* @var object
*/
private $geoIP;
/**
* Load GeoIP Classes
*
* @return void
*/
private function loadGeoIP()
{
$path = JPATH_PLUGINS . '/system/tgeoip';
if (!class_exists('TGeoIP'))
{
if (@file_exists($path . '/helper/tgeoip.php'))
{
if (@include_once($path . '/vendor/autoload.php'))
{
@include_once $path . '/helper/tgeoip.php';
}
}
}
$this->geoIP = new TGeoIP();
}
/**
* Listens to AJAX requests on ?option=com_ajax&format=raw&plugin=tgeoip
*
* @return void
*/
public function onAjaxTgeoip()
{
Session::checkToken('request') or die('Invalid Token');
// Only in admin
if (!$this->app->isClient('administrator'))
{
return;
}
$this->loadGeoIP();
$task = $this->app->input->get('task', 'update');
$this->geoIP->setKey($this->app->input->get('license_key', ''));
switch ($task)
{
// Update database and redirect
case 'update-red':
$result = $this->geoIP->updateDatabase();
if ($result === true)
{
$msg = Text::_('PLG_SYSTEM_TGEOIP_DATABASE_UPDATED');
$msgType = 'message';
} else
{
$msgType = 'error';
$msg = $result;
}
$return = base64_decode($this->app->input->get->getBase64('return', null));
$this->app->enqueueMessage($msg, $msgType);
$this->app->redirect($return);
break;
// Update database
case 'update':
echo $this->geoIP->updateDatabase();
break;
// IP Lookup
case 'get':
$ip = $this->app->input->get('ip');
echo json_encode($this->geoIP->setIP($ip)->getRecord());
break;
}
}
}

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<extension version="3.4" type="plugin" group="system" method="upgrade">
<name>plg_system_tgeoip</name>
<description>PLG_SYSTEM_TGEOIP_DESC</description>
<version>2.2.5</version>
<creationDate>06 Mar 2017</creationDate>
<author>Tassos Marinos</author>
<copyright>Copyright © 2024 Tassos Marinos All Rights Reserved</copyright>
<license>http://www.gnu.org/licenses/gpl-3.0.html GNU/GPL</license>
<authorEmail>info@tassos.gr</authorEmail>
<authorUrl>http://www.tassos.gr</authorUrl>
<scriptfile>script.install.php</scriptfile>
<files>
<filename plugin="tgeoip">tgeoip.php</filename>
<filename>script.install.helper.php</filename>
<folder>db</folder>
<folder>field</folder>
<folder>helper</folder>
<folder>language</folder>
<folder>vendor</folder>
</files>
<config>
<fields name="params" addfieldpath="/plugins/system/tgeoip/field">
<fieldset name="basic" addfieldpath="/plugins/system/nrframework/fields">
<field name="blockStart" type="nr_well"
label="PLG_SYSTEM_TGEOIP_DATABASE"
description="PLG_SYSTEM_TGEOIP_UPDATE_DATABASE_DESC"
/>
<field name="license_key" type="nrtext"
label="PLG_SYSTEM_TGEOIP_LICENSE_KEY"
description="PLG_SYSTEM_TGEOIP_LICENSE_KEY_DESC"
class="input-xlarge"
urltext="PLG_SYSTEM_TGEOIP_LICENSE_KEY_GET"
url="https://www.tassos.gr/kb/general/how-to-enable-geolocation-features-in-tassos-gr-extensions#create_maxmind_license_key"
required="true"
/>
<field name="updatebutton" type="tg_updatebutton"
label="PLG_SYSTEM_TGEOIP_UPDATE_DATABASE"
description="PLG_SYSTEM_TGEOIP_UPDATE_DATABASE_DESC"
/>
<field name="lastupdated" type="tg_lastupdated"
label="PLG_SYSTEM_TGEOIP_LAST_UPDATED"
description="PLG_SYSTEM_TGEOIP_LAST_UPDATED_DESC"
/>
<field name="blockEnd" type="nr_well"
end="1"
/>
<field name="blockStart1" type="nr_well"
label="PLG_SYSTEM_TGEOIP_CHECK_IP"
description="PLG_SYSTEM_TGEOIP_CHECK_IP_DESC"
/>
<field name="testdrive" type="tg_lookup"
label="PLG_SYSTEM_TGEOIP_CHECK_IP"
description="PLG_SYSTEM_TGEOIP_CHECK_IP_DESC"
/>
<field name="blockEnd1" type="nr_well"
end="1"
/>
</fieldset>
</fields>
</config>
</extension>

View File

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d::getLoader();

View File

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -0,0 +1,313 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Tassos\Vendor\Composer;
use Tassos\Vendor\Composer\Autoload\ClassLoader;
use Tassos\Vendor\Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = \array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return \array_keys(\array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = \true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === \false;
}
}
return \false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (\array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = \array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (\array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = \array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (\array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = \array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return \implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@\trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', \E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (\substr(__DIR__, -8, 1) !== 'C') {
self::$installed = (include __DIR__ . '/installed.php');
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = \method_exists('Tassos\\Vendor\\Composer\\Autoload\\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (\is_file($vendorDir . '/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = (require $vendorDir . '/composer/installed.php');
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && \strtr($vendorDir . '/composer', '\\', '/') === \strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[\count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (\substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = (require __DIR__ . '/installed.php');
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

View File

@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

View File

@ -0,0 +1,15 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Tassos\\Vendor\\splitbrain\\PHPArchive\\' => array($vendorDir . '/splitbrain/php-archive/src'),
'Tassos\\Vendor\\MaxMind\\WebService\\' => array($vendorDir . '/maxmind/web-service-common/src/WebService'),
'Tassos\\Vendor\\MaxMind\\Exception\\' => array($vendorDir . '/maxmind/web-service-common/src/Exception'),
'Tassos\\Vendor\\MaxMind\\Db\\' => array($vendorDir . '/maxmind-db/reader/src/MaxMind/Db'),
'Tassos\\Vendor\\GeoIp2\\' => array($vendorDir . '/geoip2/geoip2/src'),
'Tassos\\Vendor\\Composer\\CaBundle\\' => array($vendorDir . '/composer/ca-bundle/src'),
);

View File

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit087ac1c88c9dd7b872309175c7c60d7d::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

View File

@ -0,0 +1,61 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit087ac1c88c9dd7b872309175c7c60d7d
{
public static $prefixLengthsPsr4 = array (
'T' =>
array (
'Tassos\\Vendor\\splitbrain\\PHPArchive\\' => 36,
'Tassos\\Vendor\\MaxMind\\WebService\\' => 33,
'Tassos\\Vendor\\MaxMind\\Exception\\' => 32,
'Tassos\\Vendor\\MaxMind\\Db\\' => 25,
'Tassos\\Vendor\\GeoIp2\\' => 21,
'Tassos\\Vendor\\Composer\\CaBundle\\' => 32,
),
);
public static $prefixDirsPsr4 = array (
'Tassos\\Vendor\\splitbrain\\PHPArchive\\' =>
array (
0 => __DIR__ . '/..' . '/splitbrain/php-archive/src',
),
'Tassos\\Vendor\\MaxMind\\WebService\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/WebService',
),
'Tassos\\Vendor\\MaxMind\\Exception\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind/web-service-common/src/Exception',
),
'Tassos\\Vendor\\MaxMind\\Db\\' =>
array (
0 => __DIR__ . '/..' . '/maxmind-db/reader/src/MaxMind/Db',
),
'Tassos\\Vendor\\GeoIp2\\' =>
array (
0 => __DIR__ . '/..' . '/geoip2/geoip2/src',
),
'Tassos\\Vendor\\Composer\\CaBundle\\' =>
array (
0 => __DIR__ . '/..' . '/composer/ca-bundle/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit087ac1c88c9dd7b872309175c7c60d7d::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit087ac1c88c9dd7b872309175c7c60d7d::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit087ac1c88c9dd7b872309175c7c60d7d::$classMap;
}, null, ClassLoader::class);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,361 @@
<?php
/*
* This file is part of composer/ca-bundle.
*
* (c) Composer <https://github.com/composer>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Tassos\Vendor\Composer\CaBundle;
use Tassos\Vendor\Psr\Log\LoggerInterface;
use Tassos\Vendor\Symfony\Component\Process\PhpProcess;
/**
* @author Chris Smith <chris@cs278.org>
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class CaBundle
{
/** @var string|null */
private static $caPath;
/** @var array<string, bool> */
private static $caFileValidity = array();
/** @var bool|null */
private static $useOpensslParse;
/**
* Returns the system CA bundle path, or a path to the bundled one
*
* This method was adapted from Sslurp.
* https://github.com/EvanDotPro/Sslurp
*
* (c) Evan Coury <me@evancoury.com>
*
* For the full copyright and license information, please see below:
*
* Copyright (c) 2013, Evan Coury
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @param LoggerInterface $logger optional logger for information about which CA files were loaded
* @return string path to a CA bundle file or directory
*/
public static function getSystemCaRootBundlePath(LoggerInterface $logger = null)
{
if (self::$caPath !== null) {
return self::$caPath;
}
$caBundlePaths = array();
// If SSL_CERT_FILE env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$caBundlePaths[] = self::getEnvVariable('SSL_CERT_FILE');
// If SSL_CERT_DIR env variable points to a valid certificate/bundle, use that.
// This mimics how OpenSSL uses the SSL_CERT_FILE env variable.
$caBundlePaths[] = self::getEnvVariable('SSL_CERT_DIR');
$caBundlePaths[] = \ini_get('openssl.cafile');
$caBundlePaths[] = \ini_get('openssl.capath');
$otherLocations = array(
'/etc/pki/tls/certs/ca-bundle.crt',
// Fedora, RHEL, CentOS (ca-certificates package)
'/etc/ssl/certs/ca-certificates.crt',
// Debian, Ubuntu, Gentoo, Arch Linux (ca-certificates package)
'/etc/ssl/ca-bundle.pem',
// SUSE, openSUSE (ca-certificates package)
'/usr/local/share/certs/ca-root-nss.crt',
// FreeBSD (ca_root_nss_package)
'/usr/ssl/certs/ca-bundle.crt',
// Cygwin
'/opt/local/share/curl/curl-ca-bundle.crt',
// OS X macports, curl-ca-bundle package
'/usr/local/share/curl/curl-ca-bundle.crt',
// Default cURL CA bunde path (without --with-ca-bundle option)
'/usr/share/ssl/certs/ca-bundle.crt',
// Really old RedHat?
'/etc/ssl/cert.pem',
// OpenBSD
'/usr/local/etc/ssl/cert.pem',
// FreeBSD 10.x
'/usr/local/etc/openssl/cert.pem',
// OS X homebrew, openssl package
'/usr/local/etc/openssl@1.1/cert.pem',
);
foreach ($otherLocations as $location) {
$otherLocations[] = \dirname($location);
}
$caBundlePaths = \array_merge($caBundlePaths, $otherLocations);
foreach ($caBundlePaths as $caBundle) {
if ($caBundle && self::caFileUsable($caBundle, $logger)) {
return self::$caPath = $caBundle;
}
if ($caBundle && self::caDirUsable($caBundle, $logger)) {
return self::$caPath = $caBundle;
}
}
return self::$caPath = static::getBundledCaBundlePath();
// Bundled CA file, last resort
}
/**
* Returns the path to the bundled CA file
*
* In case you don't want to trust the user or the system, you can use this directly
*
* @return string path to a CA bundle file
*/
public static function getBundledCaBundlePath()
{
$caBundleFile = __DIR__ . '/../res/cacert.pem';
// cURL does not understand 'phar://' paths
// see https://github.com/composer/ca-bundle/issues/10
if (0 === \strpos($caBundleFile, 'phar://')) {
$tempCaBundleFile = \tempnam(\sys_get_temp_dir(), 'openssl-ca-bundle-');
if (\false === $tempCaBundleFile) {
throw new \RuntimeException('Could not create a temporary file to store the bundled CA file');
}
\file_put_contents($tempCaBundleFile, \file_get_contents($caBundleFile));
\register_shutdown_function(function () use($tempCaBundleFile) {
@\unlink($tempCaBundleFile);
});
$caBundleFile = $tempCaBundleFile;
}
return $caBundleFile;
}
/**
* Validates a CA file using opensl_x509_parse only if it is safe to use
*
* @param string $filename
* @param LoggerInterface $logger optional logger for information about which CA files were loaded
*
* @return bool
*/
public static function validateCaFile($filename, LoggerInterface $logger = null)
{
static $warned = \false;
if (isset(self::$caFileValidity[$filename])) {
return self::$caFileValidity[$filename];
}
$contents = \file_get_contents($filename);
// assume the CA is valid if php is vulnerable to
// https://www.sektioneins.de/advisories/advisory-012013-php-openssl_x509_parse-memory-corruption-vulnerability.html
if (!static::isOpensslParseSafe()) {
if (!$warned && $logger) {
$logger->warning(\sprintf('Your version of PHP, %s, is affected by CVE-2013-6420 and cannot safely perform certificate validation, we strongly suggest you upgrade.', \PHP_VERSION));
$warned = \true;
}
$isValid = !empty($contents);
} elseif (\is_string($contents) && \strlen($contents) > 0) {
$contents = \preg_replace("/^(\\-+(?:BEGIN|END))\\s+TRUSTED\\s+(CERTIFICATE\\-+)\$/m", '$1 $2', $contents);
if (null === $contents) {
// regex extraction failed
$isValid = \false;
} else {
$isValid = (bool) \openssl_x509_parse($contents);
}
} else {
$isValid = \false;
}
if ($logger) {
$logger->debug('Checked CA file ' . \realpath($filename) . ': ' . ($isValid ? 'valid' : 'invalid'));
}
return self::$caFileValidity[$filename] = $isValid;
}
/**
* Test if it is safe to use the PHP function openssl_x509_parse().
*
* This checks if OpenSSL extensions is vulnerable to remote code execution
* via the exploit documented as CVE-2013-6420.
*
* @return bool
*/
public static function isOpensslParseSafe()
{
if (null !== self::$useOpensslParse) {
return self::$useOpensslParse;
}
if (\PHP_VERSION_ID >= 50600) {
return self::$useOpensslParse = \true;
}
// Vulnerable:
// PHP 5.3.0 - PHP 5.3.27
// PHP 5.4.0 - PHP 5.4.22
// PHP 5.5.0 - PHP 5.5.6
if (\PHP_VERSION_ID < 50400 && \PHP_VERSION_ID >= 50328 || \PHP_VERSION_ID < 50500 && \PHP_VERSION_ID >= 50423 || \PHP_VERSION_ID >= 50507) {
// This version of PHP has the fix for CVE-2013-6420 applied.
return self::$useOpensslParse = \true;
}
if (\defined('PHP_WINDOWS_VERSION_BUILD')) {
// Windows is probably insecure in this case.
return self::$useOpensslParse = \false;
}
$compareDistroVersionPrefix = function ($prefix, $fixedVersion) {
$regex = '{^' . \preg_quote($prefix) . '([0-9]+)$}';
if (\preg_match($regex, \PHP_VERSION, $m)) {
return (int) $m[1] >= $fixedVersion;
}
return \false;
};
// Hard coded list of PHP distributions with the fix backported.
if ($compareDistroVersionPrefix('5.3.3-7+squeeze', 18) || $compareDistroVersionPrefix('5.4.4-14+deb7u', 7) || $compareDistroVersionPrefix('5.3.10-1ubuntu3.', 9)) {
return self::$useOpensslParse = \true;
}
// Symfony Process component is missing so we assume it is unsafe at this point
if (!\class_exists('Tassos\\Vendor\\Symfony\\Component\\Process\\PhpProcess')) {
return self::$useOpensslParse = \false;
}
// This is where things get crazy, because distros backport security
// fixes the chances are on NIX systems the fix has been applied but
// it's not possible to verify that from the PHP version.
//
// To verify exec a new PHP process and run the issue testcase with
// known safe input that replicates the bug.
// Based on testcase in https://github.com/php/php-src/commit/c1224573c773b6845e83505f717fbf820fc18415
// changes in https://github.com/php/php-src/commit/76a7fd893b7d6101300cc656058704a73254d593
$cert = 'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUVwRENDQTR5Z0F3SUJBZ0lKQUp6dThyNnU2ZUJjTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUhETVFzd0NRWUQKVlFRR0V3SkVSVEVjTUJvR0ExVUVDQXdUVG05eVpISm9aV2x1TFZkbGMzUm1ZV3hsYmpFUU1BNEdBMVVFQnd3SApTOE9Ed3Jac2JqRVVNQklHQTFVRUNnd0xVMlZyZEdsdmJrVnBibk14SHpBZEJnTlZCQXNNRmsxaGJHbGphVzkxCmN5QkRaWEowSUZObFkzUnBiMjR4SVRBZkJnTlZCQU1NR0cxaGJHbGphVzkxY3k1elpXdDBhVzl1WldsdWN5NWsKWlRFcU1DZ0dDU3FHU0liM0RRRUpBUlliYzNSbFptRnVMbVZ6YzJWeVFITmxhM1JwYjI1bGFXNXpMbVJsTUhVWQpaREU1TnpBd01UQXhNREF3TURBd1dnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBCkFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEKQUFBQUFBQVhEVEUwTVRFeU9ERXhNemt6TlZvd2djTXhDekFKQmdOVkJBWVRBa1JGTVJ3d0dnWURWUVFJREJOTwpiM0prY21obGFXNHRWMlZ6ZEdaaGJHVnVNUkF3RGdZRFZRUUhEQWRMdzRQQ3RteHVNUlF3RWdZRFZRUUtEQXRUClpXdDBhVzl1UldsdWN6RWZNQjBHQTFVRUN3d1dUV0ZzYVdOcGIzVnpJRU5sY25RZ1UyVmpkR2x2YmpFaE1COEcKQTFVRUF3d1liV0ZzYVdOcGIzVnpMbk5sYTNScGIyNWxhVzV6TG1SbE1Tb3dLQVlKS29aSWh2Y05BUWtCRmh0egpkR1ZtWVc0dVpYTnpaWEpBYzJWcmRHbHZibVZwYm5NdVpHVXdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCCkR3QXdnZ0VLQW9JQkFRRERBZjNobDdKWTBYY0ZuaXlFSnBTU0RxbjBPcUJyNlFQNjV1c0pQUnQvOFBhRG9xQnUKd0VZVC9OYSs2ZnNnUGpDMHVLOURaZ1dnMnRIV1dvYW5TYmxBTW96NVBINlorUzRTSFJaN2UyZERJalBqZGhqaAowbUxnMlVNTzV5cDBWNzk3R2dzOWxOdDZKUmZIODFNTjJvYlhXczROdHp0TE11RDZlZ3FwcjhkRGJyMzRhT3M4CnBrZHVpNVVhd1Raa3N5NXBMUEhxNWNNaEZHbTA2djY1Q0xvMFYyUGQ5K0tBb2tQclBjTjVLTEtlYno3bUxwazYKU01lRVhPS1A0aWRFcXh5UTdPN2ZCdUhNZWRzUWh1K3ByWTNzaTNCVXlLZlF0UDVDWm5YMmJwMHdLSHhYMTJEWAoxbmZGSXQ5RGJHdkhUY3lPdU4rblpMUEJtM3ZXeG50eUlJdlZBZ01CQUFHalFqQkFNQWtHQTFVZEV3UUNNQUF3CkVRWUpZSVpJQVliNFFnRUJCQVFEQWdlQU1Bc0dBMVVkRHdRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUVVGQUFPQ0FRRUFHMGZaWVlDVGJkajFYWWMrMVNub2FQUit2SThDOENhRAo4KzBVWWhkbnlVNGdnYTBCQWNEclk5ZTk0ZUVBdTZacXljRjZGakxxWFhkQWJvcHBXb2NyNlQ2R0QxeDMzQ2tsClZBcnpHL0t4UW9oR0QySmVxa2hJTWxEb214SE83a2EzOStPYThpMnZXTFZ5alU4QVp2V01BcnVIYTRFRU55RzcKbFcyQWFnYUZLRkNyOVRuWFRmcmR4R1ZFYnY3S1ZRNmJkaGc1cDVTanBXSDErTXEwM3VSM1pYUEJZZHlWODMxOQpvMGxWajFLRkkyRENML2xpV2lzSlJvb2YrMWNSMzVDdGQwd1lCY3BCNlRac2xNY09QbDc2ZHdLd0pnZUpvMlFnClpzZm1jMnZDMS9xT2xOdU5xLzBUenprVkd2OEVUVDNDZ2FVK1VYZTRYT1Z2a2NjZWJKbjJkZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K';
$script = <<<'EOT'
error_reporting(-1);
$info = openssl_x509_parse(base64_decode('%s'));
var_dump(PHP_VERSION, $info['issuer']['emailAddress'], $info['validFrom_time_t']);
EOT;
$script = '<' . "?php\n" . \sprintf($script, $cert);
try {
$process = new PhpProcess($script);
$process->mustRun();
} catch (\Exception $e) {
// In the case of any exceptions just accept it is not possible to
// determine the safety of openssl_x509_parse and bail out.
return self::$useOpensslParse = \false;
}
$output = \preg_split('{\\r?\\n}', \trim($process->getOutput()));
$errorOutput = \trim($process->getErrorOutput());
if (\is_array($output) && \count($output) === 3 && $output[0] === \sprintf('string(%d) "%s"', \strlen(\PHP_VERSION), \PHP_VERSION) && $output[1] === 'string(27) "stefan.esser@sektioneins.de"' && $output[2] === 'int(-1)' && \preg_match('{openssl_x509_parse\\(\\): illegal (?:ASN1 data type for|length in) timestamp in - on line \\d+}', $errorOutput)) {
// This PHP has the fix backported probably by a distro security team.
return self::$useOpensslParse = \true;
}
return self::$useOpensslParse = \false;
}
/**
* Resets the static caches
* @return void
*/
public static function reset()
{
self::$caFileValidity = array();
self::$caPath = null;
self::$useOpensslParse = null;
}
/**
* @param string $name
* @return string|false
*/
private static function getEnvVariable($name)
{
if (isset($_SERVER[$name])) {
return (string) $_SERVER[$name];
}
if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== \false && $value !== null) {
return (string) $value;
}
return \false;
}
/**
* @param string|false $certFile
* @param LoggerInterface|null $logger
* @return bool
*/
private static function caFileUsable($certFile, LoggerInterface $logger = null)
{
return $certFile && static::isFile($certFile, $logger) && static::isReadable($certFile, $logger) && static::validateCaFile($certFile, $logger);
}
/**
* @param string|false $certDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function caDirUsable($certDir, LoggerInterface $logger = null)
{
return $certDir && static::isDir($certDir, $logger) && static::isReadable($certDir, $logger) && static::glob($certDir . '/*', $logger);
}
/**
* @param string $certFile
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isFile($certFile, LoggerInterface $logger = null)
{
$isFile = @\is_file($certFile);
if (!$isFile && $logger) {
$logger->debug(\sprintf('Checked CA file %s does not exist or it is not a file.', $certFile));
}
return $isFile;
}
/**
* @param string $certDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isDir($certDir, LoggerInterface $logger = null)
{
$isDir = @\is_dir($certDir);
if (!$isDir && $logger) {
$logger->debug(\sprintf('Checked directory %s does not exist or it is not a directory.', $certDir));
}
return $isDir;
}
/**
* @param string $certFileOrDir
* @param LoggerInterface|null $logger
* @return bool
*/
private static function isReadable($certFileOrDir, LoggerInterface $logger = null)
{
$isReadable = @\is_readable($certFileOrDir);
if (!$isReadable && $logger) {
$logger->debug(\sprintf('Checked file or directory %s is not readable.', $certFileOrDir));
}
return $isReadable;
}
/**
* @param string $pattern
* @param LoggerInterface|null $logger
* @return bool
*/
private static function glob($pattern, LoggerInterface $logger = null)
{
$certs = \glob($pattern);
if ($certs === \false) {
if ($logger) {
$logger->debug(\sprintf("An error occurred while trying to find certificates for pattern: %s", $pattern));
}
return \false;
}
if (\count($certs) === 0) {
if ($logger) {
$logger->debug(\sprintf("No CA files found for pattern: %s", $pattern));
}
return \false;
}
return \true;
}
}

View File

@ -0,0 +1,327 @@
{
"packages": [
{
"name": "composer\/ca-bundle",
"version": "1.3.5",
"version_normalized": "1.3.5.0",
"source": {
"type": "git",
"url": "https:\/\/github.com\/composer\/ca-bundle.git",
"reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd"
},
"dist": {
"type": "zip",
"url": "https:\/\/api.github.com\/repos\/composer\/ca-bundle\/zipball\/74780ccf8c19d6acb8d65c5f39cd72110e132bbd",
"reference": "74780ccf8c19d6acb8d65c5f39cd72110e132bbd",
"shasum": ""
},
"require": {
"ext-openssl": "*",
"ext-pcre": "*",
"php": "^5.3.2 || ^7.0 || ^8.0"
},
"require-dev": {
"phpstan\/phpstan": "^0.12.55",
"psr\/log": "^1.0",
"symfony\/phpunit-bridge": "^4.2 || ^5",
"symfony\/process": "^2.5 || ^3.0 || ^4.0 || ^5.0 || ^6.0"
},
"time": "2023-01-11T08:27:00+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Tassos\\Vendor\\Composer\\CaBundle\\": "src"
}
},
"notification-url": "https:\/\/packagist.org\/downloads\/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jordi Boggiano",
"email": "j.boggiano@seld.be",
"homepage": "http:\/\/seld.be"
}
],
"description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.",
"keywords": [
"cabundle",
"cacert",
"certificate",
"ssl",
"tls"
],
"support": {
"irc": "irc:\/\/irc.freenode.org\/composer",
"issues": "https:\/\/github.com\/composer\/ca-bundle\/issues",
"source": "https:\/\/github.com\/composer\/ca-bundle\/tree\/1.3.5"
},
"funding": [
{
"url": "https:\/\/packagist.com",
"type": "custom"
},
{
"url": "https:\/\/github.com\/composer",
"type": "github"
},
{
"url": "https:\/\/tidelift.com\/funding\/github\/packagist\/composer\/composer",
"type": "tidelift"
}
],
"install-path": ".\/ca-bundle"
},
{
"name": "geoip2\/geoip2",
"version": "v2.13.0",
"version_normalized": "2.13.0.0",
"source": {
"type": "git",
"url": "git@github.com:maxmind\/GeoIP2-php.git",
"reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23"
},
"dist": {
"type": "zip",
"url": "https:\/\/api.github.com\/repos\/maxmind\/GeoIP2-php\/zipball\/6a41d8fbd6b90052bc34dff3b4252d0f88067b23",
"reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23",
"shasum": ""
},
"require": {
"ext-json": "*",
"maxmind-db\/reader": "~1.8",
"maxmind\/web-service-common": "~0.8",
"php": ">=7.2"
},
"require-dev": {
"friendsofphp\/php-cs-fixer": "3.*",
"phpstan\/phpstan": "*",
"phpunit\/phpunit": "^8.0 || ^9.0",
"squizlabs\/php_codesniffer": "3.*"
},
"time": "2022-08-05T20:32:58+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Tassos\\Vendor\\GeoIp2\\": "src"
}
},
"notification-url": "https:\/\/packagist.org\/downloads\/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https:\/\/www.maxmind.com\/"
}
],
"description": "MaxMind GeoIP2 PHP API",
"homepage": "https:\/\/github.com\/maxmind\/GeoIP2-php",
"keywords": [
"IP",
"geoip",
"geoip2",
"geolocation",
"maxmind"
],
"install-path": "..\/geoip2\/geoip2"
},
{
"name": "maxmind-db\/reader",
"version": "v1.11.0",
"version_normalized": "1.11.0.0",
"source": {
"type": "git",
"url": "https:\/\/github.com\/maxmind\/MaxMind-DB-Reader-php.git",
"reference": "b1f3c0699525336d09cc5161a2861268d9f2ae5b"
},
"dist": {
"type": "zip",
"url": "https:\/\/api.github.com\/repos\/maxmind\/MaxMind-DB-Reader-php\/zipball\/b1f3c0699525336d09cc5161a2861268d9f2ae5b",
"reference": "b1f3c0699525336d09cc5161a2861268d9f2ae5b",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
"ext-maxminddb": "<1.10.1,>=2.0.0"
},
"require-dev": {
"friendsofphp\/php-cs-fixer": "3.*",
"php-coveralls\/php-coveralls": "^2.1",
"phpstan\/phpstan": "*",
"phpunit\/phpcov": ">=6.0.0",
"phpunit\/phpunit": ">=8.0.0,<10.0.0",
"squizlabs\/php_codesniffer": "3.*"
},
"suggest": {
"ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
"ext-maxminddb": "A C-based database decoder that provides significantly faster lookups"
},
"time": "2021-10-18T15:23:10+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Tassos\\Vendor\\MaxMind\\Db\\": "src\/MaxMind\/Db"
}
},
"notification-url": "https:\/\/packagist.org\/downloads\/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Gregory J. Oschwald",
"email": "goschwald@maxmind.com",
"homepage": "https:\/\/www.maxmind.com\/"
}
],
"description": "MaxMind DB Reader API",
"homepage": "https:\/\/github.com\/maxmind\/MaxMind-DB-Reader-php",
"keywords": [
"database",
"geoip",
"geoip2",
"geolocation",
"maxmind"
],
"support": {
"issues": "https:\/\/github.com\/maxmind\/MaxMind-DB-Reader-php\/issues",
"source": "https:\/\/github.com\/maxmind\/MaxMind-DB-Reader-php\/tree\/v1.11.0"
},
"install-path": "..\/maxmind-db\/reader"
},
{
"name": "maxmind\/web-service-common",
"version": "v0.9.0",
"version_normalized": "0.9.0.0",
"source": {
"type": "git",
"url": "https:\/\/github.com\/maxmind\/web-service-common-php.git",
"reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53"
},
"dist": {
"type": "zip",
"url": "https:\/\/api.github.com\/repos\/maxmind\/web-service-common-php\/zipball\/4dc5a3e8df38aea4ca3b1096cee3a038094e9b53",
"reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53",
"shasum": ""
},
"require": {
"composer\/ca-bundle": "^1.0.3",
"ext-curl": "*",
"ext-json": "*",
"php": ">=7.2"
},
"require-dev": {
"friendsofphp\/php-cs-fixer": "3.*",
"phpstan\/phpstan": "*",
"phpunit\/phpunit": "^8.0 || ^9.0",
"squizlabs\/php_codesniffer": "3.*"
},
"time": "2022-03-28T17:43:20+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Tassos\\Vendor\\MaxMind\\Exception\\": "src\/Exception",
"Tassos\\Vendor\\MaxMind\\WebService\\": "src\/WebService"
}
},
"notification-url": "https:\/\/packagist.org\/downloads\/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Gregory Oschwald",
"email": "goschwald@maxmind.com"
}
],
"description": "Internal MaxMind Web Service API",
"homepage": "https:\/\/github.com\/maxmind\/web-service-common-php",
"support": {
"issues": "https:\/\/github.com\/maxmind\/web-service-common-php\/issues",
"source": "https:\/\/github.com\/maxmind\/web-service-common-php\/tree\/v0.9.0"
},
"install-path": "..\/maxmind\/web-service-common"
},
{
"name": "splitbrain\/php-archive",
"version": "1.3.1",
"version_normalized": "1.3.1.0",
"source": {
"type": "git",
"url": "https:\/\/github.com\/splitbrain\/php-archive.git",
"reference": "d274e5190ba309777926348900cf9578d9e533c9"
},
"dist": {
"type": "zip",
"url": "https:\/\/api.github.com\/repos\/splitbrain\/php-archive\/zipball\/d274e5190ba309777926348900cf9578d9e533c9",
"reference": "d274e5190ba309777926348900cf9578d9e533c9",
"shasum": ""
},
"require": {
"php": ">=7.0"
},
"require-dev": {
"ext-bz2": "*",
"ext-zip": "*",
"mikey179\/vfsstream": "^1.6",
"phpunit\/phpunit": "^8"
},
"suggest": {
"ext-bz2": "For bz2 compression",
"ext-iconv": "Used for proper filename encode handling",
"ext-mbstring": "Can be used alternatively for handling filename encoding",
"ext-zlib": "For zlib compression"
},
"time": "2022-03-23T09:21:55+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Tassos\\Vendor\\splitbrain\\PHPArchive\\": "src"
}
},
"notification-url": "https:\/\/packagist.org\/downloads\/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Gohr",
"email": "andi@splitbrain.org"
}
],
"description": "Pure-PHP implementation to read and write TAR and ZIP archives",
"keywords": [
"archive",
"extract",
"tar",
"unpack",
"unzip",
"zip"
],
"support": {
"issues": "https:\/\/github.com\/splitbrain\/php-archive\/issues",
"source": "https:\/\/github.com\/splitbrain\/php-archive\/tree\/1.3.1"
},
"install-path": "..\/splitbrain\/php-archive"
}
],
"dev": true,
"dev-package-names": []
}

View File

@ -0,0 +1,5 @@
<?php
namespace Tassos\Vendor;
return array('root' => array('name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '015eb26a70321a57e0c514ec4742cbe8a2580208', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \true), 'versions' => array('__root__' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '015eb26a70321a57e0c514ec4742cbe8a2580208', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'composer/ca-bundle' => array('pretty_version' => '1.3.5', 'version' => '1.3.5.0', 'reference' => '74780ccf8c19d6acb8d65c5f39cd72110e132bbd', 'type' => 'library', 'install_path' => __DIR__ . '/./ca-bundle', 'aliases' => array(), 'dev_requirement' => \false), 'geoip2/geoip2' => array('pretty_version' => 'v2.13.0', 'version' => '2.13.0.0', 'reference' => '6a41d8fbd6b90052bc34dff3b4252d0f88067b23', 'type' => 'library', 'install_path' => __DIR__ . '/../geoip2/geoip2', 'aliases' => array(), 'dev_requirement' => \false), 'maxmind-db/reader' => array('pretty_version' => 'v1.11.0', 'version' => '1.11.0.0', 'reference' => 'b1f3c0699525336d09cc5161a2861268d9f2ae5b', 'type' => 'library', 'install_path' => __DIR__ . '/../maxmind-db/reader', 'aliases' => array(), 'dev_requirement' => \false), 'maxmind/web-service-common' => array('pretty_version' => 'v0.9.0', 'version' => '0.9.0.0', 'reference' => '4dc5a3e8df38aea4ca3b1096cee3a038094e9b53', 'type' => 'library', 'install_path' => __DIR__ . '/../maxmind/web-service-common', 'aliases' => array(), 'dev_requirement' => \false), 'splitbrain/php-archive' => array('pretty_version' => '1.3.1', 'version' => '1.3.1.0', 'reference' => 'd274e5190ba309777926348900cf9578d9e533c9', 'type' => 'library', 'install_path' => __DIR__ . '/../splitbrain/php-archive', 'aliases' => array(), 'dev_requirement' => \false)));

View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@ -0,0 +1,23 @@
<?php
namespace Tassos\Vendor;
require __DIR__ . '/../vendor/autoload.php';
use Tassos\Vendor\GeoIp2\Database\Reader;
\srand(0);
$reader = new Reader('GeoIP2-City.mmdb');
$count = 500000;
$startTime = \microtime(\true);
for ($i = 0; $i < $count; ++$i) {
$ip = \long2ip(\rand(0, 2 ** 32 - 1));
try {
$t = $reader->city($ip);
} catch (\Tassos\Vendor\GeoIp2\Exception\AddressNotFoundException $e) {
}
if ($i % 10000 === 0) {
echo $i . ' ' . $ip . "\n";
}
}
$endTime = \microtime(\true);
$duration = $endTime - $startTime;
echo 'Requests per second: ' . $count / $duration . "\n";

View File

@ -0,0 +1,246 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Database;
use Tassos\Vendor\GeoIp2\Exception\AddressNotFoundException;
use Tassos\Vendor\GeoIp2\Model\AbstractModel;
use Tassos\Vendor\GeoIp2\Model\AnonymousIp;
use Tassos\Vendor\GeoIp2\Model\Asn;
use Tassos\Vendor\GeoIp2\Model\City;
use Tassos\Vendor\GeoIp2\Model\ConnectionType;
use Tassos\Vendor\GeoIp2\Model\Country;
use Tassos\Vendor\GeoIp2\Model\Domain;
use Tassos\Vendor\GeoIp2\Model\Enterprise;
use Tassos\Vendor\GeoIp2\Model\Isp;
use Tassos\Vendor\GeoIp2\ProviderInterface;
use Tassos\Vendor\MaxMind\Db\Reader as DbReader;
use Tassos\Vendor\MaxMind\Db\Reader\InvalidDatabaseException;
/**
* Instances of this class provide a reader for the GeoIP2 database format.
* IP addresses can be looked up using the database specific methods.
*
* ## Usage ##
*
* The basic API for this class is the same for every database. First, you
* create a reader object, specifying a file name. You then call the method
* corresponding to the specific database, passing it the IP address you want
* to look up.
*
* If the request succeeds, the method call will return a model class for
* the method you called. This model in turn contains multiple record classes,
* each of which represents part of the data returned by the database. If
* the database does not contain the requested information, the attributes
* on the record class will have a `null` value.
*
* If the address is not in the database, an
* {@link \GeoIp2\Exception\AddressNotFoundException} exception will be
* thrown. If an invalid IP address is passed to one of the methods, a
* SPL {@link \InvalidArgumentException} will be thrown. If the database is
* corrupt or invalid, a {@link \MaxMind\Db\Reader\InvalidDatabaseException}
* will be thrown.
*/
class Reader implements ProviderInterface
{
/**
* @var DbReader
*/
private $dbReader;
/**
* @var string
*/
private $dbType;
/**
* @var array<string>
*/
private $locales;
/**
* Constructor.
*
* @param string $filename the path to the GeoIP2 database file
* @param array $locales list of locale codes to use in name property
* from most preferred to least preferred
*
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function __construct(string $filename, array $locales = ['en'])
{
$this->dbReader = new DbReader($filename);
$this->dbType = $this->dbReader->metadata()->databaseType;
$this->locales = $locales;
}
/**
* This method returns a GeoIP2 City model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function city(string $ipAddress) : City
{
// @phpstan-ignore-next-line
return $this->modelFor(City::class, 'City', $ipAddress);
}
/**
* This method returns a GeoIP2 Country model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function country(string $ipAddress) : Country
{
// @phpstan-ignore-next-line
return $this->modelFor(Country::class, 'Country', $ipAddress);
}
/**
* This method returns a GeoIP2 Anonymous IP model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function anonymousIp(string $ipAddress) : AnonymousIp
{
// @phpstan-ignore-next-line
return $this->flatModelFor(AnonymousIp::class, 'GeoIP2-Anonymous-IP', $ipAddress);
}
/**
* This method returns a GeoLite2 ASN model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function asn(string $ipAddress) : Asn
{
// @phpstan-ignore-next-line
return $this->flatModelFor(Asn::class, 'GeoLite2-ASN', $ipAddress);
}
/**
* This method returns a GeoIP2 Connection Type model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function connectionType(string $ipAddress) : ConnectionType
{
// @phpstan-ignore-next-line
return $this->flatModelFor(ConnectionType::class, 'GeoIP2-Connection-Type', $ipAddress);
}
/**
* This method returns a GeoIP2 Domain model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function domain(string $ipAddress) : Domain
{
// @phpstan-ignore-next-line
return $this->flatModelFor(Domain::class, 'GeoIP2-Domain', $ipAddress);
}
/**
* This method returns a GeoIP2 Enterprise model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function enterprise(string $ipAddress) : Enterprise
{
// @phpstan-ignore-next-line
return $this->modelFor(Enterprise::class, 'Enterprise', $ipAddress);
}
/**
* This method returns a GeoIP2 ISP model.
*
* @param string $ipAddress an IPv4 or IPv6 address as a string
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address is
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*/
public function isp(string $ipAddress) : Isp
{
// @phpstan-ignore-next-line
return $this->flatModelFor(Isp::class, 'GeoIP2-ISP', $ipAddress);
}
private function modelFor(string $class, string $type, string $ipAddress) : AbstractModel
{
[$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
$record['traits']['ip_address'] = $ipAddress;
$record['traits']['prefix_len'] = $prefixLen;
return new $class($record, $this->locales);
}
private function flatModelFor(string $class, string $type, string $ipAddress) : AbstractModel
{
[$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
$record['ip_address'] = $ipAddress;
$record['prefix_len'] = $prefixLen;
return new $class($record);
}
private function getRecord(string $class, string $type, string $ipAddress) : array
{
if (\strpos($this->dbType, $type) === \false) {
$method = \lcfirst((new \ReflectionClass($class))->getShortName());
throw new \BadMethodCallException("The {$method} method cannot be used to open a {$this->dbType} database");
}
[$record, $prefixLen] = $this->dbReader->getWithPrefixLen($ipAddress);
if ($record === null) {
throw new AddressNotFoundException("The address {$ipAddress} is not in the database.");
}
if (!\is_array($record)) {
// This can happen on corrupt databases. Generally,
// MaxMind\Db\Reader will throw a
// MaxMind\Db\Reader\InvalidDatabaseException, but occasionally
// the lookup may result in a record that looks valid but is not
// an array. This mostly happens when the user is ignoring all
// exceptions and the more frequent InvalidDatabaseException
// exceptions go unnoticed.
throw new InvalidDatabaseException("Expected an array when looking up {$ipAddress} but received: " . \gettype($record));
}
return [$record, $prefixLen];
}
/**
* @throws \InvalidArgumentException if arguments are passed to the method
* @throws \BadMethodCallException if the database has been closed
*
* @return \MaxMind\Db\Reader\Metadata object for the database
*/
public function metadata() : DbReader\Metadata
{
return $this->dbReader->metadata();
}
/**
* Closes the GeoIP2 database and returns the resources to the system.
*/
public function close() : void
{
$this->dbReader->close();
}
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class AddressNotFoundException extends GeoIp2Exception
{
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class AuthenticationException extends GeoIp2Exception
{
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class GeoIp2Exception extends \Exception
{
}

View File

@ -0,0 +1,22 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents an HTTP transport error.
*/
class HttpException extends GeoIp2Exception
{
/**
* The URI queried.
*
* @var string
*/
public $uri;
public function __construct(string $message, int $httpStatus, string $uri, \Exception $previous = null)
{
$this->uri = $uri;
parent::__construct($message, $httpStatus, $previous);
}
}

View File

@ -0,0 +1,23 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents an error returned by MaxMind's GeoIP2
* web service.
*/
class InvalidRequestException extends HttpException
{
/**
* The code returned by the MaxMind web service.
*
* @var string
*/
public $error;
public function __construct(string $message, string $error, int $httpStatus, string $uri, \Exception $previous = null)
{
$this->error = $error;
parent::__construct($message, $httpStatus, $uri, $previous);
}
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Exception;
/**
* This class represents a generic error.
*/
class OutOfQueriesException extends GeoIp2Exception
{
}

View File

@ -0,0 +1,60 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
/**
* @ignore
*/
abstract class AbstractModel implements \JsonSerializable
{
/**
* @var array<string, mixed>
*/
protected $raw;
/**
* @ignore
*/
public function __construct(array $raw)
{
$this->raw = $raw;
}
/**
* @ignore
*
* @return mixed
*/
protected function get(string $field)
{
if (isset($this->raw[$field])) {
return $this->raw[$field];
}
if (\preg_match('/^is_/', $field)) {
return \false;
}
return null;
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr !== 'instance' && \property_exists($this, $attr)) {
return $this->{$attr};
}
throw new \RuntimeException("Unknown attribute: {$attr}");
}
/**
* @ignore
*/
public function __isset(string $attr) : bool
{
return $attr !== 'instance' && isset($this->{$attr});
}
public function jsonSerialize() : array
{
return $this->raw;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
use Tassos\Vendor\GeoIp2\Util;
/**
* This class provides the GeoIP2 Anonymous IP model.
*
* @property-read bool $isAnonymous This is true if the IP address belongs to
* any sort of anonymous network.
* @property-read bool $isAnonymousVpn This is true if the IP address is
* registered to an anonymous VPN provider. If a VPN provider does not
* register subnets under names associated with them, we will likely only
* flag their IP ranges using the isHostingProvider property.
* @property-read bool $isHostingProvider This is true if the IP address belongs
* to a hosting or VPN provider (see description of isAnonymousVpn property).
* @property-read bool $isPublicProxy This is true if the IP address belongs to
* a public proxy.
* @property-read bool $isResidentialProxy This is true if the IP address is
* on a suspected anonymizing network and belongs to a residential ISP.
* @property-read bool $isTorExitNode This is true if the IP address is a Tor
* exit node.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class AnonymousIp extends AbstractModel
{
/**
* @var bool
*/
protected $isAnonymous;
/**
* @var bool
*/
protected $isAnonymousVpn;
/**
* @var bool
*/
protected $isHostingProvider;
/**
* @var bool
*/
protected $isPublicProxy;
/**
* @var bool
*/
protected $isResidentialProxy;
/**
* @var bool
*/
protected $isTorExitNode;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->isAnonymous = $this->get('is_anonymous');
$this->isAnonymousVpn = $this->get('is_anonymous_vpn');
$this->isHostingProvider = $this->get('is_hosting_provider');
$this->isPublicProxy = $this->get('is_public_proxy');
$this->isResidentialProxy = $this->get('is_residential_proxy');
$this->isTorExitNode = $this->get('is_tor_exit_node');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}

View File

@ -0,0 +1,51 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
use Tassos\Vendor\GeoIp2\Util;
/**
* This class provides the GeoLite2 ASN model.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP
* address.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Asn extends AbstractModel
{
/**
* @var int|null
*/
protected $autonomousSystemNumber;
/**
* @var string|null
*/
protected $autonomousSystemOrganization;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');
$this->autonomousSystemOrganization = $this->get('autonomous_system_organization');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}

View File

@ -0,0 +1,105 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
/**
* Model class for the data returned by City Plus web service and City
* database.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
* details.
*
* @property-read \GeoIp2\Record\City $city City data for the requested IP
* address.
* @property-read \GeoIp2\Record\Location $location Location data for the
* requested IP address.
* @property-read \GeoIp2\Record\Postal $postal Postal data for the
* requested IP address.
* @property-read array $subdivisions An array \GeoIp2\Record\Subdivision
* objects representing the country subdivisions for the requested IP
* address. The number and type of subdivisions varies by country, but a
* subdivision is typically a state, province, county, etc. Subdivisions
* are ordered from most general (largest) to most specific (smallest).
* If the response did not contain any subdivisions, this method returns
* an empty array.
* @property-read \GeoIp2\Record\Subdivision $mostSpecificSubdivision An object
* representing the most specific subdivision returned. If the response
* did not contain any subdivisions, this method returns an empty
* \GeoIp2\Record\Subdivision object.
*/
class City extends Country
{
/**
* @ignore
*
* @var \GeoIp2\Record\City
*/
protected $city;
/**
* @ignore
*
* @var \GeoIp2\Record\Location
*/
protected $location;
/**
* @ignore
*
* @var \GeoIp2\Record\Postal
*/
protected $postal;
/**
* @ignore
*
* @var array<\GeoIp2\Record\Subdivision>
*/
protected $subdivisions = [];
/**
* @ignore
*/
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw, $locales);
$this->city = new \Tassos\Vendor\GeoIp2\Record\City($this->get('city'), $locales);
$this->location = new \Tassos\Vendor\GeoIp2\Record\Location($this->get('location'));
$this->postal = new \Tassos\Vendor\GeoIp2\Record\Postal($this->get('postal'));
$this->createSubdivisions($raw, $locales);
}
private function createSubdivisions(array $raw, array $locales) : void
{
if (!isset($raw['subdivisions'])) {
return;
}
foreach ($raw['subdivisions'] as $sub) {
$this->subdivisions[] = new \Tassos\Vendor\GeoIp2\Record\Subdivision($sub, $locales);
}
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr === 'mostSpecificSubdivision') {
return $this->{$attr}();
}
return parent::__get($attr);
}
/**
* @ignore
*/
public function __isset(string $attr) : bool
{
if ($attr === 'mostSpecificSubdivision') {
// We always return a mostSpecificSubdivision, even if it is the
// empty subdivision
return \true;
}
return parent::__isset($attr);
}
private function mostSpecificSubdivision() : \Tassos\Vendor\GeoIp2\Record\Subdivision
{
return empty($this->subdivisions) ? new \Tassos\Vendor\GeoIp2\Record\Subdivision([], $this->locales) : \end($this->subdivisions);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
use Tassos\Vendor\GeoIp2\Util;
/**
* This class provides the GeoIP2 Connection-Type model.
*
* @property-read string|null $connectionType The connection type may take the
* following values: "Dialup", "Cable/DSL", "Corporate", "Cellular".
* Additional values may be added in the future.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class ConnectionType extends AbstractModel
{
/**
* @var string|null
*/
protected $connectionType;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->connectionType = $this->get('connection_type');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}

View File

@ -0,0 +1,74 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Country web service and database.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more details.
*
* @property-read \GeoIp2\Record\Continent $continent Continent data for the
* requested IP address.
* @property-read \GeoIp2\Record\Country $country Country data for the requested
* IP address. This object represents the country where MaxMind believes the
* end user is located.
* @property-read \GeoIp2\Record\MaxMind $maxmind Data related to your MaxMind
* account.
* @property-read \GeoIp2\Record\Country $registeredCountry Registered country
* data for the requested IP address. This record represents the country
* where the ISP has registered a given IP block and may differ from the
* user's country.
* @property-read \GeoIp2\Record\RepresentedCountry $representedCountry
* Represented country data for the requested IP address. The represented
* country is used for things like military bases. It is only present when
* the represented country differs from the country.
* @property-read \GeoIp2\Record\Traits $traits Data for the traits of the
* requested IP address.
* @property-read array $raw The raw data from the web service.
*/
class Country extends AbstractModel
{
/**
* @var \GeoIp2\Record\Continent
*/
protected $continent;
/**
* @var \GeoIp2\Record\Country
*/
protected $country;
/**
* @var array<string>
*/
protected $locales;
/**
* @var \GeoIp2\Record\MaxMind
*/
protected $maxmind;
/**
* @var \GeoIp2\Record\Country
*/
protected $registeredCountry;
/**
* @var \GeoIp2\Record\RepresentedCountry
*/
protected $representedCountry;
/**
* @var \GeoIp2\Record\Traits
*/
protected $traits;
/**
* @ignore
*/
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw);
$this->continent = new \Tassos\Vendor\GeoIp2\Record\Continent($this->get('continent'), $locales);
$this->country = new \Tassos\Vendor\GeoIp2\Record\Country($this->get('country'), $locales);
$this->maxmind = new \Tassos\Vendor\GeoIp2\Record\MaxMind($this->get('maxmind'));
$this->registeredCountry = new \Tassos\Vendor\GeoIp2\Record\Country($this->get('registered_country'), $locales);
$this->representedCountry = new \Tassos\Vendor\GeoIp2\Record\RepresentedCountry($this->get('represented_country'), $locales);
$this->traits = new \Tassos\Vendor\GeoIp2\Record\Traits($this->get('traits'));
$this->locales = $locales;
}
}

View File

@ -0,0 +1,44 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
use Tassos\Vendor\GeoIp2\Util;
/**
* This class provides the GeoIP2 Domain model.
*
* @property-read string|null $domain The second level domain associated with the
* IP address. This will be something like "example.com" or
* "example.co.uk", not "foo.example.com".
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Domain extends AbstractModel
{
/**
* @var string|null
*/
protected $domain;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->domain = $this->get('domain');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}

View File

@ -0,0 +1,14 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Enterprise database lookups.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for more
* details.
*/
class Enterprise extends City
{
}

View File

@ -0,0 +1,14 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
/**
* Model class for the data returned by GeoIP2 Insights web service.
*
* See https://dev.maxmind.com/geoip/docs/web-services?lang=en for
* more details.
*/
class Insights extends City
{
}

View File

@ -0,0 +1,81 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Model;
use Tassos\Vendor\GeoIp2\Util;
/**
* This class provides the GeoIP2 ISP model.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP
* address.
* @property-read string|null $isp The name of the ISP associated with the IP
* address.
* @property-read string|null $mobileCountryCode The [mobile country code
* (MCC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP.
* @property-read string|null $mobileNetworkCode The [mobile network code
* (MNC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP.
* @property-read string|null $organization The name of the organization associated
* with the IP address.
* @property-read string $ipAddress The IP address that the data in the model is
* for.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
*/
class Isp extends AbstractModel
{
/**
* @var int|null
*/
protected $autonomousSystemNumber;
/**
* @var string|null
*/
protected $autonomousSystemOrganization;
/**
* @var string|null
*/
protected $isp;
/**
* @var string|null
*/
protected $mobileCountryCode;
/**
* @var string|null
*/
protected $mobileNetworkCode;
/**
* @var string|null
*/
protected $organization;
/**
* @var string
*/
protected $ipAddress;
/**
* @var string
*/
protected $network;
/**
* @ignore
*/
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');
$this->autonomousSystemOrganization = $this->get('autonomous_system_organization');
$this->isp = $this->get('isp');
$this->mobileCountryCode = $this->get('mobile_country_code');
$this->mobileNetworkCode = $this->get('mobile_network_code');
$this->organization = $this->get('organization');
$ipAddress = $this->get('ip_address');
$this->ipAddress = $ipAddress;
$this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
}
}

View File

@ -0,0 +1,20 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2;
interface ProviderInterface
{
/**
* @param string $ipAddress an IPv4 or IPv6 address to lookup
*
* @return \GeoIp2\Model\Country a Country model for the requested IP address
*/
public function country(string $ipAddress) : Model\Country;
/**
* @param string $ipAddress an IPv4 or IPv6 address to lookup
*
* @return \GeoIp2\Model\City a City model for the requested IP address
*/
public function city(string $ipAddress) : Model\City;
}

View File

@ -0,0 +1,57 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
abstract class AbstractPlaceRecord extends AbstractRecord
{
/**
* @var array<string>
*/
private $locales;
/**
* @ignore
*/
public function __construct(?array $record, array $locales = ['en'])
{
$this->locales = $locales;
parent::__construct($record);
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
if ($attr === 'name') {
return $this->name();
}
return parent::__get($attr);
}
/**
* @ignore
*/
public function __isset(string $attr) : bool
{
if ($attr === 'name') {
return $this->firstSetNameLocale() !== null;
}
return parent::__isset($attr);
}
private function name() : ?string
{
$locale = $this->firstSetNameLocale();
// @phpstan-ignore-next-line
return $locale === null ? null : $this->names[$locale];
}
private function firstSetNameLocale() : ?string
{
foreach ($this->locales as $locale) {
if (isset($this->names[$locale])) {
return $locale;
}
}
return null;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
abstract class AbstractRecord implements \JsonSerializable
{
/**
* @var array<string, mixed>
*/
private $record;
/**
* @ignore
*/
public function __construct(?array $record)
{
$this->record = isset($record) ? $record : [];
}
/**
* @ignore
*
* @return mixed
*/
public function __get(string $attr)
{
// XXX - kind of ugly but greatly reduces boilerplate code
$key = $this->attributeToKey($attr);
if ($this->__isset($attr)) {
return $this->record[$key];
}
if ($this->validAttribute($attr)) {
if (\preg_match('/^is_/', $key)) {
return \false;
}
return null;
}
throw new \RuntimeException("Unknown attribute: {$attr}");
}
public function __isset(string $attr) : bool
{
return $this->validAttribute($attr) && isset($this->record[$this->attributeToKey($attr)]);
}
private function attributeToKey(string $attr) : string
{
return \strtolower(\preg_replace('/([A-Z])/', '_\1', $attr));
}
private function validAttribute(string $attr) : bool
{
// @phpstan-ignore-next-line
return \in_array($attr, $this->validAttributes, \true);
}
public function jsonSerialize() : ?array
{
return $this->record;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* City-level data associated with an IP address.
*
* This record is returned by all location services and databases besides
* Country.
*
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the city is correct. This attribute is only available
* from the Insights service and the GeoIP2 Enterprise database.
* @property-read int|null $geonameId The GeoName ID for the city. This attribute
* is returned by all location services and databases.
* @property-read string|null $name The name of the city based on the locales list
* passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class City extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['confidence', 'geonameId', 'names'];
}

View File

@ -0,0 +1,31 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the continent record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read string|null $code A two character continent code like "NA" (North
* America) or "OC" (Oceania). This attribute is returned by all location
* services and databases.
* @property-read int|null $geonameId The GeoName ID for the continent. This
* attribute is returned by all location services and databases.
* @property-read string|null $name Returns the name of the continent based on the
* locales list passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class Continent extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['code', 'geonameId', 'names'];
}

View File

@ -0,0 +1,37 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the country record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the country is correct. This attribute is only available
* from the Insights service and the GeoIP2 Enterprise database.
* @property-read int|null $geonameId The GeoName ID for the country. This
* attribute is returned by all location services and databases.
* @property-read bool $isInEuropeanUnion This is true if the country is a
* member state of the European Union. This attribute is returned by all
* location services and databases.
* @property-read string|null $isoCode The two-character ISO 3166-1 alpha code
* for the country. See https://en.wikipedia.org/wiki/ISO_3166-1. This
* attribute is returned by all location services and databases.
* @property-read string|null $name The name of the country based on the locales
* list passed to the constructor. This attribute is returned by all location
* services and databases.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* services and databases.
*/
class Country extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['confidence', 'geonameId', 'isInEuropeanUnion', 'isoCode', 'names'];
}

View File

@ -0,0 +1,45 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the location record associated with an IP address.
*
* This record is returned by all location services and databases besides
* Country.
*
* @property-read int|null $averageIncome The average income in US dollars
* associated with the requested IP address. This attribute is only available
* from the Insights service.
* @property-read int|null $accuracyRadius The approximate accuracy radius in
* kilometers around the latitude and longitude for the IP address. This is
* the radius where we have a 67% confidence that the device using the IP
* address resides within the circle centered at the latitude and longitude
* with the provided radius.
* @property-read float|null $latitude The approximate latitude of the location
* associated with the IP address. This value is not precise and should not be
* used to identify a particular address or household.
* @property-read float|null $longitude The approximate longitude of the location
* associated with the IP address. This value is not precise and should not be
* used to identify a particular address or household.
* @property-read int|null $populationDensity The estimated population per square
* kilometer associated with the IP address. This attribute is only available
* from the Insights service.
* @property-read int|null $metroCode The metro code of the location if the location
* is in the US. MaxMind returns the same metro codes as the
* Google AdWords API. See
* https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions.
* @property-read string|null $timeZone The time zone associated with location, as
* specified by the IANA Time Zone Database, e.g., "America/New_York". See
* https://www.iana.org/time-zones.
*/
class Location extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['averageIncome', 'accuracyRadius', 'latitude', 'longitude', 'metroCode', 'populationDensity', 'postalCode', 'postalConfidence', 'timeZone'];
}

View File

@ -0,0 +1,22 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data about your account.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $queriesRemaining The number of remaining queries you
* have for the service you are calling.
*/
class MaxMind extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['queriesRemaining'];
}

View File

@ -0,0 +1,29 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the postal record associated with an IP address.
*
* This record is returned by all location databases and services besides
* Country.
*
* @property-read string|null $code The postal code of the location. Postal codes
* are not available for all countries. In some countries, this will only
* contain part of the postal code. This attribute is returned by all location
* databases and services besides Country.
* @property-read int|null $confidence A value from 0-100 indicating MaxMind's
* confidence that the postal code is correct. This attribute is only
* available from the Insights service and the GeoIP2 Enterprise
* database.
*/
class Postal extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['code', 'confidence'];
}

View File

@ -0,0 +1,25 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the represented country associated with an IP address.
*
* This class contains the country-level data associated with an IP address
* for the IP's represented country. The represented country is the country
* represented by something like a military base.
*
* @property-read string|null $type A string indicating the type of entity that is
* representing the country. Currently we only return <code>military</code>
* but this could expand to include other types in the future.
*/
class RepresentedCountry extends Country
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['confidence', 'geonameId', 'isInEuropeanUnion', 'isoCode', 'names', 'type'];
}

View File

@ -0,0 +1,38 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
/**
* Contains data for the subdivisions associated with an IP address.
*
* This record is returned by all location databases and services besides
* Country.
*
* @property-read int|null $confidence This is a value from 0-100 indicating
* MaxMind's confidence that the subdivision is correct. This attribute is
* only available from the Insights service and the GeoIP2 Enterprise
* database.
* @property-read int|null $geonameId This is a GeoName ID for the subdivision.
* This attribute is returned by all location databases and services besides
* Country.
* @property-read string|null $isoCode This is a string up to three characters long
* contain the subdivision portion of the ISO 3166-2 code. See
* https://en.wikipedia.org/wiki/ISO_3166-2. This attribute is returned by all
* location databases and services except Country.
* @property-read string|null $name The name of the subdivision based on the
* locales list passed to the constructor. This attribute is returned by all
* location databases and services besides Country.
* @property-read array|null $names An array map where the keys are locale codes
* and the values are names. This attribute is returned by all location
* databases and services besides Country.
*/
class Subdivision extends AbstractPlaceRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['confidence', 'geonameId', 'isoCode', 'names'];
}

View File

@ -0,0 +1,131 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\Record;
use Tassos\Vendor\GeoIp2\Util;
/**
* Contains data for the traits record associated with an IP address.
*
* This record is returned by all location services and databases.
*
* @property-read int|null $autonomousSystemNumber The autonomous system number
* associated with the IP address. See
* https://en.wikipedia.org/wiki/Autonomous_system_(Internet%29. This attribute
* is only available from the City Plus and Insights web services and the
* GeoIP2 Enterprise database.
* @property-read string|null $autonomousSystemOrganization The organization
* associated with the registered autonomous system number for the IP address.
* See https://en.wikipedia.org/wiki/Autonomous_system_(Internet%29. This
* attribute is only available from the City Plus and Insights web services and
* the GeoIP2 Enterprise database.
* @property-read string|null $connectionType The connection type may take the
* following values: "Dialup", "Cable/DSL", "Corporate", "Cellular".
* Additional values may be added in the future. This attribute is only
* available in the GeoIP2 Enterprise database.
* @property-read string|null $domain The second level domain associated with the
* IP address. This will be something like "example.com" or "example.co.uk",
* not "foo.example.com". This attribute is only available from the
* City Plus and Insights web services and the GeoIP2 Enterprise
* database.
* @property-read string $ipAddress The IP address that the data in the model
* is for. If you performed a "me" lookup against the web service, this
* will be the externally routable IP address for the system the code is
* running on. If the system is behind a NAT, this may differ from the IP
* address locally assigned to it. This attribute is returned by all end
* points.
* @property-read bool $isAnonymous This is true if the IP address belongs to
* any sort of anonymous network. This property is only available from GeoIP2
* Insights.
* @property-read bool $isAnonymousProxy *Deprecated.* Please see our GeoIP2
* Anonymous IP database
* (https://www.maxmind.com/en/geoip2-anonymous-ip-database) to determine
* whether the IP address is used by an anonymizing service.
* @property-read bool $isAnonymousVpn This is true if the IP address is
* registered to an anonymous VPN provider. If a VPN provider does not register
* subnets under names associated with them, we will likely only flag their IP
* ranges using the isHostingProvider property. This property is only available
* from GeoIP2 Insights.
* @property-read bool $isHostingProvider This is true if the IP address belongs
* to a hosting or VPN provider (see description of isAnonymousVpn property).
* This property is only available from GeoIP2 Insights.
* @property-read bool $isLegitimateProxy This attribute is true if MaxMind
* believes this IP address to be a legitimate proxy, such as an internal
* VPN used by a corporation. This attribute is only available in the GeoIP2
* Enterprise database.
* @property-read bool $isPublicProxy This is true if the IP address belongs to
* a public proxy. This property is only available from GeoIP2 Insights.
* @property-read bool $isResidentialProxy This is true if the IP address is
* on a suspected anonymizing network and belongs to a residential ISP. This
* property is only available from GeoIP2 Insights.
* @property-read bool $isSatelliteProvider *Deprecated.* Due to the
* increased coverage by mobile carriers, very few satellite providers now
* serve multiple countries. As a result, the output does not provide
* sufficiently relevant data for us to maintain it.
* @property-read bool $isTorExitNode This is true if the IP address is a Tor
* exit node. This property is only available from GeoIP2 Insights.
* @property-read string|null $isp The name of the ISP associated with the IP
* address. This attribute is only available from the City Plus and Insights
* web services and the GeoIP2 Enterprise database.
* @property-read string $network The network in CIDR notation associated with
* the record. In particular, this is the largest network where all of the
* fields besides $ipAddress have the same value.
* @property-read string|null $organization The name of the organization
* associated with the IP address. This attribute is only available from the
* City Plus and Insights web services and the GeoIP2 Enterprise database.
* @property-read string|null $mobileCountryCode The [mobile country code
* (MCC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP. This property is available from the City Plus and
* Insights web services and the GeoIP2 Enterprise database.
* @property-read string|null $mobileNetworkCode The [mobile network code
* (MNC)](https://en.wikipedia.org/wiki/Mobile_country_code) associated with
* the IP address and ISP. This property is available from the City Plus and
* Insights web services and the GeoIP2 Enterprise database.
* @property-read float|null $staticIpScore An indicator of how static or
* dynamic an IP address is. This property is only available from GeoIP2
* Insights.
* @property-read int|null $userCount The estimated number of users sharing
* the IP/network during the past 24 hours. For IPv4, the count is for the
* individual IP. For IPv6, the count is for the /64 network. This property is
* only available from GeoIP2 Insights.
* @property-read string|null $userType <p>The user type associated with the IP
* address. This can be one of the following values:</p>
* <ul>
* <li>business
* <li>cafe
* <li>cellular
* <li>college
* <li>consumer_privacy_network
* <li>content_delivery_network
* <li>dialup
* <li>government
* <li>hosting
* <li>library
* <li>military
* <li>residential
* <li>router
* <li>school
* <li>search_engine_spider
* <li>traveler
* </ul>
* <p>
* This attribute is only available from the Insights web service and the
* GeoIP2 Enterprise database.
* </p>
*/
class Traits extends AbstractRecord
{
/**
* @ignore
*
* @var array<string>
*/
protected $validAttributes = ['autonomousSystemNumber', 'autonomousSystemOrganization', 'connectionType', 'domain', 'ipAddress', 'isAnonymous', 'isAnonymousProxy', 'isAnonymousVpn', 'isHostingProvider', 'isLegitimateProxy', 'isp', 'isPublicProxy', 'isResidentialProxy', 'isSatelliteProvider', 'isTorExitNode', 'mobileCountryCode', 'mobileNetworkCode', 'network', 'organization', 'staticIpScore', 'userCount', 'userType'];
public function __construct(?array $record)
{
if (!isset($record['network']) && isset($record['ip_address'], $record['prefix_len'])) {
$record['network'] = Util::cidr($record['ip_address'], $record['prefix_len']);
}
parent::__construct($record);
}
}

View File

@ -0,0 +1,32 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2;
class Util
{
/**
* This returns the network in CIDR notation for the given IP and prefix
* length. This is for internal use only.
*
* @internal
* @ignore
*/
public static function cidr(string $ipAddress, int $prefixLen) : string
{
$ipBytes = \inet_pton($ipAddress);
$networkBytes = \str_repeat("\x00", \strlen($ipBytes));
$curPrefix = $prefixLen;
for ($i = 0; $i < \strlen($ipBytes) && $curPrefix > 0; $i++) {
$b = $ipBytes[$i];
if ($curPrefix < 8) {
$shiftN = 8 - $curPrefix;
$b = \chr(0xff & \ord($b) >> $shiftN << $shiftN);
}
$networkBytes[$i] = $b;
$curPrefix -= 8;
}
$network = \inet_ntop($networkBytes);
return "{$network}/{$prefixLen}";
}
}

View File

@ -0,0 +1,207 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\GeoIp2\WebService;
use Tassos\Vendor\GeoIp2\Exception\AddressNotFoundException;
use Tassos\Vendor\GeoIp2\Exception\AuthenticationException;
use Tassos\Vendor\GeoIp2\Exception\GeoIp2Exception;
use Tassos\Vendor\GeoIp2\Exception\HttpException;
use Tassos\Vendor\GeoIp2\Exception\InvalidRequestException;
use Tassos\Vendor\GeoIp2\Exception\OutOfQueriesException;
use Tassos\Vendor\GeoIp2\Model\City;
use Tassos\Vendor\GeoIp2\Model\Country;
use Tassos\Vendor\GeoIp2\Model\Insights;
use Tassos\Vendor\GeoIp2\ProviderInterface;
use Tassos\Vendor\MaxMind\WebService\Client as WsClient;
/**
* This class provides a client API for all the GeoIP2 web services.
* The services are Country, City Plus, and Insights. Each service returns
* a different set of data about an IP address, with Country returning the
* least data and Insights the most.
*
* Each web service is represented by a different model class, and these model
* classes in turn contain multiple record classes. The record classes have
* attributes which contain data about the IP address.
*
* If the web service does not return a particular piece of data for an IP
* address, the associated attribute is not populated.
*
* The web service may not return any information for an entire record, in
* which case all of the attributes for that record class will be empty.
*
* ## Usage ##
*
* The basic API for this class is the same for all of the web service end
* points. First you create a web service object with your MaxMind `$accountId`
* and `$licenseKey`, then you call the method corresponding to a specific end
* point, passing it the IP address you want to look up.
*
* If the request succeeds, the method call will return a model class for
* the service you called. This model in turn contains multiple record
* classes, each of which represents part of the data returned by the web
* service.
*
* If the request fails, the client class throws an exception.
*/
class Client implements ProviderInterface
{
/**
* @var array<string>
*/
private $locales;
/**
* @var WsClient
*/
private $client;
/**
* @var string
*/
private static $basePath = '/geoip/v2.1';
public const VERSION = 'v2.13.0';
/**
* Constructor.
*
* @param int $accountId your MaxMind account ID
* @param string $licenseKey your MaxMind license key
* @param array $locales list of locale codes to use in name property
* from most preferred to least preferred
* @param array $options array of options. Valid options include:
* * `host` - The host to use when querying the web
* service. To query the GeoLite2 web service
* instead of the GeoIP2 web service, set the
* host to `geolite.info`.
* * `timeout` - Timeout in seconds.
* * `connectTimeout` - Initial connection timeout in seconds.
* * `proxy` - The HTTP proxy to use. May include a schema, port,
* username, and password, e.g.,
* `http://username:password@127.0.0.1:10`.
*/
public function __construct(int $accountId, string $licenseKey, array $locales = ['en'], array $options = [])
{
$this->locales = $locales;
// This is for backwards compatibility. Do not remove except for a
// major version bump.
// @phpstan-ignore-next-line
if (\is_string($options)) {
$options = ['host' => $options];
}
if (!isset($options['host'])) {
$options['host'] = 'geoip.maxmind.com';
}
$options['userAgent'] = $this->userAgent();
$this->client = new WsClient($accountId, $licenseKey, $options);
}
private function userAgent() : string
{
return 'GeoIP2-API/' . self::VERSION;
}
/**
* This method calls the City Plus service.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you
* provided is not in our database (e.g., a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
* of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an issue
* with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error code or message was returned.
* This could indicate a problem with the connection between
* your server and the web service or that the web service
* returned an invalid document or 500 error code
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
* class to the above exceptions. It will be thrown directly
* if a 200 status code is returned but the body is invalid.
*/
public function city(string $ipAddress = 'me') : City
{
// @phpstan-ignore-next-line
return $this->responseFor('city', City::class, $ipAddress);
}
/**
* This method calls the Country service.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you provided is not in our database (e.g.,
* a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an
* issue with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
* code or message was returned. This could indicate a problem
* with the connection between your server and the web service
* or that the web service returned an invalid document or 500
* error code.
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent class to the above exceptions. It
* will be thrown directly if a 200 status code is returned but
* the body is invalid.
*/
public function country(string $ipAddress = 'me') : Country
{
return $this->responseFor('country', Country::class, $ipAddress);
}
/**
* This method calls the Insights service. Insights is only supported by
* the GeoIP2 web service. The GeoLite2 web service does not support it.
*
* @param string $ipAddress IPv4 or IPv6 address as a string. If no
* address is provided, the address that the web service is called
* from will be used.
*
* @throws \GeoIp2\Exception\AddressNotFoundException if the address you
* provided is not in our database (e.g., a private address).
* @throws \GeoIp2\Exception\AuthenticationException if there is a problem
* with the account ID or license key that you provided
* @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
* of queries
* @throws \GeoIp2\Exception\InvalidRequestException} if your request was received by the web service but is
* invalid for some other reason. This may indicate an
* issue with this API. Please report the error to MaxMind.
* @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error code or message was returned.
* This could indicate a problem with the connection between
* your server and the web service or that the web service
* returned an invalid document or 500 error code
* @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
* class to the above exceptions. It will be thrown directly
* if a 200 status code is returned but the body is invalid.
*/
public function insights(string $ipAddress = 'me') : Insights
{
// @phpstan-ignore-next-line
return $this->responseFor('insights', Insights::class, $ipAddress);
}
private function responseFor(string $endpoint, string $class, string $ipAddress) : Country
{
$path = \implode('/', [self::$basePath, $endpoint, $ipAddress]);
try {
$service = (new \ReflectionClass($class))->getShortName();
$body = $this->client->get('GeoIP2 ' . $service, $path);
} catch (\Tassos\Vendor\MaxMind\Exception\IpAddressNotFoundException $ex) {
throw new AddressNotFoundException($ex->getMessage(), $ex->getStatusCode(), $ex);
} catch (\Tassos\Vendor\MaxMind\Exception\AuthenticationException $ex) {
throw new AuthenticationException($ex->getMessage(), $ex->getStatusCode(), $ex);
} catch (\Tassos\Vendor\MaxMind\Exception\InsufficientFundsException $ex) {
throw new OutOfQueriesException($ex->getMessage(), $ex->getStatusCode(), $ex);
} catch (\Tassos\Vendor\MaxMind\Exception\InvalidRequestException $ex) {
throw new InvalidRequestException($ex->getMessage(), $ex->getErrorCode(), $ex->getStatusCode(), $ex->getUri(), $ex);
} catch (\Tassos\Vendor\MaxMind\Exception\HttpException $ex) {
throw new HttpException($ex->getMessage(), $ex->getStatusCode(), $ex->getUri(), $ex);
} catch (\Tassos\Vendor\MaxMind\Exception\WebServiceException $ex) {
throw new GeoIp2Exception($ex->getMessage(), $ex->getCode(), $ex);
}
return new $class($body, $this->locales);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor;
/**
* PSR-4 autoloader implementation for the MaxMind\DB namespace.
* First we define the 'mmdb_autoload' function, and then we register
* it with 'spl_autoload_register' so that PHP knows to use it.
*
* @param mixed $class
*/
/**
* Automatically include the file that defines <code>class</code>.
*
* @param string $class
* the name of the class to load
*/
function mmdb_autoload($class) : void
{
/*
* A project-specific mapping between the namespaces and where
* they're located. By convention, we include the trailing
* slashes. The one-element array here simply makes things easy
* to extend in the future if (for example) the test classes
* begin to use one another.
*/
$namespace_map = ['MaxMind\\Db\\' => __DIR__ . '/src/MaxMind/Db/'];
foreach ($namespace_map as $prefix => $dir) {
// First swap out the namespace prefix with a directory...
$path = \str_replace($prefix, $dir, $class);
// replace the namespace separator with a directory separator...
$path = \str_replace('\\', '/', $path);
// and finally, add the PHP file extension to the result.
$path = $path . '.php';
// $path should now contain the path to a PHP file defining $class
if (\file_exists($path)) {
include $path;
}
}
}
\spl_autoload_register('mmdb_autoload');

View File

@ -0,0 +1,40 @@
PHP_ARG_WITH(maxminddb,
[Whether to enable the MaxMind DB Reader extension],
[ --with-maxminddb Enable MaxMind DB Reader extension support])
PHP_ARG_ENABLE(maxminddb-debug, for MaxMind DB debug support,
[ --enable-maxminddb-debug Enable enable MaxMind DB deubg support], no, no)
if test $PHP_MAXMINDDB != "no"; then
AC_PATH_PROG(PKG_CONFIG, pkg-config, no)
AC_MSG_CHECKING(for libmaxminddb)
if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists libmaxminddb; then
dnl retrieve build options from pkg-config
if $PKG_CONFIG libmaxminddb --atleast-version 1.0.0; then
LIBMAXMINDDB_INC=`$PKG_CONFIG libmaxminddb --cflags`
LIBMAXMINDDB_LIB=`$PKG_CONFIG libmaxminddb --libs`
LIBMAXMINDDB_VER=`$PKG_CONFIG libmaxminddb --modversion`
AC_MSG_RESULT(found version $LIBMAXMINDDB_VER)
else
AC_MSG_ERROR(system libmaxminddb must be upgraded to version >= 1.0.0)
fi
PHP_EVAL_LIBLINE($LIBMAXMINDDB_LIB, MAXMINDDB_SHARED_LIBADD)
PHP_EVAL_INCLINE($LIBMAXMINDDB_INC)
else
AC_MSG_RESULT(pkg-config information missing)
AC_MSG_WARN(will use libmaxmxinddb from compiler default path)
PHP_CHECK_LIBRARY(maxminddb, MMDB_open)
PHP_ADD_LIBRARY(maxminddb, 1, MAXMINDDB_SHARED_LIBADD)
fi
if test $PHP_MAXMINDDB_DEBUG != "no"; then
CFLAGS="$CFLAGS -Wall -Wextra -Wno-unused-parameter -Wno-missing-field-initializers -Werror"
fi
PHP_SUBST(MAXMINDDB_SHARED_LIBADD)
PHP_NEW_EXTENSION(maxminddb, maxminddb.c, $ext_shared)
fi

View File

@ -0,0 +1,10 @@
ARG_WITH("maxminddb", "Enable MaxMind DB Reader extension support", "no");
if (PHP_MAXMINDDB == "yes") {
if (CHECK_HEADER_ADD_INCLUDE("maxminddb.h", "CFLAGS_MAXMINDDB", PHP_MAXMINDDB + ";" + PHP_PHP_BUILD + "\\include\\maxminddb") &&
CHECK_LIB("libmaxminddb.lib", "maxminddb", PHP_MAXMINDDB)) {
EXTENSION("maxminddb", "maxminddb.c");
} else {
WARNING('Could not find maxminddb.h or libmaxminddb.lib; skipping');
}
}

View File

@ -0,0 +1,811 @@
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#include "php_maxminddb.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <php.h>
#include <zend.h>
#include "Zend/zend_exceptions.h"
#include "Zend/zend_types.h"
#include "ext/spl/spl_exceptions.h"
#include "ext/standard/info.h"
#include <maxminddb.h>
#ifdef ZTS
#include <TSRM.h>
#endif
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
#define PHP_MAXMINDDB_NS ZEND_NS_NAME("MaxMind", "Db")
#define PHP_MAXMINDDB_READER_NS ZEND_NS_NAME(PHP_MAXMINDDB_NS, "Reader")
#define PHP_MAXMINDDB_METADATA_NS \
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "Metadata")
#define PHP_MAXMINDDB_READER_EX_NS \
ZEND_NS_NAME(PHP_MAXMINDDB_READER_NS, "InvalidDatabaseException")
#define Z_MAXMINDDB_P(zv) php_maxminddb_fetch_object(Z_OBJ_P(zv))
typedef size_t strsize_t;
typedef zend_object free_obj_t;
/* For PHP 8 compatibility */
#if PHP_VERSION_ID < 80000
#define PROP_OBJ(zv) (zv)
#else
#define PROP_OBJ(zv) Z_OBJ_P(zv)
#define TSRMLS_C
#define TSRMLS_CC
#define TSRMLS_DC
/* End PHP 8 compatibility */
#endif
#ifndef ZEND_ACC_CTOR
#define ZEND_ACC_CTOR 0
#endif
/* IS_MIXED was added in 2020 */
#ifndef IS_MIXED
#define IS_MIXED IS_UNDEF
#endif
/* ZEND_THIS was added in 7.4 */
#ifndef ZEND_THIS
#define ZEND_THIS (&EX(This))
#endif
typedef struct _maxminddb_obj {
MMDB_s *mmdb;
zend_object std;
} maxminddb_obj;
PHP_FUNCTION(maxminddb);
static int
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len);
static const MMDB_entry_data_list_s *
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static const MMDB_entry_data_list_s *
handle_array(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static const MMDB_entry_data_list_s *
handle_map(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC);
#define CHECK_ALLOCATED(val) \
if (!val) { \
zend_error(E_ERROR, "Out of memory"); \
return; \
}
static zend_object_handlers maxminddb_obj_handlers;
static zend_class_entry *maxminddb_ce, *maxminddb_exception_ce, *metadata_ce;
static inline maxminddb_obj *
php_maxminddb_fetch_object(zend_object *obj TSRMLS_DC) {
return (maxminddb_obj *)((char *)(obj)-XtOffsetOf(maxminddb_obj, std));
}
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, db_file, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, __construct) {
char *db_file = NULL;
strsize_t name_len;
zval *_this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Os",
&_this_zval,
maxminddb_ce,
&db_file,
&name_len) == FAILURE) {
return;
}
if (0 != php_check_open_basedir(db_file TSRMLS_CC) ||
0 != access(db_file, R_OK)) {
zend_throw_exception_ex(
spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"The file \"%s\" does not exist or is not readable.",
db_file);
return;
}
MMDB_s *mmdb = (MMDB_s *)ecalloc(1, sizeof(MMDB_s));
uint16_t status = MMDB_open(db_file, MMDB_MODE_MMAP, mmdb);
if (MMDB_SUCCESS != status) {
zend_throw_exception_ex(
maxminddb_exception_ce,
0 TSRMLS_CC,
"Error opening database file (%s). Is this a valid "
"MaxMind DB file?",
db_file);
efree(mmdb);
return;
}
maxminddb_obj *mmdb_obj = Z_MAXMINDDB_P(ZEND_THIS);
mmdb_obj->mmdb = mmdb;
}
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
arginfo_maxminddbreader_get, 0, 1, IS_MIXED, 1)
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, get) {
int prefix_len = 0;
get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, return_value, &prefix_len);
}
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(
arginfo_maxminddbreader_getWithPrefixLen, 0, 1, IS_ARRAY, 1)
ZEND_ARG_TYPE_INFO(0, ip_address, IS_STRING, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, getWithPrefixLen) {
zval record, z_prefix_len;
int prefix_len = 0;
if (get_record(INTERNAL_FUNCTION_PARAM_PASSTHRU, &record, &prefix_len) ==
FAILURE) {
return;
}
array_init(return_value);
add_next_index_zval(return_value, &record);
ZVAL_LONG(&z_prefix_len, prefix_len);
add_next_index_zval(return_value, &z_prefix_len);
}
static int
get_record(INTERNAL_FUNCTION_PARAMETERS, zval *record, int *prefix_len) {
char *ip_address = NULL;
strsize_t name_len;
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Os",
&this_zval,
maxminddb_ce,
&ip_address,
&name_len) == FAILURE) {
return FAILURE;
}
const maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(ZEND_THIS);
MMDB_s *mmdb = mmdb_obj->mmdb;
if (NULL == mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to read from a closed MaxMind DB.");
return FAILURE;
}
struct addrinfo hints = {
.ai_family = AF_UNSPEC,
.ai_flags = AI_NUMERICHOST,
/* We set ai_socktype so that we only get one result back */
.ai_socktype = SOCK_STREAM};
struct addrinfo *addresses = NULL;
int gai_status = getaddrinfo(ip_address, NULL, &hints, &addresses);
if (gai_status) {
zend_throw_exception_ex(spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"The value \"%s\" is not a valid IP address.",
ip_address);
return FAILURE;
}
if (!addresses || !addresses->ai_addr) {
zend_throw_exception_ex(
spl_ce_InvalidArgumentException,
0 TSRMLS_CC,
"getaddrinfo was successful but failed to set the addrinfo");
return FAILURE;
}
int sa_family = addresses->ai_addr->sa_family;
int mmdb_error = MMDB_SUCCESS;
MMDB_lookup_result_s result =
MMDB_lookup_sockaddr(mmdb, addresses->ai_addr, &mmdb_error);
freeaddrinfo(addresses);
if (MMDB_SUCCESS != mmdb_error) {
zend_class_entry *ex;
if (MMDB_IPV6_LOOKUP_IN_IPV4_DATABASE_ERROR == mmdb_error) {
ex = spl_ce_InvalidArgumentException;
} else {
ex = maxminddb_exception_ce;
}
zend_throw_exception_ex(ex,
0 TSRMLS_CC,
"Error looking up %s. %s",
ip_address,
MMDB_strerror(mmdb_error));
return FAILURE;
}
*prefix_len = result.netmask;
if (sa_family == AF_INET && mmdb->metadata.ip_version == 6) {
/* We return the prefix length given the IPv4 address. If there is
no IPv4 subtree, we return a prefix length of 0. */
*prefix_len = *prefix_len >= 96 ? *prefix_len - 96 : 0;
}
if (!result.found_entry) {
ZVAL_NULL(record);
return SUCCESS;
}
MMDB_entry_data_list_s *entry_data_list = NULL;
int status = MMDB_get_entry_data_list(&result.entry, &entry_data_list);
if (MMDB_SUCCESS != status) {
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Error while looking up data for %s. %s",
ip_address,
MMDB_strerror(status));
MMDB_free_entry_data_list(entry_data_list);
return FAILURE;
} else if (NULL == entry_data_list) {
zend_throw_exception_ex(
maxminddb_exception_ce,
0 TSRMLS_CC,
"Error while looking up data for %s. Your database may "
"be corrupt or you have found a bug in libmaxminddb.",
ip_address);
return FAILURE;
}
const MMDB_entry_data_list_s *rv =
handle_entry_data_list(entry_data_list, record TSRMLS_CC);
if (rv == NULL) {
/* We should have already thrown the exception in handle_entry_data_list
*/
return FAILURE;
}
MMDB_free_entry_data_list(entry_data_list);
return SUCCESS;
}
ZEND_BEGIN_ARG_INFO_EX(arginfo_maxminddbreader_void, 0, 0, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader, metadata) {
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"O",
&this_zval,
maxminddb_ce) == FAILURE) {
return;
}
const maxminddb_obj *const mmdb_obj =
(maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
if (NULL == mmdb_obj->mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to read from a closed MaxMind DB.");
return;
}
object_init_ex(return_value, metadata_ce);
MMDB_entry_data_list_s *entry_data_list;
MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
zval metadata_array;
const MMDB_entry_data_list_s *rv =
handle_entry_data_list(entry_data_list, &metadata_array TSRMLS_CC);
if (rv == NULL) {
return;
}
MMDB_free_entry_data_list(entry_data_list);
zend_call_method_with_1_params(PROP_OBJ(return_value),
metadata_ce,
&metadata_ce->constructor,
ZEND_CONSTRUCTOR_FUNC_NAME,
NULL,
&metadata_array);
zval_ptr_dtor(&metadata_array);
}
PHP_METHOD(MaxMind_Db_Reader, close) {
zval *this_zval = NULL;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"O",
&this_zval,
maxminddb_ce) == FAILURE) {
return;
}
maxminddb_obj *mmdb_obj = (maxminddb_obj *)Z_MAXMINDDB_P(this_zval);
if (NULL == mmdb_obj->mmdb) {
zend_throw_exception_ex(spl_ce_BadMethodCallException,
0 TSRMLS_CC,
"Attempt to close a closed MaxMind DB.");
return;
}
MMDB_close(mmdb_obj->mmdb);
efree(mmdb_obj->mmdb);
mmdb_obj->mmdb = NULL;
}
static const MMDB_entry_data_list_s *
handle_entry_data_list(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
switch (entry_data_list->entry_data.type) {
case MMDB_DATA_TYPE_MAP:
return handle_map(entry_data_list, z_value TSRMLS_CC);
case MMDB_DATA_TYPE_ARRAY:
return handle_array(entry_data_list, z_value TSRMLS_CC);
case MMDB_DATA_TYPE_UTF8_STRING:
ZVAL_STRINGL(z_value,
(char *)entry_data_list->entry_data.utf8_string,
entry_data_list->entry_data.data_size);
break;
case MMDB_DATA_TYPE_BYTES:
ZVAL_STRINGL(z_value,
(char *)entry_data_list->entry_data.bytes,
entry_data_list->entry_data.data_size);
break;
case MMDB_DATA_TYPE_DOUBLE:
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.double_value);
break;
case MMDB_DATA_TYPE_FLOAT:
ZVAL_DOUBLE(z_value, entry_data_list->entry_data.float_value);
break;
case MMDB_DATA_TYPE_UINT16:
ZVAL_LONG(z_value, entry_data_list->entry_data.uint16);
break;
case MMDB_DATA_TYPE_UINT32:
handle_uint32(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_BOOLEAN:
ZVAL_BOOL(z_value, entry_data_list->entry_data.boolean);
break;
case MMDB_DATA_TYPE_UINT64:
handle_uint64(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_UINT128:
handle_uint128(entry_data_list, z_value TSRMLS_CC);
break;
case MMDB_DATA_TYPE_INT32:
ZVAL_LONG(z_value, entry_data_list->entry_data.int32);
break;
default:
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Invalid data type arguments: %d",
entry_data_list->entry_data.type);
return NULL;
}
return entry_data_list;
}
static const MMDB_entry_data_list_s *
handle_map(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
array_init(z_value);
const uint32_t map_size = entry_data_list->entry_data.data_size;
uint32_t i;
for (i = 0; i < map_size && entry_data_list; i++) {
entry_data_list = entry_data_list->next;
char *key = estrndup((char *)entry_data_list->entry_data.utf8_string,
entry_data_list->entry_data.data_size);
if (NULL == key) {
zend_throw_exception_ex(maxminddb_exception_ce,
0 TSRMLS_CC,
"Invalid data type arguments");
return NULL;
}
entry_data_list = entry_data_list->next;
zval new_value;
entry_data_list =
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
if (entry_data_list != NULL) {
add_assoc_zval(z_value, key, &new_value);
}
efree(key);
}
return entry_data_list;
}
static const MMDB_entry_data_list_s *
handle_array(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
const uint32_t size = entry_data_list->entry_data.data_size;
array_init(z_value);
uint32_t i;
for (i = 0; i < size && entry_data_list; i++) {
entry_data_list = entry_data_list->next;
zval new_value;
entry_data_list =
handle_entry_data_list(entry_data_list, &new_value TSRMLS_CC);
if (entry_data_list != NULL) {
add_next_index_zval(z_value, &new_value);
}
}
return entry_data_list;
}
static void handle_uint128(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint64_t high = 0;
uint64_t low = 0;
#if MMDB_UINT128_IS_BYTE_ARRAY
int i;
for (i = 0; i < 8; i++) {
high = (high << 8) | entry_data_list->entry_data.uint128[i];
}
for (i = 8; i < 16; i++) {
low = (low << 8) | entry_data_list->entry_data.uint128[i];
}
#else
high = entry_data_list->entry_data.uint128 >> 64;
low = (uint64_t)entry_data_list->entry_data.uint128;
#endif
char *num_str;
spprintf(&num_str, 0, "0x%016" PRIX64 "%016" PRIX64, high, low);
CHECK_ALLOCATED(num_str);
ZVAL_STRING(z_value, num_str);
efree(num_str);
}
static void handle_uint32(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint32_t val = entry_data_list->entry_data.uint32;
#if LONG_MAX >= UINT32_MAX
ZVAL_LONG(z_value, val);
return;
#else
if (val <= LONG_MAX) {
ZVAL_LONG(z_value, val);
return;
}
char *int_str;
spprintf(&int_str, 0, "%" PRIu32, val);
CHECK_ALLOCATED(int_str);
ZVAL_STRING(z_value, int_str);
efree(int_str);
#endif
}
static void handle_uint64(const MMDB_entry_data_list_s *entry_data_list,
zval *z_value TSRMLS_DC) {
uint64_t val = entry_data_list->entry_data.uint64;
#if LONG_MAX >= UINT64_MAX
ZVAL_LONG(z_value, val);
return;
#else
if (val <= LONG_MAX) {
ZVAL_LONG(z_value, val);
return;
}
char *int_str;
spprintf(&int_str, 0, "%" PRIu64, val);
CHECK_ALLOCATED(int_str);
ZVAL_STRING(z_value, int_str);
efree(int_str);
#endif
}
static void maxminddb_free_storage(free_obj_t *object TSRMLS_DC) {
maxminddb_obj *obj =
php_maxminddb_fetch_object((zend_object *)object TSRMLS_CC);
if (obj->mmdb != NULL) {
MMDB_close(obj->mmdb);
efree(obj->mmdb);
}
zend_object_std_dtor(&obj->std TSRMLS_CC);
}
static zend_object *maxminddb_create_handler(zend_class_entry *type TSRMLS_DC) {
maxminddb_obj *obj = (maxminddb_obj *)ecalloc(1, sizeof(maxminddb_obj));
zend_object_std_init(&obj->std, type TSRMLS_CC);
object_properties_init(&(obj->std), type);
obj->std.handlers = &maxminddb_obj_handlers;
return &obj->std;
}
/* clang-format off */
static zend_function_entry maxminddb_methods[] = {
PHP_ME(MaxMind_Db_Reader, __construct, arginfo_maxminddbreader_construct,
ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
PHP_ME(MaxMind_Db_Reader, close, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, get, arginfo_maxminddbreader_get, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, getWithPrefixLen, arginfo_maxminddbreader_getWithPrefixLen, ZEND_ACC_PUBLIC)
PHP_ME(MaxMind_Db_Reader, metadata, arginfo_maxminddbreader_void, ZEND_ACC_PUBLIC)
{ NULL, NULL, NULL }
};
/* clang-format on */
ZEND_BEGIN_ARG_INFO_EX(arginfo_metadata_construct, 0, 0, 1)
ZEND_ARG_TYPE_INFO(0, metadata, IS_ARRAY, 0)
ZEND_END_ARG_INFO()
PHP_METHOD(MaxMind_Db_Reader_Metadata, __construct) {
zval *object = NULL;
zval *metadata_array = NULL;
zend_long node_count = 0;
zend_long record_size = 0;
if (zend_parse_method_parameters(ZEND_NUM_ARGS() TSRMLS_CC,
getThis(),
"Oa",
&object,
metadata_ce,
&metadata_array) == FAILURE) {
return;
}
zval *tmp = NULL;
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"binary_format_major_version",
sizeof("binary_format_major_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"binaryFormatMajorVersion",
sizeof("binaryFormatMajorVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"binary_format_minor_version",
sizeof("binary_format_minor_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"binaryFormatMinorVersion",
sizeof("binaryFormatMinorVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"build_epoch",
sizeof("build_epoch") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"buildEpoch",
sizeof("buildEpoch") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"database_type",
sizeof("database_type") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"databaseType",
sizeof("databaseType") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"description",
sizeof("description") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"description",
sizeof("description") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"ip_version",
sizeof("ip_version") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"ipVersion",
sizeof("ipVersion") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(
HASH_OF(metadata_array), "languages", sizeof("languages") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"languages",
sizeof("languages") - 1,
tmp);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"record_size",
sizeof("record_size") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"recordSize",
sizeof("recordSize") - 1,
tmp);
if (Z_TYPE_P(tmp) == IS_LONG) {
record_size = Z_LVAL_P(tmp);
}
}
if (record_size != 0) {
zend_update_property_long(metadata_ce,
PROP_OBJ(object),
"nodeByteSize",
sizeof("nodeByteSize") - 1,
record_size / 4);
}
if ((tmp = zend_hash_str_find(HASH_OF(metadata_array),
"node_count",
sizeof("node_count") - 1))) {
zend_update_property(metadata_ce,
PROP_OBJ(object),
"nodeCount",
sizeof("nodeCount") - 1,
tmp);
if (Z_TYPE_P(tmp) == IS_LONG) {
node_count = Z_LVAL_P(tmp);
}
}
if (record_size != 0) {
zend_update_property_long(metadata_ce,
PROP_OBJ(object),
"searchTreeSize",
sizeof("searchTreeSize") - 1,
record_size * node_count / 4);
}
}
// clang-format off
static zend_function_entry metadata_methods[] = {
PHP_ME(MaxMind_Db_Reader_Metadata, __construct, arginfo_metadata_construct, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
{NULL, NULL, NULL}
};
// clang-format on
PHP_MINIT_FUNCTION(maxminddb) {
zend_class_entry ce;
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_EX_NS, NULL);
maxminddb_exception_ce =
zend_register_internal_class_ex(&ce, zend_ce_exception);
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_READER_NS, maxminddb_methods);
maxminddb_ce = zend_register_internal_class(&ce TSRMLS_CC);
maxminddb_ce->create_object = maxminddb_create_handler;
INIT_CLASS_ENTRY(ce, PHP_MAXMINDDB_METADATA_NS, metadata_methods);
metadata_ce = zend_register_internal_class(&ce TSRMLS_CC);
zend_declare_property_null(metadata_ce,
"binaryFormatMajorVersion",
sizeof("binaryFormatMajorVersion") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"binaryFormatMinorVersion",
sizeof("binaryFormatMinorVersion") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "buildEpoch", sizeof("buildEpoch") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"databaseType",
sizeof("databaseType") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "description", sizeof("description") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "ipVersion", sizeof("ipVersion") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "languages", sizeof("languages") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"nodeByteSize",
sizeof("nodeByteSize") - 1,
ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "nodeCount", sizeof("nodeCount") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(
metadata_ce, "recordSize", sizeof("recordSize") - 1, ZEND_ACC_PUBLIC);
zend_declare_property_null(metadata_ce,
"searchTreeSize",
sizeof("searchTreeSize") - 1,
ZEND_ACC_PUBLIC);
memcpy(&maxminddb_obj_handlers,
zend_get_std_object_handlers(),
sizeof(zend_object_handlers));
maxminddb_obj_handlers.clone_obj = NULL;
maxminddb_obj_handlers.offset = XtOffsetOf(maxminddb_obj, std);
maxminddb_obj_handlers.free_obj = maxminddb_free_storage;
zend_declare_class_constant_string(maxminddb_ce,
"MMDB_LIB_VERSION",
sizeof("MMDB_LIB_VERSION") - 1,
MMDB_lib_version() TSRMLS_CC);
return SUCCESS;
}
static PHP_MINFO_FUNCTION(maxminddb) {
php_info_print_table_start();
php_info_print_table_row(2, "MaxMind DB Reader", "enabled");
php_info_print_table_row(
2, "maxminddb extension version", PHP_MAXMINDDB_VERSION);
php_info_print_table_row(
2, "libmaxminddb library version", MMDB_lib_version());
php_info_print_table_end();
}
zend_module_entry maxminddb_module_entry = {STANDARD_MODULE_HEADER,
PHP_MAXMINDDB_EXTNAME,
NULL,
PHP_MINIT(maxminddb),
NULL,
NULL,
NULL,
PHP_MINFO(maxminddb),
PHP_MAXMINDDB_VERSION,
STANDARD_MODULE_PROPERTIES};
#ifdef COMPILE_DL_MAXMINDDB
ZEND_GET_MODULE(maxminddb)
#endif

View File

@ -0,0 +1,24 @@
/* MaxMind, Inc., licenses this file to you under the Apache License, Version
* 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
#include <zend_interfaces.h>
#ifndef PHP_MAXMINDDB_H
#define PHP_MAXMINDDB_H 1
#define PHP_MAXMINDDB_VERSION "1.10.1"
#define PHP_MAXMINDDB_EXTNAME "maxminddb"
extern zend_module_entry maxminddb_module_entry;
#define phpext_maxminddb_ptr &maxminddb_module_entry
#endif

View File

@ -0,0 +1,63 @@
<?xml version="1.0"?>
<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0"
xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
<name>maxminddb</name>
<channel>pecl.php.net</channel>
<summary>Reader for the MaxMind DB file format</summary>
<description>This is the PHP extension for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6).</description>
<lead>
<name>Greg Oschwald</name>
<user>oschwald</user>
<email>goschwald@maxmind.com</email>
<active>yes</active>
</lead>
<date>2021-04-14</date>
<version>
<release>1.10.1</release>
<api>1.10.1</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="https://github.com/maxmind/MaxMind-DB-Reader-php/blob/main/LICENSE">Apache License 2.0</license>
<notes>* Fix a `TypeError` exception in the pure PHP reader when using large
databases on 32-bit PHP builds with the `bcmath` extension. Reported
by dodo1708. GitHub #124.</notes>
<contents>
<dir name="/">
<file role="doc" name="LICENSE"/>
<file role="doc" name="CHANGELOG.md"/>
<file role="doc" name="README.md"/>
<dir name="ext">
<file role="src" name="config.m4"/>
<file role="src" name="config.w32"/>
<file role="src" name="maxminddb.c"/>
<file role="src" name="php_maxminddb.h"/>
<dir name="tests">
<file role="test" name="001-load.phpt"/>
<file role="test" name="002-final.phpt"/>
<file role="test" name="003-open-basedir.phpt"/>
</dir>
</dir>
</dir>
</contents>
<dependencies>
<required>
<php>
<min>7.2.0</min>
</php>
<pearinstaller>
<min>1.10.0</min>
</pearinstaller>
</required>
</dependencies>
<providesextension>maxminddb</providesextension>
<extsrcrelease />
</package>

View File

@ -0,0 +1,287 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Db;
use ArgumentCountError;
use BadMethodCallException;
use Exception;
use InvalidArgumentException;
use Tassos\Vendor\MaxMind\Db\Reader\Decoder;
use Tassos\Vendor\MaxMind\Db\Reader\InvalidDatabaseException;
use Tassos\Vendor\MaxMind\Db\Reader\Metadata;
use Tassos\Vendor\MaxMind\Db\Reader\Util;
use UnexpectedValueException;
/**
* Instances of this class provide a reader for the MaxMind DB format. IP
* addresses can be looked up using the get method.
*/
class Reader
{
/**
* @var int
*/
private static $DATA_SECTION_SEPARATOR_SIZE = 16;
/**
* @var string
*/
private static $METADATA_START_MARKER = "\xab\xcd\xefMaxMind.com";
/**
* @var int
*/
private static $METADATA_START_MARKER_LENGTH = 14;
/**
* @var int
*/
private static $METADATA_MAX_SIZE = 131072;
// 128 * 1024 = 128KiB
/**
* @var Decoder
*/
private $decoder;
/**
* @var resource
*/
private $fileHandle;
/**
* @var int
*/
private $fileSize;
/**
* @var int
*/
private $ipV4Start;
/**
* @var Metadata
*/
private $metadata;
/**
* Constructs a Reader for the MaxMind DB format. The file passed to it must
* be a valid MaxMind DB file such as a GeoIp2 database file.
*
* @param string $database
* the MaxMind DB file to use
*
* @throws InvalidArgumentException for invalid database path or unknown arguments
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*/
public function __construct(string $database)
{
if (\func_num_args() !== 1) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
}
$fileHandle = @\fopen($database, 'rb');
if ($fileHandle === \false) {
throw new InvalidArgumentException("The file \"{$database}\" does not exist or is not readable.");
}
$this->fileHandle = $fileHandle;
$fileSize = @\filesize($database);
if ($fileSize === \false) {
throw new UnexpectedValueException("Error determining the size of \"{$database}\".");
}
$this->fileSize = $fileSize;
$start = $this->findMetadataStart($database);
$metadataDecoder = new Decoder($this->fileHandle, $start);
[$metadataArray] = $metadataDecoder->decode($start);
$this->metadata = new Metadata($metadataArray);
$this->decoder = new Decoder($this->fileHandle, $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE);
$this->ipV4Start = $this->ipV4StartNode();
}
/**
* Retrieves the record for the IP address.
*
* @param string $ipAddress
* the IP address to look up
*
* @throws BadMethodCallException if this method is called on a closed database
* @throws InvalidArgumentException if something other than a single IP address is passed to the method
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*
* @return mixed the record for the IP address
*/
public function get(string $ipAddress)
{
if (\func_num_args() !== 1) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
}
[$record] = $this->getWithPrefixLen($ipAddress);
return $record;
}
/**
* Retrieves the record for the IP address and its associated network prefix length.
*
* @param string $ipAddress
* the IP address to look up
*
* @throws BadMethodCallException if this method is called on a closed database
* @throws InvalidArgumentException if something other than a single IP address is passed to the method
* @throws InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
*
* @return array an array where the first element is the record and the
* second the network prefix length for the record
*/
public function getWithPrefixLen(string $ipAddress) : array
{
if (\func_num_args() !== 1) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
}
if (!\is_resource($this->fileHandle)) {
throw new BadMethodCallException('Attempt to read from a closed MaxMind DB.');
}
[$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
if ($pointer === 0) {
return [null, $prefixLen];
}
return [$this->resolveDataPointer($pointer), $prefixLen];
}
private function findAddressInTree(string $ipAddress) : array
{
$packedAddr = @\inet_pton($ipAddress);
if ($packedAddr === \false) {
throw new InvalidArgumentException("The value \"{$ipAddress}\" is not a valid IP address.");
}
$rawAddress = \unpack('C*', $packedAddr);
$bitCount = \count($rawAddress) * 8;
// The first node of the tree is always node 0, at the beginning of the
// value
$node = 0;
$metadata = $this->metadata;
// Check if we are looking up an IPv4 address in an IPv6 tree. If this
// is the case, we can skip over the first 96 nodes.
if ($metadata->ipVersion === 6) {
if ($bitCount === 32) {
$node = $this->ipV4Start;
}
} elseif ($metadata->ipVersion === 4 && $bitCount === 128) {
throw new InvalidArgumentException("Error looking up {$ipAddress}. You attempted to look up an" . ' IPv6 address in an IPv4-only database.');
}
$nodeCount = $metadata->nodeCount;
for ($i = 0; $i < $bitCount && $node < $nodeCount; ++$i) {
$tempBit = 0xff & $rawAddress[($i >> 3) + 1];
$bit = 1 & $tempBit >> 7 - $i % 8;
$node = $this->readNode($node, $bit);
}
if ($node === $nodeCount) {
// Record is empty
return [0, $i];
}
if ($node > $nodeCount) {
// Record is a data pointer
return [$node, $i];
}
throw new InvalidDatabaseException('Invalid or corrupt database. Maximum search depth reached without finding a leaf node');
}
private function ipV4StartNode() : int
{
// If we have an IPv4 database, the start node is the first node
if ($this->metadata->ipVersion === 4) {
return 0;
}
$node = 0;
for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; ++$i) {
$node = $this->readNode($node, 0);
}
return $node;
}
private function readNode(int $nodeNumber, int $index) : int
{
$baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
switch ($this->metadata->recordSize) {
case 24:
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
[, $node] = \unpack('N', "\x00" . $bytes);
return $node;
case 28:
$bytes = Util::read($this->fileHandle, $baseOffset + 3 * $index, 4);
if ($index === 0) {
$middle = (0xf0 & \ord($bytes[3])) >> 4;
} else {
$middle = 0xf & \ord($bytes[0]);
}
[, $node] = \unpack('N', \chr($middle) . \substr($bytes, $index, 3));
return $node;
case 32:
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
[, $node] = \unpack('N', $bytes);
return $node;
default:
throw new InvalidDatabaseException('Unknown record size: ' . $this->metadata->recordSize);
}
}
/**
* @return mixed
*/
private function resolveDataPointer(int $pointer)
{
$resolved = $pointer - $this->metadata->nodeCount + $this->metadata->searchTreeSize;
if ($resolved >= $this->fileSize) {
throw new InvalidDatabaseException("The MaxMind DB file's search tree is corrupt");
}
[$data] = $this->decoder->decode($resolved);
return $data;
}
/*
* This is an extremely naive but reasonably readable implementation. There
* are much faster algorithms (e.g., Boyer-Moore) for this if speed is ever
* an issue, but I suspect it won't be.
*/
private function findMetadataStart(string $filename) : int
{
$handle = $this->fileHandle;
$fstat = \fstat($handle);
$fileSize = $fstat['size'];
$marker = self::$METADATA_START_MARKER;
$markerLength = self::$METADATA_START_MARKER_LENGTH;
$minStart = $fileSize - \min(self::$METADATA_MAX_SIZE, $fileSize);
for ($offset = $fileSize - $markerLength; $offset >= $minStart; --$offset) {
if (\fseek($handle, $offset) !== 0) {
break;
}
$value = \fread($handle, $markerLength);
if ($value === $marker) {
return $offset + $markerLength;
}
}
throw new InvalidDatabaseException("Error opening database file ({$filename}). " . 'Is this a valid MaxMind DB file?');
}
/**
* @throws InvalidArgumentException if arguments are passed to the method
* @throws BadMethodCallException if the database has been closed
*
* @return Metadata object for the database
*/
public function metadata() : Metadata
{
if (\func_num_args()) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
}
// Not technically required, but this makes it consistent with
// C extension and it allows us to change our implementation later.
if (!\is_resource($this->fileHandle)) {
throw new BadMethodCallException('Attempt to read from a closed MaxMind DB.');
}
return clone $this->metadata;
}
/**
* Closes the MaxMind DB and returns resources to the system.
*
* @throws Exception
* if an I/O error occurs
*/
public function close() : void
{
if (\func_num_args()) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args()));
}
if (!\is_resource($this->fileHandle)) {
throw new BadMethodCallException('Attempt to close a closed MaxMind DB.');
}
\fclose($this->fileHandle);
}
}

View File

@ -0,0 +1,275 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Db\Reader;
// @codingStandardsIgnoreLine
use RuntimeException;
class Decoder
{
/**
* @var resource
*/
private $fileStream;
/**
* @var int
*/
private $pointerBase;
/**
* @var float
*/
private $pointerBaseByteSize;
/**
* This is only used for unit testing.
*
* @var bool
*/
private $pointerTestHack;
/**
* @var bool
*/
private $switchByteOrder;
private const _EXTENDED = 0;
private const _POINTER = 1;
private const _UTF8_STRING = 2;
private const _DOUBLE = 3;
private const _BYTES = 4;
private const _UINT16 = 5;
private const _UINT32 = 6;
private const _MAP = 7;
private const _INT32 = 8;
private const _UINT64 = 9;
private const _UINT128 = 10;
private const _ARRAY = 11;
private const _CONTAINER = 12;
private const _END_MARKER = 13;
private const _BOOLEAN = 14;
private const _FLOAT = 15;
/**
* @param resource $fileStream
*/
public function __construct($fileStream, int $pointerBase = 0, bool $pointerTestHack = \false)
{
$this->fileStream = $fileStream;
$this->pointerBase = $pointerBase;
$this->pointerBaseByteSize = $pointerBase > 0 ? \log($pointerBase, 2) / 8 : 0;
$this->pointerTestHack = $pointerTestHack;
$this->switchByteOrder = $this->isPlatformLittleEndian();
}
public function decode(int $offset) : array
{
$ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
++$offset;
$type = $ctrlByte >> 5;
// Pointers are a special case, we don't read the next $size bytes, we
// use the size to determine the length of the pointer and then follow
// it.
if ($type === self::_POINTER) {
[$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
// for unit testing
if ($this->pointerTestHack) {
return [$pointer];
}
[$result] = $this->decode($pointer);
return [$result, $offset];
}
if ($type === self::_EXTENDED) {
$nextByte = \ord(Util::read($this->fileStream, $offset, 1));
$type = $nextByte + 7;
if ($type < 8) {
throw new InvalidDatabaseException('Something went horribly wrong in the decoder. An extended type ' . 'resolved to a type number < 8 (' . $type . ')');
}
++$offset;
}
[$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
return $this->decodeByType($type, $offset, $size);
}
private function decodeByType(int $type, int $offset, int $size) : array
{
switch ($type) {
case self::_MAP:
return $this->decodeMap($size, $offset);
case self::_ARRAY:
return $this->decodeArray($size, $offset);
case self::_BOOLEAN:
return [$this->decodeBoolean($size), $offset];
}
$newOffset = $offset + $size;
$bytes = Util::read($this->fileStream, $offset, $size);
switch ($type) {
case self::_BYTES:
case self::_UTF8_STRING:
return [$bytes, $newOffset];
case self::_DOUBLE:
$this->verifySize(8, $size);
return [$this->decodeDouble($bytes), $newOffset];
case self::_FLOAT:
$this->verifySize(4, $size);
return [$this->decodeFloat($bytes), $newOffset];
case self::_INT32:
return [$this->decodeInt32($bytes, $size), $newOffset];
case self::_UINT16:
case self::_UINT32:
case self::_UINT64:
case self::_UINT128:
return [$this->decodeUint($bytes, $size), $newOffset];
default:
throw new InvalidDatabaseException('Unknown or unexpected type: ' . $type);
}
}
private function verifySize(int $expected, int $actual) : void
{
if ($expected !== $actual) {
throw new InvalidDatabaseException("The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)");
}
}
private function decodeArray(int $size, int $offset) : array
{
$array = [];
for ($i = 0; $i < $size; ++$i) {
[$value, $offset] = $this->decode($offset);
$array[] = $value;
}
return [$array, $offset];
}
private function decodeBoolean(int $size) : bool
{
return $size !== 0;
}
private function decodeDouble(string $bytes) : float
{
// This assumes IEEE 754 doubles, but most (all?) modern platforms
// use them.
[, $double] = \unpack('E', $bytes);
return $double;
}
private function decodeFloat(string $bytes) : float
{
// This assumes IEEE 754 floats, but most (all?) modern platforms
// use them.
[, $float] = \unpack('G', $bytes);
return $float;
}
private function decodeInt32(string $bytes, int $size) : int
{
switch ($size) {
case 0:
return 0;
case 1:
case 2:
case 3:
$bytes = \str_pad($bytes, 4, "\x00", \STR_PAD_LEFT);
break;
case 4:
break;
default:
throw new InvalidDatabaseException("The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)");
}
[, $int] = \unpack('l', $this->maybeSwitchByteOrder($bytes));
return $int;
}
private function decodeMap(int $size, int $offset) : array
{
$map = [];
for ($i = 0; $i < $size; ++$i) {
[$key, $offset] = $this->decode($offset);
[$value, $offset] = $this->decode($offset);
$map[$key] = $value;
}
return [$map, $offset];
}
private function decodePointer(int $ctrlByte, int $offset) : array
{
$pointerSize = ($ctrlByte >> 3 & 0x3) + 1;
$buffer = Util::read($this->fileStream, $offset, $pointerSize);
$offset = $offset + $pointerSize;
switch ($pointerSize) {
case 1:
$packed = \chr($ctrlByte & 0x7) . $buffer;
[, $pointer] = \unpack('n', $packed);
$pointer += $this->pointerBase;
break;
case 2:
$packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
[, $pointer] = \unpack('N', $packed);
$pointer += $this->pointerBase + 2048;
break;
case 3:
$packed = \chr($ctrlByte & 0x7) . $buffer;
// It is safe to use 'N' here, even on 32 bit machines as the
// first bit is 0.
[, $pointer] = \unpack('N', $packed);
$pointer += $this->pointerBase + 526336;
break;
case 4:
// We cannot use unpack here as we might overflow on 32 bit
// machines
$pointerOffset = $this->decodeUint($buffer, $pointerSize);
$pointerBase = $this->pointerBase;
if (\PHP_INT_MAX - $pointerBase >= $pointerOffset) {
$pointer = $pointerOffset + $pointerBase;
} else {
throw new RuntimeException('The database offset is too large to be represented on your platform.');
}
break;
default:
throw new InvalidDatabaseException('Unexpected pointer size ' . $pointerSize);
}
return [$pointer, $offset];
}
// @phpstan-ignore-next-line
private function decodeUint(string $bytes, int $byteLength)
{
if ($byteLength === 0) {
return 0;
}
$integer = 0;
// PHP integers are signed. PHP_INT_SIZE - 1 is the number of
// complete bytes that can be converted to an integer. However,
// we can convert another byte if the leading bit is zero.
$useRealInts = $byteLength <= \PHP_INT_SIZE - 1 || $byteLength === \PHP_INT_SIZE && (\ord($bytes[0]) & 0x80) === 0;
for ($i = 0; $i < $byteLength; ++$i) {
$part = \ord($bytes[$i]);
// We only use gmp or bcmath if the final value is too big
if ($useRealInts) {
$integer = ($integer << 8) + $part;
} elseif (\extension_loaded('gmp')) {
$integer = \gmp_strval(\gmp_add(\gmp_mul((string) $integer, '256'), $part));
} elseif (\extension_loaded('bcmath')) {
$integer = \bcadd(\bcmul((string) $integer, '256'), (string) $part);
} else {
throw new RuntimeException('The gmp or bcmath extension must be installed to read this database.');
}
}
return $integer;
}
private function sizeFromCtrlByte(int $ctrlByte, int $offset) : array
{
$size = $ctrlByte & 0x1f;
if ($size < 29) {
return [$size, $offset];
}
$bytesToRead = $size - 28;
$bytes = Util::read($this->fileStream, $offset, $bytesToRead);
if ($size === 29) {
$size = 29 + \ord($bytes);
} elseif ($size === 30) {
[, $adjust] = \unpack('n', $bytes);
$size = 285 + $adjust;
} else {
[, $adjust] = \unpack('N', "\x00" . $bytes);
$size = $adjust + 65821;
}
return [$size, $offset + $bytesToRead];
}
private function maybeSwitchByteOrder(string $bytes) : string
{
return $this->switchByteOrder ? \strrev($bytes) : $bytes;
}
private function isPlatformLittleEndian() : bool
{
$testint = 0xff;
$packed = \pack('S', $testint);
return $testint === \current(\unpack('v', $packed));
}
}

View File

@ -0,0 +1,12 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Db\Reader;
use Exception;
/**
* This class should be thrown when unexpected data is found in the database.
*/
class InvalidDatabaseException extends Exception
{
}

View File

@ -0,0 +1,104 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Db\Reader;
use ArgumentCountError;
/**
* This class provides the metadata for the MaxMind DB file.
*/
class Metadata
{
/**
* This is an unsigned 16-bit integer indicating the major version number
* for the database's binary format.
*
* @var int
*/
public $binaryFormatMajorVersion;
/**
* This is an unsigned 16-bit integer indicating the minor version number
* for the database's binary format.
*
* @var int
*/
public $binaryFormatMinorVersion;
/**
* This is an unsigned 64-bit integer that contains the database build
* timestamp as a Unix epoch value.
*
* @var int
*/
public $buildEpoch;
/**
* This is a string that indicates the structure of each data record
* associated with an IP address. The actual definition of these
* structures is left up to the database creator.
*
* @var string
*/
public $databaseType;
/**
* This key will always point to a map (associative array). The keys of
* that map will be language codes, and the values will be a description
* in that language as a UTF-8 string. May be undefined for some
* databases.
*
* @var array
*/
public $description;
/**
* This is an unsigned 16-bit integer which is always 4 or 6. It indicates
* whether the database contains IPv4 or IPv6 address data.
*
* @var int
*/
public $ipVersion;
/**
* An array of strings, each of which is a language code. A given record
* may contain data items that have been localized to some or all of
* these languages. This may be undefined.
*
* @var array
*/
public $languages;
/**
* @var int
*/
public $nodeByteSize;
/**
* This is an unsigned 32-bit integer indicating the number of nodes in
* the search tree.
*
* @var int
*/
public $nodeCount;
/**
* This is an unsigned 16-bit integer. It indicates the number of bits in a
* record in the search tree. Note that each node consists of two records.
*
* @var int
*/
public $recordSize;
/**
* @var int
*/
public $searchTreeSize;
public function __construct(array $metadata)
{
if (\func_num_args() !== 1) {
throw new ArgumentCountError(\sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args()));
}
$this->binaryFormatMajorVersion = $metadata['binary_format_major_version'];
$this->binaryFormatMinorVersion = $metadata['binary_format_minor_version'];
$this->buildEpoch = $metadata['build_epoch'];
$this->databaseType = $metadata['database_type'];
$this->languages = $metadata['languages'];
$this->description = $metadata['description'];
$this->ipVersion = $metadata['ip_version'];
$this->nodeCount = $metadata['node_count'];
$this->recordSize = $metadata['record_size'];
$this->nodeByteSize = $this->recordSize / 4;
$this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Db\Reader;
class Util
{
/**
* @param resource $stream
*/
public static function read($stream, int $offset, int $numberOfBytes) : string
{
if ($numberOfBytes === 0) {
return '';
}
if (\fseek($stream, $offset) === 0) {
$value = \fread($stream, $numberOfBytes);
// We check that the number of bytes read is equal to the number
// asked for. We use ftell as getting the length of $value is
// much slower.
if ($value !== \false && \ftell($stream) - $offset === $numberOfBytes) {
return $value;
}
}
throw new InvalidDatabaseException('The MaxMind DB file contains bad data');
}
}

View File

@ -0,0 +1,56 @@
#!/bin/bash
set -eu -o pipefail
changelog=$(cat CHANGELOG.md)
regex='
([0-9]+\.[0-9]+\.[0-9]+) \(([0-9]{4}-[0-9]{2}-[0-9]{2})\)
-*
((.|
)*)
'
if [[ ! $changelog =~ $regex ]]; then
echo "Could not find date line in change log!"
exit 1
fi
version="${BASH_REMATCH[1]}"
date="${BASH_REMATCH[2]}"
notes="$(echo "${BASH_REMATCH[3]}" | sed -n -E '/^[0-9]+\.[0-9]+\.[0-9]+/,$!p')"
if [[ "$date" != $(date +"%Y-%m-%d") ]]; then
echo "$date is not today!"
exit 1
fi
tag="v$version"
if [ -n "$(git status --porcelain)" ]; then
echo ". is not clean." >&2
exit 1
fi
php composer.phar self-update
php composer.phar update
./vendor/bin/phpunit
echo "Release notes for $tag:"
echo "$notes"
read -e -p "Commit changes and push to origin? " should_push
if [ "$should_push" != "y" ]; then
echo "Aborting"
exit 1
fi
git push
gh release create --target "$(git branch --show-current)" -t "$version" -n "$notes" "$tag"
git push --tags

View File

@ -0,0 +1,7 @@
parameters:
level: 6
paths:
- src
- tests
checkMissingIterableValueType: false

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* This class represents an error authenticating.
*/
class AuthenticationException extends InvalidRequestException
{
}

View File

@ -0,0 +1,36 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* This class represents an HTTP transport error.
*/
class HttpException extends WebServiceException
{
/**
* The URI queried.
*
* @var string
*/
private $uri;
/**
* @param string $message a message describing the error
* @param int $httpStatus the HTTP status code of the response
* @param string $uri the URI used in the request
* @param \Exception $previous the previous exception, if any
*/
public function __construct(string $message, int $httpStatus, string $uri, \Exception $previous = null)
{
$this->uri = $uri;
parent::__construct($message, $httpStatus, $previous);
}
public function getUri() : string
{
return $this->uri;
}
public function getStatusCode() : int
{
return $this->getCode();
}
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* Thrown when the account is out of credits.
*/
class InsufficientFundsException extends InvalidRequestException
{
}

View File

@ -0,0 +1,13 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* This class represents an error in creating the request to be sent to the
* web service. For example, if the array cannot be encoded as JSON or if there
* is a missing or invalid field.
*/
class InvalidInputException extends WebServiceException
{
}

View File

@ -0,0 +1,33 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* Thrown when a MaxMind web service returns an error relating to the request.
*/
class InvalidRequestException extends HttpException
{
/**
* The code returned by the MaxMind web service.
*
* @var string
*/
private $error;
/**
* @param string $message the exception message
* @param string $error the error code returned by the MaxMind web service
* @param int $httpStatus the HTTP status code of the response
* @param string $uri the URI queries
* @param \Exception $previous the previous exception, if any
*/
public function __construct(string $message, string $error, int $httpStatus, string $uri, \Exception $previous = null)
{
$this->error = $error;
parent::__construct($message, $httpStatus, $uri, $previous);
}
public function getErrorCode() : string
{
return $this->error;
}
}

View File

@ -0,0 +1,8 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
class IpAddressNotFoundException extends InvalidRequestException
{
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* This exception is thrown when the service requires permission to access.
*/
class PermissionRequiredException extends InvalidRequestException
{
}

View File

@ -0,0 +1,11 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\Exception;
/**
* This class represents a generic web service error.
*/
class WebServiceException extends \Exception
{
}

View File

@ -0,0 +1,354 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\WebService;
use Tassos\Vendor\Composer\CaBundle\CaBundle;
use Tassos\Vendor\MaxMind\Exception\AuthenticationException;
use Tassos\Vendor\MaxMind\Exception\HttpException;
use Tassos\Vendor\MaxMind\Exception\InsufficientFundsException;
use Tassos\Vendor\MaxMind\Exception\InvalidInputException;
use Tassos\Vendor\MaxMind\Exception\InvalidRequestException;
use Tassos\Vendor\MaxMind\Exception\IpAddressNotFoundException;
use Tassos\Vendor\MaxMind\Exception\PermissionRequiredException;
use Tassos\Vendor\MaxMind\Exception\WebServiceException;
use Tassos\Vendor\MaxMind\WebService\Http\RequestFactory;
/**
* This class is not intended to be used directly by an end-user of a
* MaxMind web service. Please use the appropriate client API for the service
* that you are using.
*
* @internal
*/
class Client
{
public const VERSION = '0.2.0';
/**
* @var string|null
*/
private $caBundle;
/**
* @var float|null
*/
private $connectTimeout;
/**
* @var string
*/
private $host = 'api.maxmind.com';
/**
* @var bool
*/
private $useHttps = \true;
/**
* @var RequestFactory
*/
private $httpRequestFactory;
/**
* @var string
*/
private $licenseKey;
/**
* @var string|null
*/
private $proxy;
/**
* @var float|null
*/
private $timeout;
/**
* @var string
*/
private $userAgentPrefix;
/**
* @var int
*/
private $accountId;
/**
* @param int $accountId your MaxMind account ID
* @param string $licenseKey your MaxMind license key
* @param array $options an array of options. Possible keys:
* * `host` - The host to use when connecting to the web service.
* * `useHttps` - A boolean flag for sending the request via https.(True by default)
* * `userAgent` - The prefix of the User-Agent to use in the request.
* * `caBundle` - The bundle of CA root certificates to use in the request.
* * `connectTimeout` - The connect timeout to use for the request.
* * `timeout` - The timeout to use for the request.
* * `proxy` - The HTTP proxy to use. May include a schema, port,
* username, and password, e.g., `http://username:password@127.0.0.1:10`.
*/
public function __construct(int $accountId, string $licenseKey, array $options = [])
{
$this->accountId = $accountId;
$this->licenseKey = $licenseKey;
$this->httpRequestFactory = isset($options['httpRequestFactory']) ? $options['httpRequestFactory'] : new RequestFactory();
if (isset($options['host'])) {
$this->host = $options['host'];
}
if (isset($options['useHttps'])) {
$this->useHttps = $options['useHttps'];
}
if (isset($options['userAgent'])) {
$this->userAgentPrefix = $options['userAgent'] . ' ';
}
$this->caBundle = isset($options['caBundle']) ? $this->caBundle = $options['caBundle'] : $this->getCaBundle();
if (isset($options['connectTimeout'])) {
$this->connectTimeout = $options['connectTimeout'];
}
if (isset($options['timeout'])) {
$this->timeout = $options['timeout'];
}
if (isset($options['proxy'])) {
$this->proxy = $options['proxy'];
}
}
/**
* @param string $service name of the service querying
* @param string $path the URI path to use
* @param array $input the data to be posted as JSON
*
* @throws InvalidInputException when the request has missing or invalid
* data
* @throws AuthenticationException when there is an issue authenticating the
* request
* @throws InsufficientFundsException when your account is out of funds
* @throws InvalidRequestException when the request is invalid for some
* other reason, e.g., invalid JSON in the POST.
* @throws HttpException when an unexpected HTTP error occurs
* @throws WebServiceException when some other error occurs. This also
* serves as the base class for the above exceptions.
*
* @return array|null The decoded content of a successful response
*/
public function post(string $service, string $path, array $input) : ?array
{
$requestBody = \json_encode($input);
if ($requestBody === \false) {
throw new InvalidInputException('Error encoding input as JSON: ' . $this->jsonErrorDescription());
}
$request = $this->createRequest($path, ['Content-Type: application/json']);
[$statusCode, $contentType, $responseBody] = $request->post($requestBody);
return $this->handleResponse($statusCode, $contentType, $responseBody, $service, $path);
}
public function get(string $service, string $path) : ?array
{
$request = $this->createRequest($path);
[$statusCode, $contentType, $responseBody] = $request->get();
return $this->handleResponse($statusCode, $contentType, $responseBody, $service, $path);
}
private function userAgent() : string
{
$curlVersion = \curl_version();
return $this->userAgentPrefix . 'MaxMind-WS-API/' . self::VERSION . ' PHP/' . \PHP_VERSION . ' curl/' . $curlVersion['version'];
}
private function createRequest(string $path, array $headers = []) : Http\Request
{
\array_push($headers, 'Authorization: Basic ' . \base64_encode($this->accountId . ':' . $this->licenseKey), 'Accept: application/json');
return $this->httpRequestFactory->request($this->urlFor($path), ['caBundle' => $this->caBundle, 'connectTimeout' => $this->connectTimeout, 'headers' => $headers, 'proxy' => $this->proxy, 'timeout' => $this->timeout, 'userAgent' => $this->userAgent()]);
}
/**
* @param int $statusCode the HTTP status code of the response
* @param string|null $contentType the Content-Type of the response
* @param string|null $responseBody the response body
* @param string $service the name of the service
* @param string $path the path used in the request
*
* @throws AuthenticationException when there is an issue authenticating the
* request
* @throws InsufficientFundsException when your account is out of funds
* @throws InvalidRequestException when the request is invalid for some
* other reason, e.g., invalid JSON in the POST.
* @throws HttpException when an unexpected HTTP error occurs
* @throws WebServiceException when some other error occurs. This also
* serves as the base class for the above exceptions
*
* @return array|null The decoded content of a successful response
*/
private function handleResponse(int $statusCode, ?string $contentType, ?string $responseBody, string $service, string $path) : ?array
{
if ($statusCode >= 400 && $statusCode <= 499) {
$this->handle4xx($statusCode, $contentType, $responseBody, $service, $path);
} elseif ($statusCode >= 500) {
$this->handle5xx($statusCode, $service, $path);
} elseif ($statusCode !== 200 && $statusCode !== 204) {
$this->handleUnexpectedStatus($statusCode, $service, $path);
}
return $this->handleSuccess($statusCode, $responseBody, $service);
}
/**
* @return string describing the JSON error
*/
private function jsonErrorDescription() : string
{
$errno = \json_last_error();
switch ($errno) {
case \JSON_ERROR_DEPTH:
return 'The maximum stack depth has been exceeded.';
case \JSON_ERROR_STATE_MISMATCH:
return 'Invalid or malformed JSON.';
case \JSON_ERROR_CTRL_CHAR:
return 'Control character error.';
case \JSON_ERROR_SYNTAX:
return 'Syntax error.';
case \JSON_ERROR_UTF8:
return 'Malformed UTF-8 characters.';
default:
return "Other JSON error ({$errno}).";
}
}
/**
* @param string $path the path to use in the URL
*
* @return string the constructed URL
*/
private function urlFor(string $path) : string
{
return ($this->useHttps ? 'https://' : 'http://') . $this->host . $path;
}
/**
* @param int $statusCode the HTTP status code
* @param string|null $contentType the response content-type
* @param string|null $body the response body
* @param string $service the service name
* @param string $path the path used in the request
*
* @throws AuthenticationException
* @throws HttpException
* @throws InsufficientFundsException
* @throws InvalidRequestException
*/
private function handle4xx(int $statusCode, ?string $contentType, ?string $body, string $service, string $path) : void
{
if ($body === null || $body === '') {
throw new HttpException("Received a {$statusCode} error for {$service} with no body", $statusCode, $this->urlFor($path));
}
if ($contentType === null || !\strstr($contentType, 'json')) {
throw new HttpException("Received a {$statusCode} error for {$service} with " . 'the following body: ' . $body, $statusCode, $this->urlFor($path));
}
$message = \json_decode($body, \true);
if ($message === null) {
throw new HttpException("Received a {$statusCode} error for {$service} but could " . 'not decode the response as JSON: ' . $this->jsonErrorDescription() . ' Body: ' . $body, $statusCode, $this->urlFor($path));
}
if (!isset($message['code']) || !isset($message['error'])) {
throw new HttpException('Error response contains JSON but it does not ' . 'specify code or error keys: ' . $body, $statusCode, $this->urlFor($path));
}
$this->handleWebServiceError($message['error'], $message['code'], $statusCode, $path);
}
/**
* @param string $message the error message from the web service
* @param string $code the error code from the web service
* @param int $statusCode the HTTP status code
* @param string $path the path used in the request
*
* @throws AuthenticationException
* @throws InvalidRequestException
* @throws InsufficientFundsException
*/
private function handleWebServiceError(string $message, string $code, int $statusCode, string $path) : void
{
switch ($code) {
case 'IP_ADDRESS_NOT_FOUND':
case 'IP_ADDRESS_RESERVED':
throw new IpAddressNotFoundException($message, $code, $statusCode, $this->urlFor($path));
case 'ACCOUNT_ID_REQUIRED':
case 'ACCOUNT_ID_UNKNOWN':
case 'AUTHORIZATION_INVALID':
case 'LICENSE_KEY_REQUIRED':
case 'USER_ID_REQUIRED':
case 'USER_ID_UNKNOWN':
throw new AuthenticationException($message, $code, $statusCode, $this->urlFor($path));
case 'OUT_OF_QUERIES':
case 'INSUFFICIENT_FUNDS':
throw new InsufficientFundsException($message, $code, $statusCode, $this->urlFor($path));
case 'PERMISSION_REQUIRED':
throw new PermissionRequiredException($message, $code, $statusCode, $this->urlFor($path));
default:
throw new InvalidRequestException($message, $code, $statusCode, $this->urlFor($path));
}
}
/**
* @param int $statusCode the HTTP status code
* @param string $service the service name
* @param string $path the URI path used in the request
*
* @throws HttpException
*/
private function handle5xx(int $statusCode, string $service, string $path) : void
{
throw new HttpException("Received a server error ({$statusCode}) for {$service}", $statusCode, $this->urlFor($path));
}
/**
* @param int $statusCode the HTTP status code
* @param string $service the service name
* @param string $path the URI path used in the request
*
* @throws HttpException
*/
private function handleUnexpectedStatus(int $statusCode, string $service, string $path) : void
{
throw new HttpException('Received an unexpected HTTP status ' . "({$statusCode}) for {$service}", $statusCode, $this->urlFor($path));
}
/**
* @param int $statusCode the HTTP status code
* @param string|null $body the successful request body
* @param string $service the service name
*
* @throws WebServiceException if a response body is included but not
* expected, or is not expected but not
* included, or is expected and included
* but cannot be decoded as JSON
*
* @return array|null the decoded request body
*/
private function handleSuccess(int $statusCode, ?string $body, string $service) : ?array
{
// A 204 should have no response body
if ($statusCode === 204) {
if ($body !== null && $body !== '') {
throw new WebServiceException("Received a 204 response for {$service} along with an " . "unexpected HTTP body: {$body}");
}
return null;
}
// A 200 should have a valid JSON body
if ($body === null || $body === '') {
throw new WebServiceException("Received a 200 response for {$service} but did not " . 'receive a HTTP body.');
}
$decodedContent = \json_decode($body, \true);
if ($decodedContent === null) {
throw new WebServiceException("Received a 200 response for {$service} but could " . 'not decode the response as JSON: ' . $this->jsonErrorDescription() . ' Body: ' . $body);
}
return $decodedContent;
}
private function getCaBundle() : ?string
{
$curlVersion = \curl_version();
// On OS X, when the SSL version is "SecureTransport", the system's
// keychain will be used.
if ($curlVersion['ssl_version'] === 'SecureTransport') {
return null;
}
$cert = CaBundle::getSystemCaRootBundlePath();
// Check if the cert is inside a phar. If so, we need to copy the cert
// to a temp file so that curl can see it.
if (\substr($cert, 0, 7) === 'phar://') {
$tempDir = \sys_get_temp_dir();
$newCert = \tempnam($tempDir, 'geoip2-');
if ($newCert === \false) {
throw new \RuntimeException("Unable to create temporary file in {$tempDir}");
}
if (!\copy($cert, $newCert)) {
throw new \RuntimeException("Could not copy {$cert} to {$newCert}: " . \var_export(\error_get_last(), \true));
}
// We use a shutdown function rather than the destructor as the
// destructor isn't called on a fatal error such as an uncaught
// exception.
\register_shutdown_function(function () use($newCert) {
\unlink($newCert);
});
$cert = $newCert;
}
if (!\file_exists($cert)) {
throw new \RuntimeException("CA cert does not exist at {$cert}");
}
return $cert;
}
}

View File

@ -0,0 +1,108 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\WebService\Http;
use Tassos\Vendor\MaxMind\Exception\HttpException;
/**
* This class is for internal use only. Semantic versioning does not not apply.
*
* @internal
*/
class CurlRequest implements Request
{
/**
* @var \CurlHandle
*/
private $ch;
/**
* @var string
*/
private $url;
/**
* @var array
*/
private $options;
public function __construct(string $url, array $options)
{
$this->url = $url;
$this->options = $options;
$this->ch = $options['curlHandle'];
}
/**
* @throws HttpException
*/
public function post(string $body) : array
{
$curl = $this->createCurl();
\curl_setopt($curl, \CURLOPT_POST, \true);
\curl_setopt($curl, \CURLOPT_POSTFIELDS, $body);
return $this->execute($curl);
}
public function get() : array
{
$curl = $this->createCurl();
\curl_setopt($curl, \CURLOPT_HTTPGET, \true);
return $this->execute($curl);
}
/**
* @return \CurlHandle
*/
private function createCurl()
{
\curl_reset($this->ch);
$opts = [];
$opts[\CURLOPT_URL] = $this->url;
if (!empty($this->options['caBundle'])) {
$opts[\CURLOPT_CAINFO] = $this->options['caBundle'];
}
$opts[\CURLOPT_ENCODING] = '';
$opts[\CURLOPT_SSL_VERIFYHOST] = 2;
$opts[\CURLOPT_FOLLOWLOCATION] = \false;
$opts[\CURLOPT_SSL_VERIFYPEER] = \true;
$opts[\CURLOPT_RETURNTRANSFER] = \true;
$opts[\CURLOPT_HTTPHEADER] = $this->options['headers'];
$opts[\CURLOPT_USERAGENT] = $this->options['userAgent'];
$opts[\CURLOPT_PROXY] = $this->options['proxy'];
// The defined()s are here as the *_MS opts are not available on older
// cURL versions
$connectTimeout = $this->options['connectTimeout'];
if (\defined('CURLOPT_CONNECTTIMEOUT_MS')) {
$opts[\CURLOPT_CONNECTTIMEOUT_MS] = \ceil($connectTimeout * 1000);
} else {
$opts[\CURLOPT_CONNECTTIMEOUT] = \ceil($connectTimeout);
}
$timeout = $this->options['timeout'];
if (\defined('CURLOPT_TIMEOUT_MS')) {
$opts[\CURLOPT_TIMEOUT_MS] = \ceil($timeout * 1000);
} else {
$opts[\CURLOPT_TIMEOUT] = \ceil($timeout);
}
\curl_setopt_array($this->ch, $opts);
return $this->ch;
}
/**
* @param \CurlHandle $curl
*
* @throws HttpException
*/
private function execute($curl) : array
{
$body = \curl_exec($curl);
if ($errno = \curl_errno($curl)) {
$errorMessage = \curl_error($curl);
throw new HttpException("cURL error ({$errno}): {$errorMessage}", 0, $this->url);
}
$statusCode = \curl_getinfo($curl, \CURLINFO_HTTP_CODE);
$contentType = \curl_getinfo($curl, \CURLINFO_CONTENT_TYPE);
return [
$statusCode,
// The PHP docs say "Content-Type: of the requested document. NULL
// indicates server did not send valid Content-Type: header" for
// CURLINFO_CONTENT_TYPE. However, it will return FALSE if no header
// is set. To keep our types simple, we return null in this case.
$contentType === \false ? null : $contentType,
$body,
];
}
}

View File

@ -0,0 +1,16 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\WebService\Http;
/**
* Interface Request.
*
* @internal
*/
interface Request
{
public function __construct(string $url, array $options);
public function post(string $body) : array;
public function get() : array;
}

View File

@ -0,0 +1,42 @@
<?php
declare (strict_types=1);
namespace Tassos\Vendor\MaxMind\WebService\Http;
/**
* Class RequestFactory.
*
* @internal
*/
class RequestFactory
{
/**
* Keep the cURL resource here, so that if there are multiple API requests
* done the connection is kept alive, SSL resumption can be used
* etcetera.
*
* @var \CurlHandle|null
*/
private $ch;
public function __destruct()
{
if (!empty($this->ch)) {
\curl_close($this->ch);
}
}
/**
* @return \CurlHandle
*/
private function getCurlHandle()
{
if (empty($this->ch)) {
$this->ch = \curl_init();
}
return $this->ch;
}
public function request(string $url, array $options) : Request
{
$options['curlHandle'] = $this->getCurlHandle();
return new CurlRequest($url, $options);
}
}

View File

@ -0,0 +1,37 @@
<?php
// scoper-autoload.php @generated by PhpScoper
// Backup the autoloaded Composer files
if (isset($GLOBALS['__composer_autoload_files'])) {
$existingComposerAutoloadFiles = $GLOBALS['__composer_autoload_files'];
}
$loader = require_once __DIR__.'/autoload.php';
// Ensure InstalledVersions is available
$installedVersionsPath = __DIR__.'/composer/InstalledVersions.php';
if (file_exists($installedVersionsPath)) require_once $installedVersionsPath;
// Restore the backup
if (isset($existingComposerAutoloadFiles)) {
$GLOBALS['__composer_autoload_files'] = $existingComposerAutoloadFiles;
} else {
unset($GLOBALS['__composer_autoload_files']);
}
// Class aliases. For more information see:
// https://github.com/humbug/php-scoper/blob/master/docs/further-reading.md#class-aliases
if (!function_exists('humbug_phpscoper_expose_class')) {
function humbug_phpscoper_expose_class(string $exposed, string $prefixed): void {
if (!class_exists($exposed, false) && !interface_exists($exposed, false) && !trait_exists($exposed, false)) {
spl_autoload_call($prefixed);
}
}
}
humbug_phpscoper_expose_class('ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d', 'Tassos\Vendor\ComposerAutoloaderInit087ac1c88c9dd7b872309175c7c60d7d');
// Function aliases. For more information see:
// https://github.com/humbug/php-scoper/blob/master/docs/further-reading.md#function-aliases
if (!function_exists('mmdb_autoload')) { function mmdb_autoload() { return \Tassos\Vendor\mmdb_autoload(...func_get_args()); } }
return $loader;

View File

@ -0,0 +1,4 @@
tree: Yes
deprecated: Yes
accessLevels: [public]
todo: Yes

View File

@ -0,0 +1,63 @@
#!/bin/sh
# where's the source files?
SRC='src'
# for what branch to trigger
BRANCH='master'
# github repo
REPO='splitbrain/php-archive'
# ---- About -------------------------------------------------------
#
# This script use apigen to generate the documentation for the
# repository configured above. When run locally, the documentation
# will be placed in the 'docs' folder.
# However this script can also be run from travis. This requires
# the setup of a secret token as described at http://bit.ly/1MNbPn0
#
# Additional configuration can be done within an apigen.neon file
#
# ---- no modifications below ---------------------------------------
# when on travis, build outside of repository, otherwise locally
if [ -z "$TRAVIS" ]; then
DST='docs'
else
DST='../gh-pages'
if [ "$TRAVIS_PHP_VERSION" != '7.2' ]; then exit; fi
if [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then exit; fi
if [ "$TRAVIS_PULL_REQUEST" != 'false' ]; then exit; fi
if [ -z "$GH_TOKEN" ]; then
echo "GH_TOKEN not set! See: http://bit.ly/1MNbPn0"
exit
fi
fi
# Get ApiGen.phar
wget http://www.apigen.org/apigen.phar -O apigen.phar
# Generate SDK Docs
php apigen.phar generate --template-theme="bootstrap" -s $SRC -d $DST
### if we're not on travis, we're done
if [ -z "$TRAVIS" ]; then exit; fi
# go to the generated docs
cd $DST || exit
# Set identity
git config --global user.email "travis@travis-ci.org"
git config --global user.name "Travis"
# Add branch
git init
git remote add origin https://${GH_TOKEN}@github.com/${REPO}.git > /dev/null
git checkout -B gh-pages
# Push generated files
git add .
git commit -m "Docs updated by Travis"
git push origin gh-pages -fq > /dev/null

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Test Suite">
<directory suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="false">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,122 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
abstract class Archive
{
const COMPRESS_AUTO = -1;
const COMPRESS_NONE = 0;
const COMPRESS_GZIP = 1;
const COMPRESS_BZIP = 2;
/** @var callable */
protected $callback;
/**
* Set the compression level and type
*
* @param int $level Compression level (0 to 9)
* @param int $type Type of compression to use (use COMPRESS_* constants)
* @throws ArchiveIllegalCompressionException
*/
public abstract function setCompression($level = 9, $type = Archive::COMPRESS_AUTO);
/**
* Open an existing archive file for reading
*
* @param string $file
* @throws ArchiveIOException
*/
public abstract function open($file);
/**
* Read the contents of an archive
*
* This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
*
* The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
* Reopen the file with open() again if you want to do additional operations
*
* @return FileInfo[]
*/
public abstract function contents();
/**
* Extract an existing archive
*
* The $strip parameter allows you to strip a certain number of path components from the filenames
* found in the archive file, similar to the --strip-components feature of GNU tar. This is triggered when
* an integer is passed as $strip.
* Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
* the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
*
* By default this will extract all files found in the archive. You can restrict the output using the $include
* and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
* $include is set, only files that match this expression will be extracted. Files that match the $exclude
* expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
* stripped filenames as described above.
*
* The archive is closed afterwards. Reopen the file with open() again if you want to do additional operations
*
* @param string $outdir the target directory for extracting
* @param int|string $strip either the number of path components or a fixed prefix to strip
* @param string $exclude a regular expression of files to exclude
* @param string $include a regular expression of files to include
* @throws ArchiveIOException
* @return array
*/
public abstract function extract($outdir, $strip = '', $exclude = '', $include = '');
/**
* Create a new archive file
*
* If $file is empty, the archive file will be created in memory
*
* @param string $file
*/
public abstract function create($file = '');
/**
* Add a file to the current archive using an existing file in the filesystem
*
* @param string $file path to the original file
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
* @throws ArchiveIOException
*/
public abstract function addFile($file, $fileinfo = '');
/**
* Add a file to the current archive using the given $data as content
*
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
* @param string $data binary content of the file to add
* @throws ArchiveIOException
*/
public abstract function addData($fileinfo, $data);
/**
* Close the archive, close all file handles
*
* After a call to this function no more data can be added to the archive, for
* read access no reading is allowed anymore
*/
public abstract function close();
/**
* Returns the created in-memory archive data
*
* This implicitly calls close() on the Archive
*/
public abstract function getArchive();
/**
* Save the created in-memory archive data
*
* Note: It is more memory effective to specify the filename in the create() function and
* let the library work on the new file directly.
*
* @param string $file
*/
public abstract function save($file);
/**
* Set a callback function to be called whenever a file is added or extracted.
*
* The callback is called with a FileInfo object as parameter. You can use this to show progress
* info during an operation.
*
* @param callable $callback
*/
public function setCallback($callback)
{
$this->callback = $callback;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* The archive is unreadable
*/
class ArchiveCorruptedException extends \Exception
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* Read/Write Errors
*/
class ArchiveIOException extends \Exception
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* Bad or unsupported compression settings requested
*/
class ArchiveIllegalCompressionException extends \Exception
{
}

View File

@ -0,0 +1,323 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* Class FileInfo
*
* stores meta data about a file in an Archive
*
* @author Andreas Gohr <andi@splitbrain.org>
* @package splitbrain\PHPArchive
* @license MIT
*/
class FileInfo
{
protected $isdir = \false;
protected $path = '';
protected $size = 0;
protected $csize = 0;
protected $mtime = 0;
protected $mode = 0664;
protected $owner = '';
protected $group = '';
protected $uid = 0;
protected $gid = 0;
protected $comment = '';
/**
* initialize dynamic defaults
*
* @param string $path The path of the file, can also be set later through setPath()
*/
public function __construct($path = '')
{
$this->mtime = \time();
$this->setPath($path);
}
/**
* Handle calls to deprecated methods
*
* @param string $name
* @param array $arguments
* @return mixed
*/
public function __call($name, $arguments)
{
if ($name === 'match') {
\trigger_error('FileInfo::match() is deprecated, use FileInfo::matchExpression() instead.', \E_USER_NOTICE);
return \call_user_func_array([$this, $name], $arguments);
}
\trigger_error('Call to undefined method FileInfo::' . $name . '()', \E_USER_ERROR);
return null;
}
/**
* Factory to build FileInfo from existing file or directory
*
* @param string $path path to a file on the local file system
* @param string $as optional path to use inside the archive
* @throws FileInfoException
* @return FileInfo
*/
public static function fromPath($path, $as = '')
{
\clearstatcache(\false, $path);
if (!\file_exists($path)) {
throw new FileInfoException("{$path} does not exist");
}
$stat = \stat($path);
$file = new FileInfo();
$file->setPath($path);
$file->setIsdir(\is_dir($path));
$file->setMode(\fileperms($path));
$file->setOwner(\fileowner($path));
$file->setGroup(\filegroup($path));
$file->setSize(\filesize($path));
$file->setUid($stat['uid']);
$file->setGid($stat['gid']);
$file->setMtime($stat['mtime']);
if ($as) {
$file->setPath($as);
}
return $file;
}
/**
* @return int the filesize. always 0 for directories
*/
public function getSize()
{
if ($this->isdir) {
return 0;
}
return $this->size;
}
/**
* @param int $size
*/
public function setSize($size)
{
$this->size = $size;
}
/**
* @return int
*/
public function getCompressedSize()
{
return $this->csize;
}
/**
* @param int $csize
*/
public function setCompressedSize($csize)
{
$this->csize = $csize;
}
/**
* @return int
*/
public function getMtime()
{
return $this->mtime;
}
/**
* @param int $mtime
*/
public function setMtime($mtime)
{
$this->mtime = $mtime;
}
/**
* @return int
*/
public function getGid()
{
return $this->gid;
}
/**
* @param int $gid
*/
public function setGid($gid)
{
$this->gid = $gid;
}
/**
* @return int
*/
public function getUid()
{
return $this->uid;
}
/**
* @param int $uid
*/
public function setUid($uid)
{
$this->uid = $uid;
}
/**
* @return string
*/
public function getComment()
{
return $this->comment;
}
/**
* @param string $comment
*/
public function setComment($comment)
{
$this->comment = $comment;
}
/**
* @return string
*/
public function getGroup()
{
return $this->group;
}
/**
* @param string $group
*/
public function setGroup($group)
{
$this->group = $group;
}
/**
* @return boolean
*/
public function getIsdir()
{
return $this->isdir;
}
/**
* @param boolean $isdir
*/
public function setIsdir($isdir)
{
// default mode for directories
if ($isdir && $this->mode === 0664) {
$this->mode = 0775;
}
$this->isdir = $isdir;
}
/**
* @return int
*/
public function getMode()
{
return $this->mode;
}
/**
* @param int $mode
*/
public function setMode($mode)
{
$this->mode = $mode;
}
/**
* @return string
*/
public function getOwner()
{
return $this->owner;
}
/**
* @param string $owner
*/
public function setOwner($owner)
{
$this->owner = $owner;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @param string $path
*/
public function setPath($path)
{
$this->path = $this->cleanPath($path);
}
/**
* Cleans up a path and removes relative parts, also strips leading slashes
*
* @param string $path
* @return string
*/
protected function cleanPath($path)
{
$path = \str_replace('\\', '/', $path);
$path = \explode('/', $path);
$newpath = array();
foreach ($path as $p) {
if ($p === '' || $p === '.') {
continue;
}
if ($p === '..') {
\array_pop($newpath);
continue;
}
\array_push($newpath, $p);
}
return \trim(\implode('/', $newpath), '/');
}
/**
* Strip given prefix or number of path segments from the filename
*
* The $strip parameter allows you to strip a certain number of path components from the filenames
* found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
* an integer is passed as $strip.
* Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
* the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
*
* @param int|string $strip
*/
public function strip($strip)
{
$filename = $this->getPath();
$striplen = \strlen($strip);
if (\is_int($strip)) {
// if $strip is an integer we strip this many path components
$parts = \explode('/', $filename);
if (!$this->getIsdir()) {
$base = \array_pop($parts);
// keep filename itself
} else {
$base = '';
}
$filename = \join('/', \array_slice($parts, $strip));
if ($base) {
$filename .= "/{$base}";
}
} else {
// if strip is a string, we strip a prefix here
if (\substr($filename, 0, $striplen) == $strip) {
$filename = \substr($filename, $striplen);
}
}
$this->setPath($filename);
}
/**
* Does the file match the given include and exclude expressions?
*
* Exclude rules take precedence over include rules
*
* @param string $include Regular expression of files to include
* @param string $exclude Regular expression of files to exclude
* @return bool
*/
public function matchExpression($include = '', $exclude = '')
{
$extract = \true;
if ($include && !\preg_match($include, $this->getPath())) {
$extract = \false;
}
if ($exclude && \preg_match($exclude, $this->getPath())) {
$extract = \false;
}
return $extract;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* File meta data problems
*/
class FileInfoException extends \Exception
{
}

View File

@ -0,0 +1,640 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* Class Tar
*
* Creates or extracts Tar archives. Supports gz and bzip compression
*
* Long pathnames (>100 chars) are supported in POSIX ustar and GNU longlink formats.
*
* @author Andreas Gohr <andi@splitbrain.org>
* @package splitbrain\PHPArchive
* @license MIT
*/
class Tar extends Archive
{
protected $file = '';
protected $comptype = Archive::COMPRESS_AUTO;
protected $complevel = 9;
protected $fh;
protected $memory = '';
protected $closed = \true;
protected $writeaccess = \false;
/**
* Sets the compression to use
*
* @param int $level Compression level (0 to 9)
* @param int $type Type of compression to use (use COMPRESS_* constants)
* @throws ArchiveIllegalCompressionException
*/
public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
{
$this->compressioncheck($type);
if ($level < -1 || $level > 9) {
throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
}
$this->comptype = $type;
$this->complevel = $level;
if ($level == 0) {
$this->comptype = Archive::COMPRESS_NONE;
}
if ($type == Archive::COMPRESS_NONE) {
$this->complevel = 0;
}
}
/**
* Open an existing TAR file for reading
*
* @param string $file
* @throws ArchiveIOException
* @throws ArchiveIllegalCompressionException
*/
public function open($file)
{
$this->file = $file;
// update compression to mach file
if ($this->comptype == Tar::COMPRESS_AUTO) {
$this->setCompression($this->complevel, $this->filetype($file));
}
// open file handles
if ($this->comptype === Archive::COMPRESS_GZIP) {
$this->fh = @\gzopen($this->file, 'rb');
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
$this->fh = @\bzopen($this->file, 'r');
} else {
$this->fh = @\fopen($this->file, 'rb');
}
if (!$this->fh) {
throw new ArchiveIOException('Could not open file for reading: ' . $this->file);
}
$this->closed = \false;
}
/**
* Read the contents of a TAR archive
*
* This function lists the files stored in the archive
*
* The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
* Reopen the file with open() again if you want to do additional operations
*
* @throws ArchiveIOException
* @throws ArchiveCorruptedException
* @returns FileInfo[]
*/
public function contents()
{
$result = array();
foreach ($this->yieldContents() as $fileinfo) {
$result[] = $fileinfo;
}
return $result;
}
/**
* Read the contents of a TAR archive and return each entry using yield
* for memory efficiency.
*
* @see contents()
* @throws ArchiveIOException
* @throws ArchiveCorruptedException
* @returns FileInfo[]
*/
public function yieldContents()
{
if ($this->closed || !$this->file) {
throw new ArchiveIOException('Can not read from a closed archive');
}
while ($read = $this->readbytes(512)) {
$header = $this->parseHeader($read);
if (!\is_array($header)) {
continue;
}
$this->skipbytes(\ceil($header['size'] / 512) * 512);
(yield $this->header2fileinfo($header));
}
$this->close();
}
/**
* Extract an existing TAR archive
*
* The $strip parameter allows you to strip a certain number of path components from the filenames
* found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
* an integer is passed as $strip.
* Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
* the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
*
* By default this will extract all files found in the archive. You can restrict the output using the $include
* and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
* $include is set only files that match this expression will be extracted. Files that match the $exclude
* expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
* stripped filenames as described above.
*
* The archive is closed afer reading the contents, because rewinding is not possible in bzip2 streams.
* Reopen the file with open() again if you want to do additional operations
*
* @param string $outdir the target directory for extracting
* @param int|string $strip either the number of path components or a fixed prefix to strip
* @param string $exclude a regular expression of files to exclude
* @param string $include a regular expression of files to include
* @throws ArchiveIOException
* @throws ArchiveCorruptedException
* @return FileInfo[]
*/
public function extract($outdir, $strip = '', $exclude = '', $include = '')
{
if ($this->closed || !$this->file) {
throw new ArchiveIOException('Can not read from a closed archive');
}
$outdir = \rtrim($outdir, '/');
@\mkdir($outdir, 0777, \true);
if (!\is_dir($outdir)) {
throw new ArchiveIOException("Could not create directory '{$outdir}'");
}
$extracted = array();
while ($dat = $this->readbytes(512)) {
// read the file header
$header = $this->parseHeader($dat);
if (!\is_array($header)) {
continue;
}
$fileinfo = $this->header2fileinfo($header);
// apply strip rules
$fileinfo->strip($strip);
// skip unwanted files
if (!\strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
$this->skipbytes(\ceil($header['size'] / 512) * 512);
continue;
}
// create output directory
$output = $outdir . '/' . $fileinfo->getPath();
$directory = $fileinfo->getIsdir() ? $output : \dirname($output);
if (!\file_exists($directory)) {
\mkdir($directory, 0777, \true);
}
// extract data
if (!$fileinfo->getIsdir()) {
$fp = @\fopen($output, "wb");
if (!$fp) {
throw new ArchiveIOException('Could not open file for writing: ' . $output);
}
$size = \floor($header['size'] / 512);
for ($i = 0; $i < $size; $i++) {
\fwrite($fp, $this->readbytes(512), 512);
}
if ($header['size'] % 512 != 0) {
\fwrite($fp, $this->readbytes(512), $header['size'] % 512);
}
\fclose($fp);
@\touch($output, $fileinfo->getMtime());
@\chmod($output, $fileinfo->getMode());
} else {
$this->skipbytes(\ceil($header['size'] / 512) * 512);
// the size is usually 0 for directories
}
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
$extracted[] = $fileinfo;
}
$this->close();
return $extracted;
}
/**
* Create a new TAR file
*
* If $file is empty, the tar file will be created in memory
*
* @param string $file
* @throws ArchiveIOException
* @throws ArchiveIllegalCompressionException
*/
public function create($file = '')
{
$this->file = $file;
$this->memory = '';
$this->fh = 0;
if ($this->file) {
// determine compression
if ($this->comptype == Archive::COMPRESS_AUTO) {
$this->setCompression($this->complevel, $this->filetype($file));
}
if ($this->comptype === Archive::COMPRESS_GZIP) {
$this->fh = @\gzopen($this->file, 'wb' . $this->complevel);
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
$this->fh = @\bzopen($this->file, 'w');
} else {
$this->fh = @\fopen($this->file, 'wb');
}
if (!$this->fh) {
throw new ArchiveIOException('Could not open file for writing: ' . $this->file);
}
}
$this->writeaccess = \true;
$this->closed = \false;
}
/**
* Add a file to the current TAR archive using an existing file in the filesystem
*
* @param string $file path to the original file
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
* @throws ArchiveCorruptedException when the file changes while reading it, the archive will be corrupt and should be deleted
* @throws ArchiveIOException there was trouble reading the given file, it was not added
* @throws FileInfoException trouble reading file info, it was not added
*/
public function addFile($file, $fileinfo = '')
{
if (\is_string($fileinfo)) {
$fileinfo = FileInfo::fromPath($file, $fileinfo);
}
if ($this->closed) {
throw new ArchiveIOException('Archive has been closed, files can no longer be added');
}
// create file header
$this->writeFileHeader($fileinfo);
// write data, but only if we have data to write.
// note: on Windows fopen() on a directory will fail, so we prevent
// errors on Windows by testing if we have data to write.
if (!$fileinfo->getIsdir() && $fileinfo->getSize() > 0) {
$read = 0;
$fp = @\fopen($file, 'rb');
if (!$fp) {
throw new ArchiveIOException('Could not open file for reading: ' . $file);
}
while (!\feof($fp)) {
$data = \fread($fp, 512);
$read += \strlen($data);
if ($data === \false) {
break;
}
if ($data === '') {
break;
}
$packed = \pack("a512", $data);
$this->writebytes($packed);
}
\fclose($fp);
if ($read != $fileinfo->getSize()) {
$this->close();
throw new ArchiveCorruptedException("The size of {$file} changed while reading, archive corrupted. read {$read} expected " . $fileinfo->getSize());
}
}
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
}
/**
* Add a file to the current TAR archive using the given $data as content
*
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
* @param string $data binary content of the file to add
* @throws ArchiveIOException
*/
public function addData($fileinfo, $data)
{
if (\is_string($fileinfo)) {
$fileinfo = new FileInfo($fileinfo);
}
if ($this->closed) {
throw new ArchiveIOException('Archive has been closed, files can no longer be added');
}
$len = \strlen($data);
$fileinfo->setSize($len);
$this->writeFileHeader($fileinfo);
for ($s = 0; $s < $len; $s += 512) {
$this->writebytes(\pack("a512", \substr($data, $s, 512)));
}
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
}
/**
* Add the closing footer to the archive if in write mode, close all file handles
*
* After a call to this function no more data can be added to the archive, for
* read access no reading is allowed anymore
*
* "Physically, an archive consists of a series of file entries terminated by an end-of-archive entry, which
* consists of two 512 blocks of zero bytes"
*
* @link http://www.gnu.org/software/tar/manual/html_chapter/tar_8.html#SEC134
* @throws ArchiveIOException
*/
public function close()
{
if ($this->closed) {
return;
}
// we did this already
// write footer
if ($this->writeaccess) {
$this->writebytes(\pack("a512", ""));
$this->writebytes(\pack("a512", ""));
}
// close file handles
if ($this->file) {
if ($this->comptype === Archive::COMPRESS_GZIP) {
\gzclose($this->fh);
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
\bzclose($this->fh);
} else {
\fclose($this->fh);
}
$this->file = '';
$this->fh = 0;
}
$this->writeaccess = \false;
$this->closed = \true;
}
/**
* Returns the created in-memory archive data
*
* This implicitly calls close() on the Archive
* @throws ArchiveIOException
*/
public function getArchive()
{
$this->close();
if ($this->comptype === Archive::COMPRESS_AUTO) {
$this->comptype = Archive::COMPRESS_NONE;
}
if ($this->comptype === Archive::COMPRESS_GZIP) {
return \gzencode($this->memory, $this->complevel);
}
if ($this->comptype === Archive::COMPRESS_BZIP) {
return \bzcompress($this->memory);
}
return $this->memory;
}
/**
* Save the created in-memory archive data
*
* Note: It more memory effective to specify the filename in the create() function and
* let the library work on the new file directly.
*
* @param string $file
* @throws ArchiveIOException
* @throws ArchiveIllegalCompressionException
*/
public function save($file)
{
if ($this->comptype === Archive::COMPRESS_AUTO) {
$this->setCompression($this->complevel, $this->filetype($file));
}
if (!@\file_put_contents($file, $this->getArchive())) {
throw new ArchiveIOException('Could not write to file: ' . $file);
}
}
/**
* Read from the open file pointer
*
* @param int $length bytes to read
* @return string
*/
protected function readbytes($length)
{
if ($this->comptype === Archive::COMPRESS_GZIP) {
return @\gzread($this->fh, $length);
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
return @\bzread($this->fh, $length);
} else {
return @\fread($this->fh, $length);
}
}
/**
* Write to the open filepointer or memory
*
* @param string $data
* @throws ArchiveIOException
* @return int number of bytes written
*/
protected function writebytes($data)
{
if (!$this->file) {
$this->memory .= $data;
$written = \strlen($data);
} elseif ($this->comptype === Archive::COMPRESS_GZIP) {
$written = @\gzwrite($this->fh, $data);
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
$written = @\bzwrite($this->fh, $data);
} else {
$written = @\fwrite($this->fh, $data);
}
if ($written === \false) {
throw new ArchiveIOException('Failed to write to archive stream');
}
return $written;
}
/**
* Skip forward in the open file pointer
*
* This is basically a wrapper around seek() (and a workaround for bzip2)
*
* @param int $bytes seek to this position
*/
protected function skipbytes($bytes)
{
if ($this->comptype === Archive::COMPRESS_GZIP) {
@\gzseek($this->fh, $bytes, \SEEK_CUR);
} elseif ($this->comptype === Archive::COMPRESS_BZIP) {
// there is no seek in bzip2, we simply read on
// bzread allows to read a max of 8kb at once
while ($bytes) {
$toread = \min(8192, $bytes);
@\bzread($this->fh, $toread);
$bytes -= $toread;
}
} else {
@\fseek($this->fh, $bytes, \SEEK_CUR);
}
}
/**
* Write the given file meta data as header
*
* @param FileInfo $fileinfo
* @throws ArchiveIOException
*/
protected function writeFileHeader(FileInfo $fileinfo)
{
$this->writeRawFileHeader($fileinfo->getPath(), $fileinfo->getUid(), $fileinfo->getGid(), $fileinfo->getMode(), $fileinfo->getSize(), $fileinfo->getMtime(), $fileinfo->getIsdir() ? '5' : '0');
}
/**
* Write a file header to the stream
*
* @param string $name
* @param int $uid
* @param int $gid
* @param int $perm
* @param int $size
* @param int $mtime
* @param string $typeflag Set to '5' for directories
* @throws ArchiveIOException
*/
protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $typeflag = '')
{
// handle filename length restrictions
$prefix = '';
$namelen = \strlen($name);
if ($namelen > 100) {
$file = \basename($name);
$dir = \dirname($name);
if (\strlen($file) > 100 || \strlen($dir) > 155) {
// we're still too large, let's use GNU longlink
$this->writeRawFileHeader('././@LongLink', 0, 0, 0, $namelen, 0, 'L');
for ($s = 0; $s < $namelen; $s += 512) {
$this->writebytes(\pack("a512", \substr($name, $s, 512)));
}
$name = \substr($name, 0, 100);
// cut off name
} else {
// we're fine when splitting, use POSIX ustar
$prefix = $dir;
$name = $file;
}
}
// values are needed in octal
$uid = \sprintf("%6s ", \decoct($uid));
$gid = \sprintf("%6s ", \decoct($gid));
$perm = \sprintf("%6s ", \decoct($perm));
$size = \sprintf("%11s ", \decoct($size));
$mtime = \sprintf("%11s", \decoct($mtime));
$data_first = \pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
$data_last = \pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, "");
for ($i = 0, $chks = 0; $i < 148; $i++) {
$chks += \ord($data_first[$i]);
}
for ($i = 156, $chks += 256, $j = 0; $i < 512; $i++, $j++) {
$chks += \ord($data_last[$j]);
}
$this->writebytes($data_first);
$chks = \pack("a8", \sprintf("%6s ", \decoct($chks)));
$this->writebytes($chks . $data_last);
}
/**
* Decode the given tar file header
*
* @param string $block a 512 byte block containing the header data
* @return array|false returns false when this was a null block
* @throws ArchiveCorruptedException
*/
protected function parseHeader($block)
{
if (!$block || \strlen($block) != 512) {
throw new ArchiveCorruptedException('Unexpected length of header');
}
// null byte blocks are ignored
if (\trim($block) === '') {
return \false;
}
for ($i = 0, $chks = 0; $i < 148; $i++) {
$chks += \ord($block[$i]);
}
for ($i = 156, $chks += 256; $i < 512; $i++) {
$chks += \ord($block[$i]);
}
$header = @\unpack("a100filename/a8perm/a8uid/a8gid/a12size/a12mtime/a8checksum/a1typeflag/a100link/a6magic/a2version/a32uname/a32gname/a8devmajor/a8devminor/a155prefix", $block);
if (!$header) {
throw new ArchiveCorruptedException('Failed to parse header');
}
$return['checksum'] = \OctDec(\trim($header['checksum']));
if ($return['checksum'] != $chks) {
throw new ArchiveCorruptedException('Header does not match its checksum');
}
$return['filename'] = \trim($header['filename']);
$return['perm'] = \OctDec(\trim($header['perm']));
$return['uid'] = \OctDec(\trim($header['uid']));
$return['gid'] = \OctDec(\trim($header['gid']));
$return['size'] = \OctDec(\trim($header['size']));
$return['mtime'] = \OctDec(\trim($header['mtime']));
$return['typeflag'] = $header['typeflag'];
$return['link'] = \trim($header['link']);
$return['uname'] = \trim($header['uname']);
$return['gname'] = \trim($header['gname']);
// Handle ustar Posix compliant path prefixes
if (\trim($header['prefix'])) {
$return['filename'] = \trim($header['prefix']) . '/' . $return['filename'];
}
// Handle Long-Link entries from GNU Tar
if ($return['typeflag'] == 'L') {
// following data block(s) is the filename
$filename = \trim($this->readbytes(\ceil($return['size'] / 512) * 512));
// next block is the real header
$block = $this->readbytes(512);
$return = $this->parseHeader($block);
// overwrite the filename
$return['filename'] = $filename;
}
return $return;
}
/**
* Creates a FileInfo object from the given parsed header
*
* @param $header
* @return FileInfo
*/
protected function header2fileinfo($header)
{
$fileinfo = new FileInfo();
$fileinfo->setPath($header['filename']);
$fileinfo->setMode($header['perm']);
$fileinfo->setUid($header['uid']);
$fileinfo->setGid($header['gid']);
$fileinfo->setSize($header['size']);
$fileinfo->setMtime($header['mtime']);
$fileinfo->setOwner($header['uname']);
$fileinfo->setGroup($header['gname']);
$fileinfo->setIsdir((bool) $header['typeflag']);
return $fileinfo;
}
/**
* Checks if the given compression type is available and throws an exception if not
*
* @param $comptype
* @throws ArchiveIllegalCompressionException
*/
protected function compressioncheck($comptype)
{
if ($comptype === Archive::COMPRESS_GZIP && !\function_exists('gzopen')) {
throw new ArchiveIllegalCompressionException('No gzip support available');
}
if ($comptype === Archive::COMPRESS_BZIP && !\function_exists('bzopen')) {
throw new ArchiveIllegalCompressionException('No bzip2 support available');
}
}
/**
* Guesses the wanted compression from the given file
*
* Uses magic bytes for existing files, the file extension otherwise
*
* You don't need to call this yourself. It's used when you pass Archive::COMPRESS_AUTO somewhere
*
* @param string $file
* @return int
*/
public function filetype($file)
{
// for existing files, try to read the magic bytes
if (\file_exists($file) && \is_readable($file) && \filesize($file) > 5) {
$fh = @\fopen($file, 'rb');
if (!$fh) {
return \false;
}
$magic = \fread($fh, 5);
\fclose($fh);
if (\strpos($magic, "BZ") === 0) {
return Archive::COMPRESS_BZIP;
}
if (\strpos($magic, "\x1f\x8b") === 0) {
return Archive::COMPRESS_GZIP;
}
}
// otherwise rely on file name
$file = \strtolower($file);
if (\substr($file, -3) == '.gz' || \substr($file, -4) == '.tgz') {
return Archive::COMPRESS_GZIP;
} elseif (\substr($file, -4) == '.bz2' || \substr($file, -4) == '.tbz') {
return Archive::COMPRESS_BZIP;
}
return Archive::COMPRESS_NONE;
}
}

View File

@ -0,0 +1,898 @@
<?php
namespace Tassos\Vendor\splitbrain\PHPArchive;
/**
* Class Zip
*
* Creates or extracts Zip archives
*
* for specs see http://www.pkware.com/appnote
*
* @author Andreas Gohr <andi@splitbrain.org>
* @package splitbrain\PHPArchive
* @license MIT
*/
class Zip extends Archive
{
const LOCAL_FILE_HEADER_CRC_OFFSET = 14;
protected $file = '';
protected $fh;
protected $memory = '';
protected $closed = \true;
protected $writeaccess = \false;
protected $ctrl_dir;
protected $complevel = 9;
/**
* Set the compression level.
*
* Compression Type is ignored for ZIP
*
* You can call this function before adding each file to set differen compression levels
* for each file.
*
* @param int $level Compression level (0 to 9)
* @param int $type Type of compression to use ignored for ZIP
* @throws ArchiveIllegalCompressionException
*/
public function setCompression($level = 9, $type = Archive::COMPRESS_AUTO)
{
if ($level < -1 || $level > 9) {
throw new ArchiveIllegalCompressionException('Compression level should be between -1 and 9');
}
$this->complevel = $level;
}
/**
* Open an existing ZIP file for reading
*
* @param string $file
* @throws ArchiveIOException
*/
public function open($file)
{
$this->file = $file;
$this->fh = @\fopen($this->file, 'rb');
if (!$this->fh) {
throw new ArchiveIOException('Could not open file for reading: ' . $this->file);
}
$this->closed = \false;
}
/**
* Read the contents of a ZIP archive
*
* This function lists the files stored in the archive, and returns an indexed array of FileInfo objects
*
* The archive is closed afer reading the contents, for API compatibility with TAR files
* Reopen the file with open() again if you want to do additional operations
*
* @throws ArchiveIOException
* @return FileInfo[]
*/
public function contents()
{
$result = array();
foreach ($this->yieldContents() as $fileinfo) {
$result[] = $fileinfo;
}
return $result;
}
/**
* Read the contents of a ZIP archive and return each entry using yield
* for memory efficiency.
*
* @see contents()
* @throws ArchiveIOException
* @return FileInfo[]
*/
public function yieldContents()
{
if ($this->closed || !$this->file) {
throw new ArchiveIOException('Can not read from a closed archive');
}
$centd = $this->readCentralDir();
@\rewind($this->fh);
@\fseek($this->fh, $centd['offset']);
for ($i = 0; $i < $centd['entries']; $i++) {
(yield $this->header2fileinfo($this->readCentralFileHeader()));
}
$this->close();
}
/**
* Extract an existing ZIP archive
*
* The $strip parameter allows you to strip a certain number of path components from the filenames
* found in the tar file, similar to the --strip-components feature of GNU tar. This is triggered when
* an integer is passed as $strip.
* Alternatively a fixed string prefix may be passed in $strip. If the filename matches this prefix,
* the prefix will be stripped. It is recommended to give prefixes with a trailing slash.
*
* By default this will extract all files found in the archive. You can restrict the output using the $include
* and $exclude parameter. Both expect a full regular expression (including delimiters and modifiers). If
* $include is set only files that match this expression will be extracted. Files that match the $exclude
* expression will never be extracted. Both parameters can be used in combination. Expressions are matched against
* stripped filenames as described above.
*
* @param string $outdir the target directory for extracting
* @param int|string $strip either the number of path components or a fixed prefix to strip
* @param string $exclude a regular expression of files to exclude
* @param string $include a regular expression of files to include
* @throws ArchiveIOException
* @return FileInfo[]
*/
public function extract($outdir, $strip = '', $exclude = '', $include = '')
{
if ($this->closed || !$this->file) {
throw new ArchiveIOException('Can not read from a closed archive');
}
$outdir = \rtrim($outdir, '/');
@\mkdir($outdir, 0777, \true);
$extracted = array();
$cdir = $this->readCentralDir();
$pos_entry = $cdir['offset'];
// begin of the central file directory
for ($i = 0; $i < $cdir['entries']; $i++) {
// read file header
@\fseek($this->fh, $pos_entry);
$header = $this->readCentralFileHeader();
$header['index'] = $i;
$pos_entry = \ftell($this->fh);
// position of the next file in central file directory
\fseek($this->fh, $header['offset']);
// seek to beginning of file header
$header = $this->readFileHeader($header);
$fileinfo = $this->header2fileinfo($header);
// apply strip rules
$fileinfo->strip($strip);
// skip unwanted files
if (!\strlen($fileinfo->getPath()) || !$fileinfo->matchExpression($include, $exclude)) {
continue;
}
$extracted[] = $fileinfo;
// create output directory
$output = $outdir . '/' . $fileinfo->getPath();
$directory = $header['folder'] ? $output : \dirname($output);
@\mkdir($directory, 0777, \true);
// nothing more to do for directories
if ($fileinfo->getIsdir()) {
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
continue;
}
// compressed files are written to temporary .gz file first
if ($header['compression'] == 0) {
$extractto = $output;
} else {
$extractto = $output . '.gz';
}
// open file for writing
$fp = @\fopen($extractto, "wb");
if (!$fp) {
throw new ArchiveIOException('Could not open file for writing: ' . $extractto);
}
// prepend compression header
if ($header['compression'] != 0) {
$binary_data = \pack('va1a1Va1a1', 0x8b1f, \chr($header['compression']), \chr(0x0), \time(), \chr(0x0), \chr(3));
\fwrite($fp, $binary_data, 10);
}
// read the file and store it on disk
$size = $header['compressed_size'];
while ($size != 0) {
$read_size = $size < 2048 ? $size : 2048;
$buffer = \fread($this->fh, $read_size);
$binary_data = \pack('a' . $read_size, $buffer);
\fwrite($fp, $binary_data, $read_size);
$size -= $read_size;
}
// finalize compressed file
if ($header['compression'] != 0) {
$binary_data = \pack('VV', $header['crc'], $header['size']);
\fwrite($fp, $binary_data, 8);
}
// close file
\fclose($fp);
// unpack compressed file
if ($header['compression'] != 0) {
$gzp = @\gzopen($extractto, 'rb');
if (!$gzp) {
@\unlink($extractto);
throw new ArchiveIOException('Failed file extracting. gzip support missing?');
}
$fp = @\fopen($output, 'wb');
if (!$fp) {
throw new ArchiveIOException('Could not open file for writing: ' . $extractto);
}
$size = $header['size'];
while ($size != 0) {
$read_size = $size < 2048 ? $size : 2048;
$buffer = \gzread($gzp, $read_size);
$binary_data = \pack('a' . $read_size, $buffer);
@\fwrite($fp, $binary_data, $read_size);
$size -= $read_size;
}
\fclose($fp);
\gzclose($gzp);
\unlink($extractto);
// remove temporary gz file
}
@\touch($output, $fileinfo->getMtime());
//FIXME what about permissions?
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
}
$this->close();
return $extracted;
}
/**
* Create a new ZIP file
*
* If $file is empty, the zip file will be created in memory
*
* @param string $file
* @throws ArchiveIOException
*/
public function create($file = '')
{
$this->file = $file;
$this->memory = '';
$this->fh = 0;
if ($this->file) {
$this->fh = @\fopen($this->file, 'wb');
if (!$this->fh) {
throw new ArchiveIOException('Could not open file for writing: ' . $this->file);
}
}
$this->writeaccess = \true;
$this->closed = \false;
$this->ctrl_dir = array();
}
/**
* Add a file to the current ZIP archive using an existing file in the filesystem
*
* @param string $file path to the original file
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data, empty to take from original
* @throws ArchiveIOException
*/
/**
* Add a file to the current archive using an existing file in the filesystem
*
* @param string $file path to the original file
* @param string|FileInfo $fileinfo either the name to use in archive (string) or a FileInfo oject with all meta data, empty to take from original
* @throws ArchiveIOException
* @throws FileInfoException
*/
public function addFile($file, $fileinfo = '')
{
if (\is_string($fileinfo)) {
$fileinfo = FileInfo::fromPath($file, $fileinfo);
}
if ($this->closed) {
throw new ArchiveIOException('Archive has been closed, files can no longer be added');
}
$fp = @\fopen($file, 'rb');
if ($fp === \false) {
throw new ArchiveIOException('Could not open file for reading: ' . $file);
}
$offset = $this->dataOffset();
$name = $fileinfo->getPath();
$time = $fileinfo->getMtime();
// write local file header (temporary CRC and size)
$this->writebytes($this->makeLocalFileHeader($time, 0, 0, 0, $name, (bool) $this->complevel));
// we store no encryption header
// prepare info, compress and write data to archive
$deflate_context = \deflate_init(\ZLIB_ENCODING_DEFLATE, ['level' => $this->complevel]);
$crc_context = \hash_init('crc32b');
$size = $csize = 0;
while (!\feof($fp)) {
$block = \fread($fp, 512);
if ($this->complevel) {
$is_first_block = $size === 0;
$is_last_block = \feof($fp);
if ($is_last_block) {
$c_block = \deflate_add($deflate_context, $block, \ZLIB_FINISH);
// get rid of the compression footer
$c_block = \substr($c_block, 0, -4);
} else {
$c_block = \deflate_add($deflate_context, $block, \ZLIB_NO_FLUSH);
}
// get rid of the compression header
if ($is_first_block) {
$c_block = \substr($c_block, 2);
}
$csize += \strlen($c_block);
$this->writebytes($c_block);
} else {
$this->writebytes($block);
}
$size += \strlen($block);
\hash_update($crc_context, $block);
}
\fclose($fp);
// update the local file header with the computed CRC and size
$crc = \hexdec(\hash_final($crc_context));
$csize = $this->complevel ? $csize : $size;
$this->writebytesAt($this->makeCrcAndSize($crc, $size, $csize), $offset + self::LOCAL_FILE_HEADER_CRC_OFFSET);
// we store no data descriptor
// add info to central file directory
$this->ctrl_dir[] = $this->makeCentralFileRecord($offset, $time, $crc, $size, $csize, $name, (bool) $this->complevel);
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
}
/**
* Add a file to the current Zip archive using the given $data as content
*
* @param string|FileInfo $fileinfo either the name to us in archive (string) or a FileInfo oject with all meta data
* @param string $data binary content of the file to add
* @throws ArchiveIOException
*/
public function addData($fileinfo, $data)
{
if (\is_string($fileinfo)) {
$fileinfo = new FileInfo($fileinfo);
}
if ($this->closed) {
throw new ArchiveIOException('Archive has been closed, files can no longer be added');
}
// prepare info and compress data
$size = \strlen($data);
$crc = \crc32($data);
if ($this->complevel) {
$data = \gzcompress($data, $this->complevel);
$data = \substr($data, 2, -4);
// strip compression headers
}
$csize = \strlen($data);
$offset = $this->dataOffset();
$name = $fileinfo->getPath();
$time = $fileinfo->getMtime();
// write local file header
$this->writebytes($this->makeLocalFileHeader($time, $crc, $size, $csize, $name, (bool) $this->complevel));
// we store no encryption header
// write data
$this->writebytes($data);
// we store no data descriptor
// add info to central file directory
$this->ctrl_dir[] = $this->makeCentralFileRecord($offset, $time, $crc, $size, $csize, $name, (bool) $this->complevel);
if (\is_callable($this->callback)) {
\call_user_func($this->callback, $fileinfo);
}
}
/**
* Add the closing footer to the archive if in write mode, close all file handles
*
* After a call to this function no more data can be added to the archive, for
* read access no reading is allowed anymore
* @throws ArchiveIOException
*/
public function close()
{
if ($this->closed) {
return;
}
// we did this already
if ($this->writeaccess) {
// write central directory
$offset = $this->dataOffset();
$ctrldir = \join('', $this->ctrl_dir);
$this->writebytes($ctrldir);
// write end of central directory record
$this->writebytes("PK\x05\x06");
// end of central dir signature
$this->writebytes(\pack('v', 0));
// number of this disk
$this->writebytes(\pack('v', 0));
// number of the disk with the start of the central directory
$this->writebytes(\pack('v', \count($this->ctrl_dir)));
// total number of entries in the central directory on this disk
$this->writebytes(\pack('v', \count($this->ctrl_dir)));
// total number of entries in the central directory
$this->writebytes(\pack('V', \strlen($ctrldir)));
// size of the central directory
$this->writebytes(\pack('V', $offset));
// offset of start of central directory with respect to the starting disk number
$this->writebytes(\pack('v', 0));
// .ZIP file comment length
$this->ctrl_dir = array();
}
// close file handles
if ($this->file) {
\fclose($this->fh);
$this->file = '';
$this->fh = 0;
}
$this->writeaccess = \false;
$this->closed = \true;
}
/**
* Returns the created in-memory archive data
*
* This implicitly calls close() on the Archive
* @throws ArchiveIOException
*/
public function getArchive()
{
$this->close();
return $this->memory;
}
/**
* Save the created in-memory archive data
*
* Note: It's more memory effective to specify the filename in the create() function and
* let the library work on the new file directly.
*
* @param $file
* @throws ArchiveIOException
*/
public function save($file)
{
if (!@\file_put_contents($file, $this->getArchive())) {
throw new ArchiveIOException('Could not write to file: ' . $file);
}
}
/**
* Read the central directory
*
* This key-value list contains general information about the ZIP file
*
* @return array
*/
protected function readCentralDir()
{
$size = \filesize($this->file);
if ($size < 277) {
$maximum_size = $size;
} else {
$maximum_size = 277;
}
@\fseek($this->fh, $size - $maximum_size);
$pos = \ftell($this->fh);
$bytes = 0x0;
while ($pos < $size) {
$byte = @\fread($this->fh, 1);
$bytes = $bytes << 8 & 0xffffffff | \ord($byte);
if ($bytes == 0x504b0506) {
break;
}
$pos++;
}
$data = \unpack('vdisk/vdisk_start/vdisk_entries/ventries/Vsize/Voffset/vcomment_size', \fread($this->fh, 18));
if ($data['comment_size'] != 0) {
$centd['comment'] = \fread($this->fh, $data['comment_size']);
} else {
$centd['comment'] = '';
}
$centd['entries'] = $data['entries'];
$centd['disk_entries'] = $data['disk_entries'];
$centd['offset'] = $data['offset'];
$centd['disk_start'] = $data['disk_start'];
$centd['size'] = $data['size'];
$centd['disk'] = $data['disk'];
return $centd;
}
/**
* Read the next central file header
*
* Assumes the current file pointer is pointing at the right position
*
* @return array
*/
protected function readCentralFileHeader()
{
$binary_data = \fread($this->fh, 46);
$header = \unpack('vchkid/vid/vversion/vversion_extracted/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len/vcomment_len/vdisk/vinternal/Vexternal/Voffset', $binary_data);
if ($header['filename_len'] != 0) {
$header['filename'] = \fread($this->fh, $header['filename_len']);
} else {
$header['filename'] = '';
}
if ($header['extra_len'] != 0) {
$header['extra'] = \fread($this->fh, $header['extra_len']);
$header['extradata'] = $this->parseExtra($header['extra']);
} else {
$header['extra'] = '';
$header['extradata'] = array();
}
if ($header['comment_len'] != 0) {
$header['comment'] = \fread($this->fh, $header['comment_len']);
} else {
$header['comment'] = '';
}
$header['mtime'] = $this->makeUnixTime($header['mdate'], $header['mtime']);
$header['stored_filename'] = $header['filename'];
$header['status'] = 'ok';
if (\substr($header['filename'], -1) == '/') {
$header['external'] = 0x41ff0010;
}
$header['folder'] = $header['external'] == 0x41ff0010 || $header['external'] == 16 ? 1 : 0;
return $header;
}
/**
* Reads the local file header
*
* This header precedes each individual file inside the zip file. Assumes the current file pointer is pointing at
* the right position already. Enhances the given central header with the data found at the local header.
*
* @param array $header the central file header read previously (see above)
* @return array
*/
protected function readFileHeader($header)
{
$binary_data = \fread($this->fh, 30);
$data = \unpack('vchk/vid/vversion/vflag/vcompression/vmtime/vmdate/Vcrc/Vcompressed_size/Vsize/vfilename_len/vextra_len', $binary_data);
$header['filename'] = \fread($this->fh, $data['filename_len']);
if ($data['extra_len'] != 0) {
$header['extra'] = \fread($this->fh, $data['extra_len']);
$header['extradata'] = \array_merge($header['extradata'], $this->parseExtra($header['extra']));
} else {
$header['extra'] = '';
$header['extradata'] = array();
}
$header['compression'] = $data['compression'];
foreach (array('size', 'compressed_size', 'crc') as $hd) {
// On ODT files, these headers are 0. Keep the previous value.
if ($data[$hd] != 0) {
$header[$hd] = $data[$hd];
}
}
$header['flag'] = $data['flag'];
$header['mtime'] = $this->makeUnixTime($data['mdate'], $data['mtime']);
$header['stored_filename'] = $header['filename'];
$header['status'] = "ok";
$header['folder'] = $header['external'] == 0x41ff0010 || $header['external'] == 16 ? 1 : 0;
return $header;
}
/**
* Parse the extra headers into fields
*
* @param string $header
* @return array
*/
protected function parseExtra($header)
{
$extra = array();
// parse all extra fields as raw values
while (\strlen($header) !== 0) {
$set = \unpack('vid/vlen', $header);
$header = \substr($header, 4);
$value = \substr($header, 0, $set['len']);
$header = \substr($header, $set['len']);
$extra[$set['id']] = $value;
}
// handle known ones
if (isset($extra[0x6375])) {
$extra['utf8comment'] = \substr($extra[0x7075], 5);
// strip version and crc
}
if (isset($extra[0x7075])) {
$extra['utf8path'] = \substr($extra[0x7075], 5);
// strip version and crc
}
return $extra;
}
/**
* Create fileinfo object from header data
*
* @param $header
* @return FileInfo
*/
protected function header2fileinfo($header)
{
$fileinfo = new FileInfo();
$fileinfo->setSize($header['size']);
$fileinfo->setCompressedSize($header['compressed_size']);
$fileinfo->setMtime($header['mtime']);
$fileinfo->setComment($header['comment']);
$fileinfo->setIsdir($header['external'] == 0x41ff0010 || $header['external'] == 16);
if (isset($header['extradata']['utf8path'])) {
$fileinfo->setPath($header['extradata']['utf8path']);
} else {
$fileinfo->setPath($this->cpToUtf8($header['filename']));
}
if (isset($header['extradata']['utf8comment'])) {
$fileinfo->setComment($header['extradata']['utf8comment']);
} else {
$fileinfo->setComment($this->cpToUtf8($header['comment']));
}
return $fileinfo;
}
/**
* Convert the given CP437 encoded string to UTF-8
*
* Tries iconv with the correct encoding first, falls back to mbstring with CP850 which is
* similar enough. CP437 seems not to be available in mbstring. Lastly falls back to keeping the
* string as is, which is still better than nothing.
*
* On some systems iconv is available, but the codepage is not. We also check for that.
*
* @param $string
* @return string
*/
protected function cpToUtf8($string)
{
if (\function_exists('iconv') && @\iconv_strlen('', 'CP437') !== \false) {
return \iconv('CP437', 'UTF-8', $string);
} elseif (\function_exists('mb_convert_encoding')) {
return \mb_convert_encoding($string, 'UTF-8', 'CP850');
} else {
return $string;
}
}
/**
* Convert the given UTF-8 encoded string to CP437
*
* Same caveats as for cpToUtf8() apply
*
* @param $string
* @return string
*/
protected function utf8ToCp($string)
{
// try iconv first
if (\function_exists('iconv')) {
$conv = @\iconv('UTF-8', 'CP437//IGNORE', $string);
if ($conv) {
return $conv;
}
// it worked
}
// still here? iconv failed to convert the string. Try another method
// see http://php.net/manual/en/function.iconv.php#108643
if (\function_exists('mb_convert_encoding')) {
return \mb_convert_encoding($string, 'CP850', 'UTF-8');
} else {
return $string;
}
}
/**
* Write to the open filepointer or memory
*
* @param string $data
* @throws ArchiveIOException
* @return int number of bytes written
*/
protected function writebytes($data)
{
if (!$this->file) {
$this->memory .= $data;
$written = \strlen($data);
} else {
$written = @\fwrite($this->fh, $data);
}
if ($written === \false) {
throw new ArchiveIOException('Failed to write to archive stream');
}
return $written;
}
/**
* Write to the open filepointer or memory at the specified offset
*
* @param string $data
* @param int $offset
* @throws ArchiveIOException
* @return int number of bytes written
*/
protected function writebytesAt($data, $offset)
{
if (!$this->file) {
$this->memory .= \substr_replace($this->memory, $data, $offset);
$written = \strlen($data);
} else {
@\fseek($this->fh, $offset);
$written = @\fwrite($this->fh, $data);
@\fseek($this->fh, 0, \SEEK_END);
}
if ($written === \false) {
throw new ArchiveIOException('Failed to write to archive stream');
}
return $written;
}
/**
* Current data pointer position
*
* @fixme might need a -1
* @return int
*/
protected function dataOffset()
{
if ($this->file) {
return \ftell($this->fh);
} else {
return \strlen($this->memory);
}
}
/**
* Create a DOS timestamp from a UNIX timestamp
*
* DOS timestamps start at 1980-01-01, earlier UNIX stamps will be set to this date
*
* @param $time
* @return int
*/
protected function makeDosTime($time)
{
$timearray = \getdate($time);
if ($timearray['year'] < 1980) {
$timearray['year'] = 1980;
$timearray['mon'] = 1;
$timearray['mday'] = 1;
$timearray['hours'] = 0;
$timearray['minutes'] = 0;
$timearray['seconds'] = 0;
}
return $timearray['year'] - 1980 << 25 | $timearray['mon'] << 21 | $timearray['mday'] << 16 | $timearray['hours'] << 11 | $timearray['minutes'] << 5 | $timearray['seconds'] >> 1;
}
/**
* Create a UNIX timestamp from a DOS timestamp
*
* @param $mdate
* @param $mtime
* @return int
*/
protected function makeUnixTime($mdate = null, $mtime = null)
{
if ($mdate && $mtime) {
$year = (($mdate & 0xfe00) >> 9) + 1980;
$month = ($mdate & 0x1e0) >> 5;
$day = $mdate & 0x1f;
$hour = ($mtime & 0xf800) >> 11;
$minute = ($mtime & 0x7e0) >> 5;
$seconde = ($mtime & 0x1f) << 1;
$mtime = \mktime($hour, $minute, $seconde, $month, $day, $year);
} else {
$mtime = \time();
}
return $mtime;
}
/**
* Returns a local file header for the given data
*
* @param int $offset location of the local header
* @param int $ts unix timestamp
* @param int $crc CRC32 checksum of the uncompressed data
* @param int $len length of the uncompressed data
* @param int $clen length of the compressed data
* @param string $name file name
* @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
* @return string
*/
protected function makeCentralFileRecord($offset, $ts, $crc, $len, $clen, $name, $comp = null)
{
if (\is_null($comp)) {
$comp = $len != $clen;
}
$comp = $comp ? 8 : 0;
$dtime = \dechex($this->makeDosTime($ts));
list($name, $extra) = $this->encodeFilename($name);
$header = "PK\x01\x02";
// central file header signature
$header .= \pack('v', 14);
// version made by - VFAT
$header .= \pack('v', 20);
// version needed to extract - 2.0
$header .= \pack('v', 0);
// general purpose flag - no flags set
$header .= \pack('v', $comp);
// compression method - deflate|none
$header .= \pack('H*', $dtime[6] . $dtime[7] . $dtime[4] . $dtime[5] . $dtime[2] . $dtime[3] . $dtime[0] . $dtime[1]);
// last mod file time and date
$header .= \pack('V', $crc);
// crc-32
$header .= \pack('V', $clen);
// compressed size
$header .= \pack('V', $len);
// uncompressed size
$header .= \pack('v', \strlen($name));
// file name length
$header .= \pack('v', \strlen($extra));
// extra field length
$header .= \pack('v', 0);
// file comment length
$header .= \pack('v', 0);
// disk number start
$header .= \pack('v', 0);
// internal file attributes
$header .= \pack('V', 0);
// external file attributes @todo was 0x32!?
$header .= \pack('V', $offset);
// relative offset of local header
$header .= $name;
// file name
$header .= $extra;
// extra (utf-8 filename)
return $header;
}
/**
* Returns a local file header for the given data
*
* @param int $ts unix timestamp
* @param int $crc CRC32 checksum of the uncompressed data
* @param int $len length of the uncompressed data
* @param int $clen length of the compressed data
* @param string $name file name
* @param boolean|null $comp if compression is used, if null it's determined from $len != $clen
* @return string
*/
protected function makeLocalFileHeader($ts, $crc, $len, $clen, $name, $comp = null)
{
if (\is_null($comp)) {
$comp = $len != $clen;
}
$comp = $comp ? 8 : 0;
$dtime = \dechex($this->makeDosTime($ts));
list($name, $extra) = $this->encodeFilename($name);
$header = "PK\x03\x04";
// local file header signature
$header .= \pack('v', 20);
// version needed to extract - 2.0
$header .= \pack('v', 0);
// general purpose flag - no flags set
$header .= \pack('v', $comp);
// compression method - deflate|none
$header .= \pack('H*', $dtime[6] . $dtime[7] . $dtime[4] . $dtime[5] . $dtime[2] . $dtime[3] . $dtime[0] . $dtime[1]);
// last mod file time and date
$header .= \pack('V', $crc);
// crc-32
$header .= \pack('V', $clen);
// compressed size
$header .= \pack('V', $len);
// uncompressed size
$header .= \pack('v', \strlen($name));
// file name length
$header .= \pack('v', \strlen($extra));
// extra field length
$header .= $name;
// file name
$header .= $extra;
// extra (utf-8 filename)
return $header;
}
/**
* Returns only a part of the local file header containing the CRC, size and compressed size.
* Used to update these fields for an already written header.
*
* @param int $crc CRC32 checksum of the uncompressed data
* @param int $len length of the uncompressed data
* @param int $clen length of the compressed data
* @return string
*/
protected function makeCrcAndSize($crc, $len, $clen)
{
$header = \pack('V', $crc);
// crc-32
$header .= \pack('V', $clen);
// compressed size
$header .= \pack('V', $len);
// uncompressed size
return $header;
}
/**
* Returns an allowed filename and an extra field header
*
* When encoding stuff outside the 7bit ASCII range it needs to be placed in a separate
* extra field
*
* @param $original
* @return array($filename, $extra)
*/
protected function encodeFilename($original)
{
$cp437 = $this->utf8ToCp($original);
if ($cp437 === $original) {
return array($original, '');
}
$extra = \pack(
'vvCV',
0x7075,
// tag
\strlen($original) + 5,
// length of file + version + crc
1,
// version
\crc32($original)
);
$extra .= $original;
return array($cp437, $extra);
}
}