Skip to content

Commit a975097

Browse files
committed
Validate curl_setopt_array parameter array
1 parent 89ee746 commit a975097

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use Override;
6+
use PhpParser\Node;
7+
use PhpParser\NodeVisitorAbstract;
8+
use PHPStan\DependencyInjection\AutowiredService;
9+
10+
#[AutowiredService]
11+
final class CurlSetOptArrayArgVisitor extends NodeVisitorAbstract
12+
{
13+
14+
public const ATTRIBUTE_NAME = 'isCurlSetOptArrayArg';
15+
16+
#[Override]
17+
public function enterNode(Node $node): ?Node
18+
{
19+
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name) {
20+
$functionName = $node->name->toLowerString();
21+
if ($functionName === 'curl_setopt_array') {
22+
$args = $node->getRawArgs();
23+
if (isset($args[1])) {
24+
$args[1]->setAttribute(self::ATTRIBUTE_NAME, true);
25+
}
26+
}
27+
}
28+
return null;
29+
}
30+
31+
}

src/Reflection/ParametersAcceptorSelector.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Parser\ClosureBindArgVisitor;
1616
use PHPStan\Parser\ClosureBindToVarVisitor;
1717
use PHPStan\Parser\CurlSetOptArgVisitor;
18+
use PHPStan\Parser\CurlSetOptArrayArgVisitor;
1819
use PHPStan\Parser\ImplodeArgVisitor;
1920
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
2021
use PHPStan\Reflection\Native\NativeParameterReflection;
@@ -26,6 +27,8 @@
2627
use PHPStan\Type\ArrayType;
2728
use PHPStan\Type\BooleanType;
2829
use PHPStan\Type\CallableType;
30+
use PHPStan\Type\Constant\ConstantArrayType;
31+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
2932
use PHPStan\Type\Constant\ConstantIntegerType;
3033
use PHPStan\Type\Generic\TemplateType;
3134
use PHPStan\Type\Generic\TemplateTypeMap;
@@ -156,6 +159,54 @@ public static function selectFromArgs(
156159
}
157160
}
158161

162+
if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) {
163+
$optArrayType = $scope->getType($args[1]->value);
164+
165+
$builder = ConstantArrayTypeBuilder::createEmpty();
166+
foreach($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) {
167+
if (!is_int($optValue)) {
168+
$builder = null;
169+
break;
170+
}
171+
172+
$optValueType = self::getCurlOptValueType($optValue);
173+
if ($optValueType === null) {
174+
$builder = null;
175+
break;
176+
}
177+
178+
$builder->setOffsetValueType(
179+
new ConstantIntegerType($optValue),
180+
$optValueType
181+
);
182+
}
183+
184+
if ($builder !== null) {
185+
$acceptor = $parametersAcceptors[0];
186+
$parameters = $acceptor->getParameters();
187+
188+
$parameters[1] = new NativeParameterReflection(
189+
$parameters[1]->getName(),
190+
$parameters[1]->isOptional(),
191+
$builder->getArray(),
192+
$parameters[1]->passedByReference(),
193+
$parameters[1]->isVariadic(),
194+
$parameters[1]->getDefaultValue(),
195+
);
196+
197+
$parametersAcceptors = [
198+
new FunctionVariant(
199+
$acceptor->getTemplateTypeMap(),
200+
$acceptor->getResolvedTemplateTypeMap(),
201+
array_values($parameters),
202+
$acceptor->isVariadic(),
203+
$acceptor->getReturnType(),
204+
$acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
205+
),
206+
];
207+
}
208+
}
209+
159210
if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) {
160211
if (isset($args[2])) {
161212
$mode = $scope->getType($args[2]->value);

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,6 +1378,28 @@ public function testCurlSetOpt(): void
13781378
]);
13791379
}
13801380

1381+
public function testCurlSetOptArray(): void
1382+
{
1383+
$this->analyse([__DIR__ . '/data/curl-setopt-array.php'], [
1384+
[
1385+
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int, 10036: non-empty-string|null}, array{19913: true, 10102: '', 68: 10, 13: 30, 84: 2, 10036: CurlSetOptArray\RequestMethod::POST} given.",
1386+
30,
1387+
"Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\RequestMethod::POST."
1388+
],
1389+
[
1390+
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int, 10036: non-empty-string|null}, array{19913: true, 10102: '', 68: 10, 13: 30, 84: 2, 10036: CurlSetOptArray\BackedRequestMethod::POST} given.",
1391+
42,
1392+
"Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\BackedRequestMethod::POST."
1393+
],
1394+
[
1395+
"Parameter #2 \$options of function curl_setopt_array expects array{19913: bool, 10102: string, 68: int, 13: int, 84: int}, array{19913: '123', 10102: '', 68: 10, 13: 30, 84: false} given.",
1396+
54,
1397+
"Offset 19913 (bool) does not accept type string."
1398+
],
1399+
1400+
]);
1401+
}
1402+
13811403
public function testBug8280(): void
13821404
{
13831405
$this->analyse([__DIR__ . '/data/bug-8280.php'], []);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php // lint >= 8.1
2+
3+
namespace CurlSetOptArray;
4+
5+
enum RequestMethod
6+
{
7+
case GET;
8+
case POST;
9+
}
10+
11+
enum BackedRequestMethod: string
12+
{
13+
case POST = 'POST';
14+
case GET = 'GET';
15+
}
16+
17+
function allOptionsFine() {
18+
$curl = curl_init();
19+
curl_setopt_array($curl, [
20+
\CURLOPT_RETURNTRANSFER => true,
21+
\CURLOPT_ENCODING => '',
22+
\CURLOPT_MAXREDIRS => 10,
23+
\CURLOPT_TIMEOUT => 30,
24+
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
25+
]);
26+
}
27+
28+
function doFoo() {
29+
$curl = curl_init();
30+
curl_setopt_array($curl, [
31+
\CURLOPT_RETURNTRANSFER => true,
32+
\CURLOPT_ENCODING => '',
33+
\CURLOPT_MAXREDIRS => 10,
34+
\CURLOPT_TIMEOUT => 30,
35+
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
36+
\CURLOPT_CUSTOMREQUEST => RequestMethod::POST, // invalid
37+
]);
38+
}
39+
40+
function doFoo2() {
41+
$curl = curl_init();
42+
curl_setopt_array($curl, [
43+
\CURLOPT_RETURNTRANSFER => true,
44+
\CURLOPT_ENCODING => '',
45+
\CURLOPT_MAXREDIRS => 10,
46+
\CURLOPT_TIMEOUT => 30,
47+
\CURLOPT_HTTP_VERSION => \CURL_HTTP_VERSION_1_1,
48+
\CURLOPT_CUSTOMREQUEST => BackedRequestMethod::POST,
49+
]);
50+
}
51+
52+
function doFooBar() {
53+
$curl = curl_init();
54+
curl_setopt_array($curl, [
55+
\CURLOPT_RETURNTRANSFER => '123', // invalid
56+
\CURLOPT_ENCODING => '',
57+
\CURLOPT_MAXREDIRS => 10,
58+
\CURLOPT_TIMEOUT => 30,
59+
\CURLOPT_HTTP_VERSION => false, // invalid
60+
]);
61+
}
62+
63+
function doBar($options) {
64+
$curl = curl_init();
65+
curl_setopt_array($curl, $options);
66+
}
67+

0 commit comments

Comments
 (0)