MDL-70303 libraries: upgrade GeoIP to 2.11.0 and MaxMind DB to 1.9.0

This commit is contained in:
Ferran Recio 2020-12-22 17:21:35 +01:00
parent c381757f2a
commit 23b681c7b5
42 changed files with 465 additions and 333 deletions

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Database;
use GeoIp2\Exception\AddressNotFoundException;
@ -48,8 +50,8 @@ class Reader implements ProviderInterface
* is corrupt or invalid
*/
public function __construct(
$filename,
$locales = ['en']
string $filename,
array $locales = ['en']
) {
$this->dbReader = new DbReader($filename);
$this->dbType = $this->dbReader->metadata()->databaseType;
@ -65,10 +67,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\City
*/
public function city($ipAddress)
public function city(string $ipAddress): \GeoIp2\Model\City
{
return $this->modelFor('City', 'City', $ipAddress);
}
@ -82,10 +82,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\Country
*/
public function country($ipAddress)
public function country(string $ipAddress): \GeoIp2\Model\Country
{
return $this->modelFor('Country', 'Country', $ipAddress);
}
@ -99,10 +97,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\AnonymousIp
*/
public function anonymousIp($ipAddress)
public function anonymousIp(string $ipAddress): \GeoIp2\Model\AnonymousIp
{
return $this->flatModelFor(
'AnonymousIp',
@ -120,10 +116,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\Asn
*/
public function asn($ipAddress)
public function asn(string $ipAddress): \GeoIp2\Model\Asn
{
return $this->flatModelFor(
'Asn',
@ -141,10 +135,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\ConnectionType
*/
public function connectionType($ipAddress)
public function connectionType(string $ipAddress): \GeoIp2\Model\ConnectionType
{
return $this->flatModelFor(
'ConnectionType',
@ -162,10 +154,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\Domain
*/
public function domain($ipAddress)
public function domain(string $ipAddress): \GeoIp2\Model\Domain
{
return $this->flatModelFor(
'Domain',
@ -183,10 +173,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\Enterprise
*/
public function enterprise($ipAddress)
public function enterprise(string $ipAddress): \GeoIp2\Model\Enterprise
{
return $this->modelFor('Enterprise', 'Enterprise', $ipAddress);
}
@ -200,10 +188,8 @@ class Reader implements ProviderInterface
* not in the database
* @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
* is corrupt or invalid
*
* @return \GeoIp2\Model\Isp
*/
public function isp($ipAddress)
public function isp(string $ipAddress): \GeoIp2\Model\Isp
{
return $this->flatModelFor(
'Isp',
@ -212,7 +198,7 @@ class Reader implements ProviderInterface
);
}
private function modelFor($class, $type, $ipAddress)
private function modelFor(string $class, string $type, string $ipAddress)
{
list($record, $prefixLen) = $this->getRecord($class, $type, $ipAddress);
@ -224,7 +210,7 @@ class Reader implements ProviderInterface
return new $class($record, $this->locales);
}
private function flatModelFor($class, $type, $ipAddress)
private function flatModelFor(string $class, string $type, string $ipAddress)
{
list($record, $prefixLen) = $this->getRecord($class, $type, $ipAddress);
@ -235,7 +221,7 @@ class Reader implements ProviderInterface
return new $class($record);
}
private function getRecord($class, $type, $ipAddress)
private function getRecord(string $class, string $type, string $ipAddress): array
{
if (strpos($this->dbType, $type) === false) {
$method = lcfirst($class);
@ -272,7 +258,7 @@ class Reader implements ProviderInterface
*
* @return \MaxMind\Db\Reader\Metadata object for the database
*/
public function metadata()
public function metadata(): \MaxMind\Db\Reader\Metadata
{
return $this->dbReader->metadata();
}
@ -280,7 +266,7 @@ class Reader implements ProviderInterface
/**
* Closes the GeoIP2 database and returns the resources to the system.
*/
public function close()
public function close(): void
{
$this->dbReader->close();
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
@ -13,9 +15,9 @@ class HttpException extends GeoIp2Exception
public $uri;
public function __construct(
$message,
$httpStatus,
$uri,
string $message,
int $httpStatus,
string $uri,
\Exception $previous = null
) {
$this->uri = $uri;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**
@ -14,10 +16,10 @@ class InvalidRequestException extends HttpException
public $error;
public function __construct(
$message,
$error,
$httpStatus,
$uri,
string $message,
string $error,
int $httpStatus,
string $uri,
\Exception $previous = null
) {
$this->error = $error;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Exception;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
@ -11,20 +13,16 @@ abstract class AbstractModel implements \JsonSerializable
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
$this->raw = $raw;
}
/**
* @ignore
*
* @param mixed $field
*/
protected function get($field)
protected function get(string $field)
{
if (isset($this->raw[$field])) {
return $this->raw[$field];
@ -38,10 +36,8 @@ abstract class AbstractModel implements \JsonSerializable
/**
* @ignore
*
* @param mixed $attr
*/
public function __get($attr)
public function __get(string $attr)
{
if ($attr !== 'instance' && property_exists($this, $attr)) {
return $this->$attr;
@ -52,15 +48,13 @@ abstract class AbstractModel implements \JsonSerializable
/**
* @ignore
*
* @param mixed $attr
*/
public function __isset($attr)
public function __isset(string $attr): bool
{
return $attr !== 'instance' && isset($this->$attr);
}
public function jsonSerialize()
public function jsonSerialize(): array
{
return $this->raw;
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
@ -17,6 +19,8 @@ use GeoIp2\Util;
* 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
@ -31,16 +35,15 @@ class AnonymousIp extends AbstractModel
protected $isAnonymousVpn;
protected $isHostingProvider;
protected $isPublicProxy;
protected $isResidentialProxy;
protected $isTorExitNode;
protected $ipAddress;
protected $network;
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
parent::__construct($raw);
@ -48,6 +51,7 @@ class AnonymousIp extends AbstractModel
$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;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
@ -27,10 +29,8 @@ class Asn extends AbstractModel
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
@ -48,11 +50,8 @@ class City extends Country
/**
* @ignore
*
* @param mixed $raw
* @param mixed $locales
*/
public function __construct($raw, $locales = ['en'])
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw, $locales);
@ -63,7 +62,7 @@ class City extends Country
$this->createSubdivisions($raw, $locales);
}
private function createSubdivisions($raw, $locales)
private function createSubdivisions(array $raw, array $locales): void
{
if (!isset($raw['subdivisions'])) {
return;
@ -79,10 +78,8 @@ class City extends Country
/**
* @ignore
*
* @param mixed $attr
*/
public function __get($attr)
public function __get(string $attr)
{
if ($attr === 'mostSpecificSubdivision') {
return $this->$attr();
@ -93,10 +90,8 @@ class City extends Country
/**
* @ignore
*
* @param mixed $attr
*/
public function __isset($attr)
public function __isset(string $attr): bool
{
if ($attr === 'mostSpecificSubdivision') {
// We always return a mostSpecificSubdivision, even if it is the
@ -107,7 +102,7 @@ class City extends Country
return parent::__isset($attr);
}
private function mostSpecificSubdivision()
private function mostSpecificSubdivision(): \GeoIp2\Record\Subdivision
{
return empty($this->subdivisions) ?
new \GeoIp2\Record\Subdivision([], $this->locales) :

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
@ -24,10 +26,8 @@ class ConnectionType extends AbstractModel
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
parent::__construct($raw);

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**
@ -39,11 +41,8 @@ class Country extends AbstractModel
/**
* @ignore
*
* @param mixed $raw
* @param mixed $locales
*/
public function __construct($raw, $locales = ['en'])
public function __construct(array $raw, array $locales = ['en'])
{
parent::__construct($raw);

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
@ -24,10 +26,8 @@ class Domain extends AbstractModel
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
parent::__construct($raw);

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Model;
use GeoIp2\Util;
@ -33,10 +35,8 @@ class Isp extends AbstractModel
/**
* @ignore
*
* @param mixed $raw
*/
public function __construct($raw)
public function __construct(array $raw)
{
parent::__construct($raw);
$this->autonomousSystemNumber = $this->get('autonomous_system_number');

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2;
interface ProviderInterface
@ -9,12 +11,12 @@ interface ProviderInterface
*
* @return \GeoIp2\Model\Country a Country model for the requested IP address
*/
public function country($ipAddress);
public function country(string $ipAddress): \GeoIp2\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($ipAddress);
public function city(string $ipAddress): \GeoIp2\Model\City;
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
abstract class AbstractPlaceRecord extends AbstractRecord
@ -8,11 +10,8 @@ abstract class AbstractPlaceRecord extends AbstractRecord
/**
* @ignore
*
* @param mixed $record
* @param mixed $locales
*/
public function __construct($record, $locales = ['en'])
public function __construct(?array $record, array $locales = ['en'])
{
$this->locales = $locales;
parent::__construct($record);
@ -20,10 +19,8 @@ abstract class AbstractPlaceRecord extends AbstractRecord
/**
* @ignore
*
* @param mixed $attr
*/
public function __get($attr)
public function __get(string $attr)
{
if ($attr === 'name') {
return $this->name();
@ -34,26 +31,24 @@ abstract class AbstractPlaceRecord extends AbstractRecord
/**
* @ignore
*
* @param mixed $attr
*/
public function __isset($attr)
public function __isset(string $attr): bool
{
if ($attr === 'name') {
return $this->firstSetNameLocale() === null ? false : true;
return $this->firstSetNameLocale() !== null;
}
return parent::__isset($attr);
}
private function name()
private function name(): ?string
{
$locale = $this->firstSetNameLocale();
return $locale === null ? null : $this->names[$locale];
}
private function firstSetNameLocale()
private function firstSetNameLocale(): ?string
{
foreach ($this->locales as $locale) {
if (isset($this->names[$locale])) {

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
abstract class AbstractRecord implements \JsonSerializable
@ -8,20 +10,16 @@ abstract class AbstractRecord implements \JsonSerializable
/**
* @ignore
*
* @param mixed $record
*/
public function __construct($record)
public function __construct(?array $record)
{
$this->record = isset($record) ? $record : [];
}
/**
* @ignore
*
* @param mixed $attr
*/
public function __get($attr)
public function __get(string $attr)
{
// XXX - kind of ugly but greatly reduces boilerplate code
$key = $this->attributeToKey($attr);
@ -38,23 +36,23 @@ abstract class AbstractRecord implements \JsonSerializable
throw new \RuntimeException("Unknown attribute: $attr");
}
public function __isset($attr)
public function __isset(string $attr): bool
{
return $this->validAttribute($attr) &&
isset($this->record[$this->attributeToKey($attr)]);
}
private function attributeToKey($attr)
private function attributeToKey(string $attr): string
{
return strtolower(preg_replace('/([A-Z])/', '_\1', $attr));
}
private function validAttribute($attr)
private function validAttribute(string $attr): bool
{
return \in_array($attr, $this->validAttributes, true);
}
public function jsonSerialize()
public function jsonSerialize(): ?array
{
return $this->record;
}

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
/**

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\Record;
use GeoIp2\Util;
@ -56,6 +58,9 @@ use GeoIp2\Util;
* @property-read bool $isPublicProxy This is true if the IP address belongs to
* a public proxy. This property is only available from GeoIP2 Precision
* 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 Precision 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
@ -120,6 +125,7 @@ class Traits extends AbstractRecord
'isLegitimateProxy',
'isp',
'isPublicProxy',
'isResidentialProxy',
'isSatelliteProvider',
'isTorExitNode',
'network',
@ -129,7 +135,7 @@ class Traits extends AbstractRecord
'userType',
];
public function __construct($record)
public function __construct(?array $record)
{
if (!isset($record['network']) && isset($record['ip_address']) && isset($record['prefix_len'])) {
$record['network'] = Util::cidr($record['ip_address'], $record['prefix_len']);

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2;
class Util
@ -10,11 +12,8 @@ class Util
*
* @internal
* @ignore
*
* @param mixed $ipAddress
* @param mixed $prefixLen
*/
public static function cidr($ipAddress, $prefixLen)
public static function cidr(string $ipAddress, int $prefixLen): string
{
$ipBytes = inet_pton($ipAddress);
$networkBytes = str_repeat("\0", \strlen($ipBytes));

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace GeoIp2\WebService;
use GeoIp2\Exception\AddressNotFoundException;
@ -47,7 +49,7 @@ class Client implements ProviderInterface
private $client;
private static $basePath = '/geoip/v2.1';
const VERSION = 'v2.10.0';
const VERSION = 'v2.11.0';
/**
* Constructor.
@ -65,10 +67,10 @@ class Client implements ProviderInterface
* `http://username:password@127.0.0.1:10`.
*/
public function __construct(
$accountId,
$licenseKey,
$locales = ['en'],
$options = []
int $accountId,
string $licenseKey,
array $locales = ['en'],
array $options = []
) {
$this->locales = $locales;
@ -87,7 +89,7 @@ class Client implements ProviderInterface
$this->client = new WsClient($accountId, $licenseKey, $options);
}
private function userAgent()
private function userAgent(): string
{
return 'GeoIP2-API/' . self::VERSION;
}
@ -115,10 +117,8 @@ class Client implements ProviderInterface
* @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.
*
* @return \GeoIp2\Model\City
*/
public function city($ipAddress = 'me')
public function city(string $ipAddress = 'me'): \GeoIp2\Model\City
{
return $this->responseFor('city', 'City', $ipAddress);
}
@ -146,10 +146,8 @@ class Client implements ProviderInterface
* @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.
*
* @return \GeoIp2\Model\Country
*/
public function country($ipAddress = 'me')
public function country(string $ipAddress = 'me'): \GeoIp2\Model\Country
{
return $this->responseFor('country', 'Country', $ipAddress);
}
@ -177,15 +175,13 @@ class Client implements ProviderInterface
* @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.
*
* @return \GeoIp2\Model\Insights
*/
public function insights($ipAddress = 'me')
public function insights(string $ipAddress = 'me'): \GeoIp2\Model\Insights
{
return $this->responseFor('insights', 'Insights', $ipAddress);
}
private function responseFor($endpoint, $class, $ipAddress)
private function responseFor(string $endpoint, string $class, string $ipAddress)
{
$path = implode('/', [self::$basePath, $endpoint, $ipAddress]);

View File

@ -1,6 +1,45 @@
CHANGELOG
=========
1.9.0 (2021-01-07)
------------------
* The `maxminddb` extension is now buildable on Windows. Pull request
by Jan Ehrhardt. GitHub #115.
1.8.0 (2020-10-01)
------------------
* Fixes for PHP 8.0. Pull Request by Remi Collet. GitHub #108.
1.7.0 (2020-08-07)
------------------
* IMPORTANT: PHP 7.2 or greater is now required.
* The extension no longer depends on the pure PHP classes in
`maxmind-db/reader`. You can use it independently.
* Type hints have been added to both the pure PHP implementation
and the extension.
* The `metadata` method on the reader now returns a new copy of the
metadata object rather than the actual object used by the reader.
* Work around PHP `is_readable()` bug. Reported by Ben Roberts. GitHub
#92.
* This is the first release of the extension as a PECL package.
GitHub #34.
1.6.0 (2019-12-19)
------------------
* 1.5.0 and 1.5.1 contained a possible memory corruptions when using
`getWithPrefixLen`. This has been fixed. Reported by proton-ab.
GitHub #96.
* The `composer.json` file now conflicts with all versions of the
`maxminddb` C extension less than the Composer version. This is to
reduce the chance of having an older, conflicting version of the
extension installed. You will need to upgrade the extension before
running `composer update`. Pull request by Benoît Burnichon. GitHub
#97.
1.5.1 (2019-12-12)
------------------

View File

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db;
use ArgumentCountError;
use BadMethodCallException;
use Exception;
use InvalidArgumentException;
@ -17,15 +20,42 @@ use UnexpectedValueException;
*/
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;
private static $METADATA_MAX_SIZE = 131072; // 128 * 1024 = 128KB
/**
* @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;
/**
@ -35,40 +65,38 @@ class Reader
* @param string $database
* the MaxMind DB file to use
*
* @throws InvalidArgumentException for invalid database path or unknown arguments
* @throws \MaxMind\Db\Reader\InvalidDatabaseException
* if the database is invalid or there is an error reading
* from it
* @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($database)
public function __construct(string $database)
{
if (\func_num_args() !== 1) {
throw new InvalidArgumentException(
'The constructor takes exactly one argument.'
throw new ArgumentCountError(
sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
if (!is_readable($database)) {
$fileHandle = @fopen($database, 'rb');
if ($fileHandle === false) {
throw new InvalidArgumentException(
"The file \"$database\" does not exist or is not readable."
);
}
$this->fileHandle = @fopen($database, 'rb');
if ($this->fileHandle === false) {
throw new InvalidArgumentException(
"Error opening \"$database\"."
);
}
$this->fileSize = @filesize($database);
if ($this->fileSize === false) {
$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);
list($metadataArray) = $metadataDecoder->decode($start);
[$metadataArray] = $metadataDecoder->decode($start);
$this->metadata = new Metadata($metadataArray);
$this->decoder = new Decoder(
$this->fileHandle,
@ -91,14 +119,14 @@ class Reader
*
* @return mixed the record for the IP address
*/
public function get($ipAddress)
public function get(string $ipAddress)
{
if (\func_num_args() !== 1) {
throw new InvalidArgumentException(
'Method takes exactly one argument.'
throw new ArgumentCountError(
sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
list($record) = $this->getWithPrefixLen($ipAddress);
[$record] = $this->getWithPrefixLen($ipAddress);
return $record;
}
@ -118,11 +146,11 @@ class Reader
* @return array an array where the first element is the record and the
* second the network prefix length for the record
*/
public function getWithPrefixLen($ipAddress)
public function getWithPrefixLen(string $ipAddress): array
{
if (\func_num_args() !== 1) {
throw new InvalidArgumentException(
'Method takes exactly one argument.'
throw new ArgumentCountError(
sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
);
}
@ -132,13 +160,7 @@ class Reader
);
}
if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
throw new InvalidArgumentException(
"The value \"$ipAddress\" is not a valid IP address."
);
}
list($pointer, $prefixLen) = $this->findAddressInTree($ipAddress);
[$pointer, $prefixLen] = $this->findAddressInTree($ipAddress);
if ($pointer === 0) {
return [null, $prefixLen];
}
@ -146,9 +168,16 @@ class Reader
return [$this->resolveDataPointer($pointer), $prefixLen];
}
private function findAddressInTree($ipAddress)
private function findAddressInTree(string $ipAddress): array
{
$rawAddress = unpack('C*', inet_pton($ipAddress));
$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;
@ -186,10 +215,12 @@ class Reader
// Record is a data pointer
return [$node, $i];
}
throw new InvalidDatabaseException('Something bad happened');
throw new InvalidDatabaseException(
'Invalid or corrupt database. Maximum search depth reached without finding a leaf node'
);
}
private function ipV4StartNode()
private function ipV4StartNode(): int
{
// If we have an IPv4 database, the start node is the first node
if ($this->metadata->ipVersion === 4) {
@ -205,14 +236,14 @@ class Reader
return $node;
}
private function readNode($nodeNumber, $index)
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);
list(, $node) = unpack('N', "\x00" . $bytes);
[, $node] = unpack('N', "\x00" . $bytes);
return $node;
case 28:
@ -222,12 +253,12 @@ class Reader
} else {
$middle = 0x0F & \ord($bytes[0]);
}
list(, $node) = unpack('N', \chr($middle) . substr($bytes, $index, 3));
[, $node] = unpack('N', \chr($middle) . substr($bytes, $index, 3));
return $node;
case 32:
$bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
list(, $node) = unpack('N', $bytes);
[, $node] = unpack('N', $bytes);
return $node;
default:
@ -238,7 +269,10 @@ class Reader
}
}
private function resolveDataPointer($pointer)
/**
* @return mixed
*/
private function resolveDataPointer(int $pointer)
{
$resolved = $pointer - $this->metadata->nodeCount
+ $this->metadata->searchTreeSize;
@ -248,7 +282,7 @@ class Reader
);
}
list($data) = $this->decoder->decode($resolved);
[$data] = $this->decoder->decode($resolved);
return $data;
}
@ -258,7 +292,7 @@ class Reader
* 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($filename)
private function findMetadataStart(string $filename): int
{
$handle = $this->fileHandle;
$fstat = fstat($handle);
@ -290,11 +324,11 @@ class Reader
*
* @return Metadata object for the database
*/
public function metadata()
public function metadata(): Metadata
{
if (\func_num_args()) {
throw new InvalidArgumentException(
'Method takes no arguments.'
throw new ArgumentCountError(
sprintf('%s() expects exactly 0 parameters, %d given', __METHOD__, \func_num_args())
);
}
@ -306,7 +340,7 @@ class Reader
);
}
return $this->metadata;
return clone $this->metadata;
}
/**
@ -315,8 +349,14 @@ class Reader
* @throws Exception
* if an I/O error occurs
*/
public function close()
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.'

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
// @codingStandardsIgnoreLine
@ -14,50 +16,53 @@ use RuntimeException;
class Decoder
{
/**
* @var resource
*/
private $fileStream;
/**
* @var int
*/
private $pointerBase;
/**
* @var float
*/
private $pointerBaseByteSize;
// This is only used for unit testing
/**
* This is only used for unit testing.
*
* @var bool
*/
private $pointerTestHack;
/**
* @var bool
*/
private $switchByteOrder;
/** @ignore */
const _EXTENDED = 0;
/** @ignore */
const _POINTER = 1;
/** @ignore */
const _UTF8_STRING = 2;
/** @ignore */
const _DOUBLE = 3;
/** @ignore */
const _BYTES = 4;
/** @ignore */
const _UINT16 = 5;
/** @ignore */
const _UINT32 = 6;
/** @ignore */
const _MAP = 7;
/** @ignore */
const _INT32 = 8;
/** @ignore */
const _UINT64 = 9;
/** @ignore */
const _UINT128 = 10;
/** @ignore */
const _ARRAY = 11;
/** @ignore */
const _CONTAINER = 12;
/** @ignore */
const _END_MARKER = 13;
/** @ignore */
const _BOOLEAN = 14;
/** @ignore */
const _FLOAT = 15;
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,
$pointerBase = 0,
$pointerTestHack = false
int $pointerBase = 0,
bool $pointerTestHack = false
) {
$this->fileStream = $fileStream;
$this->pointerBase = $pointerBase;
@ -68,7 +73,7 @@ class Decoder
$this->switchByteOrder = $this->isPlatformLittleEndian();
}
public function decode($offset)
public function decode(int $offset): array
{
$ctrlByte = \ord(Util::read($this->fileStream, $offset, 1));
++$offset;
@ -79,14 +84,14 @@ class Decoder
// use the size to determine the length of the pointer and then follow
// it.
if ($type === self::_POINTER) {
list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
[$pointer, $offset] = $this->decodePointer($ctrlByte, $offset);
// for unit testing
if ($this->pointerTestHack) {
return [$pointer];
}
list($result) = $this->decode($pointer);
[$result] = $this->decode($pointer);
return [$result, $offset];
}
@ -108,12 +113,12 @@ class Decoder
++$offset;
}
list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
[$size, $offset] = $this->sizeFromCtrlByte($ctrlByte, $offset);
return $this->decodeByType($type, $offset, $size);
}
private function decodeByType($type, $offset, $size)
private function decodeByType(int $type, int $offset, int $size): array
{
switch ($type) {
case self::_MAP:
@ -152,7 +157,7 @@ class Decoder
}
}
private function verifySize($expected, $actual)
private function verifySize(int $expected, int $actual): void
{
if ($expected !== $actual) {
throw new InvalidDatabaseException(
@ -161,50 +166,42 @@ class Decoder
}
}
private function decodeArray($size, $offset)
private function decodeArray(int $size, int $offset): array
{
$array = [];
for ($i = 0; $i < $size; ++$i) {
list($value, $offset) = $this->decode($offset);
array_push($array, $value);
[$value, $offset] = $this->decode($offset);
$array[] = $value;
}
return [$array, $offset];
}
private function decodeBoolean($size)
private function decodeBoolean(int $size): bool
{
return $size === 0 ? false : true;
return $size !== 0;
}
private function decodeDouble($bits)
private function decodeDouble(string $bytes): float
{
// This assumes IEEE 754 doubles, but most (all?) modern platforms
// use them.
//
// We are not using the "E" format as that was only added in
// 7.0.15 and 7.1.1. As such, we must switch byte order on
// little endian machines.
list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
[, $double] = unpack('E', $bytes);
return $double;
}
private function decodeFloat($bits)
private function decodeFloat(string $bytes): float
{
// This assumes IEEE 754 floats, but most (all?) modern platforms
// use them.
//
// We are not using the "G" format as that was only added in
// 7.0.15 and 7.1.1. As such, we must switch byte order on
// little endian machines.
list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
[, $float] = unpack('G', $bytes);
return $float;
}
private function decodeInt32($bytes, $size)
private function decodeInt32(string $bytes, int $size): int
{
switch ($size) {
case 0:
@ -222,25 +219,25 @@ class Decoder
);
}
list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
[, $int] = unpack('l', $this->maybeSwitchByteOrder($bytes));
return $int;
}
private function decodeMap($size, $offset)
private function decodeMap(int $size, int $offset): array
{
$map = [];
for ($i = 0; $i < $size; ++$i) {
list($key, $offset) = $this->decode($offset);
list($value, $offset) = $this->decode($offset);
[$key, $offset] = $this->decode($offset);
[$value, $offset] = $this->decode($offset);
$map[$key] = $value;
}
return [$map, $offset];
}
private function decodePointer($ctrlByte, $offset)
private function decodePointer(int $ctrlByte, int $offset): array
{
$pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
@ -250,12 +247,12 @@ class Decoder
switch ($pointerSize) {
case 1:
$packed = \chr($ctrlByte & 0x7) . $buffer;
list(, $pointer) = unpack('n', $packed);
[, $pointer] = unpack('n', $packed);
$pointer += $this->pointerBase;
break;
case 2:
$packed = "\x00" . \chr($ctrlByte & 0x7) . $buffer;
list(, $pointer) = unpack('N', $packed);
[, $pointer] = unpack('N', $packed);
$pointer += $this->pointerBase + 2048;
break;
case 3:
@ -263,7 +260,7 @@ class Decoder
// It is safe to use 'N' here, even on 32 bit machines as the
// first bit is 0.
list(, $pointer) = unpack('N', $packed);
[, $pointer] = unpack('N', $packed);
$pointer += $this->pointerBase + 526336;
break;
case 4:
@ -278,18 +275,23 @@ class Decoder
} elseif (\extension_loaded('gmp')) {
$pointer = gmp_strval(gmp_add($pointerOffset, $this->pointerBase));
} elseif (\extension_loaded('bcmath')) {
$pointer = bcadd($pointerOffset, $this->pointerBase);
$pointer = bcadd($pointerOffset, (string) $this->pointerBase);
} else {
throw new RuntimeException(
'The gmp or bcmath extension must be installed to read this database.'
);
}
break;
default:
throw new InvalidDatabaseException(
'Unexpected pointer size ' . $pointerSize
);
}
return [$pointer, $offset];
}
private function decodeUint($bytes, $byteLength)
private function decodeUint(string $bytes, int $byteLength)
{
if ($byteLength === 0) {
return 0;
@ -304,9 +306,9 @@ class Decoder
if ($byteLength <= _MM_MAX_INT_BYTES) {
$integer = ($integer << 8) + $part;
} elseif (\extension_loaded('gmp')) {
$integer = gmp_strval(gmp_add(gmp_mul($integer, 256), $part));
$integer = gmp_strval(gmp_add(gmp_mul((string) $integer, '256'), $part));
} elseif (\extension_loaded('bcmath')) {
$integer = bcadd(bcmul($integer, 256), $part);
$integer = bcadd(bcmul((string) $integer, '256'), (string) $part);
} else {
throw new RuntimeException(
'The gmp or bcmath extension must be installed to read this database.'
@ -317,7 +319,7 @@ class Decoder
return $integer;
}
private function sizeFromCtrlByte($ctrlByte, $offset)
private function sizeFromCtrlByte(int $ctrlByte, int $offset): array
{
$size = $ctrlByte & 0x1f;
@ -331,22 +333,22 @@ class Decoder
if ($size === 29) {
$size = 29 + \ord($bytes);
} elseif ($size === 30) {
list(, $adjust) = unpack('n', $bytes);
[, $adjust] = unpack('n', $bytes);
$size = 285 + $adjust;
} elseif ($size > 30) {
list(, $adjust) = unpack('N', "\x00" . $bytes);
} else {
[, $adjust] = unpack('N', "\x00" . $bytes);
$size = $adjust + 65821;
}
return [$size, $offset + $bytesToRead];
}
private function maybeSwitchByteOrder($bytes)
private function maybeSwitchByteOrder(string $bytes): string
{
return $this->switchByteOrder ? strrev($bytes) : $bytes;
}
private function isPlatformLittleEndian()
private function isPlatformLittleEndian(): bool
{
$testint = 0x00FF;
$packed = pack('S', $testint);

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
use Exception;

View File

@ -1,71 +1,100 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
use ArgumentCountError;
/**
* This class provides the metadata for the MaxMind DB file.
*
* @property int $nodeCount This is an unsigned 32-bit
* integer indicating the number of
* nodes in the search tree.
* @property int $recordSize 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.
* @property int $ipVersion This is an unsigned 16-bit
* integer which is always 4 or 6.
* It indicates whether the database
* contains IPv4 or IPv6 address
* data.
* @property string $databaseType 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.
* @property array $languages 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.
* @property int $binaryFormatMajorVersion This is an unsigned 16-bit
* integer indicating the major
* version number for the database's
* binary format.
* @property int $binaryFormatMinorVersion This is an unsigned 16-bit
* integer indicating the minor
* version number for the database's
* binary format.
* @property int $buildEpoch This is an unsigned 64-bit
* integer that contains the
* database build timestamp as a
* Unix epoch value.
* @property array $description 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.
*/
class Metadata
{
private $binaryFormatMajorVersion;
private $binaryFormatMinorVersion;
private $buildEpoch;
private $databaseType;
private $description;
private $ipVersion;
private $languages;
private $nodeByteSize;
private $nodeCount;
private $recordSize;
private $searchTreeSize;
/**
* 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($metadata)
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 =
@ -80,9 +109,4 @@ class Metadata
$this->nodeByteSize = $this->recordSize / 4;
$this->searchTreeSize = $this->nodeCount * $this->nodeByteSize;
}
public function __get($var)
{
return $this->$var;
}
}

View File

@ -1,10 +1,15 @@
<?php
declare(strict_types=1);
namespace MaxMind\Db\Reader;
class Util
{
public static function read($stream, $offset, $numberOfBytes)
/**
* @param resource $stream
*/
public static function read($stream, int $offset, int $numberOfBytes): string
{
if ($numberOfBytes === 0) {
return '';
@ -15,7 +20,7 @@ class Util
// 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 (ftell($stream) - $offset === $numberOfBytes) {
if ($value !== false && ftell($stream) - $offset === $numberOfBytes) {
return $value;
}
}

View File

@ -157,7 +157,7 @@ client API, please see [our support page](https://www.maxmind.com/en/support).
## Requirements ##
This library requires PHP 5.6 or greater.
This library requires PHP 7.2 or greater.
The GMP or BCMath extension may be required to read some databases
using the pure PHP API.
@ -173,6 +173,6 @@ The MaxMind DB Reader PHP API uses [Semantic Versioning](https://semver.org/).
## Copyright and License ##
This software is Copyright (c) 2014-2019 by MaxMind, Inc.
This software is Copyright (c) 2014-2020 by MaxMind, Inc.
This is free software, licensed under the Apache License, Version 2.0.

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
/**
* PSR-4 autoloader implementation for the MaxMind\DB namespace.
* First we define the 'mmdb_autoload' function, and then we register
@ -14,7 +16,7 @@
* @param string $class
* the name of the class to load
*/
function mmdb_autoload($class)
function mmdb_autoload($class): void
{
/*
* A project-specific mapping between the namespaces and where

View File

@ -13,18 +13,21 @@
}
],
"require": {
"php": ">=5.6"
"php": ">=7.2"
},
"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"
},
"conflict": {
"ext-maxminddb": "<1.9.0,>=2.0.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "2.*",
"phpunit/phpunit": "5.*",
"friendsofphp/php-cs-fixer": "*",
"phpunit/phpunit": ">=8.0.0,<10.0.0",
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpcov": "^3.0",
"phpunit/phpcov": ">=6.0.0",
"squizlabs/php_codesniffer": "3.*"
},
"autoload": {

View File

@ -16,15 +16,24 @@ Installation
------------
1) Download the latest versions of GeoIP2-php and MaxMind-DB-Reader-php
wget https://github.com/maxmind/GeoIP2-php/archive/v2.10.0.zip
wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/v1.5.1.zip
wget https://github.com/maxmind/GeoIP2-php/archive/vX.Y.Z.zip
wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/vX.Y.Z.zip
2) Unzip the archives
unzip v2.10.0.zip
unzip v1.5.1.zip
unzip vX.Y.Z.zip
unzip vX.Y.Z.zip
3) Move the source code directories into place
mv GeoIP2-php-2.10.0/src/ /path/to/moodle/lib/maxmind/GeoIp2/
mv MaxMind-DB-Reader-php-1.5.1/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
mv GeoIP2-php-X.Y.Z/src/ /path/to/moodle/lib/maxmind/GeoIp2/
mv MaxMind-DB-Reader-php-X.Y.Z/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
4) Run unit tests on iplookup/tests/geoip_test.php.
4) Update other MaxMind related files:
mv MaxMind-DB-Reader-php-X.Y.Z/LICENSE /path/to/moodle/lib/maxmind/MaxMind/
mv MaxMind-DB-Reader-php-X.Y.Z/CHANGELOG.md /path/to/moodle/lib/maxmind/MaxMind/
mv MaxMind-DB-Reader-php-X.Y.Z/README.md /path/to/moodle/lib/maxmind/MaxMind/
mv MaxMind-DB-Reader-php-X.Y.Z/composer.json /path/to/moodle/lib/maxmind/MaxMind/
mv MaxMind-DB-Reader-php-X.Y.Z/autoload.php /path/to/moodle/lib/maxmind/MaxMind/
5) Run unit tests on iplookup/tests/geoip_test.php.
6) Update maxmind/GeoIp2 and maxmind/MaxMin versions in lib/thirdpartylibs.xml

View File

@ -248,13 +248,13 @@
<location>maxmind/GeoIp2</location>
<name>GeoIP2 PHP API</name>
<license>Apache 2.0</license>
<version>2.10.0</version>
<version>2.11.0</version>
</library>
<library>
<location>maxmind/MaxMind</location>
<name>MaxMind DB Reader API</name>
<license>Apache 2.0</license>
<version>1.5.1</version>
<version>1.9.0</version>
</library>
<library>
<location>ltiprovider</location>