'
+ ].join('');
+ },
+ collectPixelPayload: function ($box) {
+ var readList = function (platform) {
+ var rows = [];
+ $box.find('.split-pixel-list[data-platform="' + platform + '"] tbody .split-pixel-row').each(function () {
+ var $row = $(this);
+ rows.push({
+ id: $row.attr('data-row-id') || Controller.api.pixelRowUid(platform === 'facebook' ? 'fb' : 'tk'),
+ enabled: $row.find('.pixel-enabled').prop('checked') ? 1 : 0,
+ server_postback: $row.find('.pixel-server-postback').prop('checked') ? 1 : 0,
+ pixel_id: $.trim($row.find('.pixel-id').val()),
+ access_token: $.trim($row.find('.pixel-access-token').val()),
+ test_code: $.trim($row.find('.pixel-test-code').val()),
+ event: $row.find('.pixel-event').val() || 'PageView',
+ sort: Math.max(0, parseInt($row.find('.pixel-sort-input').val(), 10) || 0)
+ });
+ });
+ return rows;
+ };
+ return {
+ facebook: readList('facebook'),
+ tiktok: readList('tiktok')
+ };
+ },
loadCopyModalData: function (callback) {
Fast.api.ajax({
url: 'split.link/copyinfo',
@@ -283,7 +610,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
});
}
var myDomains = $.isArray(data.my_domains) ? data.my_domains : [];
- var domainIndexUrl = data.domain_index_url || 'domain/index';
+ var domainIndexUrl = Controller.api.normalizeAdminRouteUrl(data.domain_index_url, 'domain');
+ var domainAddUrl = Controller.api.normalizeAdminRouteUrl(data.domain_add_url, 'domain/add');
var configIndexUrl = data.config_index_url || 'general/config/index';
var defaultType = platformDomains.length ? 'platform' : 'my';
var defaultDomain = platformDomains.length ? platformDomains[0] : (myDomains.length ? myDomains[0].domain : '');
@@ -418,10 +746,16 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.copyText(linkCode);
});
- $box.on('click', '.btn-manage-domain, .btn-go-add-domain', function (e) {
+ $box.on('click', '.btn-manage-domain', function (e) {
e.preventDefault();
Layer.close(index);
- Backend.api.addtabs(Fast.api.fixurl(domainIndexUrl), __('Manage my domains'), 'fa fa-globe');
+ Backend.api.addtabs(Fast.api.fixurl(domainIndexUrl), __('Domain management'), 'fa fa-globe');
+ });
+
+ $box.on('click', '.btn-go-add-domain', function (e) {
+ e.preventDefault();
+ Layer.close(index);
+ Backend.api.addtabs(Fast.api.fixurl(domainAddUrl), __('Domain management'), 'fa fa-plus');
});
$box.on('click', '.btn-goto-config', function (e) {
diff --git a/patches/runtime/split_rr/7.cnt b/patches/runtime/split_rr/7.cnt
new file mode 100644
index 0000000..f11c82a
--- /dev/null
+++ b/patches/runtime/split_rr/7.cnt
@@ -0,0 +1 @@
+9
\ No newline at end of file
diff --git a/patches/runtime/split_rr/999001.cnt b/patches/runtime/split_rr/999001.cnt
new file mode 100644
index 0000000..bf0d87a
--- /dev/null
+++ b/patches/runtime/split_rr/999001.cnt
@@ -0,0 +1 @@
+4
\ No newline at end of file
diff --git a/patches/third_party/geoip2/geoip2/CHANGELOG.md b/patches/third_party/geoip2/geoip2/CHANGELOG.md
new file mode 100644
index 0000000..6af744f
--- /dev/null
+++ b/patches/third_party/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.
diff --git a/patches/third_party/geoip2/geoip2/LICENSE b/patches/third_party/geoip2/geoip2/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/patches/third_party/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.
diff --git a/patches/third_party/geoip2/geoip2/README.md b/patches/third_party/geoip2/geoip2/README.md
new file mode 100644
index 0000000..ea08e27
--- /dev/null
+++ b/patches/third_party/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
+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
+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
+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
+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
+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
+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
+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.
diff --git a/patches/third_party/geoip2/geoip2/composer.json b/patches/third_party/geoip2/geoip2/composer.json
new file mode 100644
index 0000000..e29513b
--- /dev/null
+++ b/patches/third_party/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"
+ }
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/examples/benchmark.php b/patches/third_party/geoip2/geoip2/examples/benchmark.php
new file mode 100644
index 0000000..a735a78
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/examples/benchmark.php
@@ -0,0 +1,26 @@
+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";
diff --git a/patches/third_party/geoip2/geoip2/src/Database/Reader.php b/patches/third_party/geoip2/geoip2/src/Database/Reader.php
new file mode 100644
index 0000000..4dabc5d
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Database/Reader.php
@@ -0,0 +1,299 @@
+
+ */
+ 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();
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Exception/AddressNotFoundException.php b/patches/third_party/geoip2/geoip2/src/Exception/AddressNotFoundException.php
new file mode 100644
index 0000000..628fb06
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Exception/AddressNotFoundException.php
@@ -0,0 +1,12 @@
+uri = $uri;
+ parent::__construct($message, $httpStatus, $previous);
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Exception/InvalidRequestException.php b/patches/third_party/geoip2/geoip2/src/Exception/InvalidRequestException.php
new file mode 100644
index 0000000..925b68d
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Exception/InvalidRequestException.php
@@ -0,0 +1,30 @@
+error = $error;
+ parent::__construct($message, $httpStatus, $uri, $previous);
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Exception/OutOfQueriesException.php b/patches/third_party/geoip2/geoip2/src/Exception/OutOfQueriesException.php
new file mode 100644
index 0000000..9734c8c
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Exception/OutOfQueriesException.php
@@ -0,0 +1,12 @@
+
+ */
+ 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;
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/AnonymousIp.php b/patches/third_party/geoip2/geoip2/src/Model/AnonymousIp.php
new file mode 100644
index 0000000..5586bd0
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/AnonymousIp.php
@@ -0,0 +1,91 @@
+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'));
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/Asn.php b/patches/third_party/geoip2/geoip2/src/Model/Asn.php
new file mode 100644
index 0000000..f05177e
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/Asn.php
@@ -0,0 +1,58 @@
+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'));
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/City.php b/patches/third_party/geoip2/geoip2/src/Model/City.php
new file mode 100644
index 0000000..b2e81dc
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/City.php
@@ -0,0 +1,123 @@
+
+ */
+ 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);
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/ConnectionType.php b/patches/third_party/geoip2/geoip2/src/Model/ConnectionType.php
new file mode 100644
index 0000000..36d4529
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/ConnectionType.php
@@ -0,0 +1,50 @@
+connectionType = $this->get('connection_type');
+ $ipAddress = $this->get('ip_address');
+ $this->ipAddress = $ipAddress;
+ $this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/Country.php b/patches/third_party/geoip2/geoip2/src/Model/Country.php
new file mode 100644
index 0000000..fdffc63
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/Country.php
@@ -0,0 +1,96 @@
+
+ */
+ 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;
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/Domain.php b/patches/third_party/geoip2/geoip2/src/Model/Domain.php
new file mode 100644
index 0000000..067a507
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/Domain.php
@@ -0,0 +1,50 @@
+domain = $this->get('domain');
+ $ipAddress = $this->get('ip_address');
+ $this->ipAddress = $ipAddress;
+ $this->network = Util::cidr($ipAddress, $this->get('prefix_len'));
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Model/Enterprise.php b/patches/third_party/geoip2/geoip2/src/Model/Enterprise.php
new file mode 100644
index 0000000..c63469b
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Model/Enterprise.php
@@ -0,0 +1,15 @@
+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'));
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/ProviderInterface.php b/patches/third_party/geoip2/geoip2/src/ProviderInterface.php
new file mode 100644
index 0000000..7d14891
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/ProviderInterface.php
@@ -0,0 +1,22 @@
+
+ */
+ 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;
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/AbstractRecord.php b/patches/third_party/geoip2/geoip2/src/Record/AbstractRecord.php
new file mode 100644
index 0000000..5ddb3c6
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/AbstractRecord.php
@@ -0,0 +1,67 @@
+
+ */
+ 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;
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/City.php b/patches/third_party/geoip2/geoip2/src/Record/City.php
new file mode 100644
index 0000000..f25dcb3
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/City.php
@@ -0,0 +1,33 @@
+
+ */
+ protected $validAttributes = ['confidence', 'geonameId', 'names'];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Continent.php b/patches/third_party/geoip2/geoip2/src/Record/Continent.php
new file mode 100644
index 0000000..103e2e3
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Continent.php
@@ -0,0 +1,36 @@
+
+ */
+ protected $validAttributes = [
+ 'code',
+ 'geonameId',
+ 'names',
+ ];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Country.php b/patches/third_party/geoip2/geoip2/src/Record/Country.php
new file mode 100644
index 0000000..3009ebc
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Country.php
@@ -0,0 +1,44 @@
+
+ */
+ protected $validAttributes = [
+ 'confidence',
+ 'geonameId',
+ 'isInEuropeanUnion',
+ 'isoCode',
+ 'names',
+ ];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Location.php b/patches/third_party/geoip2/geoip2/src/Record/Location.php
new file mode 100644
index 0000000..cb6111c
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Location.php
@@ -0,0 +1,56 @@
+
+ */
+ protected $validAttributes = [
+ 'averageIncome',
+ 'accuracyRadius',
+ 'latitude',
+ 'longitude',
+ 'metroCode',
+ 'populationDensity',
+ 'postalCode',
+ 'postalConfidence',
+ 'timeZone',
+ ];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/MaxMind.php b/patches/third_party/geoip2/geoip2/src/Record/MaxMind.php
new file mode 100644
index 0000000..e972506
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/MaxMind.php
@@ -0,0 +1,23 @@
+
+ */
+ protected $validAttributes = ['queriesRemaining'];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Postal.php b/patches/third_party/geoip2/geoip2/src/Record/Postal.php
new file mode 100644
index 0000000..3e9c237
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Postal.php
@@ -0,0 +1,30 @@
+
+ */
+ protected $validAttributes = ['code', 'confidence'];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/RepresentedCountry.php b/patches/third_party/geoip2/geoip2/src/Record/RepresentedCountry.php
new file mode 100644
index 0000000..727c034
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/RepresentedCountry.php
@@ -0,0 +1,33 @@
+military
+ * but this could expand to include other types in the future.
+ */
+class RepresentedCountry extends Country
+{
+ /**
+ * @ignore
+ *
+ * @var array
+ */
+ protected $validAttributes = [
+ 'confidence',
+ 'geonameId',
+ 'isInEuropeanUnion',
+ 'isoCode',
+ 'names',
+ 'type',
+ ];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Subdivision.php b/patches/third_party/geoip2/geoip2/src/Record/Subdivision.php
new file mode 100644
index 0000000..0e83549
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Subdivision.php
@@ -0,0 +1,44 @@
+
+ */
+ protected $validAttributes = [
+ 'confidence',
+ 'geonameId',
+ 'isoCode',
+ 'names',
+ ];
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Record/Traits.php b/patches/third_party/geoip2/geoip2/src/Record/Traits.php
new file mode 100644
index 0000000..8000d50
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Record/Traits.php
@@ -0,0 +1,158 @@
+The user type associated with the IP
+ * address. This can be one of the following values:
+ *
+ *
business
+ *
cafe
+ *
cellular
+ *
college
+ *
consumer_privacy_network
+ *
content_delivery_network
+ *
dialup
+ *
government
+ *
hosting
+ *
library
+ *
military
+ *
residential
+ *
router
+ *
school
+ *
search_engine_spider
+ *
traveler
+ *
+ *
+ * This attribute is only available from the Insights web service and the
+ * GeoIP2 Enterprise database.
+ *
+ */
+class Traits extends AbstractRecord
+{
+ /**
+ * @ignore
+ *
+ * @var array
+ */
+ 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);
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/Util.php b/patches/third_party/geoip2/geoip2/src/Util.php
new file mode 100644
index 0000000..e0a03be
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/Util.php
@@ -0,0 +1,36 @@
+ 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";
+ }
+}
diff --git a/patches/third_party/geoip2/geoip2/src/WebService/Client.php b/patches/third_party/geoip2/geoip2/src/WebService/Client.php
new file mode 100644
index 0000000..18d979d
--- /dev/null
+++ b/patches/third_party/geoip2/geoip2/src/WebService/Client.php
@@ -0,0 +1,255 @@
+
+ */
+ 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);
+ }
+}
diff --git a/patches/third_party/load_geoip2.php b/patches/third_party/load_geoip2.php
new file mode 100644
index 0000000..c8f3357
--- /dev/null
+++ b/patches/third_party/load_geoip2.php
@@ -0,0 +1,35 @@
+ $base . '/geoip2/geoip2/src/',
+ 'MaxMind\\Exception\\' => $base . '/maxmind/web-service-common/src/Exception/',
+ ];
+ foreach ($map as $prefix => $dir) {
+ if (strpos($class, $prefix) !== 0) {
+ continue;
+ }
+ $relative = str_replace('\\', '/', substr($class, strlen($prefix)));
+ $path = $dir . $relative . '.php';
+ if (is_file($path)) {
+ require_once $path;
+ }
+ return;
+ }
+ });
+})();
diff --git a/patches/third_party/maxmind-db/reader/CHANGELOG.md b/patches/third_party/maxmind-db/reader/CHANGELOG.md
new file mode 100644
index 0000000..cd63841
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/CHANGELOG.md
@@ -0,0 +1,261 @@
+CHANGELOG
+=========
+
+1.13.1 (2025-11-21)
+-------------------
+
+* First PIE release. No other changes.
+
+1.13.0 (2025-11-20)
+-------------------
+
+* A redundant `filesize()` call in the reader's constructor was removed.
+ Pull request by Pavel Djundik. GitHub #189.
+
+1.12.1 (2025-05-05)
+-------------------
+
+* The C extension now checks that the database metadata lookup was
+ successful.
+
+1.12.0 (2024-11-14)
+-------------------
+
+* Improve the error handling when the user tries to open a directory
+ with the pure PHP reader.
+* Improve the typehints on arrays in the PHPDocs.
+
+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
diff --git a/patches/third_party/maxmind-db/reader/LICENSE b/patches/third_party/maxmind-db/reader/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/patches/third_party/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.
diff --git a/patches/third_party/maxmind-db/reader/README.md b/patches/third_party/maxmind-db/reader/README.md
new file mode 100644
index 0000000..9843cf5
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/README.md
@@ -0,0 +1,214 @@
+# 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 ##
+
+### C Extension (Recommended for Performance) ###
+
+For significantly faster IP lookups, we recommend installing the C extension via
+[PIE](https://github.com/php/pie):
+
+```bash
+pie install maxmind-db/reader-ext
+```
+
+The C extension requires the [libmaxminddb](https://github.com/maxmind/libmaxminddb)
+C library. See the [installation instructions](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
+for your platform.
+
+### Pure PHP (No Compilation Required) ###
+
+If you prefer not to compile a C extension or need maximum portability, you can
+install the pure PHP implementation 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.13.1
+```
+
+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
+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 via PIE (Recommended) ###
+
+We recommend installing the extension via [PIE](https://github.com/php/pie):
+
+```bash
+pie install maxmind-db/reader-ext
+```
+
+See the [extension repository](https://github.com/maxmind/MaxMind-DB-Reader-php-ext#prerequisites)
+for prerequisites including libmaxminddb installation instructions.
+
+### Installing Extension via PECL (Legacy) ###
+
+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
+```
+
+### Installing Extension from Source ###
+
+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-2025 by MaxMind, Inc.
+
+This is free software, licensed under the Apache License, Version 2.0.
diff --git a/patches/third_party/maxmind-db/reader/autoload.php b/patches/third_party/maxmind-db/reader/autoload.php
new file mode 100644
index 0000000..fdd2f1c
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/autoload.php
@@ -0,0 +1,47 @@
+class.
+ *
+ * @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');
diff --git a/patches/third_party/maxmind-db/reader/composer.json b/patches/third_party/maxmind-db/reader/composer.json
new file mode 100644
index 0000000..f33af98
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/composer.json
@@ -0,0 +1,43 @@
+{
+ "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",
+ "maxmind-db/reader-ext": "C extension for significantly faster IP lookups (install via PIE: pie install maxmind-db/reader-ext)"
+ },
+ "conflict": {
+ "ext-maxminddb": "<1.11.1 || >=2.0.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "3.*",
+ "phpunit/phpunit": ">=8.0.0,<10.0.0",
+ "squizlabs/php_codesniffer": "4.*",
+ "phpstan/phpstan": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "MaxMind\\Db\\": "src/MaxMind/Db"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "MaxMind\\Db\\Test\\Reader\\": "tests/MaxMind/Db/Test/Reader"
+ }
+ }
+}
diff --git a/patches/third_party/maxmind-db/reader/ext/config.m4 b/patches/third_party/maxmind-db/reader/ext/config.m4
new file mode 100644
index 0000000..c09151e
--- /dev/null
+++ b/patches/third_party/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
diff --git a/patches/third_party/maxmind-db/reader/ext/config.w32 b/patches/third_party/maxmind-db/reader/ext/config.w32
new file mode 100644
index 0000000..4eb18f8
--- /dev/null
+++ b/patches/third_party/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');
+ }
+}
diff --git a/patches/third_party/maxmind-db/reader/ext/maxminddb.c b/patches/third_party/maxmind-db/reader/ext/maxminddb.c
new file mode 100644
index 0000000..b4e078b
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/ext/maxminddb.c
@@ -0,0 +1,819 @@
+/* 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
+#include
+
+#include "Zend/zend_exceptions.h"
+#include "Zend/zend_types.h"
+#include "ext/spl/spl_exceptions.h"
+#include "ext/standard/info.h"
+#include
+
+#ifdef ZTS
+#include
+#endif
+
+#define __STDC_FORMAT_MACROS
+#include
+
+#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;
+ int status =
+ MMDB_get_metadata_as_entry_data_list(mmdb_obj->mmdb, &entry_data_list);
+ if (status != MMDB_SUCCESS) {
+ zend_throw_exception_ex(maxminddb_exception_ce,
+ 0 TSRMLS_CC,
+ "Error while decoding metadata. %s",
+ MMDB_strerror(status));
+ return;
+ }
+
+ 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
diff --git a/patches/third_party/maxmind-db/reader/ext/php_maxminddb.h b/patches/third_party/maxmind-db/reader/ext/php_maxminddb.h
new file mode 100644
index 0000000..30e2461
--- /dev/null
+++ b/patches/third_party/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
+
+#ifndef PHP_MAXMINDDB_H
+#define PHP_MAXMINDDB_H 1
+#define PHP_MAXMINDDB_VERSION "1.13.1"
+#define PHP_MAXMINDDB_EXTNAME "maxminddb"
+
+extern zend_module_entry maxminddb_module_entry;
+#define phpext_maxminddb_ptr &maxminddb_module_entry
+
+#endif
diff --git a/patches/third_party/maxmind-db/reader/ext/tests/001-load.phpt b/patches/third_party/maxmind-db/reader/ext/tests/001-load.phpt
new file mode 100644
index 0000000..09810ee
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/ext/tests/001-load.phpt
@@ -0,0 +1,12 @@
+--TEST--
+Check for maxminddb presence
+--SKIPIF--
+
+--FILE--
+
+--EXPECT--
+maxminddb extension is available
diff --git a/patches/third_party/maxmind-db/reader/ext/tests/002-final.phpt b/patches/third_party/maxmind-db/reader/ext/tests/002-final.phpt
new file mode 100644
index 0000000..d91b7d0
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/ext/tests/002-final.phpt
@@ -0,0 +1,13 @@
+--TEST--
+Check that Reader class is not final
+--SKIPIF--
+
+--FILE--
+isFinal());
+?>
+--EXPECT--
+bool(false)
diff --git a/patches/third_party/maxmind-db/reader/ext/tests/003-open-basedir.phpt b/patches/third_party/maxmind-db/reader/ext/tests/003-open-basedir.phpt
new file mode 100644
index 0000000..26e9781
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/ext/tests/003-open-basedir.phpt
@@ -0,0 +1,12 @@
+--TEST--
+openbase_dir is followed
+--INI--
+open_basedir=/--dne--
+--FILE--
+
+--EXPECTREGEX--
+.*open_basedir restriction in effect.*
diff --git a/patches/third_party/maxmind-db/reader/package.xml b/patches/third_party/maxmind-db/reader/package.xml
new file mode 100644
index 0000000..15db600
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/package.xml
@@ -0,0 +1,61 @@
+
+
+
+ maxminddb
+ pecl.php.net
+ Reader for the MaxMind DB file format
+ 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).
+
+ Greg Oschwald
+ oschwald
+ goschwald@maxmind.com
+ yes
+
+ 2025-11-21
+
+ 1.13.1
+ 1.13.1
+
+
+ stable
+ stable
+
+ Apache License 2.0
+ * First PIE release. No other changes.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 7.2.0
+
+
+ 1.10.0
+
+
+
+ maxminddb
+
+
diff --git a/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader.php b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader.php
new file mode 100644
index 0000000..a0b28b4
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader.php
@@ -0,0 +1,404 @@
+
+ */
+ 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())
+ );
+ }
+
+ if (is_dir($database)) {
+ // This matches the error that the C extension throws.
+ throw new InvalidDatabaseException(
+ "Error opening database file ($database). Is this a valid MaxMind DB file?"
+ );
+ }
+
+ $fileHandle = @fopen($database, 'rb');
+ if ($fileHandle === false) {
+ throw new \InvalidArgumentException(
+ "The file \"$database\" does not exist or is not readable."
+ );
+ }
+ $this->fileHandle = $fileHandle;
+
+ $fstat = fstat($fileHandle);
+ if ($fstat === false) {
+ throw new \UnexpectedValueException(
+ "Error determining the size of \"$database\"."
+ );
+ }
+ $this->fileSize = $fstat['size'];
+
+ $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{0:mixed, 1:int} 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];
+ }
+
+ /**
+ * @return array{0:int, 1:int}
+ */
+ 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;
+ $fileSize = $this->fileSize;
+ $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);
+ }
+}
diff --git a/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php
new file mode 100644
index 0000000..1bb6731
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php
@@ -0,0 +1,452 @@
+fileStream = $fileStream;
+ $this->pointerBase = $pointerBase;
+
+ $this->pointerTestHack = $pointerTestHack;
+
+ $this->switchByteOrder = $this->isPlatformLittleEndian();
+ }
+
+ /**
+ * @return array
+ */
+ 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
+ *
+ * @return array{0:mixed, 1:int}
+ */
+ 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)"
+ );
+ }
+ }
+
+ /**
+ * @return array{0:array, 1:int}
+ */
+ 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;
+ }
+
+ /**
+ * @return array{0:array, 1: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];
+ }
+
+ /**
+ * @return array{0:int, 1:int}
+ */
+ 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;
+ }
+
+ /**
+ * @return array{0:int, 1:int}
+ */
+ 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);
+ }
+}
diff --git a/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php
new file mode 100644
index 0000000..b1da1ed
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php
@@ -0,0 +1,11 @@
+
+ */
+ 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;
+
+ /**
+ * @param array $metadata
+ */
+ public function __construct(array $metadata)
+ {
+ if (\func_num_args() !== 1) {
+ throw new \ArgumentCountError(
+ \sprintf('%s() expects exactly 1 parameter, %d given', __METHOD__, \func_num_args())
+ );
+ }
+
+ $this->binaryFormatMajorVersion
+ = $metadata['binary_format_major_version'];
+ $this->binaryFormatMinorVersion
+ = $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;
+ }
+}
diff --git a/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php
new file mode 100644
index 0000000..c2c3212
--- /dev/null
+++ b/patches/third_party/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php
@@ -0,0 +1,33 @@
+ $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'
+ );
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/CHANGELOG.md b/patches/third_party/maxmind/web-service-common/CHANGELOG.md
new file mode 100644
index 0000000..667ccc8
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/CHANGELOG.md
@@ -0,0 +1,129 @@
+CHANGELOG
+=========
+
+0.11.1 (2026-01-13)
+-------------------
+
+* Removed deprecated `curl_close()` calls. These were no-ops since PHP 8.0
+ and are deprecated in PHP 8.5. Pull request by Sam Reed. GitHub #105.
+* Added native property and return types. PHPStan level increased from 6 to 8.
+
+0.11.0 (2025-11-20)
+-------------------
+
+* Type hints have been improved.
+
+0.10.0 (2024-11-14)
+-------------------
+
+* PHP 8.1 or greater is now required.
+* Type hints for PHPStan have been improved.
+
+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.
diff --git a/patches/third_party/maxmind/web-service-common/LICENSE b/patches/third_party/maxmind/web-service-common/LICENSE
new file mode 100644
index 0000000..62589ed
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ https://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
+
+ https://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.
diff --git a/patches/third_party/maxmind/web-service-common/README.md b/patches/third_party/maxmind/web-service-common/README.md
new file mode 100644
index 0000000..26092c5
--- /dev/null
+++ b/patches/third_party/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 8.1 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-12
+style guidelines. Please include unit tests whenever possible.
+
+## Versioning ##
+
+This API uses [Semantic Versioning](https://semver.org/).
+
+## Copyright and License ##
+
+This software is Copyright (c) 2015-2026 by MaxMind, Inc.
+
+This is free software, licensed under the Apache License, Version 2.0.
diff --git a/patches/third_party/maxmind/web-service-common/composer.json b/patches/third_party/maxmind/web-service-common/composer.json
new file mode 100644
index 0000000..c9c7bd2
--- /dev/null
+++ b/patches/third_party/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": ">=8.1",
+ "composer/ca-bundle": "^1.0.3",
+ "ext-curl": "*",
+ "ext-json": "*"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "3.*",
+ "phpunit/phpunit": "^10.0",
+ "squizlabs/php_codesniffer": "4.*",
+ "phpstan/phpstan": "*"
+ },
+ "autoload": {
+ "psr-4": {
+ "MaxMind\\Exception\\": "src/Exception",
+ "MaxMind\\WebService\\": "src/WebService"
+ }
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/dev-bin/release.sh b/patches/third_party/maxmind/web-service-common/dev-bin/release.sh
new file mode 100755
index 0000000..552998d
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/dev-bin/release.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+set -eu -o pipefail
+
+# Pre-flight checks - verify all required tools are available and configured
+# before making any changes to the repository
+
+check_command() {
+ if ! command -v "$1" &>/dev/null; then
+ echo "Error: $1 is not installed or not in PATH"
+ exit 1
+ fi
+}
+
+# Verify gh CLI is authenticated
+if ! gh auth status &>/dev/null; then
+ echo "Error: gh CLI is not authenticated. Run 'gh auth login' first."
+ exit 1
+fi
+
+# Verify we can access this repository via gh
+if ! gh repo view --json name &>/dev/null; then
+ echo "Error: Cannot access repository via gh. Check your authentication and repository access."
+ exit 1
+fi
+
+# Verify git can connect to the remote (catches SSH key issues, etc.)
+if ! git ls-remote origin &>/dev/null; then
+ echo "Error: Cannot connect to git remote. Check your git credentials/SSH keys."
+ exit 1
+fi
+
+check_command php
+
+# Check that we're not on the main branch
+current_branch=$(git branch --show-current)
+if [ "$current_branch" = "main" ]; then
+ echo "Error: Releases should not be done directly on the main branch."
+ echo "Please create a release branch and run this script from there."
+ exit 1
+fi
+
+# Fetch latest changes and check that we're not behind origin/main
+echo "Fetching from origin..."
+git fetch origin
+
+if ! git merge-base --is-ancestor origin/main HEAD; then
+ echo "Error: Current branch is behind origin/main."
+ echo "Please merge or rebase with origin/main before releasing."
+ exit 1
+fi
+
+changelog=$(cat CHANGELOG.md)
+
+regex='
+([0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-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[3]}"
+notes="$(echo "${BASH_REMATCH[4]}" | sed -n -E '/^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-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 -r -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"
diff --git a/patches/third_party/maxmind/web-service-common/phpstan.neon b/patches/third_party/maxmind/web-service-common/phpstan.neon
new file mode 100644
index 0000000..fee38d3
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/phpstan.neon
@@ -0,0 +1,6 @@
+parameters:
+ level: 8
+ paths:
+ - src
+ - tests
+
diff --git a/patches/third_party/maxmind/web-service-common/src/Exception/AuthenticationException.php b/patches/third_party/maxmind/web-service-common/src/Exception/AuthenticationException.php
new file mode 100644
index 0000000..5b016ce
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/Exception/AuthenticationException.php
@@ -0,0 +1,12 @@
+uri = $uri;
+ parent::__construct($message, $httpStatus, $previous);
+ }
+
+ public function getUri(): string
+ {
+ return $this->uri;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->getCode();
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/src/Exception/InsufficientFundsException.php b/patches/third_party/maxmind/web-service-common/src/Exception/InsufficientFundsException.php
new file mode 100644
index 0000000..2831456
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/Exception/InsufficientFundsException.php
@@ -0,0 +1,12 @@
+error = $error;
+ parent::__construct($message, $httpStatus, $uri, $previous);
+ }
+
+ public function getErrorCode(): string
+ {
+ return $this->error;
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php b/patches/third_party/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php
new file mode 100644
index 0000000..97af06c
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php
@@ -0,0 +1,12 @@
+ $options an array of options. Possible keys:
+ * * `host` - The host to use when connecting to the web service.
+ * * `useHttps` - Set to false to disable HTTPS.
+ * * `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 = $options['httpRequestFactory'] ?? new RequestFactory();
+ $this->host = $options['host'] ?? 'api.maxmind.com';
+ $this->useHttps = $options['useHttps'] ?? true;
+ $this->userAgentPrefix = isset($options['userAgent']) ? $options['userAgent'] . ' ' : '';
+ $this->caBundle = $options['caBundle'] ?? $this->getCaBundle();
+ $this->connectTimeout = $options['connectTimeout'] ?? null;
+ $this->timeout = $options['timeout'] ?? null;
+ $this->proxy = $options['proxy'] ?? null;
+ }
+
+ /**
+ * @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
+ );
+ }
+
+ /**
+ * @return array|null
+ */
+ 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();
+ if ($curlVersion === false) {
+ throw new \RuntimeException('curl_version() returned false');
+ }
+
+ return $this->userAgentPrefix . 'MaxMind-WS-API/' . self::VERSION . ' PHP/' . \PHP_VERSION
+ . ' curl/' . $curlVersion['version'];
+ }
+
+ /**
+ * @param array $headers
+ */
+ private function createRequest(string $path, array $headers = []): Http\Request
+ {
+ $headers = [
+ ...$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 || !str_contains($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();
+ if ($curlVersion === false) {
+ throw new \RuntimeException('curl_version() returned false');
+ }
+
+ // 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 (str_starts_with($cert, '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;
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/src/WebService/Http/CurlRequest.php b/patches/third_party/maxmind/web-service-common/src/WebService/Http/CurlRequest.php
new file mode 100644
index 0000000..7318db5
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/WebService/Http/CurlRequest.php
@@ -0,0 +1,146 @@
+,
+ * proxy: string|null,
+ * timeout: float|int,
+ * userAgent: string
+ * }
+ */
+ private readonly array $options;
+
+ /**
+ * @param array{
+ * caBundle?: string,
+ * connectTimeout: float|int,
+ * curlHandle: \CurlHandle,
+ * headers: array,
+ * proxy: string|null,
+ * timeout: float|int,
+ * userAgent: string
+ * } $options
+ */
+ public function __construct(string $url, array $options)
+ {
+ $this->url = $url;
+ $this->options = $options;
+ $this->ch = $options['curlHandle'];
+ }
+
+ /**
+ * @throws HttpException
+ *
+ * @return array{0:int, 1:string|null, 2:string|null}
+ */
+ 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);
+ }
+
+ /**
+ * @return array{0:int, 1:string|null, 2:string|null}
+ */
+ public function get(): array
+ {
+ $curl = $this->createCurl();
+
+ curl_setopt($curl, \CURLOPT_HTTPGET, true);
+
+ return $this->execute($curl);
+ }
+
+ private function createCurl(): \CurlHandle
+ {
+ 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'];
+ if ($this->options['proxy'] !== null) {
+ $opts[\CURLOPT_PROXY] = $this->options['proxy'];
+ }
+
+ $connectTimeout = $this->options['connectTimeout'];
+ $opts[\CURLOPT_CONNECTTIMEOUT_MS] = (int) ceil($connectTimeout * 1000);
+
+ $timeout = $this->options['timeout'];
+ $opts[\CURLOPT_TIMEOUT_MS] = (int) ceil($timeout * 1000);
+
+ // @phpstan-ignore argument.type (PHPStan's curl stubs require non-empty-string for URL/userAgent)
+ curl_setopt_array($this->ch, $opts);
+
+ return $this->ch;
+ }
+
+ /**
+ * @throws HttpException
+ *
+ * @return array{0:int, 1:string|null, 2:string|null}
+ */
+ private function execute(\CurlHandle $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,
+ // curl_exec returns false on failure, but we've already checked
+ // for errors above and thrown an exception. Cast to string to
+ // satisfy PHPStan since curl_exec technically returns string|bool.
+ $body === false ? null : (string) $body,
+ ];
+ }
+}
diff --git a/patches/third_party/maxmind/web-service-common/src/WebService/Http/Request.php b/patches/third_party/maxmind/web-service-common/src/WebService/Http/Request.php
new file mode 100644
index 0000000..9a03e5f
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/WebService/Http/Request.php
@@ -0,0 +1,28 @@
+ $options
+ */
+ public function __construct(string $url, array $options);
+
+ /**
+ * @return array{0:int, 1:string|null, 2:string|null}
+ */
+ public function post(string $body): array;
+
+ /**
+ * @return array{0:int, 1:string|null, 2:string|null}
+ */
+ public function get(): array;
+}
diff --git a/patches/third_party/maxmind/web-service-common/src/WebService/Http/RequestFactory.php b/patches/third_party/maxmind/web-service-common/src/WebService/Http/RequestFactory.php
new file mode 100644
index 0000000..d0a19f6
--- /dev/null
+++ b/patches/third_party/maxmind/web-service-common/src/WebService/Http/RequestFactory.php
@@ -0,0 +1,44 @@
+ch === null) {
+ $ch = curl_init();
+ if ($ch === false) {
+ throw new \RuntimeException('Unable to initialize cURL handle');
+ }
+ $this->ch = $ch;
+ }
+
+ return $this->ch;
+ }
+
+ /**
+ * @param array $options
+ */
+ public function request(string $url, array $options): Request
+ {
+ $options['curlHandle'] = $this->getCurlHandle();
+
+ // @phpstan-ignore argument.type (options array is built dynamically by Client)
+ return new CurlRequest($url, $options);
+ }
+}
diff --git a/public/assets/js/backend/domain.js b/public/assets/js/backend/domain.js
index 5b31450..9912702 100755
--- a/public/assets/js/backend/domain.js
+++ b/public/assets/js/backend/domain.js
@@ -1,10 +1,25 @@
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
+ /**
+ * 列表 Ajax 仅保留 addtabs 参数。
+ * 切勿拼接 location.search 中的 sort/filter/op 等,否则从其它模块跳入 /domain/index 时会带错字段导致列表为空。
+ */
+ var buildListQuery = function () {
+ var rawSearch = location.search || '';
+ if (!rawSearch) {
+ return '';
+ }
+ var addtabsMatch = rawSearch.match(/(?:[?&])addtabs=[^&]*/);
+ return addtabsMatch ? ('?' + addtabsMatch[0].replace(/^[?&]/, '')) : '';
+ };
+
var Controller = {
index: function () {
+ var listQuery = buildListQuery();
+ var indexUrl = 'domain/index' + listQuery;
Table.api.init({
extend: {
- index_url: 'domain/index' + location.search,
+ index_url: indexUrl,
add_url: 'domain/add',
edit_url: '',
del_url: '',
@@ -25,7 +40,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
columns: [
[
{checkbox: true},
- {field: 'domain', title: __('Domain'), operate: 'LIKE'},
+ {field: 'domain', title: __('Domain'), operate: 'LIKE', renderDefault: false},
{field: 'full_url', title: __('Full_url'), operate: false, formatter: Table.api.formatter.url},
{field: 'zone_status', title: __('Zone_status'), searchList: Config.zoneStatusList, formatter: Table.api.formatter.status},
{field: 'ns_status', title: __('Ns_status'), searchList: Config.nsStatusList, formatter: Table.api.formatter.status},
diff --git a/public/assets/js/backend/split/link.js b/public/assets/js/backend/split/link.js
index a8de5ab..c8e78ff 100644
--- a/public/assets/js/backend/split/link.js
+++ b/public/assets/js/backend/split/link.js
@@ -37,8 +37,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
field: 'auto_reply_text',
title: __('Reply statements column'),
operate: false,
- class: 'autocontent',
- hover: true,
formatter: Controller.api.formatter.autoReplyText
},
{field: 'ip_protect', title: __('Ip_protect'), searchList: Config.ipProtectList, formatter: Table.api.formatter.status},
@@ -58,6 +56,14 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
icon: 'fa fa-commenting-o',
classname: 'btn btn-warning btn-xs btn-split-autoreply',
url: 'javascript:;'
+ },
+ {
+ name: 'pixel',
+ text: __('Pixel config'),
+ title: __('Pixel config'),
+ icon: 'fa fa-bullseye',
+ classname: 'btn btn-info btn-xs btn-split-pixel',
+ url: 'javascript:;'
}
],
formatter: Table.api.formatter.operate
@@ -97,6 +103,20 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.openAutoReplyModal(row);
});
+ table.on('click', '.btn-split-pixel', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ e.stopImmediatePropagation();
+ var rowIndex = $(this).data('row-index');
+ var row = Table.api.getrowbyindex(table, rowIndex);
+ if (!row || !row.id) {
+ return false;
+ }
+ Controller.api.openPixelModal(row);
+ });
+
+ Controller.api.bindAutoReplyPreviewTips(table);
+
Table.api.bindevent(table);
},
add: function () {
@@ -107,8 +127,45 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.bindevent();
},
api: {
+ /**
+ * 规范化后台跨模块跳转 URL,避免在 split.link 页面内相对解析成 split.link/domain
+ *
+ * @param {string} url API 返回的地址
+ * @param {string} fallback 相对 admin 模块根路径,如 domain / domain/add
+ * @return {string}
+ */
+ normalizeAdminRouteUrl: function (url, fallback) {
+ fallback = fallback || 'domain';
+ url = $.trim(url || '');
+ if (url === '' || /split\.link\/domain/i.test(url)) {
+ return fallback;
+ }
+ var modulePrefix = (Config.moduleurl || '').replace(/\/+$/, '');
+ if (url.indexOf('://') !== -1) {
+ try {
+ var parsed = new URL(url, window.location.origin);
+ var path = (parsed.pathname || '').replace(/\/+$/, '');
+ if (modulePrefix && path.indexOf(modulePrefix) === 0) {
+ path = path.slice(modulePrefix.length);
+ }
+ path = path.replace(/^\/+/, '');
+ return /^split\.link\/domain/i.test(path) ? fallback : (path || fallback);
+ } catch (e) {
+ return fallback;
+ }
+ }
+ if (url.charAt(0) === '/') {
+ url = url.replace(/^\/+/, '');
+ var moduleKey = modulePrefix.replace(/^\/+/, '');
+ if (moduleKey && url.indexOf(moduleKey + '/') === 0) {
+ url = url.slice(moduleKey.length + 1);
+ }
+ }
+ return /^split\.link\/domain/i.test(url) ? fallback : url;
+ },
/** 弹窗样式(仅注入一次) */
modalStyleInjected: false,
+ pixelModalStyleInjected: false,
injectModalStyles: function () {
if (Controller.api.modalStyleInjected) {
return;
@@ -140,18 +197,63 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
].join('');
$('').text(css).appendTo('head');
},
+ injectPixelModalStyles: function () {
+ if (Controller.api.pixelModalStyleInjected) {
+ return;
+ }
+ Controller.api.pixelModalStyleInjected = true;
+ var css = [
+ '.split-pixel-layer .layui-layer-content{padding:0;overflow:hidden;max-height:calc(86vh - 108px);}',
+ '.split-pixel-layer .layui-layer-btn{border-top:1px solid #e8e8e8;background:#fafafa;}',
+ '.split-pixel-modal{padding:18px 22px 14px;box-sizing:border-box;display:flex;flex-direction:column;min-height:480px;height:calc(86vh - 108px);max-height:720px;}',
+ '.split-pixel-modal .split-pixel-tip{margin:0 0 14px;padding:10px 14px;background:#f0f7ff;border-left:3px solid #337ab7;border-radius:0 4px 4px 0;color:#555;font-size:13px;line-height:1.65;}',
+ '.split-pixel-modal .split-pixel-tabs{margin-bottom:0;border-bottom:1px solid #ddd;}',
+ '.split-pixel-modal .split-pixel-tabs>li>a{padding:9px 18px;font-weight:600;color:#666;}',
+ '.split-pixel-modal .split-pixel-tabs>li.active>a{color:#337ab7;border-bottom-color:#fff;}',
+ '.split-pixel-modal .split-pixel-tab-content{flex:1;display:flex;flex-direction:column;min-height:0;padding-top:14px;}',
+ '.split-pixel-modal .split-pixel-tab-content>.tab-pane{display:none;flex:1;flex-direction:column;min-height:0;}',
+ '.split-pixel-modal .split-pixel-tab-content>.tab-pane.active{display:flex;}',
+ '.split-pixel-modal .split-pixel-list{flex:1;display:flex;flex-direction:column;min-height:0;}',
+ '.split-pixel-modal .split-pixel-toolbar{margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;}',
+ '.split-pixel-modal .split-pixel-toolbar .btn-add-pixel-row{font-weight:600;padding:6px 14px;}',
+ '.split-pixel-modal .split-pixel-table-wrap{flex:1;min-height:320px;overflow:auto;border:1px solid #dce3eb;border-radius:6px;background:#fff;box-shadow:inset 0 1px 2px rgba(0,0,0,.03);}',
+ '.split-pixel-modal .split-pixel-table{margin-bottom:0;font-size:13px;table-layout:auto;width:100%;}',
+ '.split-pixel-modal .split-pixel-table thead th{background:linear-gradient(180deg,#f8fafc 0%,#eef2f6 100%);white-space:nowrap;vertical-align:middle;text-align:center;font-weight:600;color:#444;border-bottom:2px solid #dce3eb;padding:10px 8px;position:sticky;top:0;z-index:3;box-shadow:0 1px 0 #dce3eb;}',
+ '.split-pixel-modal .split-pixel-table tbody td{vertical-align:middle;padding:10px 8px;border-color:#edf1f5;}',
+ '.split-pixel-modal .split-pixel-table tbody tr.split-pixel-row:hover{background:#f7fbff;}',
+ '.split-pixel-modal .split-pixel-table tbody tr.split-pixel-row:nth-child(even){background:#fbfcfd;}',
+ '.split-pixel-modal .split-pixel-table tbody tr.split-pixel-row:nth-child(even):hover{background:#f7fbff;}',
+ '.split-pixel-modal .split-pixel-table .form-control{min-width:0;height:32px;line-height:1.42857143;padding:6px 10px;border-radius:4px;}',
+ '.split-pixel-modal .split-pixel-table .pixel-id{min-width:130px;}',
+ '.split-pixel-modal .split-pixel-table .pixel-access-token{min-width:150px;}',
+ '.split-pixel-modal .split-pixel-table .pixel-test-code{min-width:100px;}',
+ '.split-pixel-modal .split-pixel-table th.pixel-event-col,.split-pixel-modal .split-pixel-table td.pixel-event-cell{min-width:148px;width:148px;}',
+ '.split-pixel-modal .split-pixel-table .pixel-event{width:100%;min-width:132px;max-width:none;padding-right:28px;text-overflow:clip;overflow:visible;white-space:nowrap;cursor:pointer;}',
+ '.split-pixel-modal .split-pixel-empty-row td{padding:48px 16px;color:#999;text-align:center;font-size:13px;background:#fafbfc;}',
+ '.split-pixel-modal .split-pixel-sort-group{width:118px;margin:0 auto;}',
+ '.split-pixel-modal .split-pixel-sort-group .form-control{text-align:center;padding-left:4px;padding-right:4px;}',
+ '.split-pixel-modal .pixel-switch-wrap{text-align:center;}',
+ '.split-pixel-modal .pixel-switch-wrap input[type=checkbox]{width:17px;height:17px;margin:0;cursor:pointer;vertical-align:middle;}',
+ '.split-pixel-modal .pixel-row-index{display:inline-block;min-width:26px;height:26px;line-height:26px;border-radius:13px;background:#e8eef5;color:#4a6785;font-weight:600;font-size:12px;}',
+ '.split-pixel-modal .btn-pixel-row-remove{padding:4px 8px;border-radius:4px;}',
+ '.split-pixel-modal .col-token{min-width:160px;}'
+ ].join('');
+ $('').text(css).appendTo('head');
+ },
formatter: {
/**
- * 回复语:单元格内省略,悬停显示完整内容
+ * 回复语:列表最多 50 字 + ...,悬停保留换行显示全文
*/
autoReplyText: function (value, row, index) {
- value = value == null ? '' : value.toString();
- if (value === '') {
+ var full = (row.auto_reply != null && row.auto_reply !== '') ? String(row.auto_reply) : '';
+ if (full === '') {
return '-';
}
- var safe = Fast.api.escape(value);
- return "
" + safe + "
";
+ var preview = value != null && value !== '' ? String(value) : full;
+ var safePreview = Fast.api.escape(preview);
+ var encFull = encodeURIComponent(full);
+ return ''
+ + safePreview + '';
},
/**
* 分流链接列:链接样式 + COPY 图标,点击链接打开弹窗
@@ -173,6 +275,39 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
Controller.api.renderCopyModal(linkCode, data);
});
},
+ /** 回复语列悬停提示(保留换行) */
+ autoReplyTipIndex: null,
+ bindAutoReplyPreviewTips: function (table) {
+ table.off('mouseenter.splitAutoReply mouseleave.splitAutoReply')
+ .on('mouseenter.splitAutoReply', '.split-auto-reply-preview', function () {
+ var enc = $(this).attr('data-full');
+ if (!enc) {
+ return;
+ }
+ var full = '';
+ try {
+ full = decodeURIComponent(enc);
+ } catch (e) {
+ return;
+ }
+ var html = '