Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]

   1  <?php
   2  
   3  declare(strict_types=1);
   4  
   5  namespace GeoIp2\Database;
   6  
   7  use GeoIp2\Exception\AddressNotFoundException;
   8  use GeoIp2\Model\AbstractModel;
   9  use GeoIp2\Model\AnonymousIp;
  10  use GeoIp2\Model\Asn;
  11  use GeoIp2\Model\City;
  12  use GeoIp2\Model\ConnectionType;
  13  use GeoIp2\Model\Country;
  14  use GeoIp2\Model\Domain;
  15  use GeoIp2\Model\Enterprise;
  16  use GeoIp2\Model\Isp;
  17  use GeoIp2\ProviderInterface;
  18  use MaxMind\Db\Reader as DbReader;
  19  use MaxMind\Db\Reader\InvalidDatabaseException;
  20  
  21  /**
  22   * Instances of this class provide a reader for the GeoIP2 database format.
  23   * IP addresses can be looked up using the database specific methods.
  24   *
  25   * ## Usage ##
  26   *
  27   * The basic API for this class is the same for every database. First, you
  28   * create a reader object, specifying a file name. You then call the method
  29   * corresponding to the specific database, passing it the IP address you want
  30   * to look up.
  31   *
  32   * If the request succeeds, the method call will return a model class for
  33   * the method you called. This model in turn contains multiple record classes,
  34   * each of which represents part of the data returned by the database. If
  35   * the database does not contain the requested information, the attributes
  36   * on the record class will have a `null` value.
  37   *
  38   * If the address is not in the database, an
  39   * {@link \GeoIp2\Exception\AddressNotFoundException} exception will be
  40   * thrown. If an invalid IP address is passed to one of the methods, a
  41   * SPL {@link \InvalidArgumentException} will be thrown. If the database is
  42   * corrupt or invalid, a {@link \MaxMind\Db\Reader\InvalidDatabaseException}
  43   * will be thrown.
  44   */
  45  class Reader implements ProviderInterface
  46  {
  47      /**
  48       * @var DbReader
  49       */
  50      private $dbReader;
  51  
  52      /**
  53       * @var string
  54       */
  55      private $dbType;
  56  
  57      /**
  58       * @var array<string>
  59       */
  60      private $locales;
  61  
  62      /**
  63       * Constructor.
  64       *
  65       * @param string $filename the path to the GeoIP2 database file
  66       * @param array  $locales  list of locale codes to use in name property
  67       *                         from most preferred to least preferred
  68       *
  69       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
  70       *                                                     is corrupt or invalid
  71       */
  72      public function __construct(
  73          string $filename,
  74          array $locales = ['en']
  75      ) {
  76          $this->dbReader = new DbReader($filename);
  77          $this->dbType = $this->dbReader->metadata()->databaseType;
  78          $this->locales = $locales;
  79      }
  80  
  81      /**
  82       * This method returns a GeoIP2 City model.
  83       *
  84       * @param string $ipAddress an IPv4 or IPv6 address as a string
  85       *
  86       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
  87       *                                                     not in the database
  88       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
  89       *                                                     is corrupt or invalid
  90       */
  91      public function city(string $ipAddress): City
  92      {
  93          // @phpstan-ignore-next-line
  94          return $this->modelFor(City::class, 'City', $ipAddress);
  95      }
  96  
  97      /**
  98       * This method returns a GeoIP2 Country model.
  99       *
 100       * @param string $ipAddress an IPv4 or IPv6 address as a string
 101       *
 102       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 103       *                                                     not in the database
 104       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 105       *                                                     is corrupt or invalid
 106       */
 107      public function country(string $ipAddress): Country
 108      {
 109          // @phpstan-ignore-next-line
 110          return $this->modelFor(Country::class, 'Country', $ipAddress);
 111      }
 112  
 113      /**
 114       * This method returns a GeoIP2 Anonymous IP model.
 115       *
 116       * @param string $ipAddress an IPv4 or IPv6 address as a string
 117       *
 118       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 119       *                                                     not in the database
 120       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 121       *                                                     is corrupt or invalid
 122       */
 123      public function anonymousIp(string $ipAddress): AnonymousIp
 124      {
 125          // @phpstan-ignore-next-line
 126          return $this->flatModelFor(
 127              AnonymousIp::class,
 128              'GeoIP2-Anonymous-IP',
 129              $ipAddress
 130          );
 131      }
 132  
 133      /**
 134       * This method returns a GeoLite2 ASN model.
 135       *
 136       * @param string $ipAddress an IPv4 or IPv6 address as a string
 137       *
 138       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 139       *                                                     not in the database
 140       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 141       *                                                     is corrupt or invalid
 142       */
 143      public function asn(string $ipAddress): Asn
 144      {
 145          // @phpstan-ignore-next-line
 146          return $this->flatModelFor(
 147              Asn::class,
 148              'GeoLite2-ASN',
 149              $ipAddress
 150          );
 151      }
 152  
 153      /**
 154       * This method returns a GeoIP2 Connection Type model.
 155       *
 156       * @param string $ipAddress an IPv4 or IPv6 address as a string
 157       *
 158       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 159       *                                                     not in the database
 160       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 161       *                                                     is corrupt or invalid
 162       */
 163      public function connectionType(string $ipAddress): ConnectionType
 164      {
 165          // @phpstan-ignore-next-line
 166          return $this->flatModelFor(
 167              ConnectionType::class,
 168              'GeoIP2-Connection-Type',
 169              $ipAddress
 170          );
 171      }
 172  
 173      /**
 174       * This method returns a GeoIP2 Domain model.
 175       *
 176       * @param string $ipAddress an IPv4 or IPv6 address as a string
 177       *
 178       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 179       *                                                     not in the database
 180       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 181       *                                                     is corrupt or invalid
 182       */
 183      public function domain(string $ipAddress): Domain
 184      {
 185          // @phpstan-ignore-next-line
 186          return $this->flatModelFor(
 187              Domain::class,
 188              'GeoIP2-Domain',
 189              $ipAddress
 190          );
 191      }
 192  
 193      /**
 194       * This method returns a GeoIP2 Enterprise model.
 195       *
 196       * @param string $ipAddress an IPv4 or IPv6 address as a string
 197       *
 198       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 199       *                                                     not in the database
 200       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 201       *                                                     is corrupt or invalid
 202       */
 203      public function enterprise(string $ipAddress): Enterprise
 204      {
 205          // @phpstan-ignore-next-line
 206          return $this->modelFor(Enterprise::class, 'Enterprise', $ipAddress);
 207      }
 208  
 209      /**
 210       * This method returns a GeoIP2 ISP model.
 211       *
 212       * @param string $ipAddress an IPv4 or IPv6 address as a string
 213       *
 214       * @throws \GeoIp2\Exception\AddressNotFoundException  if the address is
 215       *                                                     not in the database
 216       * @throws \MaxMind\Db\Reader\InvalidDatabaseException if the database
 217       *                                                     is corrupt or invalid
 218       */
 219      public function isp(string $ipAddress): Isp
 220      {
 221          // @phpstan-ignore-next-line
 222          return $this->flatModelFor(
 223              Isp::class,
 224              'GeoIP2-ISP',
 225              $ipAddress
 226          );
 227      }
 228  
 229      private function modelFor(string $class, string $type, string $ipAddress): AbstractModel
 230      {
 231          [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
 232  
 233          $record['traits']['ip_address'] = $ipAddress;
 234          $record['traits']['prefix_len'] = $prefixLen;
 235  
 236          return new $class($record, $this->locales);
 237      }
 238  
 239      private function flatModelFor(string $class, string $type, string $ipAddress): AbstractModel
 240      {
 241          [$record, $prefixLen] = $this->getRecord($class, $type, $ipAddress);
 242  
 243          $record['ip_address'] = $ipAddress;
 244          $record['prefix_len'] = $prefixLen;
 245  
 246          return new $class($record);
 247      }
 248  
 249      private function getRecord(string $class, string $type, string $ipAddress): array
 250      {
 251          if (strpos($this->dbType, $type) === false) {
 252              $method = lcfirst((new \ReflectionClass($class))->getShortName());
 253  
 254              throw new \BadMethodCallException(
 255                  "The $method method cannot be used to open a {$this->dbType} database"
 256              );
 257          }
 258          [$record, $prefixLen] = $this->dbReader->getWithPrefixLen($ipAddress);
 259          if ($record === null) {
 260              throw new AddressNotFoundException(
 261                  "The address $ipAddress is not in the database."
 262              );
 263          }
 264          if (!\is_array($record)) {
 265              // This can happen on corrupt databases. Generally,
 266              // MaxMind\Db\Reader will throw a
 267              // MaxMind\Db\Reader\InvalidDatabaseException, but occasionally
 268              // the lookup may result in a record that looks valid but is not
 269              // an array. This mostly happens when the user is ignoring all
 270              // exceptions and the more frequent InvalidDatabaseException
 271              // exceptions go unnoticed.
 272              throw new InvalidDatabaseException(
 273                  "Expected an array when looking up $ipAddress but received: "
 274                  . \gettype($record)
 275              );
 276          }
 277  
 278          return [$record, $prefixLen];
 279      }
 280  
 281      /**
 282       * @throws \InvalidArgumentException if arguments are passed to the method
 283       * @throws \BadMethodCallException   if the database has been closed
 284       *
 285       * @return \MaxMind\Db\Reader\Metadata object for the database
 286       */
 287      public function metadata(): DbReader\Metadata
 288      {
 289          return $this->dbReader->metadata();
 290      }
 291  
 292      /**
 293       * Closes the GeoIP2 database and returns the resources to the system.
 294       */
 295      public function close(): void
 296      {
 297          $this->dbReader->close();
 298      }
 299  }