Skip to content

Commit

Permalink
Merge branch refs/heads/1.11.x into 1.12.x
Browse files Browse the repository at this point in the history
  • Loading branch information
phpstan-bot authored Aug 25, 2024
2 parents 3175c81 + 347ceff commit 030acbb
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -330,4 +330,12 @@ public function hasDateTimeExceptions(): bool
return $this->versionId >= 80300;
}

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

}
68 changes: 66 additions & 2 deletions src/Type/Php/CurlInitReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,36 @@

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use function array_map;
use function count;
use function is_string;
use function parse_url;
use function str_contains;
use function strcasecmp;
use function strlen;

final class CurlInitReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

/** @see https://github.com/curl/curl/blob/curl-8_9_1/lib/urldata.h#L135 */
private const CURL_MAX_INPUT_LENGTH = 8000000;

public function __construct(private PhpVersion $phpVersion)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'curl_init';
Expand All @@ -26,13 +45,58 @@ public function getTypeFromFunctionCall(
Scope $scope,
): Type
{
$argsCount = count($functionCall->getArgs());
$args = $functionCall->getArgs();
$argsCount = count($args);
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
$notFalseReturnType = TypeCombinator::remove($returnType, new ConstantBooleanType(false));
if ($argsCount === 0) {
return TypeCombinator::remove($returnType, new ConstantBooleanType(false));
return $notFalseReturnType;
}

$urlArgType = $scope->getType($args[0]->value);
if ($urlArgType->isConstantScalarValue()->yes() && (new UnionType([new NullType(), new StringType()]))->isSuperTypeOf($urlArgType)->yes()) {
$urlArgReturnTypes = array_map(
fn ($value) => $this->getUrlArgValueReturnType($value, $returnType, $notFalseReturnType),
$urlArgType->getConstantScalarValues(),
);
return TypeCombinator::union(...$urlArgReturnTypes);
}

return $returnType;
}

private function getUrlArgValueReturnType(mixed $urlArgValue, Type $returnType, Type $notFalseReturnType): Type
{
if ($urlArgValue === null) {
return $notFalseReturnType;
}
if (!is_string($urlArgValue)) {
throw new ShouldNotHappenException();
}
if (str_contains($urlArgValue, "\0")) {
if (!$this->phpVersion->throwsValueErrorForInternalFunctions()) {
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L112-L115
return new ConstantBooleanType(false);
}
// https://github.com/php/php-src/blob/php-8.0.0/ext/curl/interface.c#L104-L107
return new NeverType();
}
if ($this->phpVersion->isCurloptUrlCheckingFileSchemeWithOpenBasedir()) {
// Before PHP 8.0 an unparsable URL or a file:// scheme would fail if open_basedir is used
// Since we can't detect open_basedir properly, we'll always consider a failure possible if these
// conditions are given
// https://github.com/php/php-src/blob/php-7.4.33/ext/curl/interface.c#L139-L158
$parsedUrlArgValue = parse_url($urlArgValue);
if ($parsedUrlArgValue === false || (isset($parsedUrlArgValue['scheme']) && strcasecmp($parsedUrlArgValue['scheme'], 'file') === 0)) {
return $returnType;
}
}
if (strlen($urlArgValue) > self::CURL_MAX_INPUT_LENGTH) {
// Since libcurl 7.65.0 this would always fail, but no current PHP version requires it at the moment
// https://github.com/curl/curl/commit/5fc28510a4664f46459d9a40187d81cc08571e60
return $returnType;
}
return $notFalseReturnType;
}

}
8 changes: 0 additions & 8 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3079,14 +3079,6 @@ public function dataBinaryOperations(): array
'bool',
'array_key_exists(\'foo\', $generalArray)',
],
[
PHP_VERSION_ID < 80000 ? 'resource' : 'CurlHandle',
'curl_init()',
],
[
PHP_VERSION_ID < 80000 ? 'resource|false' : 'CurlHandle|false',
'curl_init($string)',
],
[
'string',
'sprintf($string, $string, 1)',
Expand Down
32 changes: 32 additions & 0 deletions tests/PHPStan/Type/Php/CurlInitReturnTypeExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PHPStan\Testing\TypeInferenceTestCase;
use const PHP_VERSION_ID;

class CurlInitReturnTypeExtensionTest extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
if (PHP_VERSION_ID < 80000) {
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-7.php');
} else {
yield from self::gatherAssertTypes(__DIR__ . '/data/curl-init-php-8.php');
}
}

/**
* @dataProvider dataFileAsserts
*/
public function testFileAsserts(
string $assertType,
string $file,
mixed ...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

}
65 changes: 65 additions & 0 deletions tests/PHPStan/Type/Php/data/curl-init-php-7.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php declare(strict_types = 1);

namespace CurlInitReturnTypeExtensionTestPhp7;

use function PHPStan\Testing\assertType;

function (string $unknownString) {
assertType('resource', curl_init());
assertType('resource', curl_init('https://phpstan.org'));
assertType('resource|false', curl_init($unknownString));
assertType('resource', curl_init(null));
assertType('resource', curl_init(''));
assertType('resource|false', curl_init(':'));
assertType('resource|false', curl_init('file://host/text.txt'));
assertType('resource|false', curl_init('FIle://host/text.txt'));
assertType('resource', curl_init('host/text.txt'));
assertType('false', curl_init("\0"));
assertType('false', curl_init("https://phpstan.org\0"));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = null;
assertType('resource', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'https://phpstan.org/try';
assertType('resource', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = "\0";
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = "https://phpstan.org\0";
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = $unknownString;
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = ':';
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'file://host/text.txt';
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'FIle://host/text.txt';
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'host/text.txt';
assertType('resource', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'file://host/text.txt';
if (rand(0,1)) $url = null;
assertType('resource|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = null;
if (rand(0,1)) $url = $unknownString;
assertType('resource|false', curl_init($url));
};
69 changes: 69 additions & 0 deletions tests/PHPStan/Type/Php/data/curl-init-php-8.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace CurlInitReturnTypeExtensionTestPhp8;

use function PHPStan\Testing\assertType;

function (string $unknownString) {
assertType('CurlHandle', curl_init());
assertType('CurlHandle', curl_init('https://phpstan.org'));
assertType('CurlHandle|false', curl_init($unknownString));
assertType('CurlHandle', curl_init(null));
assertType('CurlHandle', curl_init(''));
assertType('CurlHandle', curl_init(':'));
assertType('CurlHandle', curl_init('file://host/text.txt'));
assertType('CurlHandle', curl_init('FIle://host/text.txt'));
assertType('CurlHandle', curl_init('host/text.txt'));
assertType('*NEVER*', curl_init("\0"));
assertType('*NEVER*', curl_init("https://phpstan.org\0"));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = null;
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'https://phpstan.org/try';
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = "\0";
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = "https://phpstan.org\0";
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = $unknownString;
assertType('CurlHandle|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url .= $unknownString;
assertType('CurlHandle|false', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = ':';
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'file://host/text.txt';
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'FIle://host/text.txt';
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'host/text.txt';
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = 'file://host/text.txt';
if (rand(0,1)) $url = null;
assertType('CurlHandle', curl_init($url));

$url = 'https://phpstan.org';
if (rand(0,1)) $url = null;
if (rand(0,1)) $url = $unknownString;
assertType('CurlHandle|false', curl_init($url));
};

0 comments on commit 030acbb

Please sign in to comment.