diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e79bb3f..210994c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ CHANGELOG ========= -3.3.1 (unreleased) +3.4.0 (unreleased) ------------------ +* A new `residential` property has been added to `GeoIp2\Record\Anonymizer`. + This property is an instance of the new `GeoIp2\Record\AnonymizerFeed` + class and provides residential proxy data for the network, including + `confidence`, `networkLastSeen`, and `providerName`. This may be the only + property with data even when the other anonymizer properties are unset. + This data is available from the GeoIP Insights web service. * Updated the `GeoIp2\Model\City` constructor to handle an empty `subdivisions` array. This provides compatibility with some third-party databases that publish empty subdivisions arrays. Pull request by Jarek diff --git a/src/Record/Anonymizer.php b/src/Record/Anonymizer.php index 79d0f69a..3a8ee59b 100644 --- a/src/Record/Anonymizer.php +++ b/src/Record/Anonymizer.php @@ -70,6 +70,14 @@ class Anonymizer implements \JsonSerializable */ public readonly ?string $providerName; + /** + * @var AnonymizerFeed Residential proxy data for the network. This may be the only + * property with data even when the other anonymizer properties are + * unset. This attribute is only available from the GeoIP Insights + * web service. + */ + public readonly AnonymizerFeed $residential; + /** * @ignore * @@ -86,6 +94,7 @@ public function __construct(array $record) $this->isTorExitNode = $record['is_tor_exit_node'] ?? false; $this->networkLastSeen = $record['network_last_seen'] ?? null; $this->providerName = $record['provider_name'] ?? null; + $this->residential = new AnonymizerFeed($record['residential'] ?? []); } /** @@ -122,6 +131,10 @@ public function jsonSerialize(): array if ($this->providerName !== null) { $js['provider_name'] = $this->providerName; } + $residential = $this->residential->jsonSerialize(); + if (!empty($residential)) { + $js['residential'] = $residential; + } return $js; } diff --git a/src/Record/AnonymizerFeed.php b/src/Record/AnonymizerFeed.php new file mode 100644 index 00000000..f59329fd --- /dev/null +++ b/src/Record/AnonymizerFeed.php @@ -0,0 +1,67 @@ + $record + */ + public function __construct(array $record) + { + $this->confidence = $record['confidence'] ?? null; + $this->networkLastSeen = $record['network_last_seen'] ?? null; + $this->providerName = $record['provider_name'] ?? null; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + $js = []; + + if ($this->confidence !== null) { + $js['confidence'] = $this->confidence; + } + if ($this->networkLastSeen !== null) { + $js['network_last_seen'] = $this->networkLastSeen; + } + if ($this->providerName !== null) { + $js['provider_name'] = $this->providerName; + } + + return $js; + } +} diff --git a/tests/GeoIp2/Test/Model/InsightsTest.php b/tests/GeoIp2/Test/Model/InsightsTest.php index 195623b9..804889e7 100644 --- a/tests/GeoIp2/Test/Model/InsightsTest.php +++ b/tests/GeoIp2/Test/Model/InsightsTest.php @@ -27,6 +27,11 @@ public function testFull(): void 'is_tor_exit_node' => true, 'network_last_seen' => '2025-04-14', 'provider_name' => 'NordVPN', + 'residential' => [ + 'confidence' => 82, + 'network_last_seen' => '2026-05-11', + 'provider_name' => 'quickshift', + ], ], 'city' => [ 'confidence' => 76, @@ -232,6 +237,30 @@ public function testFull(): void '$model->anonymizer->providerName is NordVPN' ); + $this->assertInstanceOf( + 'GeoIp2\Record\AnonymizerFeed', + $model->anonymizer->residential, + '$model->anonymizer->residential' + ); + + $this->assertSame( + 82, + $model->anonymizer->residential->confidence, + '$model->anonymizer->residential->confidence is 82' + ); + + $this->assertSame( + '2026-05-11', + $model->anonymizer->residential->networkLastSeen, + '$model->anonymizer->residential->networkLastSeen is 2026-05-11' + ); + + $this->assertSame( + 'quickshift', + $model->anonymizer->residential->providerName, + '$model->anonymizer->residential->providerName is quickshift' + ); + $this->assertSame( 15.37, $model->traits->ipRiskSnapshot, @@ -385,6 +414,11 @@ public function testFull(): void 'is_tor_exit_node' => true, 'network_last_seen' => '2025-04-14', 'provider_name' => 'NordVPN', + 'residential' => [ + 'confidence' => 82, + 'network_last_seen' => '2026-05-11', + 'provider_name' => 'quickshift', + ], ], ], $model->jsonSerialize(), @@ -392,6 +426,65 @@ public function testFull(): void ); } + public function testAnonymizerResidentialOnly(): void + { + // Residential proxy data may be present even when the other + // anonymizer properties are unset, so the anonymizer object may + // be returned with only the residential key set. + $raw = [ + 'traits' => ['ip_address' => '6.0.42.17'], + 'anonymizer' => [ + 'residential' => [ + 'confidence' => 95, + 'network_last_seen' => '2026-05-14', + 'provider_name' => 'novada', + ], + ], + ]; + + $model = new Insights($raw, ['en']); + + $this->assertNull( + $model->anonymizer->confidence, + '$model->anonymizer->confidence is null' + ); + + $this->assertFalse( + $model->anonymizer->isAnonymous, + '$model->anonymizer->isAnonymous is false' + ); + + $this->assertInstanceOf( + 'GeoIp2\Record\AnonymizerFeed', + $model->anonymizer->residential, + '$model->anonymizer->residential' + ); + + $this->assertSame( + 95, + $model->anonymizer->residential->confidence, + '$model->anonymizer->residential->confidence is 95' + ); + + $this->assertSame( + '2026-05-14', + $model->anonymizer->residential->networkLastSeen, + '$model->anonymizer->residential->networkLastSeen is 2026-05-14' + ); + + $this->assertSame( + 'novada', + $model->anonymizer->residential->providerName, + '$model->anonymizer->residential->providerName is novada' + ); + + $this->assertSame( + $raw, + $model->jsonSerialize(), + 'jsonSerialize returns only the residential key in anonymizer' + ); + } + public function testEmptyObjects(): void { $raw = ['traits' => ['ip_address' => '5.6.7.8', 'network' => '5.6.7.0/24']]; @@ -458,6 +551,33 @@ public function testEmptyObjects(): void '$model->traits' ); + $this->assertInstanceOf( + 'GeoIp2\Record\Anonymizer', + $model->anonymizer, + '$model->anonymizer' + ); + + $this->assertInstanceOf( + 'GeoIp2\Record\AnonymizerFeed', + $model->anonymizer->residential, + '$model->anonymizer->residential' + ); + + $this->assertNull( + $model->anonymizer->residential->confidence, + '$model->anonymizer->residential->confidence is null' + ); + + $this->assertNull( + $model->anonymizer->residential->networkLastSeen, + '$model->anonymizer->residential->networkLastSeen is null' + ); + + $this->assertNull( + $model->anonymizer->residential->providerName, + '$model->anonymizer->residential->providerName is null' + ); + $this->assertSame( $raw, $model->jsonSerialize(),