Skip to content

Commit 030acbb

Browse files
authored
Merge branch refs/heads/1.11.x into 1.12.x
2 parents 3175c81 + 347ceff commit 030acbb

File tree

6 files changed

+240
-10
lines changed

6 files changed

+240
-10
lines changed

src/Php/PhpVersion.php

+8
Original file line numberDiff line numberDiff line change
@@ -330,4 +330,12 @@ public function hasDateTimeExceptions(): bool
330330
return $this->versionId >= 80300;
331331
}
332332

333+
public function isCurloptUrlCheckingFileSchemeWithOpenBasedir(): bool
334+
{
335+
// Before PHP 8.0, when setting CURLOPT_URL, an unparsable URL or a file:// scheme would fail if open_basedir is used
336+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
337+
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L128-L130
338+
return $this->versionId < 80000;
339+
}
340+
333341
}

src/Type/Php/CurlInitReturnTypeExtension.php

+66-2
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,36 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Php\PhpVersion;
78
use PHPStan\Reflection\FunctionReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\ShouldNotHappenException;
911
use PHPStan\Type\Constant\ConstantBooleanType;
1012
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\NeverType;
14+
use PHPStan\Type\NullType;
15+
use PHPStan\Type\StringType;
1116
use PHPStan\Type\Type;
1217
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\UnionType;
19+
use function array_map;
1320
use function count;
21+
use function is_string;
22+
use function parse_url;
23+
use function str_contains;
24+
use function strcasecmp;
25+
use function strlen;
1426

1527
final class CurlInitReturnTypeExtension implements DynamicFunctionReturnTypeExtension
1628
{
1729

30+
/** @see https://github.com/curl/curl/blob/curl-8_9_1/lib/urldata.h#L135 */
31+
private const CURL_MAX_INPUT_LENGTH = 8000000;
32+
33+
public function __construct(private PhpVersion $phpVersion)
34+
{
35+
}
36+
1837
public function isFunctionSupported(FunctionReflection $functionReflection): bool
1938
{
2039
return $functionReflection->getName() === 'curl_init';
@@ -26,13 +45,58 @@ public function getTypeFromFunctionCall(
2645
Scope $scope,
2746
): Type
2847
{
29-
$argsCount = count($functionCall->getArgs());
48+
$args = $functionCall->getArgs();
49+
$argsCount = count($args);
3050
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
51+
$notFalseReturnType = TypeCombinator::remove($returnType, new ConstantBooleanType(false));
3152
if ($argsCount === 0) {
32-
return TypeCombinator::remove($returnType, new ConstantBooleanType(false));
53+
return $notFalseReturnType;
54+
}
55+
56+
$urlArgType = $scope->getType($args[0]->value);
57+
if ($urlArgType->isConstantScalarValue()->yes() && (new UnionType([new NullType(), new StringType()]))->isSuperTypeOf($urlArgType)->yes()) {
58+
$urlArgReturnTypes = array_map(
59+
fn ($value) => $this->getUrlArgValueReturnType($value, $returnType, $notFalseReturnType),
60+
$urlArgType->getConstantScalarValues(),
61+
);
62+
return TypeCombinator::union(...$urlArgReturnTypes);
3363
}
3464

3565
return $returnType;
3666
}
3767

68+
private function getUrlArgValueReturnType(mixed $urlArgValue, Type $returnType, Type $notFalseReturnType): Type
69+
{
70+
if ($urlArgValue === null) {
71+
return $notFalseReturnType;
72+
}
73+
if (!is_string($urlArgValue)) {
74+
throw new ShouldNotHappenException();
75+
}
76+
if (str_contains($urlArgValue, "\0")) {
77+
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
78+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115
79+
return new ConstantBooleanType(false);
80+
}
81+
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107
82+
return new NeverType();
83+
}
84+
if ($this->phpVersion->isCurloptUrlCheckingFileSchemeWithOpenBasedir()) {
85+
// Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used
86+
// Since we can't detect open_basedir properly, we'll always consider a failure possible if these
87+
// conditions are given
88+
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
89+
$parsedUrlArgValue = parse_url($urlArgValue);
90+
if ($parsedUrlArgValue === false || (isset($parsedUrlArgValue['scheme']) && strcasecmp($parsedUrlArgValue['scheme'], 'file') === 0)) {
91+
return $returnType;
92+
}
93+
}
94+
if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) {
95+
// Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment
96+
// https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60
97+
return $returnType;
98+
}
99+
return $notFalseReturnType;
100+
}
101+
38102
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

