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,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);
}
}