Kaynağa Gözat

登陆增加IP限制;登陆失败次数限制

zhangl 1 yıl önce
ebeveyn
işleme
ac0072ce7a
74 değiştirilmiş dosya ile 7069 ekleme ve 1 silme
  1. 7 1
      backendApi/modules/v1/models/LoginForm.php
  2. 29 0
      common/helpers/Tool.php
  3. 323 0
      vendor/geoip2/geoip2/CHANGELOG.md
  4. 202 0
      vendor/geoip2/geoip2/LICENSE
  5. 442 0
      vendor/geoip2/geoip2/README.md
  6. 32 0
      vendor/geoip2/geoip2/composer.json
  7. 26 0
      vendor/geoip2/geoip2/examples/benchmark.php
  8. 299 0
      vendor/geoip2/geoip2/src/Database/Reader.php
  9. 12 0
      vendor/geoip2/geoip2/src/Exception/AddressNotFoundException.php
  10. 12 0
      vendor/geoip2/geoip2/src/Exception/AuthenticationException.php
  11. 12 0
      vendor/geoip2/geoip2/src/Exception/GeoIp2Exception.php
  12. 28 0
      vendor/geoip2/geoip2/src/Exception/HttpException.php
  13. 30 0
      vendor/geoip2/geoip2/src/Exception/InvalidRequestException.php
  14. 12 0
      vendor/geoip2/geoip2/src/Exception/OutOfQueriesException.php
  15. 68 0
      vendor/geoip2/geoip2/src/Model/AbstractModel.php
  16. 91 0
      vendor/geoip2/geoip2/src/Model/AnonymousIp.php
  17. 58 0
      vendor/geoip2/geoip2/src/Model/Asn.php
  18. 123 0
      vendor/geoip2/geoip2/src/Model/City.php
  19. 50 0
      vendor/geoip2/geoip2/src/Model/ConnectionType.php
  20. 96 0
      vendor/geoip2/geoip2/src/Model/Country.php
  21. 50 0
      vendor/geoip2/geoip2/src/Model/Domain.php
  22. 15 0
      vendor/geoip2/geoip2/src/Model/Enterprise.php
  23. 15 0
      vendor/geoip2/geoip2/src/Model/Insights.php
  24. 93 0
      vendor/geoip2/geoip2/src/Model/Isp.php
  25. 22 0
      vendor/geoip2/geoip2/src/ProviderInterface.php
  26. 67 0
      vendor/geoip2/geoip2/src/Record/AbstractPlaceRecord.php
  27. 67 0
      vendor/geoip2/geoip2/src/Record/AbstractRecord.php
  28. 33 0
      vendor/geoip2/geoip2/src/Record/City.php
  29. 36 0
      vendor/geoip2/geoip2/src/Record/Continent.php
  30. 44 0
      vendor/geoip2/geoip2/src/Record/Country.php
  31. 56 0
      vendor/geoip2/geoip2/src/Record/Location.php
  32. 23 0
      vendor/geoip2/geoip2/src/Record/MaxMind.php
  33. 30 0
      vendor/geoip2/geoip2/src/Record/Postal.php
  34. 33 0
      vendor/geoip2/geoip2/src/Record/RepresentedCountry.php
  35. 44 0
      vendor/geoip2/geoip2/src/Record/Subdivision.php
  36. 158 0
      vendor/geoip2/geoip2/src/Record/Traits.php
  37. 36 0
      vendor/geoip2/geoip2/src/Util.php
  38. 255 0
      vendor/geoip2/geoip2/src/WebService/Client.php
  39. 237 0
      vendor/maxmind-db/reader/CHANGELOG.md
  40. 202 0
      vendor/maxmind-db/reader/LICENSE
  41. 185 0
      vendor/maxmind-db/reader/README.md
  42. 47 0
      vendor/maxmind-db/reader/autoload.php
  43. 44 0
      vendor/maxmind-db/reader/composer.json
  44. 40 0
      vendor/maxmind-db/reader/ext/config.m4
  45. 10 0
      vendor/maxmind-db/reader/ext/config.w32
  46. 811 0
      vendor/maxmind-db/reader/ext/maxminddb.c
  47. 24 0
      vendor/maxmind-db/reader/ext/php_maxminddb.h
  48. 12 0
      vendor/maxmind-db/reader/ext/tests/001-load.phpt
  49. 13 0
      vendor/maxmind-db/reader/ext/tests/002-final.phpt
  50. 12 0
      vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt
  51. 63 0
      vendor/maxmind-db/reader/package.xml
  52. 403 0
      vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php
  53. 435 0
      vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php
  54. 11 0
      vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php
  55. 120 0
      vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php
  56. 33 0
      vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php
  57. 111 0
      vendor/maxmind/web-service-common/CHANGELOG.md
  58. 202 0
      vendor/maxmind/web-service-common/LICENSE
  59. 25 0
      vendor/maxmind/web-service-common/README.md
  60. 32 0
      vendor/maxmind/web-service-common/composer.json
  61. 56 0
      vendor/maxmind/web-service-common/dev-bin/release.sh
  62. 7 0
      vendor/maxmind/web-service-common/phpstan.neon
  63. 12 0
      vendor/maxmind/web-service-common/src/Exception/AuthenticationException.php
  64. 44 0
      vendor/maxmind/web-service-common/src/Exception/HttpException.php
  65. 12 0
      vendor/maxmind/web-service-common/src/Exception/InsufficientFundsException.php
  66. 14 0
      vendor/maxmind/web-service-common/src/Exception/InvalidInputException.php
  67. 41 0
      vendor/maxmind/web-service-common/src/Exception/InvalidRequestException.php
  68. 9 0
      vendor/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php
  69. 12 0
      vendor/maxmind/web-service-common/src/Exception/PermissionRequiredException.php
  70. 12 0
      vendor/maxmind/web-service-common/src/Exception/WebServiceException.php
  71. 546 0
      vendor/maxmind/web-service-common/src/WebService/Client.php
  72. 136 0
      vendor/maxmind/web-service-common/src/WebService/Http/CurlRequest.php
  73. 19 0
      vendor/maxmind/web-service-common/src/WebService/Http/Request.php
  74. 48 0
      vendor/maxmind/web-service-common/src/WebService/Http/RequestFactory.php

+ 7 - 1
backendApi/modules/v1/models/LoginForm.php

@@ -3,6 +3,7 @@ namespace backendApi\modules\v1\models;
 
 use common\components\Model;
 use common\helpers\LoggerTool;
+use common\helpers\Tool;
 use common\libs\LoginIpChecker;
 use Yii;
 use yii\base\Exception;
@@ -113,13 +114,18 @@ class LoginForm extends Model {
         }
         $transaction = \Yii::$app->db->beginTransaction();
         try{
+            // 登陆IP限制
+            $loginIp = $_SERVER['REMOTE_ADDR'];
+            if (!Tool::remoteAddrCall($loginIp)) {
+                throw new Exception('用户名或者密码错误');
+            }
+
             $this->getUser();
             if(!$this->_user){
                 throw new Exception('账号不存在');
             }
             // 失败次数到达上限次数
             $loginFailNums = Yii::$app->redis->get('FAIL_NUMS:' . $this->adminName) ?? 0;
-            LoggerTool::info('FAIL_NUMS:' . $this->adminName . ': ' . $loginFailNums);
             if ($loginFailNums >= 3) {
                 throw new Exception('用户名或者密码错误');
             }

+ 29 - 0
common/helpers/Tool.php

@@ -13,6 +13,9 @@ use common\models\AlarmCall;
 use common\models\ApproachOrderCall;
 use common\models\WstOrderCall;
 use Faker\Provider\Uuid;
+use GeoIp2\Database\Reader;
+use GeoIp2\Exception\AddressNotFoundException;
+use MaxMind\Db\Reader\InvalidDatabaseException;
 use yii\base\Exception;
 use yii\helpers\Url;
 use yii\httpclient\Client;
@@ -546,4 +549,30 @@ class Tool {
             LoggerTool::error(sprintf('[%s] [%s] [%s]', $e->getFile(), $e->getLine(), $e->getMessage()));
         }
     }
+
+    /**
+     * @throws AddressNotFoundException
+     * @throws InvalidDatabaseException
+     */
+    public static function remoteAddrCall($remoteAddr): bool
+    {
+        // 是否有效的IP
+        if (!filter_var($remoteAddr, FILTER_VALIDATE_IP)) {
+            return false;
+        }
+
+        // 替换为 GeoLite2 数据库文件的实际路径
+        $dbPath = './GeoLite2-Country.mmdb';
+        // 初始化 MaxMind 数据库读取器
+        $reader = new Reader($dbPath);
+        // 查询 IP 地址的地理位置
+        $record = $reader->country($remoteAddr);
+        // 返回国家名称
+        $countryName = $record->country->name;
+        if (!in_array($countryName, ['China', 'Malaysia'])) {
+            return false;
+        }
+
+        return true;
+    }
 }