-8
Original file line numberDiff line numberDiff line change
@@ -3079,14 +3079,6 @@ public function dataBinaryOperations(): array
30793079
'bool',
30803080
'array_key_exists(\'foo\', $generalArray)',
30813081
],
3082-
[
3083-
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
3084-
'curl_init()',
3085-
],
3086-
[
3087-
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false',
3088-
'curl_init($string)',
3089-
],
30903082
[
30913083
'string',
30923084
'sprintf($string, $string, 1)',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
use const PHP_VERSION_ID;
7+
8+
class CurlInitReturnTypeExtensionTest extends TypeInferenceTestCase
9+
{
10+
11+
public static function dataFileAsserts(): iterable
12+
{
13+
if (PHP_VERSION_ID < 80000) {
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-7.php');
15+
} else {
16+
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-8.php');
17+
}
18+
}
19+
20+
/**
21+
* @dataProvider dataFileAsserts
22+
*/
23+
public function testFileAsserts(
24+
string $assertType,
25+
string $file,
26+
mixed ...$args,
27+
): void
28+
{
29+
$this->assertFileAsserts($assertType, $file, ...$args);
30+
}
31+
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace CurlInitReturnTypeExtensionTestPhp7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $unknownString) {
8+
assertType('resource', curl_init());
9+
assertType('resource', curl_init('https://phpstan.org'));
10+
assertType('resource|false', curl_init($unknownString));
11+
assertType('resource', curl_init(null));
12+
assertType('resource', curl_init(''));
13+
assertType('resource|false', curl_init(':'));
14+
assertType('resource|false', curl_init('file://host/text.txt'));
15+
assertType('resource|false', curl_init('FIle://host/text.txt'));
16+
assertType('resource', curl_init('host/text.txt'));
17+
assertType('false', curl_init("\0"));
18+
assertType('false', curl_init("https://phpstan.org\0"));
19+
20+
$url = 'https://phpstan.org';
21+
if (rand(0,1)) $url = null;
22+
assertType('resource', curl_init($url));
23+
24+
$url = 'https://phpstan.org';
25+
if (rand(0,1)) $url = 'https://phpstan.org/try';
26+
assertType('resource', curl_init($url));
27+
28+
$url = 'https://phpstan.org';
29+
if (rand(0,1)) $url = "\0";
30+
assertType('resource|false', curl_init($url));
31+
32+
$url = 'https://phpstan.org';
33+
if (rand(0,1)) $url = "https://phpstan.org\0";
34+
assertType('resource|false', curl_init($url));
35+
36+
$url = 'https://phpstan.org';
37+
if (rand(0,1)) $url = $unknownString;
38+
assertType('resource|false', curl_init($url));
39+
40+
$url = 'https://phpstan.org';
41+
if (rand(0,1)) $url = ':';
42+
assertType('resource|false', curl_init($url));
43+
44+
$url = 'https://phpstan.org';
45+
if (rand(0,1)) $url = 'file://host/text.txt';
46+
assertType('resource|false', curl_init($url));
47+
48+
$url = 'https://phpstan.org';
49+
if (rand(0,1)) $url = 'FIle://host/text.txt';
50+
assertType('resource|false', curl_init($url));
51+
52+
$url = 'https://phpstan.org';
53+
if (rand(0,1)) $url = 'host/text.txt';
54+
assertType('resource', curl_init($url));
55+
56+
$url = 'https://phpstan.org';
57+
if (rand(0,1)) $url = 'file://host/text.txt';
58+
if (rand(0,1)) $url = null;
59+
assertType('resource|false', curl_init($url));
60+
61+
$url = 'https://phpstan.org';
62+
if (rand(0,1)) $url = null;
63+
if (rand(0,1)) $url = $unknownString;
64+
assertType('resource|false', curl_init($url));
65+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace CurlInitReturnTypeExtensionTestPhp8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function (string $unknownString) {
8+
assertType('CurlHandle', curl_init());
9+
assertType('CurlHandle', curl_init('https://phpstan.org'));
10+
assertType('CurlHandle|false', curl_init($unknownString));
11+
assertType('CurlHandle', curl_init(null));
12+
assertType('CurlHandle', curl_init(''));
13+
assertType('CurlHandle', curl_init(':'));
14+
assertType('CurlHandle', curl_init('file://host/text.txt'));
15+
assertType('CurlHandle', curl_init('FIle://host/text.txt'));
16+
assertType('CurlHandle', curl_init('host/text.txt'));
17+
assertType('*NEVER*', curl_init("\0"));
18+
assertType('*NEVER*', curl_init("https://phpstan.org\0"));
19+
20+
$url = 'https://phpstan.org';
21+
if (rand(0,1)) $url = null;
22+
assertType('CurlHandle', curl_init($url));
23+
24+
$url = 'https://phpstan.org';
25+
if (rand(0,1)) $url = 'https://phpstan.org/try';
26+
assertType('CurlHandle', curl_init($url));
27+
28+
$url = 'https://phpstan.org';
29+
if (rand(0,1)) $url = "\0";
30+
assertType('CurlHandle', curl_init($url));
31+
32+
$url = 'https://phpstan.org';
33+
if (rand(0,1)) $url = "https://phpstan.org\0";
34+
assertType('CurlHandle', curl_init($url));
35+
36+
$url = 'https://phpstan.org';
37+
if (rand(0,1)) $url = $unknownString;
38+
assertType('CurlHandle|false', curl_init($url));
39+
40+
$url = 'https://phpstan.org';
41+
if (rand(0,1)) $url .= $unknownString;
42+
assertType('CurlHandle|false', curl_init($url));
43+
44+
$url = 'https://phpstan.org';
45+
if (rand(0,1)) $url = ':';
46+
assertType('CurlHandle', curl_init($url));
47+
48+
$url = 'https://phpstan.org';
49+
if (rand(0,1)) $url = 'file://host/text.txt';
50+
assertType('CurlHandle', curl_init($url));
51+
52+
$url = 'https://phpstan.org';
53+
if (rand(0,1)) $url = 'FIle://host/text.txt';
54+
assertType('CurlHandle', curl_init($url));
55+
56+
$url = 'https://phpstan.org';
57+
if (rand(0,1)) $url = 'host/text.txt';
58+
assertType('CurlHandle', curl_init($url));
59+
60+
$url = 'https://phpstan.org';
61+
if (rand(0,1)) $url = 'file://host/text.txt';
62+
if (rand(0,1)) $url = null;
63+
assertType('CurlHandle', curl_init($url));
64+
65+
$url = 'https://phpstan.org';
66+
if (rand(0,1)) $url = null;
67+
if (rand(0,1)) $url = $unknownString;
68+
assertType('CurlHandle|false', curl_init($url));
69+
};

0 commit comments

Comments
 (0)