Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"prefer-stable": true,
"require": {
"php": ">=8.1",
"ext-filter": "*",
"ext-mbstring": "*",
"psr/http-factory": "^1.0",
"psr/http-message": "^2.0",
Expand Down
18 changes: 14 additions & 4 deletions src/http/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use function base64_decode;
use function count;
use function explode;
use function filter_var;
use function is_array;
use function is_string;
use function mb_check_encoding;
Expand Down Expand Up @@ -476,14 +477,15 @@ public function getRemoteHost(): string|null
/**
* Retrieves the remote IP address for the current request, supporting PSR-7 and Yii2 fallback.
*
* Returns the remote IP address as provided by the PSR-7 ServerRequestAdapter if the adapter is set.
* Returns the remote IP address as determined by the PSR-7 adapter if present, using the 'REMOTE_ADDR' server
* parameter.
*
* If no adapter is present, falls back to the parent implementation.
* If no adapter is set, falls back to the parent implementation.
*
* This method enables seamless access to the remote IP address in both PSR-7 and Yii2 environments, supporting
* interoperability with modern HTTP stacks and legacy workflows.
*
* @return string|null Remote IP address for the current request, or `null` if not available.
* @return string|null Remote IP address for the current request, or `null` if not available or invalid.
*
* Usage example:
* ```php
Expand All @@ -495,7 +497,15 @@ public function getRemoteIP(): string|null
if ($this->adapter !== null) {
$remoteIP = $this->getServerParam('REMOTE_ADDR');

return is_string($remoteIP) ? $remoteIP : null;
if (is_string($remoteIP) === false) {
return null;
}

if (filter_var($remoteIP, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) {
return null;
}

return $remoteIP;
}

return parent::getRemoteIP();
Expand Down
47 changes: 47 additions & 0 deletions tests/adapter/ServerParamsPsr7Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,53 @@ public function testReturnRemoteHostFromServerParamsCases(mixed $serverValue, st
);
}

#[Group('remote-ip')]
public function testReturnRemoteIPFromPsr7ServerParamsOverridesGlobalServer(): void
{
$_SERVER['REMOTE_ADDR'] = '192.168.1.100';

$request = new Request();

$request->setPsr7Request(
FactoryHelper::createRequest(
'GET',
'https://old.example.com/api',
serverParams: ['REMOTE_ADDR' => '10.0.0.1'],
),
);

self::assertSame(
'10.0.0.1',
$request->getRemoteIP(),
"'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams', not from global " .
'$_SERVER.',
);
}

#[DataProviderExternal(RequestProvider::class, 'remoteIPCases')]
#[Group('remote-ip')]
public function testReturnRemoteIPFromServerParamsCases(mixed $serverValue, string|null $expected): void
{
$request = new Request();

$request->setPsr7Request(
FactoryHelper::createRequest('GET', '/test', serverParams: ['REMOTE_ADDR' => $serverValue]),
);

$actual = $request->getRemoteIP();

self::assertSame(
$expected,
$actual,
sprintf(
"'getRemoteIP()' should return '%s' when 'REMOTE_ADDR' is '%s' in PSR-7 'serverParams'. Got: '%s'",
var_export($expected, true),
var_export($serverValue, true),
var_export($actual, true),
),
);
}

#[DataProviderExternal(RequestProvider::class, 'serverNameCases')]
#[Group('server-name')]
public function testReturnServerNameFromServerParamsCases(mixed $serverValue, string|null $expected): void
Expand Down
41 changes: 0 additions & 41 deletions tests/adapter/ServerRequestAdapterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1179,47 +1179,6 @@ public function testReturnRawBodyWhenAdapterIsSetWithEmptyBody(): void
);
}

public function testReturnRemoteIPFromPsr7ServerParams(): void
{
$request = new Request();

$request->setPsr7Request(
FactoryHelper::createRequest(
'GET',
'https://old.example.com/api',
serverParams: ['REMOTE_ADDR' => '192.168.1.100'],
),
);

self::assertSame(
'192.168.1.100',
$request->getRemoteIP(),
"'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams'.",
);
}

public function testReturnRemoteIPFromPsr7ServerParamsOverridesGlobalServer(): void
{
$_SERVER['REMOTE_ADDR'] = '192.168.1.100';

$request = new Request();

$request->setPsr7Request(
FactoryHelper::createRequest(
'GET',
'https://old.example.com/api',
serverParams: ['REMOTE_ADDR' => '10.0.0.1'],
),
);

self::assertSame(
'10.0.0.1',
$request->getRemoteIP(),
"'getRemoteIP()' should return the 'REMOTE_ADDR' value from PSR-7 'serverParams', not from global " .
'$_SERVER.',
);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
Expand Down
97 changes: 97 additions & 0 deletions tests/provider/RequestProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,103 @@ public static function remoteHostCases(): array
];
}

/**
* @phpstan-return array<array-key, array{mixed, string|null}>
*/
public static function remoteIPCases(): array
{
return [
'boolean-false' => [
false,
null,
],
'boolean-true' => [
true,
null,
],
'empty-array' => [
[],
null,
],
'empty-string' => [
'',
null,
],
'float' => [
123.45,
null,
],
'integer' => [
12345,
null,
],
'integer-zero' => [
0,
null,
],
'invalid-ip' => [
'999.999.999.999',
null,
],
'IPv4' => [
'192.168.1.100',
'192.168.1.100',
],
'ipv4-with-port' => [
'10.0.0.1:8080',
null,
],
'IPv4-local' => [
'127.0.0.1',
'127.0.0.1',
],
'IPv6' => [
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
'2001:0db8:85a3:0000:0000:8a2e:0370:7334',
],
'ipv6-bracketed' => [
'[::1]',
null,
],
'IPv6-compressed' => [
'::1',
'::1',
],
'ipv6-with-port' => [
'[::1]:443',
null,
],
'localhost' => [
'localhost',
null,
],
'null' => [
null,
null,
],
'numeric string' => [
'123',
null,
],
'object' => [
(object) ['foo' => 'bar'],
null,
],
'spaces-around' => [
' 127.0.0.1 ',
null,
],
'string-zero' => [
'0',
null,
],
'zero-address' => [
'0.0.0.0',
'0.0.0.0',
],
];
}

/**
* @phpstan-return array<array-key, array{mixed, string|null}>
*/
Expand Down
Loading