+ 323 - 0
vendor/geoip2/geoip2/CHANGELOG.md

@@ -0,0 +1,323 @@
+CHANGELOG
+=========
+
+2.13.0 (2022-08-05)
+-------------------
+
+* The model class names are no longer constructed by concatenating strings.
+  This change was made to improve support for tools like PHP-Scoper.
+  Reported by Andrew Mead. GitHub #194.
+* Box 4.0.1 is now used to generate the `geoip2.phar` file.
+
+2.12.2 (2021-11-30)
+-------------------
+
+* The `geoip2.phar` now works when included from another directory.
+  Reported by Eduardo Ruiz. GitHub #179.
+
+2.12.1 (2021-11-23)
+-------------------
+
+* The `geoip2.phar` included in 2.12.0 would only work in CLI applications.
+  This was due to a change in Box 3.x. The Phar should now work in all
+  applications. This release only affects users of the Phar file.
+
+2.12.0 (2021-11-18)
+-------------------
+
+* Support for mobile country code (MCC) and mobile network codes (MNC) was
+  added for the GeoIP2 ISP and Enterprise databases as well as the GeoIP2
+  City and Insights web services. `$mobileCountryCode` and
+  `$mobileNetworkCode` properties were added to `GeoIp2\Model\Isp`
+  for the GeoIP2 ISP database and `GeoIp2\Record\Traits` for the Enterprise
+  database and the GeoIP2 City and Insights web services. We expect this data
+  to be available by late January, 2022.
+* `geoip2.phar` is now generated with Box 3.x.
+
+2.11.0 (2020-10-01)
+-------------------
+
+* IMPORTANT: PHP 7.2 or greater is now required.
+* Added the `isResidentialProxy` property to `GeoIp2\Model\AnonymousIP` and
+  `GeoIp2\Record\Traits`.
+* Additional type hints have been added.
+
+2.10.0 (2019-12-12)
+-------------------
+
+* PHP 5.6 or greater is now required.
+* The `network` property was added to `GeoIp2\Record\Traits`,
+  `GeoIp2\Model\AnonymousIp`, `GeoIp2\Model\Asn`,
+  `GeoIp2\Model\ConnectionType`, `Geoip2\Model\Domain`,
+  and `GeoIp2\Model\Isp`. This is a string in CIDR format representing the
+  largest network where all of the properties besides `ipAddress` have the
+  same value.
+* Updated documentation of anonymizer properties - `isAnonymousVpn`
+  and `isHostingProvider` - to be more descriptive.
+* The `userCount` property was added to `GeoIp2\Record\Traits`. This is an
+  integer which indicates the estimated number of users sharing the
+  IP/network during the past 24 hours. This output is available from GeoIP2
+  Precision Insights.
+* The `staticIpScore` property was added to `GeoIp2\Record\Traits`. This is
+  a float which indicates how static or dynamic an IP address is. This
+  output is available from GeoIP2 Precision Insights.
+
+2.9.0 (2018-04-10)
+------------------
+
+* Refer to account IDs using the terminology "account" rather than "user".
+
+2.8.0 (2018-01-18)
+------------------
+
+* The `isInEuropeanUnion` property was added to `GeoIp2\Record\Country`
+  and `GeoIp2\Record\RepresentedCountry`. This property is `true` if the
+  country is a member state of the European Union.
+
+2.7.0 (2017-10-27)
+------------------
+
+* The following new anonymizer properties were added to `GeoIp2\Record\Traits`
+  for use with GeoIP2 Precision Insights: `isAnonymous`, `isAnonymousVpn`,
+  `isHostingProvider`, `isPublicProxy`, and `isTorExitNode`.
+
+2.6.0 (2017-07-10)
+-----------------
+
+* Code clean-up and tidying.
+* Set minimum required PHP version to 5.4 in `composer.json`. Previously,
+  5.3 would work but was not tested. Now 5.4 is hard minimum version.
+
+2.5.0 (2017-05-08)
+------------------
+
+* Support for PHP 5.3 was dropped.
+* Added support for GeoLite2 ASN database.
+
+2.4.5 (2017-01-31)
+------------------
+
+* Additional error checking on the data returned from `MaxMind\Db\Reader`
+  was added to help detect corrupt databases. GitHub #83.
+
+2.4.4 (2016-10-11)
+------------------
+
+* `isset()` on `mostSpecificSubdivision` attribute now returns the
+  correct value. Reported by Juan Francisco Giordana. GitHub #81.
+
+2.4.3 (2016-10-11)
+------------------
+
+* `isset()` on `name` attribute now returns the correct value. Reported by
+  Juan Francisco Giordana. GitHub #79.
+
+2.4.2 (2016-08-17)
+------------------
+
+* Updated documentation to clarify what the accuracy radius refers to.
+* Upgraded `maxmind/web-service-common` to 0.3.0. This version uses
+  `composer/ca-bundle` rather than our own CA bundle. GitHub #75.
+* Improved PHP documentation generation.
+
+2.4.1 (2016-06-10)
+------------------
+
+* Corrected type annotations in documentation. GitHub #66.
+* Updated documentation to reflect that the accuracy radius is now included
+  in City.
+* Upgraded web service client, which supports setting a proxy. GitHub #59.
+
+2.4.0 (2016-04-15)
+------------------
+
+* Added support for the GeoIP2 Enterprise database.
+
+2.3.3 (2015-09-24)
+------------------
+
+* Corrected case on `JsonSerializable` interface. Reported by Axel Etcheverry.
+  GitHub #56.
+
+2.3.2 (2015-09-23)
+------------------
+
+* `JsonSerializable` compatibility interface was moved to `GeoIp2\Compat`
+  rather than the global namespace to prevent autoloading issues. Reported by
+  Tomas Buteler. GitHub #54.
+* Missing documentation for the `$postal` property was added to the
+  `GeoIp2\Model\City` class. Fix by Roy Sindre Norangshol. GitHub #51.
+* In the Phar distribution, source files for this module no longer have their
+  documentation stripped, allowing IDE introspection to work properly.
+  Reported by Dominic Black. GitHub #52.
+
+2.3.1 (2015-06-30)
+------------------
+
+* Updated `maxmind/web-service-common` to version with fixes for PHP 5.3 and
+  5.4.
+
+2.3.0 (2015-06-29)
+------------------
+
+* Support for demographics fields `averageIncome` and `populationDensity` in
+  the `Location` record, returned by the Insights endpoint.
+* The `isAnonymousProxy` and `isSatelliteProvider` properties on
+  `GeoIP2\Record\Traits` have been deprecated. Please use our [GeoIP2
+  Anonymous IP database](https://www.maxmind.com/en/geoip2-anonymous-ip-database)
+  to determine whether an IP address is used by an anonymizing service.
+
+2.2.0-beta1 (2015-06-09)
+------------------------
+
+* Typo fix in documentation.
+
+2.2.0-alpha2 (2015-06-01)
+-------------------------
+
+* `maxmind-ws/web-service-common` was renamed to `maxmind/web-service-common`.
+
+2.2.0-alpha1 (2015-05-22)
+-------------------------
+
+* The library no longer uses Guzzle and instead uses curl directly.
+* Support for `timeout` and `connectTimout` were added to the `$options` array
+  passed to the `GeoIp2\WebService\Client` constructor. Pull request by Will
+  Bradley. GitHub #36.
+
+2.1.1 (2014-12-03)
+------------------
+
+* The 2.1.0 Phar builds included a shebang line, causing issues when loading
+  it as a library. This has been corrected. GitHub #33.
+
+2.1.0 (2014-10-29)
+------------------
+
+* Update ApiGen dependency to version that isn't broken on case sensitive
+  file systems.
+* Added support for the GeoIP2 Anonymous IP database. The
+  `GeoIP2\Database\Reader` class now has an `anonymousIp` method which returns
+  a `GeoIP2\Model\AnonymousIp` object.
+* Boolean attributes like those in the `GeoIP2\Record\Traits` class now return
+ `false` instead of `null` when they were not true.
+
+2.0.0 (2014-09-22)
+------------------
+
+* First production release.
+
+0.9.0 (2014-09-15)
+------------------
+
+* IMPORTANT: The deprecated `omni()` and `cityIspOrg()` methods have been
+  removed from `GeoIp2\WebService\Client`.
+
+0.8.1 (2014-09-12)
+------------------
+
+* The check added to the `GeoIP2\Database\Reader` lookup methods in 0.8.0 did
+  not work with the GeoIP2 City Database Subset by Continent with World
+  Countries. This has been fixed. Fixes GitHub issue #23.
+
+0.8.0 (2014-09-10)
+------------------
+
+* The `GeoIp2\Database\Reader` lookup methods (e.g., `city()`, `isp()`) now
+  throw a `BadMethodCallException` if they are used with a database that
+  does not match the method. In particular, doing a `city()` lookup on a
+  GeoIP2 Country database will result in an exception, and vice versa.
+* A `metadata()` method has been added to the `GeoIP2\Database\Reader` class.
+  This returns a `MaxMind\Db\Reader\Metadata` class with information about the
+  database.
+* The name attribute was missing from the RepresentedCountry class.
+
+0.7.0 (2014-07-22)
+------------------
+
+* The web service client API has been updated for the v2.1 release of the web
+  service. In particular, the `cityIspOrg` and `omni` methods on
+  `GeoIp2\WebService\Client` should be considered deprecated. The `city`
+  method now provides all of the data formerly provided by `cityIspOrg`, and
+  the `omni` method has been replaced by the `insights` method.
+* Support was added for GeoIP2 Connection Type, Domain and ISP databases.
+
+
+0.6.3 (2014-05-12)
+------------------
+
+* With the previous Phar builds, some users received `phar error: invalid url
+  or non-existent phar` errors. The correct alias is now used for the Phar,
+  and this should no longer be an issue.
+
+0.6.2 (2014-05-08)
+------------------
+
+* The Phar build was broken with Guzzle 3.9.0+. This has been fixed.
+
+0.6.1 (2014-05-01)
+------------------
+
+* This API now officially supports HHVM.
+* The `maxmind-db/reader` dependency was updated to a version that does not
+  require BC Math.
+* The Composer compatibility autoload rules are now targeted more narrowly.
+* A `box.json` file is included to build a Phar package.
+
+0.6.0 (2014-02-19)
+------------------
+
+* This API is now licensed under the Apache License, Version 2.0.
+* Model and record classes now implement `JsonSerializable`.
+* `isset` now works with model and record classes.
+
+0.5.0 (2013-10-21)
+------------------
+
+* Renamed $languages constructor parameters to $locales for both the Client
+  and Reader classes.
+* Documentation and code clean-up (Ben Morel).
+* Added the interface `GeoIp2\ProviderInterface`, which is implemented by both
+  `\GeoIp2\Database\Reader` and `\GeoIp2\WebService\Client`.
+
+0.4.0 (2013-07-16)
+------------------
+
+* This is the first release with the GeoIP2 database reader. Please see the
+  `README.md` file and the `\GeoIp2\Database\Reader` class.
+* The general exception classes were replaced with specific exception classes
+  representing particular types of errors, such as an authentication error.
+
+0.3.0 (2013-07-12)
+------------------
+
+* In namespaces and class names, "GeoIP2" was renamed to "GeoIp2" to improve
+  consistency.
+
+0.2.1 (2013-06-10)
+------------------
+
+* First official beta release.
+* Documentation updates and corrections.
+
+0.2.0 (2013-05-29)
+------------------
+
+* `GenericException` was renamed to `GeoIP2Exception`.
+* We now support more languages. The new languages are de, es, fr, and pt-BR.
+* The REST API now returns a record with data about your account. There is
+  a new `GeoIP\Records\MaxMind` class for this data.
+* The `continentCode` attribute on `Continent` was renamed to `code`.
+* Documentation updates.
+
+0.1.1 (2013-05-14)
+------------------
+
+* Updated Guzzle version requirement.
+* Fixed Composer example in README.md.
+
+
+0.1.0 (2013-05-13)
+------------------
+
+* Initial release.

+ 202 - 0
vendor/geoip2/geoip2/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 442 - 0
vendor/geoip2/geoip2/README.md

@@ -0,0 +1,442 @@
+# GeoIP2 PHP API #
+
+## Description ##
+
+This package provides an API for the GeoIP2 and GeoLite2
+[web services](https://dev.maxmind.com/geoip/docs/web-services?lang=en) and
+[databases](https://dev.maxmind.com/geoip/docs/databases?lang=en).
+
+## Install via Composer ##
+
+We recommend installing this package with [Composer](https://getcomposer.org/).
+
+### Download Composer ###
+
+To download Composer, run in the root directory of your project:
+
+```bash
+curl -sS https://getcomposer.org/installer | php
+```
+
+You should now have the file `composer.phar` in your project directory.
+
+### Install Dependencies ###
+
+Run in your project root:
+
+```sh
+php composer.phar require geoip2/geoip2:~2.0
+```
+
+You should now have the files `composer.json` and `composer.lock` as well as
+the directory `vendor` in your project directory. If you use a version control
+system, `composer.json` should be added to it.
+
+### Require Autoloader ###
+
+After installing the dependencies, you need to require the Composer autoloader
+from your code:
+
+```php
+require 'vendor/autoload.php';
+```
+
+## Install via Phar ##
+
+Although we strongly recommend using Composer, we also provide a
+[phar archive](https://php.net/manual/en/book.phar.php) containing most of the
+dependencies for GeoIP2. Our latest phar archive is available on
+[our releases page](https://github.com/maxmind/GeoIP2-php/releases).
+
+### Install Dependencies ###
+
+In order to use the phar archive, you must have the PHP
+[Phar extension](https://php.net/manual/en/book.phar.php) installed and
+enabled.
+
+If you will be making web service requests, you must have the PHP
+[cURL extension](https://php.net/manual/en/book.curl.php)
+installed to use this archive. For Debian based distributions, this can
+typically be found in the the `php-curl` package. For other operating
+systems, please consult the relevant documentation. After installing the
+extension you may need to restart your web server.
+
+If you are missing this extension, you will see errors like the following:
+
+```
+PHP Fatal error:  Uncaught Error: Call to undefined function MaxMind\WebService\curl_version()
+```
+
+### Require Package ###
+
+To use the archive, just require it from your script:
+
+```php
+require 'geoip2.phar';
+```
+
+## Optional C Extension ##
+
+The [MaxMind DB API](https://github.com/maxmind/MaxMind-DB-Reader-php)
+includes an optional C extension that you may install to dramatically increase
+the performance of lookups in GeoIP2 or GeoLite2 databases. To install, please
+follow the instructions included with that API.
+
+The extension has no effect on web-service lookups.
+
+## IP Geolocation Usage ##
+
+IP geolocation is inherently imprecise. Locations are often near the center of
+the population. Any location provided by a GeoIP2 database or web service
+should not be used to identify a particular address or household.
+
+## Database Reader ##
+
+### Usage ###
+
+To use this API, you must create a new `\GeoIp2\Database\Reader` object with
+the path to the database file as the first argument to the constructor. You
+may then call the method corresponding to the database you are using.
+
+If the lookup succeeds, the method call will return a model class for the
+record in the database. This model in turn contains multiple container
+classes for the different parts of the data such as the city in which the
+IP address is located.
+
+If the record is not found, a `\GeoIp2\Exception\AddressNotFoundException`
+is thrown. If the database is invalid or corrupt, a
+`\MaxMind\Db\InvalidDatabaseException` will be thrown.
+
+See the API documentation for more details.
+
+### City Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');
+
+// Replace "city" with the appropriate method for your database, e.g.,
+// "country".
+$record = $reader->city('128.101.101.101');
+
+print($record->country->isoCode . "\n"); // 'US'
+print($record->country->name . "\n"); // 'United States'
+print($record->country->names['zh-CN'] . "\n"); // '美国'
+
+print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
+print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
+
+print($record->city->name . "\n"); // 'Minneapolis'
+
+print($record->postal->code . "\n"); // '55455'
+
+print($record->location->latitude . "\n"); // 44.9733
+print($record->location->longitude . "\n"); // -93.2323
+
+print($record->traits->network . "\n"); // '128.101.101.101/32'
+
+```
+
+### Anonymous IP Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Anonymous-IP.mmdb');
+
+$record = $reader->anonymousIp('128.101.101.101');
+
+if ($record->isAnonymous) { print "anon\n"; }
+print($record->ipAddress . "\n"); // '128.101.101.101'
+print($record->network . "\n"); // '128.101.101.101/32'
+
+```
+
+### Connection-Type Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Connection-Type.mmdb');
+
+$record = $reader->connectionType('128.101.101.101');
+
+print($record->connectionType . "\n"); // 'Corporate'
+print($record->ipAddress . "\n"); // '128.101.101.101'
+print($record->network . "\n"); // '128.101.101.101/32'
+
+```
+
+### Domain Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Domain.mmdb');
+
+$record = $reader->domain('128.101.101.101');
+
+print($record->domain . "\n"); // 'umn.edu'
+print($record->ipAddress . "\n"); // '128.101.101.101'
+print($record->network . "\n"); // '128.101.101.101/32'
+
+```
+
+### Enterprise Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-Enterprise.mmdb');
+
+// Use the ->enterprise method to do a lookup in the Enterprise database
+$record = $reader->enterprise('128.101.101.101');
+
+print($record->country->confidence . "\n"); // 99
+print($record->country->isoCode . "\n"); // 'US'
+print($record->country->name . "\n"); // 'United States'
+print($record->country->names['zh-CN'] . "\n"); // '美国'
+
+print($record->mostSpecificSubdivision->confidence . "\n"); // 77
+print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
+print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
+
+print($record->city->confidence . "\n"); // 60
+print($record->city->name . "\n"); // 'Minneapolis'
+
+print($record->postal->code . "\n"); // '55455'
+
+print($record->location->accuracyRadius . "\n"); // 50
+print($record->location->latitude . "\n"); // 44.9733
+print($record->location->longitude . "\n"); // -93.2323
+
+print($record->traits->network . "\n"); // '128.101.101.101/32'
+
+```
+
+### ISP Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\Database\Reader;
+
+// This creates the Reader object, which should be reused across
+// lookups.
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-ISP.mmdb');
+
+$record = $reader->isp('128.101.101.101');
+
+print($record->autonomousSystemNumber . "\n"); // 217
+print($record->autonomousSystemOrganization . "\n"); // 'University of Minnesota'
+print($record->isp . "\n"); // 'University of Minnesota'
+print($record->organization . "\n"); // 'University of Minnesota'
+
+print($record->ipAddress . "\n"); // '128.101.101.101'
+print($record->network . "\n"); // '128.101.101.101/32'
+
+```
+
+## Database Updates ##
+
+You can keep your databases up to date with our
+[GeoIP Update program](https://github.com/maxmind/geoipupdate/releases).
+[Learn more about GeoIP Update on our developer
+portal.](https://dev.maxmind.com/geoip/updating-databases?lang=en)
+
+There is also a third-party tool for updating databases using PHP and
+Composer. MaxMind does not offer support for this tool or maintain it.
+[Learn more about the Geoip2 Update tool for PHP and Composer on its
+GitHub page.](https://github.com/tronovav/geoip2-update)
+
+## Web Service Client ##
+
+### Usage ###
+
+To use this API, you must create a new `\GeoIp2\WebService\Client`
+object with your `$accountId` and `$licenseKey`:
+
+```php
+$client = new Client(42, 'abcdef123456');
+```
+
+You may also call the constructor with additional arguments. The third argument
+specifies the language preferences when using the `->name` method on the model
+classes that this client creates. The fourth argument is additional options
+such as `host` and `timeout`.
+
+For instance, to call the GeoLite2 web service instead of the GeoIP2 web
+service:
+
+```php
+$client = new Client(42, 'abcdef123456', ['en'], ['host' => 'geolite.info']);
+```
+
+After creating the client, you may now call the method corresponding to a
+specific endpoint with the IP address to look up, e.g.:
+
+```php
+$record = $client->city('128.101.101.101');
+```
+
+If the request succeeds, the method call will return a model class for the
+endpoint you called. This model in turn contains multiple record classes, each
+of which represents part of the data returned by the web service.
+
+If there is an error, a structured exception is thrown.
+
+See the API documentation for more details.
+
+### Example ###
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+use GeoIp2\WebService\Client;
+
+// This creates a Client object that can be reused across requests.
+// Replace "42" with your account ID and "license_key" with your license
+// key. Set the "host" to "geolite.info" in the fourth argument options
+// array to use the GeoLite2 web service instead of the GeoIP2 web
+// service.
+$client = new Client(42, 'abcdef123456');
+
+// Replace "city" with the method corresponding to the web service that
+// you are using, e.g., "country", "insights".
+$record = $client->city('128.101.101.101');
+
+print($record->country->isoCode . "\n"); // 'US'
+print($record->country->name . "\n"); // 'United States'
+print($record->country->names['zh-CN'] . "\n"); // '美国'
+
+print($record->mostSpecificSubdivision->name . "\n"); // 'Minnesota'
+print($record->mostSpecificSubdivision->isoCode . "\n"); // 'MN'
+
+print($record->city->name . "\n"); // 'Minneapolis'
+
+print($record->postal->code . "\n"); // '55455'
+
+print($record->location->latitude . "\n"); // 44.9733
+print($record->location->longitude . "\n"); // -93.2323
+
+print($record->traits->network . "\n"); // '128.101.101.101/32'
+
+```
+
+## Values to use for Database or Array Keys ##
+
+**We strongly discourage you from using a value from any `names` property as
+a key in a database or array.**
+
+These names may change between releases. Instead we recommend using one of the
+following:
+
+* `GeoIp2\Record\City` - `$city->geonameId`
+* `GeoIp2\Record\Continent` - `$continent->code` or `$continent->geonameId`
+* `GeoIp2\Record\Country` and `GeoIp2\Record\RepresentedCountry` -
+  `$country->isoCode` or `$country->geonameId`
+* `GeoIp2\Record\Subdivision` - `$subdivision->isoCode` or `$subdivision->geonameId`
+
+### What data is returned? ###
+
+While many of the end points return the same basic records, the attributes
+which can be populated vary between end points. In addition, while an end
+point may offer a particular piece of data, MaxMind does not always have every
+piece of data for any given IP address.
+
+Because of these factors, it is possible for any end point to return a record
+where some or all of the attributes are unpopulated.
+
+See the
+[GeoIP2 web service docs](https://dev.maxmind.com/geoip/docs/web-services?lang=en)
+for details on what data each end point may return.
+
+The only piece of data which is always returned is the `ipAddress`
+attribute in the `GeoIp2\Record\Traits` record.
+
+## Integration with GeoNames ##
+
+[GeoNames](https://www.geonames.org/) offers web services and downloadable
+databases with data on geographical features around the world, including
+populated places. They offer both free and paid premium data. Each
+feature is unique identified by a `geonameId`, which is an integer.
+
+Many of the records returned by the GeoIP2 web services and databases
+include a `geonameId` property. This is the ID of a geographical feature
+(city, region, country, etc.) in the GeoNames database.
+
+Some of the data that MaxMind provides is also sourced from GeoNames. We
+source things like place names, ISO codes, and other similar data from
+the GeoNames premium data set.
+
+## Reporting data problems ##
+
+If the problem you find is that an IP address is incorrectly mapped,
+please
+[submit your correction to MaxMind](https://www.maxmind.com/en/correction).
+
+If you find some other sort of mistake, like an incorrect spelling,
+please check the [GeoNames site](https://www.geonames.org/) first. Once
+you've searched for a place and found it on the GeoNames map view, there
+are a number of links you can use to correct data ("move", "edit",
+"alternate names", etc.). Once the correction is part of the GeoNames
+data set, it will be automatically incorporated into future MaxMind
+releases.
+
+If you are a paying MaxMind customer and you're not sure where to submit
+a correction, please
+[contact MaxMind support](https://www.maxmind.com/en/support) for help.
+
+## Other Support ##
+
+Please report all issues with this code using the
+[GitHub issue tracker](https://github.com/maxmind/GeoIP2-php/issues).
+
+If you are having an issue with a MaxMind service that is not specific
+to the client API, please see
+[our support page](https://www.maxmind.com/en/support).
+
+## Requirements  ##
+
+This library requires PHP 7.2 or greater.
+
+This library also relies on the [MaxMind DB Reader](https://github.com/maxmind/MaxMind-DB-Reader-php).
+
+## Contributing ##
+
+Patches and pull requests are encouraged. All code should follow the PSR-2
+style guidelines. Please include unit tests whenever possible. You may obtain
+the test data for the maxmind-db folder by running `git submodule update
+--init --recursive` or adding `--recursive` to your initial clone, or from
+https://github.com/maxmind/MaxMind-DB
+
+## Versioning ##
+
+The GeoIP2 PHP API uses [Semantic Versioning](https://semver.org/).
+
+## Copyright and License ##
+
+This software is Copyright (c) 2013-2020 by MaxMind, Inc.
+
+This is free software, licensed under the Apache License, Version 2.0.

+ 32 - 0
vendor/geoip2/geoip2/composer.json

@@ -0,0 +1,32 @@
+{
+    "name": "geoip2/geoip2",
+    "description": "MaxMind GeoIP2 PHP API",
+    "keywords": ["geoip", "geoip2", "geolocation", "ip", "maxmind"],
+    "homepage": "https://github.com/maxmind/GeoIP2-php",
+    "type": "library",
+    "license": "Apache-2.0",
+    "authors": [
+        {
+            "name": "Gregory J. Oschwald",
+            "email": "goschwald@maxmind.com",
+            "homepage": "https://www.maxmind.com/"
+        }
+    ],
+    "require": {
+        "maxmind-db/reader": "~1.8",
+        "maxmind/web-service-common": "~0.8",
+        "php": ">=7.2",
+        "ext-json": "*"
+    },
+    "require-dev": {
+        "friendsofphp/php-cs-fixer": "3.*",
+        "phpunit/phpunit": "^8.0 || ^9.0",
+        "squizlabs/php_codesniffer": "3.*",
+        "phpstan/phpstan": "*"
+    },
+    "autoload": {
+        "psr-4": {
+            "GeoIp2\\": "src"
+        }
+    }
+}

+ 26 - 0
vendor/geoip2/geoip2/examples/benchmark.php

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

+ 299 - 0
vendor/geoip2/geoip2/src/Database/Reader.php

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

+ 12 - 0
vendor/geoip2/geoip2/src/Exception/AddressNotFoundException.php

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

+ 12 - 0
vendor/geoip2/geoip2/src/Exception/AuthenticationException.php

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

+ 12 - 0
vendor/geoip2/geoip2/src/Exception/GeoIp2Exception.php

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

+ 28 - 0
vendor/geoip2/geoip2/src/Exception/HttpException.php

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

+ 30 - 0
vendor/geoip2/geoip2/src/Exception/InvalidRequestException.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+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.
+     *
+     * @var string
+     */
+    public $error;
+
+    public function __construct(
+        string $message,
+        string $error,
+        int $httpStatus,
+        string $uri,
+        \Exception $previous = null
+    ) {
+        $this->error = $error;
+        parent::__construct($message, $httpStatus, $uri, $previous);
+    }
+}

+ 12 - 0
vendor/geoip2/geoip2/src/Exception/OutOfQueriesException.php

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

+ 68 - 0
vendor/geoip2/geoip2/src/Model/AbstractModel.php

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

+ 91 - 0
vendor/geoip2/geoip2/src/Model/AnonymousIp.php

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

+ 58 - 0
vendor/geoip2/geoip2/src/Model/Asn.php

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

+ 123 - 0
vendor/geoip2/geoip2/src/Model/City.php

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

+ 50 - 0
vendor/geoip2/geoip2/src/Model/ConnectionType.php

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

+ 96 - 0
vendor/geoip2/geoip2/src/Model/Country.php

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

+ 50 - 0
vendor/geoip2/geoip2/src/Model/Domain.php

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

+ 15 - 0
vendor/geoip2/geoip2/src/Model/Enterprise.php

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

+ 15 - 0
vendor/geoip2/geoip2/src/Model/Insights.php

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

+ 93 - 0
vendor/geoip2/geoip2/src/Model/Isp.php

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

+ 22 - 0
vendor/geoip2/geoip2/src/ProviderInterface.php

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

+ 67 - 0
vendor/geoip2/geoip2/src/Record/AbstractPlaceRecord.php

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

+ 67 - 0
vendor/geoip2/geoip2/src/Record/AbstractRecord.php

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

+ 33 - 0
vendor/geoip2/geoip2/src/Record/City.php

@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+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 An array map where the keys are locale codes
+ * and the values are names. This attribute is returned by all location
+ * services and databases.
+ */
+class City extends AbstractPlaceRecord
+{
+    /**
+     * @ignore
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = ['confidence', 'geonameId', 'names'];
+}

+ 36 - 0
vendor/geoip2/geoip2/src/Record/Continent.php

@@ -0,0 +1,36 @@
+<?php
+
+declare(strict_types=1);
+
+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
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = [
+        'code',
+        'geonameId',
+        'names',
+    ];
+}

+ 44 - 0
vendor/geoip2/geoip2/src/Record/Country.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+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 all location services and databases.
+ * @property-read bool $isInEuropeanUnion This is true if the country is a
+ * member state of the European Union. This attribute is returned by all
+ * location services and databases.
+ * @property-read string|null $isoCode The two-character ISO 3166-1 alpha code
+ * for the country. See https://en.wikipedia.org/wiki/ISO_3166-1. This
+ * attribute is returned by all location services and databases.
+ * @property-read string|null $name The name of the country based on the locales
+ * list passed to the constructor. This attribute is returned by all location
+ * services and databases.
+ * @property-read array|null $names An array map where the keys are locale codes
+ * and the values are names. This attribute is returned by all location
+ * services and databases.
+ */
+class Country extends AbstractPlaceRecord
+{
+    /**
+     * @ignore
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = [
+        'confidence',
+        'geonameId',
+        'isInEuropeanUnion',
+        'isoCode',
+        'names',
+    ];
+}

+ 56 - 0
vendor/geoip2/geoip2/src/Record/Location.php

@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+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
+ * Google AdWords API. See
+ * https://developers.google.com/adwords/api/docs/appendix/cities-DMAregions.
+ * @property-read string|null $timeZone The time zone associated with location, as
+ * specified by the IANA Time Zone Database, e.g., "America/New_York". See
+ * https://www.iana.org/time-zones.
+ */
+class Location extends AbstractRecord
+{
+    /**
+     * @ignore
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = [
+        'averageIncome',
+        'accuracyRadius',
+        'latitude',
+        'longitude',
+        'metroCode',
+        'populationDensity',
+        'postalCode',
+        'postalConfidence',
+        'timeZone',
+    ];
+}

+ 23 - 0
vendor/geoip2/geoip2/src/Record/MaxMind.php

@@ -0,0 +1,23 @@
+<?php
+
+declare(strict_types=1);
+
+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
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = ['queriesRemaining'];
+}

+ 30 - 0
vendor/geoip2/geoip2/src/Record/Postal.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+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
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = ['code', 'confidence'];
+}

+ 33 - 0
vendor/geoip2/geoip2/src/Record/RepresentedCountry.php

@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+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 string|null $type A string indicating the type of entity that is
+ * representing the country. Currently we only return <code>military</code>
+ * but this could expand to include other types in the future.
+ */
+class RepresentedCountry extends Country
+{
+    /**
+     * @ignore
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = [
+        'confidence',
+        'geonameId',
+        'isInEuropeanUnion',
+        'isoCode',
+        'names',
+        'type',
+    ];
+}

+ 44 - 0
vendor/geoip2/geoip2/src/Record/Subdivision.php

@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+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 ISO 3166-2 code. See
+ * https://en.wikipedia.org/wiki/ISO_3166-2. This attribute is returned by all
+ * location databases and services except Country.
+ * @property-read string|null $name The name of the subdivision based on the
+ * locales list passed to the constructor. This attribute is returned by all
+ * location databases and services besides Country.
+ * @property-read array|null $names An array map where the keys are locale codes
+ * and the values are names. This attribute is returned by all location
+ * databases and services besides Country.
+ */
+class Subdivision extends AbstractPlaceRecord
+{
+    /**
+     * @ignore
+     *
+     * @var array<string>
+     */
+    protected $validAttributes = [
+        'confidence',
+        'geonameId',
+        'isoCode',
+        'names',
+    ];
+}

+ 158 - 0
vendor/geoip2/geoip2/src/Record/Traits.php

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

+ 36 - 0
vendor/geoip2/geoip2/src/Util.php

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

+ 255 - 0
vendor/geoip2/geoip2/src/WebService/Client.php

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

+ 237 - 0
vendor/maxmind-db/reader/CHANGELOG.md

@@ -0,0 +1,237 @@
+CHANGELOG
+=========
+
+1.11.1 (2023-12-01)
+-------------------
+
+* Resolve warnings when compiling the C extension.
+* Fix various type issues detected by PHPStan level. Pull request by
+  LauraTaylorUK. GitHub #160.
+
+1.11.0 (2021-10-18)
+-------------------
+
+* Replace runtime define of a constant to facilitate opcache preloading.
+  Reported by vedadkajtaz. GitHub #134.
+* Resolve minor issue found by the Clang static analyzer in the C
+  extension.
+
+1.10.1 (2021-04-14)
+-------------------
+
+* Fix a `TypeError` exception in the pure PHP reader when using large
+  databases on 32-bit PHP builds with the `bcmath` extension. Reported
+  by dodo1708. GitHub #124.
+
+1.10.0 (2021-02-09)
+-------------------
+
+* When using the pure PHP reader, unsigned integers up to PHP_MAX_INT
+  will now be integers in PHP rather than strings. Previously integers
+  greater than 2^24 on 32-bit platforms and 2^56 on 64-bit platforms
+  would be strings due to the use of `gmp` or `bcmath` to decode them.
+  Reported by Alejandro Celaya. GitHub #119.
+
+1.9.0 (2021-01-07)
+------------------
+
+* The `maxminddb` extension is now buildable on Windows. Pull request
+  by Jan Ehrhardt. GitHub #115.
+
+1.8.0 (2020-10-01)
+------------------
+
+* Fixes for PHP 8.0. Pull Request by Remi Collet. GitHub #108.
+
+1.7.0 (2020-08-07)
+------------------
+
+* IMPORTANT: PHP 7.2 or greater is now required.
+* The extension no longer depends on the pure PHP classes in
+  `maxmind-db/reader`. You can use it independently.
+* Type hints have been added to both the pure PHP implementation
+  and the extension.
+* The `metadata` method on the reader now returns a new copy of the
+  metadata object rather than the actual object used by the reader.
+* Work around PHP `is_readable()` bug. Reported by Ben Roberts. GitHub
+  #92.
+* This is the first release of the extension as a PECL package.
+  GitHub #34.
+
+1.6.0 (2019-12-19)
+------------------
+
+* 1.5.0 and 1.5.1 contained a possible memory corruptions when using
+  `getWithPrefixLen`. This has been fixed. Reported by proton-ab.
+  GitHub #96.
+* The `composer.json` file now conflicts with all versions of the
+  `maxminddb` C extension less than the Composer version. This is to
+  reduce the chance of having an older, conflicting version of the
+  extension installed. You will need to upgrade the extension before
+  running `composer update`. Pull request by Benoît Burnichon. GitHub
+  #97.
+
+1.5.1 (2019-12-12)
+------------------
+
+* Minor performance improvements.
+* Make tests pass with older versions of libmaxminddb. PR by Remi
+  Collet. GitHub #90.
+* Test enhancements. PR by Chun-Sheng, Li. GitHub #91.
+
+1.5.0 (2019-09-30)
+------------------
+
+* PHP 5.6 or greater is now required.
+* The C extension now supports PHP 8. Pull request by John Boehr.
+  GitHub #87.
+* A new method, `getWithPrefixLen`, was added to the `Reader` class.
+  This method returns an array containing the record and the prefix
+  length for that record. GitHub #89.
+
+1.4.1 (2019-01-04)
+------------------
+
+* The `maxminddb` extension now returns a string when a `uint32`
+  value is greater than `LONG_MAX`. Previously, the value would
+  overflow. This generally only affects 32-bit machines.  Reported
+  by Remi Collet. GitHub #79.
+* For `uint64` values, the `maxminddb` extension now returns an
+  integer rather than a string when the value is less than or equal
+  to `LONG_MAX`. This more closely matches the behavior of the pure
+  PHP reader.
+
+1.4.0 (2018-11-20)
+------------------
+
+* The `maxminddb` extension now has the arginfo when using reflection.
+  PR by Remi Collet. GitHub #75.
+* The `maxminddb` extension now provides `MINFO()` function that
+  displays the extension version and the libmaxminddb version. PR by
+  Remi Collet. GitHub #74.
+* The `maxminddb` `configure` script now uses `pkg-config` when
+  available to get libmaxmindb build info. PR by Remi Collet.
+  GitHub #73.
+* The pure PHP reader now correctly decodes integers on 32-bit platforms.
+  Previously, large integers would overflow. Reported by Remi Collet.
+  GitHub #77.
+* There are small performance improvements for the pure PHP reader.
+
+1.3.0 (2018-02-21)
+------------------
+
+* IMPORTANT: The `maxminddb` extension now obeys `open_basedir`. If
+  `open_basedir` is set, you _must_ store the database within the
+  specified directory. Placing the file outside of this directory
+  will result in an exception. Please test your integration before
+  upgrading the extension. This does not affect the pure PHP
+  implementation, which has always had this restriction. Reported
+  by Benoît Burnichon. GitHub #61.
+* A custom `autoload.php` file is provided for installations without
+  Composer. GitHub #56.
+
+1.2.0 (2017-10-27)
+------------------
+
+* PHP 5.4 or greater is now required.
+* The `Reader` class for the `maxminddb` extension is no longer final.
+  This was change to match the behavior of the pure PHP class.
+  Reported and fixed by venyii. GitHub #52 & #54.
+
+1.1.3 (2017-01-19)
+------------------
+
+* Fix incorrect version in `ext/php_maxminddb.h`. GitHub #48.
+
+1.1.2 (2016-11-22)
+------------------
+
+* Searching for database metadata only occurs within the last 128KB
+  (128 * 1024 bytes) of the file, speeding detection of corrupt
+  datafiles. Reported by Eric Teubert. GitHub #42.
+* Suggest relevant extensions when installing with Composer. GitHub #37.
+
+1.1.1 (2016-09-15)
+------------------
+
+* Development files were added to the `.gitattributes` as `export-ignore` so
+  that they are not part of the Composer release. Pull request by Michele
+  Locati. GitHub #39.
+
+1.1.0 (2016-01-04)
+------------------
+
+* The MaxMind DB extension now supports PHP 7. Pull request by John Boehr.
+  GitHub #27.
+
+1.0.3 (2015-03-13)
+------------------
+
+* All uses of `strlen` were removed. This should prevent issues in situations
+  where the function is overloaded or otherwise broken.
+
+1.0.2 (2015-01-19)
+------------------
+
+* Previously the MaxMind DB extension would cause a segfault if the Reader
+  object's destructor was called without first having called the constructor.
+  (Reported by Matthias Saou & Juan Peri. GitHub #20.)
+
+1.0.1 (2015-01-12)
+------------------
+
+* In the last several releases, the version number in the extension was
+  incorrect. This release is being done to correct it. No other code changes
+  are included.
+
+1.0.0 (2014-09-22)
+------------------
+
+* First production release.
+* In the pure PHP reader, a string length test after `fread()` was replaced
+  with the difference between the start pointer and the end pointer. This
+  provided a 15% speed increase.
+
+0.3.3 (2014-09-15)
+------------------
+
+* Clarified behavior of 128-bit type in documentation.
+* Updated phpunit and fixed some test breakage from the newer version.
+
+0.3.2 (2014-09-10)
+------------------
+
+* Fixed invalid reference to global class RuntimeException from namespaced
+  code. Fixed by Steven Don. GitHub issue #15.
+* Additional documentation of `Metadata` class as well as misc. documentation
+  cleanup.
+
+0.3.1 (2014-05-01)
+------------------
+
+* The API now works when `mbstring.func_overload` is set.
+* BCMath is no longer required. If the decoder encounters a big integer,
+  it will try to use GMP and then BCMath. If both of those fail, it will
+  throw an exception. No databases released by MaxMind currently use big
+  integers.
+* The API now officially supports HHVM when using the pure PHP reader.
+
+0.3.0 (2014-02-19)
+------------------
+
+* This API is now licensed under the Apache License, Version 2.0.
+* The code for the C extension was cleaned up, fixing several potential
+  issues.
+
+0.2.0 (2013-10-21)
+------------------
+
+* Added optional C extension for using libmaxminddb in place of the pure PHP
+  reader.
+* Significantly improved error handling in pure PHP reader.
+* Improved performance for IPv4 lookups in an IPv6 database.
+
+0.1.0 (2013-07-16)
+------------------
+
+* Initial release

+ 202 - 0
vendor/maxmind-db/reader/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 185 - 0
vendor/maxmind-db/reader/README.md

@@ -0,0 +1,185 @@
+# MaxMind DB Reader PHP API #
+
+## Description ##
+
+This is the PHP API for reading MaxMind DB files. MaxMind DB is a binary file
+format that stores data indexed by IP address subnets (IPv4 or IPv6).
+
+## Installation (Composer) ##
+
+We recommend installing this package with [Composer](https://getcomposer.org/).
+
+### Download Composer ###
+
+To download Composer, run in the root directory of your project:
+
+```bash
+curl -sS https://getcomposer.org/installer | php
+```
+
+You should now have the file `composer.phar` in your project directory.
+
+### Install Dependencies ###
+
+Run in your project root:
+
+```
+php composer.phar require maxmind-db/reader:~1.0
+```
+
+You should now have the files `composer.json` and `composer.lock` as well as
+the directory `vendor` in your project directory. If you use a version control
+system, `composer.json` should be added to it.
+
+### Require Autoloader ###
+
+After installing the dependencies, you need to require the Composer autoloader
+from your code:
+
+```php
+require 'vendor/autoload.php';
+```
+
+## Installation (Standalone) ##
+
+If you don't want to use Composer for some reason, a custom
+`autoload.php` is provided for you in the project root. To use the
+library, simply include that file,
+
+```php
+require('/path/to/MaxMind-DB-Reader-php/autoload.php');
+```
+
+and then instantiate the reader class normally:
+
+```php
+use MaxMind\Db\Reader;
+$reader = new Reader('example.mmdb');
+```
+
+## Installation (RPM)
+
+RPMs are available in the [official Fedora repository](https://apps.fedoraproject.org/packages/php-maxminddb).
+
+To install on Fedora, run:
+
+```bash
+dnf install php-maxminddb
+```
+
+To install on CentOS or RHEL 7, first [enable the EPEL repository](https://fedoraproject.org/wiki/EPEL)
+and then run:
+
+```bash
+yum install php-maxminddb
+```
+
+Please note that these packages are *not* maintained by MaxMind.
+
+## Usage ##
+
+## Example ##
+
+```php
+<?php
+require_once 'vendor/autoload.php';
+
+use MaxMind\Db\Reader;
+
+$ipAddress = '24.24.24.24';
+$databaseFile = 'GeoIP2-City.mmdb';
+
+$reader = new Reader($databaseFile);
+
+// get returns just the record for the IP address
+print_r($reader->get($ipAddress));
+
+// getWithPrefixLen returns an array containing the record and the
+// associated prefix length for that record.
+print_r($reader->getWithPrefixLen($ipAddress));
+
+$reader->close();
+```
+
+## Optional PHP C Extension ##
+
+MaxMind provides an optional C extension that is a drop-in replacement for
+`MaxMind\Db\Reader`. In order to use this extension, you must install the
+Reader API as described above and install the extension as described below. If
+you are using an autoloader, no changes to your code should be necessary.
+
+### Installing Extension ###
+
+First install [libmaxminddb](https://github.com/maxmind/libmaxminddb) as
+described in its [README.md
+file](https://github.com/maxmind/libmaxminddb/blob/main/README.md#installing-from-a-tarball).
+After successfully installing libmaxmindb, you may install the extension
+from [pecl](https://pecl.php.net/package/maxminddb):
+
+```
+pecl install maxminddb
+```
+
+Alternatively, you may install it from the source. To do so, run the following
+commands from the top-level directory of this distribution:
+
+```
+cd ext
+phpize
+./configure
+make
+make test
+sudo make install
+```
+
+You then must load your extension. The recommended method is to add the
+following to your `php.ini` file:
+
+```
+extension=maxminddb.so
+```
+
+Note: You may need to install the PHP development package on your OS such as
+php5-dev for Debian-based systems or php-devel for RedHat/Fedora-based ones.
+
+## 128-bit Integer Support ##
+
+The MaxMind DB format includes 128-bit unsigned integer as a type. Although
+no MaxMind-distributed database currently makes use of this type, both the
+pure PHP reader and the C extension support this type. The pure PHP reader
+requires gmp or bcmath to read databases with 128-bit unsigned integers.
+
+The integer is currently returned as a hexadecimal string (prefixed with "0x")
+by the C extension and a decimal string (no prefix) by the pure PHP reader.
+Any change to make the reader implementations always return either a
+hexadecimal or decimal representation of the integer will NOT be considered a
+breaking change.
+
+## Support ##
+
+Please report all issues with this code using the [GitHub issue tracker](https://github.com/maxmind/MaxMind-DB-Reader-php/issues).
+
+If you are having an issue with a MaxMind service that is not specific to the
+client API, please see [our support page](https://www.maxmind.com/en/support).
+
+## Requirements  ##
+
+This library requires PHP 7.2 or greater.
+
+The GMP or BCMath extension may be required to read some databases
+using the pure PHP API.
+
+## Contributing ##
+
+Patches and pull requests are encouraged. All code should follow the PSR-1 and
+PSR-2 style guidelines. Please include unit tests whenever possible.
+
+## Versioning ##
+
+The MaxMind DB Reader PHP API uses [Semantic Versioning](https://semver.org/).
+
+## Copyright and License ##
+
+This software is Copyright (c) 2014-2023 by MaxMind, Inc.
+
+This is free software, licensed under the Apache License, Version 2.0.

+ 47 - 0
vendor/maxmind-db/reader/autoload.php

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

+ 44 - 0
vendor/maxmind-db/reader/composer.json

@@ -0,0 +1,44 @@
+{
+    "name": "maxmind-db/reader",
+    "description": "MaxMind DB Reader API",
+    "keywords": ["database", "geoip", "geoip2", "geolocation", "maxmind"],
+    "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php",
+    "type": "library",
+    "license": "Apache-2.0",
+    "authors": [
+        {
+            "name": "Gregory J. Oschwald",
+            "email": "goschwald@maxmind.com",
+            "homepage": "https://www.maxmind.com/"
+        }
+    ],
+    "require": {
+        "php": ">=7.2"
+    },
+    "suggest": {
+        "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
+        "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder",
+        "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups"
+    },
+    "conflict": {
+        "ext-maxminddb": "<1.11.1,>=2.0.0"
+    },
+    "require-dev": {
+        "friendsofphp/php-cs-fixer": "3.*",
+        "phpunit/phpunit": ">=8.0.0,<10.0.0",
+        "php-coveralls/php-coveralls": "^2.1",
+        "phpunit/phpcov": ">=6.0.0",
+        "squizlabs/php_codesniffer": "3.*",
+        "phpstan/phpstan": "*"
+    },
+    "autoload": {
+        "psr-4": {
+            "MaxMind\\Db\\": "src/MaxMind/Db"
+        }
+    },
+    "autoload-dev": {
+        "psr-4": {
+            "MaxMind\\Db\\Test\\Reader\\": "tests/MaxMind/Db/Test/Reader"
+        }
+    }
+}

+ 40 - 0
vendor/maxmind-db/reader/ext/config.m4

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

+ 10 - 0
vendor/maxmind-db/reader/ext/config.w32

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

+ 811 - 0
vendor/maxmind-db/reader/ext/maxminddb.c

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

+ 24 - 0
vendor/maxmind-db/reader/ext/php_maxminddb.h

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

+ 12 - 0
vendor/maxmind-db/reader/ext/tests/001-load.phpt

@@ -0,0 +1,12 @@
+--TEST--
+Check for maxminddb presence
+--SKIPIF--
+<?php if (!extension_loaded('maxminddb')) {
+    echo 'skip';
+} ?>
+--FILE--
+<?php
+echo 'maxminddb extension is available';
+?>
+--EXPECT--
+maxminddb extension is available

+ 13 - 0
vendor/maxmind-db/reader/ext/tests/002-final.phpt

@@ -0,0 +1,13 @@
+--TEST--
+Check that Reader class is not final
+--SKIPIF--
+<?php if (!extension_loaded('maxminddb')) {
+    echo 'skip';
+} ?>
+--FILE--
+<?php
+$reflectionClass = new \ReflectionClass('MaxMind\Db\Reader');
+var_dump($reflectionClass->isFinal());
+?>
+--EXPECT--
+bool(false)

+ 12 - 0
vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt

@@ -0,0 +1,12 @@
+--TEST--
+openbase_dir is followed
+--INI--
+open_basedir=/--dne--
+--FILE--
+<?php
+use MaxMind\Db\Reader;
+
+$reader = new Reader('/usr/local/share/GeoIP/GeoIP2-City.mmdb');
+?>
+--EXPECTREGEX--
+.*open_basedir restriction in effect.*

+ 63 - 0
vendor/maxmind-db/reader/package.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0"
+    xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd">
+
+    <name>maxminddb</name>
+    <channel>pecl.php.net</channel>
+    <summary>Reader for the MaxMind DB file format</summary>
+    <description>This is the PHP extension for reading MaxMind DB files. MaxMind DB is a binary file format that stores data indexed by IP address subnets (IPv4 or IPv6).</description>
+    <lead>
+        <name>Greg Oschwald</name>
+        <user>oschwald</user>
+        <email>goschwald@maxmind.com</email>
+        <active>yes</active>
+    </lead>
+    <date>2023-12-01</date>
+    <version>
+        <release>1.11.1</release>
+        <api>1.11.1</api>
+    </version>
+    <stability>
+        <release>stable</release>
+        <api>stable</api>
+    </stability>
+    <license uri="https://github.com/maxmind/MaxMind-DB-Reader-php/blob/main/LICENSE">Apache License 2.0</license>
+    <notes>* Resolve warnings when compiling the C extension.
+* Fix various type issues detected by PHPStan level. Pull request by
+  LauraTaylorUK. GitHub #160.</notes>
+    <contents>
+        <dir name="/">
+            <file role="doc" name="LICENSE"/>
+            <file role="doc" name="CHANGELOG.md"/>
+            <file role="doc" name="README.md"/>
+
+            <dir name="ext">
+                <file role="src" name="config.m4"/>
+                <file role="src" name="config.w32"/>
+
+                <file role="src" name="maxminddb.c"/>
+                <file role="src" name="php_maxminddb.h"/>
+
+                <dir name="tests">
+                    <file role="test" name="001-load.phpt"/>
+                    <file role="test" name="002-final.phpt"/>
+                    <file role="test" name="003-open-basedir.phpt"/>
+                </dir>
+            </dir>
+        </dir>
+    </contents>
+    <dependencies>
+        <required>
+            <php>
+                <min>7.2.0</min>
+            </php>
+            <pearinstaller>
+                <min>1.10.0</min>
+            </pearinstaller>
+        </required>
+    </dependencies>
+    <providesextension>maxminddb</providesextension>
+    <extsrcrelease />
+</package>

+ 403 - 0
vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php

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

+ 435 - 0
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php

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

+ 11 - 0
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php

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

+ 120 - 0
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php

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

+ 33 - 0
vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php

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

+ 111 - 0
vendor/maxmind/web-service-common/CHANGELOG.md

@@ -0,0 +1,111 @@
+CHANGELOG
+=========
+
+0.9.0 (2022-03-28)
+------------------
+
+* Improved internal type hint usage.
+
+0.8.1 (2020-11-02)
+------------------
+
+* We now correctly handle responses without a `Content-Type` header. In 0.8.0,
+  such responses could lead to a type error. In particular, this affected the
+  minFraud Report Transaction endpoint, which returns a response with no
+  content. Reported by Dmitry Malashko. GitHub #99 on
+  `maxmind/minfraud-api-php`.
+
+0.8.0 (2020-10-01)
+------------------
+
+* PHP 7.2 or greater is now required.
+* Added additional type hints.
+
+0.7.0 (2020-05-06)
+------------------
+
+* Responses with a 204 status code are accepted as successes.
+
+0.6.0 (2019-12-12)
+------------------
+
+* Curl handles are now reused across requests. Pull request by Willem
+  Stuursma-Ruwen. GitHub #24.
+* PHP 5.6 is now required.
+
+0.5.0 (2018-02-12)
+------------------
+
+* Refer to account IDs using the terminology "account" rather than "user".
+
+0.4.0 (2017-07-10)
+------------------
+
+* PHP 5.4 is now required.
+
+0.3.1 (2016-08-10)
+------------------
+
+* On Mac OS X when using a curl built against SecureTransport, the certs
+  in the system's keychain will now be used instead of the CA bundle on
+  the file system.
+
+0.3.0 (2016-08-09)
+------------------
+
+* This package now uses `composer/ca-bundle` by default rather than a CA
+  bundle distributed with this package. `composer/ca-bundle` will first try
+  to use the system CA bundle and will fall back to the Mozilla CA bundle
+  when no system bundle is available. You may still specify your own bundle
+  using the `caBundle` option.
+
+0.2.1 (2016-06-13)
+------------------
+
+* Fix typo in code to copy cert to temp directory.
+
+0.2.0 (2016-06-10)
+------------------
+
+* Added handling of additional error codes that the web service may return.
+* A `USER_ID_UNKNOWN` error will now throw a
+  `MaxMind\Exception\AuthenticationException`.
+* Added support for `proxy` option. Closes #6.
+
+0.1.0 (2016-05-23)
+------------------
+
+* A `PERMISSION_REQUIRED` error will now throw a `PermissionRequiredException`
+  exception.
+* Added a `.gitattributes` file to exclude tests from Composer releases.
+  GitHub #7.
+* Updated included cert bundle.
+
+0.0.4 (2015-07-21)
+------------------
+
+* Added extremely basic tests for the curl calls.
+* Fixed broken POSTs.
+
+0.0.3 (2015-06-30)
+------------------
+
+* Floats now work with the `timeout` and `connectTimeout` options. Fix by
+  Benjamin Pick. GitHub PR #2.
+* `curl_error` is now used instead of `curl_strerror`. The latter is only
+  available for PHP 5.5 or later. Fix by Benjamin Pick. GitHub PR #1.
+
+
+0.0.2 (2015-06-09)
+------------------
+
+* An exception is now immediately thrown curl error rather than letting later
+  status code checks throw an exception. This improves the exception message
+  greatly.
+* If this library is inside a phar archive, the CA certs are copied out of the
+  archive to a temporary file so that curl can use them.
+
+0.0.1 (2015-06-01)
+------------------
+
+* Initial release.

+ 202 - 0
vendor/maxmind/web-service-common/LICENSE

@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 25 - 0
vendor/maxmind/web-service-common/README.md

@@ -0,0 +1,25 @@
+# Common Code for MaxMind Web Service Clients #
+
+This is _not_ intended for direct use by third parties. Rather, it is for
+shared code between MaxMind's various web service client APIs.
+
+## Requirements  ##
+
+The library requires PHP 7.2 or greater.
+
+There are several other dependencies as defined in the `composer.json` file.
+
+## Contributing ##
+
+Patches and pull requests are encouraged. All code should follow the PSR-2
+style guidelines. Please include unit tests whenever possible.
+
+## Versioning ##
+
+This API uses [Semantic Versioning](http://semver.org/).
+
+## Copyright and License ##
+
+This software is Copyright (c) 2015-2020 by MaxMind, Inc.
+
+This is free software, licensed under the Apache License, Version 2.0.

+ 32 - 0
vendor/maxmind/web-service-common/composer.json

@@ -0,0 +1,32 @@
+{
+  "name": "maxmind/web-service-common",
+  "description": "Internal MaxMind Web Service API",
+  "minimum-stability": "stable",
+  "homepage": "https://github.com/maxmind/web-service-common-php",
+  "type": "library",
+  "license": "Apache-2.0",
+  "authors": [
+    {
+      "name": "Gregory Oschwald",
+      "email": "goschwald@maxmind.com"
+    }
+  ],
+  "require": {
+    "php": ">=7.2",
+    "composer/ca-bundle": "^1.0.3",
+    "ext-curl": "*",
+    "ext-json": "*"
+  },
+  "require-dev": {
+    "friendsofphp/php-cs-fixer": "3.*",
+    "phpunit/phpunit": "^8.0 || ^9.0",
+    "squizlabs/php_codesniffer": "3.*",
+    "phpstan/phpstan": "*"
+  },
+  "autoload": {
+    "psr-4": {
+      "MaxMind\\Exception\\": "src/Exception",
+      "MaxMind\\WebService\\": "src/WebService"
+    }
+  }
+}

+ 56 - 0
vendor/maxmind/web-service-common/dev-bin/release.sh

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

+ 7 - 0
vendor/maxmind/web-service-common/phpstan.neon

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

+ 12 - 0
vendor/maxmind/web-service-common/src/Exception/AuthenticationException.php

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

+ 44 - 0
vendor/maxmind/web-service-common/src/Exception/HttpException.php

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

+ 12 - 0
vendor/maxmind/web-service-common/src/Exception/InsufficientFundsException.php

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

+ 14 - 0
vendor/maxmind/web-service-common/src/Exception/InvalidInputException.php

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

+ 41 - 0
vendor/maxmind/web-service-common/src/Exception/InvalidRequestException.php

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

+ 9 - 0
vendor/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php

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

+ 12 - 0
vendor/maxmind/web-service-common/src/Exception/PermissionRequiredException.php

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

+ 12 - 0
vendor/maxmind/web-service-common/src/Exception/WebServiceException.php

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

+ 546 - 0
vendor/maxmind/web-service-common/src/WebService/Client.php

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

+ 136 - 0
vendor/maxmind/web-service-common/src/WebService/Http/CurlRequest.php

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

+ 19 - 0
vendor/maxmind/web-service-common/src/WebService/Http/Request.php

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

+ 48 - 0
vendor/maxmind/web-service-common/src/WebService/Http/RequestFactory.php

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