MDL-48766 lib: Import MaxMind GeoIP2 PHP API
authorDan Poltawski <dan@moodle.com>
Thu, 26 Nov 2015 12:50:34 +0000 (12:50 +0000)
committerDan Poltawski <dan@moodle.com>
Mon, 19 Sep 2016 07:59:38 +0000 (08:59 +0100)
No changes from the upstream version have been made, it is recommended by
upstream to install these depdencies via composer - but the composer
installation is bundled with a load of test files, shell scripts etc (and
we don't use composer to manage 'production dependencies') so we have to
do it manually.

41 files changed:
.eslintignore
.stylelintignore
lib/classes/component.php
lib/maxmind/GeoIp2/Compat/JsonSerializable.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Database/Reader.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/AddressNotFoundException.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/AuthenticationException.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/GeoIp2Exception.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/HttpException.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/InvalidRequestException.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Exception/OutOfQueriesException.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/AbstractModel.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/AnonymousIp.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/City.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/ConnectionType.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/Country.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/Domain.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/Enterprise.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/Insights.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Model/Isp.php [new file with mode: 0644]
lib/maxmind/GeoIp2/ProviderInterface.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/AbstractPlaceRecord.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/AbstractRecord.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/City.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Continent.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Country.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Location.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/MaxMind.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Postal.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/RepresentedCountry.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Subdivision.php [new file with mode: 0644]
lib/maxmind/GeoIp2/Record/Traits.php [new file with mode: 0644]
lib/maxmind/GeoIp2/WebService/Client.php [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader.php [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader/Decoder.php [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader/InvalidDatabaseException.php [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader/Metadata.php [new file with mode: 0644]
lib/maxmind/MaxMind/Db/Reader/Util.php [new file with mode: 0644]
lib/maxmind/README_moodle.txt [new file with mode: 0644]
lib/thirdpartylibs.xml
version.php

index 576c8c4..c3d7f41 100644 (file)
@@ -49,6 +49,8 @@ lib/amd/src/mustache.js
 lib/graphlib.php
 lib/spout/
 lib/amd/src/chartjs-lazy.js
+lib/maxmind/GeoIP2/
+lib/maxmind/MaxMind/
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/bootstrapbase/less/bootstrap/
index c73d41a..1120efe 100644 (file)
@@ -48,6 +48,8 @@ lib/amd/src/mustache.js
 lib/graphlib.php
 lib/spout/
 lib/amd/src/chartjs-lazy.js
+lib/maxmind/GeoIP2/
+lib/maxmind/MaxMind/
 mod/assign/feedback/editpdf/fpdi/
 repository/s3/S3.php
 theme/bootstrapbase/less/bootstrap/
index c49ed69..6544ab4 100644 (file)
@@ -73,6 +73,8 @@ class core_component {
     );
     /** @var array associative array of PRS-4 namespaces and corresponding paths. */
     protected static $psr4namespaces = array(
+        'MaxMind' => 'lib/maxmind/MaxMind',
+        'GeoIp2' => 'lib/maxmind/GeoIP2',
     );
 
     /**
diff --git a/lib/maxmind/GeoIp2/Compat/JsonSerializable.php b/lib/maxmind/GeoIp2/Compat/JsonSerializable.php
new file mode 100644 (file)
index 0000000..4846ce7
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace GeoIp2\Compat;
+
+// @codingStandardsIgnoreFile
+
+/**
+  * This interface exists to provide backwards compatibility with PHP 5.3
+  *
+  * This should _not_ be used by any third-party code.
+  *
+  * @ignore
+  */
+if (interface_exists('JsonSerializable')) {
+    interface JsonSerializable extends \JsonSerializable
+    {
+    }
+} else {
+    interface JsonSerializable
+    {
+        /**
+         * Returns data that can be serialized by json_encode
+         * @ignore
+         */
+        public function jsonSerialize();
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Database/Reader.php b/lib/maxmind/GeoIp2/Database/Reader.php
new file mode 100644 (file)
index 0000000..b19b5f1
--- /dev/null
@@ -0,0 +1,246 @@
+<?php
+
+namespace GeoIp2\Database;
+
+use GeoIp2\Exception\AddressNotFoundException;
+use GeoIp2\ProviderInterface;
+use MaxMind\Db\Reader as DbReader;
+
+/**
+ * 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
+{
+    private $dbReader;
+    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(
+        $filename,
+        $locales = array('en')
+    ) {
+        $this->dbReader = new DbReader($filename);
+        $this->locales = $locales;
+    }
+
+    /**
+     * This method returns a GeoIP2 City model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\City
+     *
+     * @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($ipAddress)
+    {
+        return $this->modelFor('City', 'City', $ipAddress);
+    }
+
+    /**
+     * This method returns a GeoIP2 Country model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\Country
+     *
+     * @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($ipAddress)
+    {
+        return $this->modelFor('Country', 'Country', $ipAddress);
+    }
+
+    /**
+     * This method returns a GeoIP2 Anonymous IP model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\AnonymousIp
+     *
+     * @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($ipAddress)
+    {
+        return $this->flatModelFor(
+            'AnonymousIp',
+            'GeoIP2-Anonymous-IP',
+            $ipAddress
+        );
+    }
+
+    /**
+     * This method returns a GeoIP2 Connection Type model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\ConnectionType
+     *
+     * @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($ipAddress)
+    {
+        return $this->flatModelFor(
+            'ConnectionType',
+            'GeoIP2-Connection-Type',
+            $ipAddress
+        );
+    }
+
+    /**
+     * This method returns a GeoIP2 Domain model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\Domain
+     *
+     * @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($ipAddress)
+    {
+        return $this->flatModelFor(
+            'Domain',
+            'GeoIP2-Domain',
+            $ipAddress
+        );
+    }
+
+    /**
+     * This method returns a GeoIP2 Enterprise model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\Enterprise
+     *
+     * @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($ipAddress)
+    {
+        return $this->modelFor('Enterprise', 'Enterprise', $ipAddress);
+    }
+
+    /**
+     * This method returns a GeoIP2 ISP model.
+     *
+     * @param string $ipAddress IPv4 or IPv6 address as a string.
+     *
+     * @return \GeoIp2\Model\Isp
+     *
+     * @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($ipAddress)
+    {
+        return $this->flatModelFor(
+            'Isp',
+            'GeoIP2-ISP',
+            $ipAddress
+        );
+    }
+
+    private function modelFor($class, $type, $ipAddress)
+    {
+        $record = $this->getRecord($class, $type, $ipAddress);
+
+        $record['traits']['ip_address'] = $ipAddress;
+        $class = "GeoIp2\\Model\\" . $class;
+
+        return new $class($record, $this->locales);
+    }
+
+    private function flatModelFor($class, $type, $ipAddress)
+    {
+        $record = $this->getRecord($class, $type, $ipAddress);
+
+        $record['ip_address'] = $ipAddress;
+        $class = "GeoIp2\\Model\\" . $class;
+
+        return new $class($record);
+    }
+
+    private function getRecord($class, $type, $ipAddress)
+    {
+        if (strpos($this->metadata()->databaseType, $type) === false) {
+            $method = lcfirst($class);
+            throw new \BadMethodCallException(
+                "The $method method cannot be used to open a "
+                . $this->metadata()->databaseType . " database"
+            );
+        }
+        $record = $this->dbReader->get($ipAddress);
+        if ($record === null) {
+            throw new AddressNotFoundException(
+                "The address $ipAddress is not in the database."
+            );
+        }
+        return $record;
+    }
+
+    /**
+     * @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()
+    {
+        return $this->dbReader->metadata();
+    }
+
+    /**
+     * Closes the GeoIP2 database and returns the resources to the system.
+     */
+    public function close()
+    {
+        $this->dbReader->close();
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Exception/AddressNotFoundException.php b/lib/maxmind/GeoIp2/Exception/AddressNotFoundException.php
new file mode 100644 (file)
index 0000000..d548338
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace GeoIp2\Exception;
+
+/**
+ * This class represents a generic error.
+ */
+class AddressNotFoundException extends GeoIp2Exception
+{
+}
diff --git a/lib/maxmind/GeoIp2/Exception/AuthenticationException.php b/lib/maxmind/GeoIp2/Exception/AuthenticationException.php
new file mode 100644 (file)
index 0000000..2a8b592
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace GeoIp2\Exception;
+
+/**
+ * This class represents a generic error.
+ */
+class AuthenticationException extends GeoIp2Exception
+{
+}
diff --git a/lib/maxmind/GeoIp2/Exception/GeoIp2Exception.php b/lib/maxmind/GeoIp2/Exception/GeoIp2Exception.php
new file mode 100644 (file)
index 0000000..7c4d745
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace GeoIp2\Exception;
+
+/**
+ * This class represents a generic error.
+ */
+class GeoIp2Exception extends \Exception
+{
+}
diff --git a/lib/maxmind/GeoIp2/Exception/HttpException.php b/lib/maxmind/GeoIp2/Exception/HttpException.php
new file mode 100644 (file)
index 0000000..931fd46
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+namespace GeoIp2\Exception;
+
+/**
+ *  This class represents an HTTP transport error.
+ */
+
+class HttpException extends GeoIp2Exception
+{
+    /**
+     * The URI queried
+     */
+    public $uri;
+
+    public function __construct(
+        $message,
+        $httpStatus,
+        $uri,
+        \Exception $previous = null
+    ) {
+        $this->uri = $uri;
+        parent::__construct($message, $httpStatus, $previous);
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Exception/InvalidRequestException.php b/lib/maxmind/GeoIp2/Exception/InvalidRequestException.php
new file mode 100644 (file)
index 0000000..6712d73
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace 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
+     */
+    public $error;
+
+    public function __construct(
+        $message,
+        $error,
+        $httpStatus,
+        $uri,
+        \Exception $previous = null
+    ) {
+        $this->error = $error;
+        parent::__construct($message, $httpStatus, $uri, $previous);
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Exception/OutOfQueriesException.php b/lib/maxmind/GeoIp2/Exception/OutOfQueriesException.php
new file mode 100644 (file)
index 0000000..87a6ade
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace GeoIp2\Exception;
+
+/**
+ * This class represents a generic error.
+ */
+class OutOfQueriesException extends GeoIp2Exception
+{
+}
diff --git a/lib/maxmind/GeoIp2/Model/AbstractModel.php b/lib/maxmind/GeoIp2/Model/AbstractModel.php
new file mode 100644 (file)
index 0000000..c242666
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+namespace GeoIp2\Model;
+
+use GeoIp2\Compat\JsonSerializable;
+
+/**
+ * @ignore
+ */
+abstract class AbstractModel implements JsonSerializable
+{
+    protected $raw;
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw)
+    {
+        $this->raw = $raw;
+    }
+
+    /**
+     * @ignore
+     */
+    protected function get($field)
+    {
+        if (isset($this->raw[$field])) {
+            return $this->raw[$field];
+        } else {
+            if (preg_match('/^is_/', $field)) {
+                return false;
+            } else {
+                return null;
+            }
+        }
+    }
+
+    /**
+     * @ignore
+     */
+    public function __get($attr)
+    {
+        if ($attr != "instance" && property_exists($this, $attr)) {
+            return $this->$attr;
+        }
+
+        throw new \RuntimeException("Unknown attribute: $attr");
+    }
+
+    /**
+     * @ignore
+     */
+    public function __isset($attr)
+    {
+        return $attr != "instance" && isset($this->$attr);
+    }
+
+    public function jsonSerialize()
+    {
+        return $this->raw;
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/AnonymousIp.php b/lib/maxmind/GeoIp2/Model/AnonymousIp.php
new file mode 100644 (file)
index 0000000..90e7706
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * This class provides the GeoIP2 Anonymous IP model.
+ *
+ * @property-read boolean $isAnonymous This is true if the IP address belongs to
+ *     any sort of anonymous network.
+ *
+ * @property-read boolean $isAnonymousVpn This is true if the IP address belongs to
+ *     an anonymous VPN system.
+ *
+ * @property-read boolean $isHostingProvider This is true if the IP address belongs
+ *     to a hosting provider.
+ *
+ * @property-read boolean $isPublicProxy This is true if the IP address belongs to
+ *     a public proxy.
+ *
+ * @property-read boolean $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.
+ *
+ */
+class AnonymousIp extends AbstractModel
+{
+    protected $isAnonymous;
+    protected $isAnonymousVpn;
+    protected $isHostingProvider;
+    protected $isPublicProxy;
+    protected $isTorExitNode;
+    protected $ipAddress;
+
+    /**
+     * @ignore
+     */
+    public function __construct($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->isTorExitNode = $this->get('is_tor_exit_node');
+        $this->ipAddress = $this->get('ip_address');
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/City.php b/lib/maxmind/GeoIp2/Model/City.php
new file mode 100644 (file)
index 0000000..fb7ddda
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * Model class for the data returned by GeoIP2 City web service and database.
+ *
+ * The only difference between the City and Insights model classes is which
+ * fields in each record may be populated. See
+ * http://dev.maxmind.com/geoip/geoip2/web-services more details.
+ *
+ * @property-read \GeoIp2\Record\City $city City data for the requested IP
+ * address.
+ *
+ * @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\Location $location Location data for the
+ * requested IP address.
+ *
+ * @property-read \GeoIp2\Record\Postal $postal Postal data for the
+ * requested IP address.
+ *
+ * @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 array $subdivisions An array of {@link \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
+ * {@link \GeoIp2\Record\Subdivision} object.
+ *
+ * @property-read \GeoIp2\Record\Traits $traits Data for the traits of the
+ * requested IP address.
+ */
+class City extends Country
+{
+    /**
+     * @ignore
+     */
+    protected $city;
+    /**
+     * @ignore
+     */
+    protected $location;
+    /**
+     * @ignore
+     */
+    protected $postal;
+    /**
+     * @ignore
+     */
+    protected $subdivisions = array();
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw, $locales = array('en'))
+    {
+        parent::__construct($raw, $locales);
+
+        $this->city = new \GeoIp2\Record\City($this->get('city'), $locales);
+        $this->location = new \GeoIp2\Record\Location($this->get('location'));
+        $this->postal = new \GeoIp2\Record\Postal($this->get('postal'));
+
+        $this->createSubdivisions($raw, $locales);
+    }
+
+    private function createSubdivisions($raw, $locales)
+    {
+        if (!isset($raw['subdivisions'])) {
+            return;
+        }
+
+        foreach ($raw['subdivisions'] as $sub) {
+            array_push(
+                $this->subdivisions,
+                new \GeoIp2\Record\Subdivision($sub, $locales)
+            );
+        }
+    }
+
+    /**
+     * @ignore
+     */
+    public function __get($attr)
+    {
+        if ($attr == 'mostSpecificSubdivision') {
+            return $this->$attr();
+        } else {
+            return parent::__get($attr);
+        }
+    }
+
+    private function mostSpecificSubdivision()
+    {
+        return empty($this->subdivisions) ?
+            new \GeoIp2\Record\Subdivision(array(), $this->locales) :
+            end($this->subdivisions);
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/ConnectionType.php b/lib/maxmind/GeoIp2/Model/ConnectionType.php
new file mode 100644 (file)
index 0000000..dccd500
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * 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.
+ *
+ */
+class ConnectionType extends AbstractModel
+{
+    protected $connectionType;
+    protected $ipAddress;
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw)
+    {
+        parent::__construct($raw);
+
+        $this->connectionType = $this->get('connection_type');
+        $this->ipAddress = $this->get('ip_address');
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/Country.php b/lib/maxmind/GeoIp2/Model/Country.php
new file mode 100644 (file)
index 0000000..05cdea7
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * Model class for the data returned by GeoIP2 Country web service and database.
+ *
+ * The only difference between the City and Insights model classes is which
+ * fields in each record may be populated. See
+ * http://dev.maxmind.com/geoip/geoip2/web-services 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.
+ */
+class Country extends AbstractModel
+{
+    protected $continent;
+    protected $country;
+    protected $locales;
+    protected $maxmind;
+    protected $registeredCountry;
+    protected $representedCountry;
+    protected $traits;
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw, $locales = array('en'))
+    {
+        parent::__construct($raw);
+
+        $this->continent = new \GeoIp2\Record\Continent(
+            $this->get('continent'),
+            $locales
+        );
+        $this->country = new \GeoIp2\Record\Country(
+            $this->get('country'),
+            $locales
+        );
+        $this->maxmind = new \GeoIp2\Record\MaxMind($this->get('maxmind'));
+        $this->registeredCountry = new \GeoIp2\Record\Country(
+            $this->get('registered_country'),
+            $locales
+        );
+        $this->representedCountry = new \GeoIp2\Record\RepresentedCountry(
+            $this->get('represented_country'),
+            $locales
+        );
+        $this->traits = new \GeoIp2\Record\Traits($this->get('traits'));
+
+        $this->locales = $locales;
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/Domain.php b/lib/maxmind/GeoIp2/Model/Domain.php
new file mode 100644 (file)
index 0000000..c540644
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * 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.
+ *
+ */
+class Domain extends AbstractModel
+{
+    protected $domain;
+    protected $ipAddress;
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw)
+    {
+        parent::__construct($raw);
+
+        $this->domain = $this->get('domain');
+        $this->ipAddress = $this->get('ip_address');
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Model/Enterprise.php b/lib/maxmind/GeoIp2/Model/Enterprise.php
new file mode 100644 (file)
index 0000000..12d45bd
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * Model class for the data returned by GeoIP2 Enterprise database lookups.
+ *
+ * The only difference between the City and Enterprise model classes is which
+ * fields in each record may be populated. See
+ * http://dev.maxmind.com/geoip/geoip2/web-services more details.
+ *
+ * @property-read \GeoIp2\Record\City $city City data for the requested IP
+ * address.
+ *
+ * @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\Location $location Location data for the
+ * requested IP address.
+ *
+ * @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 array $subdivisions An array of {@link \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
+ * {@link \GeoIp2\Record\Subdivision} object.
+ *
+ * @property-read \GeoIp2\Record\Traits $traits Data for the traits of the
+ * requested IP address.
+ */
+class Enterprise extends City
+{
+}
diff --git a/lib/maxmind/GeoIp2/Model/Insights.php b/lib/maxmind/GeoIp2/Model/Insights.php
new file mode 100644 (file)
index 0000000..7c0c9e1
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * Model class for the data returned by GeoIP2 Precision: Insights web service.
+ *
+ * The only difference between the City and Insights model classes is which
+ * fields in each record may be populated. See
+ * http://dev.maxmind.com/geoip/geoip2/web-services more details.
+ *
+ * @property-read \GeoIp2\Record\City $city City data for the requested IP
+ * address.
+ *
+ * @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\Location $location Location data for the
+ * requested IP address.
+ *
+ * @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 array $subdivisions An array of {@link \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
+ * {@link \GeoIp2\Record\Subdivision} object.
+ *
+ * @property-read \GeoIp2\Record\Traits $traits Data for the traits of the
+ * requested IP address.
+ */
+class Insights extends City
+{
+}
diff --git a/lib/maxmind/GeoIp2/Model/Isp.php b/lib/maxmind/GeoIp2/Model/Isp.php
new file mode 100644 (file)
index 0000000..c2dd357
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+namespace GeoIp2\Model;
+
+/**
+ * This class provides the GeoIP2 Connection-Type model.
+ *
+ * @property-read integer|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 $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.
+ *
+ */
+class Isp extends AbstractModel
+{
+    protected $autonomousSystemNumber;
+    protected $autonomousSystemOrganization;
+    protected $isp;
+    protected $organization;
+    protected $ipAddress;
+
+    /**
+     * @ignore
+     */
+    public function __construct($raw)
+    {
+        parent::__construct($raw);
+        $this->autonomousSystemNumber = $this->get('autonomous_system_number');
+        $this->autonomousSystemOrganization =
+            $this->get('autonomous_system_organization');
+        $this->isp = $this->get('isp');
+        $this->organization = $this->get('organization');
+
+        $this->ipAddress = $this->get('ip_address');
+    }
+}
diff --git a/lib/maxmind/GeoIp2/ProviderInterface.php b/lib/maxmind/GeoIp2/ProviderInterface.php
new file mode 100644 (file)
index 0000000..6c3992f
--- /dev/null
@@ -0,0 +1,20 @@
+<?php
+
+namespace GeoIp2;
+
+interface ProviderInterface
+{
+    /**
+     * @param ipAddress
+     *            IPv4 or IPv6 address to lookup.
+     * @return \GeoIp2\Model\Country A Country model for the requested IP address.
+     */
+    public function country($ipAddress);
+
+    /**
+     * @param ipAddress
+     *            IPv4 or IPv6 address to lookup.
+     * @return \GeoIp2\Model\City A City model for the requested IP address.
+     */
+    public function city($ipAddress);
+}
diff --git a/lib/maxmind/GeoIp2/Record/AbstractPlaceRecord.php b/lib/maxmind/GeoIp2/Record/AbstractPlaceRecord.php
new file mode 100644 (file)
index 0000000..0b7e7d6
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+namespace GeoIp2\Record;
+
+abstract class AbstractPlaceRecord extends AbstractRecord
+{
+    private $locales;
+
+    /**
+     * @ignore
+     */
+    public function __construct($record, $locales = array('en'))
+    {
+        $this->locales = $locales;
+        parent::__construct($record);
+    }
+
+    /**
+     * @ignore
+     */
+    public function __get($attr)
+    {
+        if ($attr == 'name') {
+            return $this->name();
+        } else {
+            return parent::__get($attr);
+        }
+    }
+
+    private function name()
+    {
+        foreach ($this->locales as $locale) {
+            if (isset($this->names[$locale])) {
+                return $this->names[$locale];
+            }
+        }
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Record/AbstractRecord.php b/lib/maxmind/GeoIp2/Record/AbstractRecord.php
new file mode 100644 (file)
index 0000000..7bb1c5b
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+namespace GeoIp2\Record;
+
+use GeoIp2\Compat\JsonSerializable;
+
+abstract class AbstractRecord implements JsonSerializable
+{
+    private $record;
+
+    /**
+     * @ignore
+     */
+    public function __construct($record)
+    {
+        $this->record = isset($record) ? $record : array();
+    }
+
+    /**
+     * @ignore
+     */
+    public function __get($attr)
+    {
+        // XXX - kind of ugly but greatly reduces boilerplate code
+        $key = $this->attributeToKey($attr);
+
+        if ($this->__isset($attr)) {
+            return $this->record[$key];
+        } elseif ($this->validAttribute($attr)) {
+            if (preg_match('/^is_/', $key)) {
+                return false;
+            } else {
+                return null;
+            }
+        } else {
+            throw new \RuntimeException("Unknown attribute: $attr");
+        }
+    }
+
+    public function __isset($attr)
+    {
+        return $this->validAttribute($attr) &&
+             isset($this->record[$this->attributeToKey($attr)]);
+    }
+
+    private function attributeToKey($attr)
+    {
+        return strtolower(preg_replace('/([A-Z])/', '_\1', $attr));
+    }
+
+    private function validAttribute($attr)
+    {
+        return in_array($attr, $this->validAttributes);
+    }
+
+    public function jsonSerialize()
+    {
+        return $this->record;
+    }
+}
diff --git a/lib/maxmind/GeoIp2/Record/City.php b/lib/maxmind/GeoIp2/Record/City.php
new file mode 100644 (file)
index 0000000..9f1560f
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace 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 A 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
+     */
+    protected $validAttributes = array('confidence', 'geonameId', 'names');
+}
diff --git a/lib/maxmind/GeoIp2/Record/Continent.php b/lib/maxmind/GeoIp2/Record/Continent.php
new file mode 100644 (file)
index 0000000..4893e9e
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace 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
+     */
+    protected $validAttributes = array(
+        'code',
+        'geonameId',
+        'names'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/Record/Country.php b/lib/maxmind/GeoIp2/Record/Country.php
new file mode 100644 (file)
index 0000000..922195b
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+namespace 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 location services and databases.
+ *
+ * @property-read string|null $isoCode The {@link
+ * http://en.wikipedia.org/wiki/ISO_3166-1 two-character ISO 3166-1 alpha
+ * code} for the country. 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
+     */
+    protected $validAttributes = array(
+        'confidence',
+        'geonameId',
+        'isoCode',
+        'names'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/Record/Location.php b/lib/maxmind/GeoIp2/Record/Location.php
new file mode 100644 (file)
index 0000000..5d0742d
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+
+namespace 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
+ * {@link
+ * https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions
+ * Google AdWords API}.
+ *
+ * @property-read string|null $timeZone The time zone associated with location, as
+ * specified by the {@link http://www.iana.org/time-zones IANA Time Zone
+ * Database}, e.g., "America/New_York".
+ */
+class Location extends AbstractRecord
+{
+    /**
+     * @ignore
+     */
+    protected $validAttributes = array(
+        'averageIncome',
+        'accuracyRadius',
+        'latitude',
+        'longitude',
+        'metroCode',
+        'populationDensity',
+        'postalCode',
+        'postalConfidence',
+        'timeZone'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/Record/MaxMind.php b/lib/maxmind/GeoIp2/Record/MaxMind.php
new file mode 100644 (file)
index 0000000..8971e96
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace 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
+     */
+    protected $validAttributes = array('queriesRemaining');
+}
diff --git a/lib/maxmind/GeoIp2/Record/Postal.php b/lib/maxmind/GeoIp2/Record/Postal.php
new file mode 100644 (file)
index 0000000..81d3011
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace 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
+     */
+    protected $validAttributes = array('code', 'confidence');
+}
diff --git a/lib/maxmind/GeoIp2/Record/RepresentedCountry.php b/lib/maxmind/GeoIp2/Record/RepresentedCountry.php
new file mode 100644 (file)
index 0000000..dd49114
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+
+namespace 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 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.
+ *
+ * @property-read string|null $isoCode The {@link http://en.wikipedia.org/wiki/ISO_3166-1
+ * two-character ISO 3166-1 alpha code} for the country.
+ *
+ * @property-read string|null $name The name of the country based on the locales list
+ * passed to the constructor.
+ *
+ * @property-read array|null $names An array map where the keys are locale codes and
+ * the values are names.
+ *
+ * @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
+{
+    protected $validAttributes = array(
+        'confidence',
+        'geonameId',
+        'isoCode',
+        'names',
+        'type'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/Record/Subdivision.php b/lib/maxmind/GeoIp2/Record/Subdivision.php
new file mode 100644 (file)
index 0000000..cb5256a
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace 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 {@link
+ * http://en.wikipedia.org/wiki/ISO_3166-2 ISO 3166-2 code}. 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
+     */
+    protected $validAttributes = array(
+        'confidence',
+        'geonameId',
+        'isoCode',
+        'names'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/Record/Traits.php b/lib/maxmind/GeoIp2/Record/Traits.php
new file mode 100644 (file)
index 0000000..7d8710f
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+
+namespace GeoIp2\Record;
+
+/**
+ *
+ * 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 {@link
+ * http://en.wikipedia.org/wiki/Autonomous_system_(Internet) autonomous
+ * system number} associated with the IP address. This attribute is only
+ * available from the City and Insights web service and the GeoIP2
+ * Enterprise database.
+ *
+ * @property-read string|null $autonomousSystemOrganization The organization
+ * associated with the registered {@link
+ * http://en.wikipedia.org/wiki/Autonomous_system_(Internet) autonomous
+ * system number} for the IP address. This attribute is only available from
+ * the City and Insights web service 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 and Insights web service 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 boolean $isAnonymousProxy *Deprecated.* Please see our {@link
+ * https://www.maxmind.com/en/geoip2-anonymous-ip-database GeoIP2
+ * Anonymous IP database} to determine whether the IP address is used by an
+ * anonymizing service.
+ *
+ * @property-read boolean $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 boolean $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 string|null $isp The name of the ISP associated with the IP
+ * address. This attribute is only available from the City and Insights web
+ * services and the GeoIP2 Enterprise database.
+ *
+ * @property-read string|null $organization The name of the organization associated
+ * with the IP address. This attribute is only available from the City and
+ * Insights web services and the GeoIP2 Enterprise database.
+ *
+ * @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>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
+     */
+    protected $validAttributes = array(
+        'autonomousSystemNumber',
+        'autonomousSystemOrganization',
+        'connectionType',
+        'domain',
+        'isAnonymousProxy',
+        'isLegitimateProxy',
+        'isSatelliteProvider',
+        'isp',
+        'ipAddress',
+        'organization',
+        'userType'
+    );
+}
diff --git a/lib/maxmind/GeoIp2/WebService/Client.php b/lib/maxmind/GeoIp2/WebService/Client.php
new file mode 100644 (file)
index 0000000..f42ec81
--- /dev/null
@@ -0,0 +1,242 @@
+<?php
+
+namespace GeoIp2\WebService;
+
+use GeoIp2\Exception\AddressNotFoundException;
+use GeoIp2\Exception\AuthenticationException;
+use GeoIp2\Exception\GeoIp2Exception;
+use GeoIp2\Exception\HttpException;
+use GeoIp2\Exception\InvalidRequestException;
+use GeoIp2\Exception\OutOfQueriesException;
+use GeoIp2\ProviderInterface;
+use MaxMind\Exception\InvalidInputException;
+use MaxMind\WebService\Client as WsClient;
+
+/**
+ * This class provides a client API for all the GeoIP2 Precision web services.
+ * The services are Country, City, 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 `$userId`
+ * 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
+{
+    private $locales;
+    private $client;
+    private static $basePath = '/geoip/v2.1';
+
+    const VERSION = 'v2.4.2';
+
+    /**
+     * Constructor.
+     *
+     * @param int $userId     Your MaxMind user 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.
+     *      * `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(
+        $userId,
+        $licenseKey,
+        $locales = array('en'),
+        $options = array()
+    ) {
+        $this->locales = $locales;
+
+        // This is for backwards compatibility. Do not remove except for a
+        // major version bump.
+        if (is_string($options)) {
+            $options = array( 'host' => $options );
+        }
+
+        if (!isset($options['host'])) {
+            $options['host'] = 'geoip.maxmind.com';
+        }
+
+        $options['userAgent'] = $this->userAgent();
+
+        $this->client = new WsClient($userId, $licenseKey, $options);
+    }
+
+    private function userAgent()
+    {
+        return 'GeoIP2-API/' . Client::VERSION;
+    }
+
+    /**
+     * This method calls the GeoIP2 Precision: City 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.
+     *
+     * @return \GeoIp2\Model\City
+     *
+     * @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 user 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($ipAddress = 'me')
+    {
+        return $this->responseFor('city', 'City', $ipAddress);
+    }
+
+    /**
+     * This method calls the GeoIP2 Precision: 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.
+     *
+     * @return \GeoIp2\Model\Country
+     *
+     * @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 user 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($ipAddress = 'me')
+    {
+        return $this->responseFor('country', 'Country', $ipAddress);
+    }
+
+    /**
+     * This method calls the GeoIP2 Precision: Insights 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.
+     *
+     * @return \GeoIp2\Model\Insights
+     *
+     * @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 user 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($ipAddress = 'me')
+    {
+        return $this->responseFor('insights', 'Insights', $ipAddress);
+    }
+
+    private function responseFor($endpoint, $class, $ipAddress)
+    {
+        $path = implode('/', array(self::$basePath, $endpoint, $ipAddress));
+
+        try {
+            $body = $this->client->get('GeoIP2 ' . $class, $path);
+        } catch (\MaxMind\Exception\IpAddressNotFoundException $ex) {
+            throw new AddressNotFoundException(
+                $ex->getMessage(),
+                $ex->getStatusCode(),
+                $ex
+            );
+        } catch (\MaxMind\Exception\AuthenticationException $ex) {
+            throw new AuthenticationException(
+                $ex->getMessage(),
+                $ex->getStatusCode(),
+                $ex
+            );
+        } catch (\MaxMind\Exception\InsufficientFundsException $ex) {
+            throw new OutOfQueriesException(
+                $ex->getMessage(),
+                $ex->getStatusCode(),
+                $ex
+            );
+        } catch (\MaxMind\Exception\InvalidRequestException $ex) {
+            throw new InvalidRequestException(
+                $ex->getMessage(),
+                $ex->getErrorCode(),
+                $ex->getStatusCode(),
+                $ex->getUri(),
+                $ex
+            );
+        } catch (\MaxMind\Exception\HttpException $ex) {
+            throw new HttpException(
+                $ex->getMessage(),
+                $ex->getStatusCode(),
+                $ex->getUri(),
+                $ex
+            );
+        } catch (\MaxMind\Exception\WebServiceException $ex) {
+            throw new GeoIp2Exception(
+                $ex->getMessage(),
+                $ex->getCode(),
+                $ex
+            );
+        }
+
+        $class = "GeoIp2\\Model\\" . $class;
+        return new $class($body, $this->locales);
+    }
+}
diff --git a/lib/maxmind/MaxMind/Db/Reader.php b/lib/maxmind/MaxMind/Db/Reader.php
new file mode 100644 (file)
index 0000000..f524e70
--- /dev/null
@@ -0,0 +1,296 @@
+<?php
+
+namespace MaxMind\Db;
+
+use MaxMind\Db\Reader\Decoder;
+use MaxMind\Db\Reader\InvalidDatabaseException;
+use MaxMind\Db\Reader\Metadata;
+use MaxMind\Db\Reader\Util;
+
+/**
+ * Instances of this class provide a reader for the MaxMind DB format. IP
+ * addresses can be looked up using the <code>get</code> method.
+ */
+class Reader
+{
+    private static $DATA_SECTION_SEPARATOR_SIZE = 16;
+    private static $METADATA_START_MARKER = "\xAB\xCD\xEFMaxMind.com";
+    private static $METADATA_START_MARKER_LENGTH = 14;
+
+    private $decoder;
+    private $fileHandle;
+    private $fileSize;
+    private $ipV4Start;
+    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 \MaxMind\Db\Reader\InvalidDatabaseException
+     *             if the database is invalid or there is an error reading
+     *             from it.
+     */
+    public function __construct($database)
+    {
+        if (func_num_args() != 1) {
+            throw new \InvalidArgumentException(
+                'The constructor takes exactly one argument.'
+            );
+        }
+
+        if (!is_readable($database)) {
+            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) {
+            throw new \UnexpectedValueException(
+                "Error determining the size of \"$database\"."
+            );
+        }
+
+        $start = $this->findMetadataStart($database);
+        $metadataDecoder = new Decoder($this->fileHandle, $start);
+        list($metadataArray) = $metadataDecoder->decode($start);
+        $this->metadata = new Metadata($metadataArray);
+        $this->decoder = new Decoder(
+            $this->fileHandle,
+            $this->metadata->searchTreeSize + self::$DATA_SECTION_SEPARATOR_SIZE
+        );
+    }
+
+    /**
+     * Looks up the <code>address</code> in the MaxMind DB.
+     *
+     * @param string $ipAddress
+     *            the IP address to look up.
+     * @return array the record for the IP address.
+     * @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.
+     */
+    public function get($ipAddress)
+    {
+        if (func_num_args() != 1) {
+            throw new \InvalidArgumentException(
+                'Method takes exactly one argument.'
+            );
+        }
+
+        if (!is_resource($this->fileHandle)) {
+            throw new \BadMethodCallException(
+                'Attempt to read from a closed MaxMind DB.'
+            );
+        }
+
+        if (!filter_var($ipAddress, FILTER_VALIDATE_IP)) {
+            throw new \InvalidArgumentException(
+                "The value \"$ipAddress\" is not a valid IP address."
+            );
+        }
+
+        if ($this->metadata->ipVersion == 4 && strrpos($ipAddress, ':')) {
+            throw new \InvalidArgumentException(
+                "Error looking up $ipAddress. You attempted to look up an"
+                . " IPv6 address in an IPv4-only database."
+            );
+        }
+        $pointer = $this->findAddressInTree($ipAddress);
+        if ($pointer == 0) {
+            return null;
+        }
+        return $this->resolveDataPointer($pointer);
+    }
+
+    private function findAddressInTree($ipAddress)
+    {
+        // XXX - could simplify. Done as a byte array to ease porting
+        $rawAddress = array_merge(unpack('C*', inet_pton($ipAddress)));
+
+        $bitCount = count($rawAddress) * 8;
+
+        // The first node of the tree is always node 0, at the beginning of the
+        // value
+        $node = $this->startNode($bitCount);
+
+        for ($i = 0; $i < $bitCount; $i++) {
+            if ($node >= $this->metadata->nodeCount) {
+                break;
+            }
+            $tempBit = 0xFF & $rawAddress[$i >> 3];
+            $bit = 1 & ($tempBit >> 7 - ($i % 8));
+
+            $node = $this->readNode($node, $bit);
+        }
+        if ($node == $this->metadata->nodeCount) {
+            // Record is empty
+            return 0;
+        } elseif ($node > $this->metadata->nodeCount) {
+            // Record is a data pointer
+            return $node;
+        }
+        throw new InvalidDatabaseException("Something bad happened");
+    }
+
+
+    private function startNode($length)
+    {
+        // 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 ($this->metadata->ipVersion == 6 && $length == 32) {
+            return $this->ipV4StartNode();
+        }
+        // The first node of the tree is always node 0, at the beginning of the
+        // value
+        return 0;
+    }
+
+    private function ipV4StartNode()
+    {
+        // This is a defensive check. There is no reason to call this when you
+        // have an IPv4 tree.
+        if ($this->metadata->ipVersion == 4) {
+            return 0;
+        }
+
+        if ($this->ipV4Start != 0) {
+            return $this->ipV4Start;
+        }
+        $node = 0;
+
+        for ($i = 0; $i < 96 && $node < $this->metadata->nodeCount; $i++) {
+            $node = $this->readNode($node, 0);
+        }
+        $this->ipV4Start = $node;
+        return $node;
+    }
+
+    private function readNode($nodeNumber, $index)
+    {
+        $baseOffset = $nodeNumber * $this->metadata->nodeByteSize;
+
+        // XXX - probably could condense this.
+        switch ($this->metadata->recordSize) {
+            case 24:
+                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 3, 3);
+                list(, $node) = unpack('N', "\x00" . $bytes);
+                return $node;
+            case 28:
+                $middleByte = Util::read($this->fileHandle, $baseOffset + 3, 1);
+                list(, $middle) = unpack('C', $middleByte);
+                if ($index == 0) {
+                    $middle = (0xF0 & $middle) >> 4;
+                } else {
+                    $middle = 0x0F & $middle;
+                }
+                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 3);
+                list(, $node) = unpack('N', chr($middle) . $bytes);
+                return $node;
+            case 32:
+                $bytes = Util::read($this->fileHandle, $baseOffset + $index * 4, 4);
+                list(, $node) = unpack('N', $bytes);
+                return $node;
+            default:
+                throw new InvalidDatabaseException(
+                    'Unknown record size: '
+                    . $this->metadata->recordSize
+                );
+        }
+    }
+
+    private function resolveDataPointer($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"
+            );
+        }
+
+        list($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($filename)
+    {
+        $handle = $this->fileHandle;
+        $fstat = fstat($handle);
+        $fileSize = $fstat['size'];
+        $marker = self::$METADATA_START_MARKER;
+        $markerLength = self::$METADATA_START_MARKER_LENGTH;
+
+        for ($i = 0; $i < $fileSize - $markerLength + 1; $i++) {
+            for ($j = 0; $j < $markerLength; $j++) {
+                fseek($handle, $fileSize - $i - $j - 1);
+                $matchBit = fgetc($handle);
+                if ($matchBit != $marker[$markerLength - $j - 1]) {
+                    continue 2;
+                }
+            }
+            return $fileSize - $i;
+        }
+        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()
+    {
+        if (func_num_args()) {
+            throw new \InvalidArgumentException(
+                'Method takes no arguments.'
+            );
+        }
+
+        // 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 $this->metadata;
+    }
+
+    /**
+     * Closes the MaxMind DB and returns resources to the system.
+     *
+     * @throws \Exception
+     *             if an I/O error occurs.
+     */
+    public function close()
+    {
+        if (!is_resource($this->fileHandle)) {
+            throw new \BadMethodCallException(
+                'Attempt to close a closed MaxMind DB.'
+            );
+        }
+        fclose($this->fileHandle);
+    }
+}
diff --git a/lib/maxmind/MaxMind/Db/Reader/Decoder.php b/lib/maxmind/MaxMind/Db/Reader/Decoder.php
new file mode 100644 (file)
index 0000000..4575b27
--- /dev/null
@@ -0,0 +1,309 @@
+<?php
+
+namespace MaxMind\Db\Reader;
+
+use MaxMind\Db\Reader\InvalidDatabaseException;
+use MaxMind\Db\Reader\Util;
+
+class Decoder
+{
+
+    private $fileStream;
+    private $pointerBase;
+    // This is only used for unit testing
+    private $pointerTestHack;
+    private $switchByteOrder;
+
+    private $types = array(
+        0 => 'extended',
+        1 => 'pointer',
+        2 => 'utf8_string',
+        3 => 'double',
+        4 => 'bytes',
+        5 => 'uint16',
+        6 => 'uint32',
+        7 => 'map',
+        8 => 'int32',
+        9 => 'uint64',
+        10 => 'uint128',
+        11 => 'array',
+        12 => 'container',
+        13 => 'end_marker',
+        14 => 'boolean',
+        15 => 'float',
+    );
+
+    public function __construct(
+        $fileStream,
+        $pointerBase = 0,
+        $pointerTestHack = false
+    ) {
+        $this->fileStream = $fileStream;
+        $this->pointerBase = $pointerBase;
+        $this->pointerTestHack = $pointerTestHack;
+
+        $this->switchByteOrder = $this->isPlatformLittleEndian();
+    }
+
+
+    public function decode($offset)
+    {
+        list(, $ctrlByte) = unpack(
+            'C',
+            Util::read($this->fileStream, $offset, 1)
+        );
+        $offset++;
+
+        $type = $this->types[$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 == 'pointer') {
+            list($pointer, $offset) = $this->decodePointer($ctrlByte, $offset);
+
+            // for unit testing
+            if ($this->pointerTestHack) {
+                return array($pointer);
+            }
+
+            list($result) = $this->decode($pointer);
+
+            return array($result, $offset);
+        }
+
+        if ($type == 'extended') {
+            list(, $nextByte) = unpack(
+                'C',
+                Util::read($this->fileStream, $offset, 1)
+            );
+
+            $typeNum = $nextByte + 7;
+
+            if ($typeNum < 8) {
+                throw new InvalidDatabaseException(
+                    "Something went horribly wrong in the decoder. An extended type "
+                    . "resolved to a type number < 8 ("
+                    . $this->types[$typeNum]
+                    . ")"
+                );
+            }
+
+            $type = $this->types[$typeNum];
+            $offset++;
+        }
+
+        list($size, $offset) = $this->sizeFromCtrlByte($ctrlByte, $offset);
+
+        return $this->decodeByType($type, $offset, $size);
+    }
+
+    private function decodeByType($type, $offset, $size)
+    {
+        switch ($type) {
+            case 'map':
+                return $this->decodeMap($size, $offset);
+            case 'array':
+                return $this->decodeArray($size, $offset);
+            case 'boolean':
+                return array($this->decodeBoolean($size), $offset);
+        }
+
+        $newOffset = $offset + $size;
+        $bytes = Util::read($this->fileStream, $offset, $size);
+        switch ($type) {
+            case 'utf8_string':
+                return array($this->decodeString($bytes), $newOffset);
+            case 'double':
+                $this->verifySize(8, $size);
+                return array($this->decodeDouble($bytes), $newOffset);
+            case 'float':
+                $this->verifySize(4, $size);
+                return array($this->decodeFloat($bytes), $newOffset);
+            case 'bytes':
+                return array($bytes, $newOffset);
+            case 'uint16':
+            case 'uint32':
+                return array($this->decodeUint($bytes), $newOffset);
+            case 'int32':
+                return array($this->decodeInt32($bytes), $newOffset);
+            case 'uint64':
+            case 'uint128':
+                return array($this->decodeBigUint($bytes, $size), $newOffset);
+            default:
+                throw new InvalidDatabaseException(
+                    "Unknown or unexpected type: " . $type
+                );
+        }
+    }
+
+    private function verifySize($expected, $actual)
+    {
+        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($size, $offset)
+    {
+        $array = array();
+
+        for ($i = 0; $i < $size; $i++) {
+            list($value, $offset) = $this->decode($offset);
+            array_push($array, $value);
+        }
+
+        return array($array, $offset);
+    }
+
+    private function decodeBoolean($size)
+    {
+        return $size == 0 ? false : true;
+    }
+
+    private function decodeDouble($bits)
+    {
+        // XXX - Assumes IEEE 754 double on platform
+        list(, $double) = unpack('d', $this->maybeSwitchByteOrder($bits));
+        return $double;
+    }
+
+    private function decodeFloat($bits)
+    {
+        // XXX - Assumes IEEE 754 floats on platform
+        list(, $float) = unpack('f', $this->maybeSwitchByteOrder($bits));
+        return $float;
+    }
+
+    private function decodeInt32($bytes)
+    {
+        $bytes = $this->zeroPadLeft($bytes, 4);
+        list(, $int) = unpack('l', $this->maybeSwitchByteOrder($bytes));
+        return $int;
+    }
+
+    private function decodeMap($size, $offset)
+    {
+
+        $map = array();
+
+        for ($i = 0; $i < $size; $i++) {
+            list($key, $offset) = $this->decode($offset);
+            list($value, $offset) = $this->decode($offset);
+            $map[$key] = $value;
+        }
+
+        return array($map, $offset);
+    }
+
+    private $pointerValueOffset = array(
+        1 => 0,
+        2 => 2048,
+        3 => 526336,
+        4 => 0,
+    );
+
+    private function decodePointer($ctrlByte, $offset)
+    {
+        $pointerSize = (($ctrlByte >> 3) & 0x3) + 1;
+
+        $buffer = Util::read($this->fileStream, $offset, $pointerSize);
+        $offset = $offset + $pointerSize;
+
+        $packed = $pointerSize == 4
+            ? $buffer
+            : (pack('C', $ctrlByte & 0x7)) . $buffer;
+
+        $unpacked = $this->decodeUint($packed);
+        $pointer = $unpacked + $this->pointerBase
+            + $this->pointerValueOffset[$pointerSize];
+
+        return array($pointer, $offset);
+    }
+
+    private function decodeUint($bytes)
+    {
+        list(, $int) = unpack('N', $this->zeroPadLeft($bytes, 4));
+        return $int;
+    }
+
+    private function decodeBigUint($bytes, $byteLength)
+    {
+        $maxUintBytes = log(PHP_INT_MAX, 2) / 8;
+
+        if ($byteLength == 0) {
+            return 0;
+        }
+
+        $numberOfLongs = ceil($byteLength / 4);
+        $paddedLength = $numberOfLongs * 4;
+        $paddedBytes = $this->zeroPadLeft($bytes, $paddedLength);
+        $unpacked = array_merge(unpack("N$numberOfLongs", $paddedBytes));
+
+        $integer = 0;
+
+        // 2^32
+        $twoTo32 = '4294967296';
+
+        foreach ($unpacked as $part) {
+            // We only use gmp or bcmath if the final value is too big
+            if ($byteLength <= $maxUintBytes) {
+                $integer = ($integer << 32) + $part;
+            } elseif (extension_loaded('gmp')) {
+                $integer = gmp_strval(gmp_add(gmp_mul($integer, $twoTo32), $part));
+            } elseif (extension_loaded('bcmath')) {
+                $integer = bcadd(bcmul($integer, $twoTo32), $part);
+            } else {
+                throw new \RuntimeException(
+                    'The gmp or bcmath extension must be installed to read this database.'
+                );
+            }
+        }
+        return $integer;
+    }
+
+    private function decodeString($bytes)
+    {
+        // XXX - NOOP. As far as I know, the end user has to explicitly set the
+        // encoding in PHP. Strings are just bytes.
+        return $bytes;
+    }
+
+    private function sizeFromCtrlByte($ctrlByte, $offset)
+    {
+        $size = $ctrlByte & 0x1f;
+        $bytesToRead = $size < 29 ? 0 : $size - 28;
+        $bytes = Util::read($this->fileStream, $offset, $bytesToRead);
+        $decoded = $this->decodeUint($bytes);
+
+        if ($size == 29) {
+            $size = 29 + $decoded;
+        } elseif ($size == 30) {
+            $size = 285 + $decoded;
+        } elseif ($size > 30) {
+            $size = ($decoded & (0x0FFFFFFF >> (32 - (8 * $bytesToRead))))
+                + 65821;
+        }
+
+        return array($size, $offset + $bytesToRead);
+    }
+
+    private function zeroPadLeft($content, $desiredLength)
+    {
+        return str_pad($content, $desiredLength, "\x00", STR_PAD_LEFT);
+    }
+
+    private function maybeSwitchByteOrder($bytes)
+    {
+        return $this->switchByteOrder ? strrev($bytes) : $bytes;
+    }
+
+    private function isPlatformLittleEndian()
+    {
+        $testint = 0x00FF;
+        $packed = pack('S', $testint);
+        return $testint === current(unpack('v', $packed));
+    }
+}
diff --git a/lib/maxmind/MaxMind/Db/Reader/InvalidDatabaseException.php b/lib/maxmind/MaxMind/Db/Reader/InvalidDatabaseException.php
new file mode 100644 (file)
index 0000000..d2a9a77
--- /dev/null
@@ -0,0 +1,10 @@
+<?php
+
+namespace MaxMind\Db\Reader;
+
+/**
+ * This class should be thrown when unexpected data is found in the database.
+ */
+class InvalidDatabaseException extends \Exception
+{
+}
diff --git a/lib/maxmind/MaxMind/Db/Reader/Metadata.php b/lib/maxmind/MaxMind/Db/Reader/Metadata.php
new file mode 100644 (file)
index 0000000..3ac908b
--- /dev/null
@@ -0,0 +1,77 @@
+<?php
+
+namespace MaxMind\Db\Reader;
+
+/**
+ * This class provides the metadata for the MaxMind DB file.
+ *
+ * @property integer nodeCount This is an unsigned 32-bit integer indicating
+ * the number of nodes in the search tree.
+ *
+ * @property integer 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 integer 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 integer binaryFormatMajorVersion This is an unsigned 16-bit
+ * integer indicating the major version number for the database's binary
+ * format.
+ *
+ * @property integer binaryFormatMinorVersion This is an unsigned 16-bit
+ * integer indicating the minor version number for the database's binary format.
+ *
+ * @property integer 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;
+
+    public function __construct($metadata)
+    {
+        $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;
+    }
+
+    public function __get($var)
+    {
+        return $this->$var;
+    }
+}
diff --git a/lib/maxmind/MaxMind/Db/Reader/Util.php b/lib/maxmind/MaxMind/Db/Reader/Util.php
new file mode 100644 (file)
index 0000000..dc8ec80
--- /dev/null
@@ -0,0 +1,28 @@
+<?php
+
+namespace MaxMind\Db\Reader;
+
+use MaxMind\Db\Reader\InvalidDatabaseException;
+
+class Util
+{
+    public static function read($stream, $offset, $numberOfBytes)
+    {
+        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 (ftell($stream) - $offset === $numberOfBytes) {
+                return $value;
+            }
+        }
+        throw new InvalidDatabaseException(
+            "The MaxMind DB file contains bad data"
+        );
+    }
+}
diff --git a/lib/maxmind/README_moodle.txt b/lib/maxmind/README_moodle.txt
new file mode 100644 (file)
index 0000000..e9a5bf3
--- /dev/null
@@ -0,0 +1,30 @@
+GeoIP2 PHP API
+==============
+
+No changes from the upstream version have been made, it is recommended by upstream
+to install these depdencies via composer - but the composer installation is bundled
+with a load of test files, shell scripts etc (and we don't use composer to manage
+'production depdendencies') so we have to do it manually.
+
+Information
+-----------
+
+URL: http://maxmind.github.io/GeoIP2-php/
+License: Apache License, Version 2.0.
+
+Installation
+------------
+
+1) Download the latest versions of GeoIP2-php and MaxMind-DB-Reader-php
+wget https://github.com/maxmind/GeoIP2-php/archive/v2.4.2.zip
+wget https://github.com/maxmind/MaxMind-DB-Reader-php/archive/v1.1.0.zip
+
+2) Unzip the archives
+unzip v2.4.2.zip
+unzip v1.1.0.zip
+
+3) Move the source code directories into place
+mv GeoIP2-php-2.4.2/src/ /path/to/moodle/lib/maxmind/GeoIp2/
+mv MaxMind-DB-Reader-php-1.1.0/src/MaxMind/ /path/to/moodle/lib/maxmind/MaxMind/
+
+4) Run unit tests on iplookup/tests/geoip_test.php with PHPUNIT_LONGTEST defined.
index d0a78f5..d527750 100644 (file)
     <license>MIT</license>
     <version>2.1.6</version>
   </library>
+  <library>
+    <location>maxmind/GeoIP2</location>
+    <name>GeoIP2 PHP API</name>
+    <license>Apache 2.0</license>
+    <version>2.4.2</version>
+  </library>
+  <library>
+    <location>maxmind/MaxMind</location>
+    <name>MaxMind DB Reader API</name>
+    <license>Apache 2.0</license>
+    <version>1.1.0</version>
+  </library>
 </libraries>
index 6ff6fe9..7eea9fc 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2016091500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2016091501.00;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.