From 3fad712a91a8a8f6f6f904aff3d895e3b06b24c7 Mon Sep 17 00:00:00 2001 From: Nick Pope Date: Tue, 15 Oct 2024 09:48:59 +0100 Subject: [PATCH] Fixed #35841 -- Restored support for DB-IP databases in GeoIP2. Thanks Felix Farquharson for the report and Claude Paroz for the review. Regression in 40b5b1596f7505416bd30d5d7582b5a9004ea7d5. Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> --- django/contrib/gis/geoip2.py | 28 +++++++++++++--- docs/releases/5.1.3.txt | 3 ++ tests/gis_tests/data/geoip2/README.md | 14 ++++++++ .../data/geoip2/dbip-city-lite-test.mmdb | Bin 0 -> 1481 bytes .../data/geoip2/dbip-country-lite-test.mmdb | Bin 0 -> 1314 bytes tests/gis_tests/test_geoip2.py | 30 ++++++++++++++++-- 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb create mode 100644 tests/gis_tests/data/geoip2/dbip-country-lite-test.mmdb diff --git a/django/contrib/gis/geoip2.py b/django/contrib/gis/geoip2.py index f5058c1c05..a5fe429b89 100644 --- a/django/contrib/gis/geoip2.py +++ b/django/contrib/gis/geoip2.py @@ -34,6 +34,18 @@ else: __all__ += ["GeoIP2", "GeoIP2Exception"] +# These are the values stored in the `database_type` field of the metadata. +# See https://maxmind.github.io/MaxMind-DB/#database_type for details. +SUPPORTED_DATABASE_TYPES = { + "DBIP-City-Lite", + "DBIP-Country-Lite", + "GeoIP2-City", + "GeoIP2-Country", + "GeoLite2-City", + "GeoLite2-Country", +} + + class GeoIP2Exception(Exception): pass @@ -106,7 +118,7 @@ class GeoIP2: ) database_type = self._metadata.database_type - if not database_type.endswith(("City", "Country")): + if database_type not in SUPPORTED_DATABASE_TYPES: raise GeoIP2Exception(f"Unable to handle database edition: {database_type}") def __del__(self): @@ -123,6 +135,14 @@ class GeoIP2: def _metadata(self): return self._reader.metadata() + @cached_property + def is_city(self): + return "City" in self._metadata.database_type + + @cached_property + def is_country(self): + return "Country" in self._metadata.database_type + def _query(self, query, *, require_city=False): if not isinstance(query, (str, ipaddress.IPv4Address, ipaddress.IPv6Address)): raise TypeError( @@ -130,9 +150,7 @@ class GeoIP2: "IPv6Address, not type %s" % type(query).__name__, ) - is_city = self._metadata.database_type.endswith("City") - - if require_city and not is_city: + if require_city and not self.is_city: raise GeoIP2Exception(f"Invalid GeoIP city data file: {self._path}") try: @@ -141,7 +159,7 @@ class GeoIP2: # GeoIP2 only takes IP addresses, so try to resolve a hostname. query = socket.gethostbyname(query) - function = self._reader.city if is_city else self._reader.country + function = self._reader.city if self.is_city else self._reader.country return function(query) def city(self, query): diff --git a/docs/releases/5.1.3.txt b/docs/releases/5.1.3.txt index e3c62072b5..0dd5b42cb8 100644 --- a/docs/releases/5.1.3.txt +++ b/docs/releases/5.1.3.txt @@ -14,3 +14,6 @@ Bugfixes :class:`~django.core.validators.DomainNameValidator` accepted any input value that contained a valid domain name, rather than only input values that were a valid domain name (:ticket:`35845`). + +* Fixed a regression in Django 5.1 that prevented the use of DB-IP databases + with :class:`~django.contrib.gis.geoip2.GeoIP2` (:ticket:`35841`). diff --git a/tests/gis_tests/data/geoip2/README.md b/tests/gis_tests/data/geoip2/README.md index 36328671b2..f2a703b457 100644 --- a/tests/gis_tests/data/geoip2/README.md +++ b/tests/gis_tests/data/geoip2/README.md @@ -12,3 +12,17 @@ Updates can be found in [this repository][1]. [0]: https://github.com/maxmind/MaxMind-DB/blob/main/LICENSE-MIT [1]: https://github.com/maxmind/MaxMind-DB/tree/main/test-data + +# DB-IP Lite Test Databases + +The following test databases are provided under [this license][2]: + +- `dbip-city-lite-test.mmdb` +- `dbip-country-lite-test.mmdb` + +They have been modified to strip them down to a minimal dataset for testing. + +Updates can be found at [this download page][3] from DB-IP. + +[2]: https://creativecommons.org/licenses/by/4.0/ +[3]: https://db-ip.com/db/lite.php diff --git a/tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb b/tests/gis_tests/data/geoip2/dbip-city-lite-test.mmdb new file mode 100644 index 0000000000000000000000000000000000000000..5f0d657c841ad687085c88a037010b02816e54c8 GIT binary patch literal 1481 zcmZ9}&2tn*7{~D@Mj{{q1$h&2KvV+mk_3qIB9dj7-OVN;Auph))VhS8)?=#x1xNU&C$qI&Q~K zOyCYoVhVR+8Z(&1F3e#!_FymW!anR5;*b!#$pbiuLpY3ka4)`rZ{j|D3*W|f(8hPs z!92R?p~U?-Aw)rlB3V5cIY2mwB`l*B!VsdW3gJB={0Ad}5FxpWBRGn`VN_Kk9L6K~ zK7N2l@k1QLkMJ0NjGy4A_!&mco*c31W9(nF2T@}_QQ`#xZU@{?ff49+|(bnpLqT`Lq zQmG~D8XYPvb@*4(HJ(hQhPn#U)GKV|6+8Fbj+z$2YMQi~cc!YosmQczsyg&*e_I{J z&g7C?W6}Je3T&lq$pS~)RjrH`O&Jv!n{Cqxw&)$wDwLj;Q@Y@pa;9WlCybWf$&{jZ zSoLIad-nJK#eX!}*^<$PsiEt?W{&Op^Pg*JEyGdcAM9<9x3zDKw{^6}Ti3O3=nkrR zPmQP`+9;UTILS%rlrEGU?OnThzsEV$qqG-yP5IuwywZ+ev-can?1XmNIbeKyMEdpG z@rM7;D*bTwYA#<@CC`=>;}#nm3e97edORm|@=hS_uvU@1$wamS2_Xp}D5!|KY_f-Bft-!I8=4J!@0}~T)u-?Q z9ACg225-Fb6E z;{sfWi*PY6!4h1G%WyfC;tDLomADF5;~HFx8m`0jxB)lfCftl$upGDIHr$RoFn|?U ziB(vQHCT%~aTo5!J-8S5;eI@T2k{Ud#v^zX>oAD*7{V|%U<4a6iZP616EZ2_ z6CxwTaqj(;u-A1vv>~AqmJEZU;>k9qQoBbf3TDgX|nZuq?gc#{rCYD z+L*-)=%9-p=5PQ9aR`TT1TUgr>Se+eyo#fE4X@)3yotB)Hr~OzLcAl~6XHJM0Y1b> z_!ytyQ+$Tc@ddubSNIy=;9H#T>a^mYXEK_!m1ilbyvb0~Hf1o}5lcy185yZtX1eaw zSHB=3!czn8L{O@RaL%!_GLp1&%5#RlX(j2pP}!GJe^I;T>Xy_lw~VX)M&Z3 z&e@TmRz9Iswsa`VlcpB8RLZn75hcAr+v#hnsi>~0s_@^+>e`Q=e>59I&6YANl6K~M zSHe<;GpzU6PR8)`jL~a5`hav?{|n>!|Ib?L@9g_{B4_oRy3E?ibU}WqxVxj-G(00= zxKj6qv$8D|jJ5{qr>_%e@+*s*(oH&6*7Faax^ibI7>Kq`)u9EnXshPg+OOp?EBmL4 n!f3ytQaK|f-Mj*SS4{cs^zZGr>C;%;kxAPzb=Mk^") - self.assertIs(g._metadata.database_type.endswith("Country"), True) + self.assertIs(g.is_city, False) + self.assertIs(g.is_country, True) for query in self.query_values: with self.subTest(query=query): self.assertEqual(g.country(query), self.expected_country) @@ -137,7 +138,8 @@ class GeoLite2Test(SimpleTestCase): def test_country_using_city_database(self): g = GeoIP2(country="") - self.assertIs(g._metadata.database_type.endswith("City"), True) + self.assertIs(g.is_city, True) + self.assertIs(g.is_country, False) for query in self.query_values: with self.subTest(query=query): self.assertEqual(g.country(query), self.expected_country) @@ -150,7 +152,8 @@ class GeoLite2Test(SimpleTestCase): def test_city(self): g = GeoIP2(country="") - self.assertIs(g._metadata.database_type.endswith("City"), True) + self.assertIs(g.is_city, True) + self.assertIs(g.is_country, False) for query in self.query_values: with self.subTest(query=query): self.assertEqual(g.city(query), self.expected_city) @@ -224,6 +227,27 @@ class GeoIP2Test(GeoLite2Test): """Non-free GeoIP2 databases are supported.""" +@skipUnless(HAS_GEOIP2, "GeoIP2 is required.") +@override_settings( + GEOIP_CITY="dbip-city-lite-test.mmdb", + GEOIP_COUNTRY="dbip-country-lite-test.mmdb", +) +class DBIPLiteTest(GeoLite2Test): + """DB-IP Lite databases are supported.""" + + expected_city = GeoLite2Test.expected_city | { + "accuracy_radius": None, + "city": "London (Shadwell)", + "latitude": 51.5181, + "longitude": -0.0714189, + "postal_code": None, + "region_code": None, + "time_zone": None, + # Kept for backward compatibility. + "region": None, + } + + @skipUnless(HAS_GEOIP2, "GeoIP2 is required.") class ErrorTest(SimpleTestCase): def test_missing_path(self):