Skip to content

Commit f54eeec

Browse files
authored
Validate curl_setopt_array parameter array
1 parent f1d4c64 commit f54eeec

File tree

7 files changed

+176
-0
lines changed

7 files changed

+176
-0
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ parameters:
1414
rawMessageInBaseline: true
1515
reportNestedTooWideType: false
1616
assignToByRefForeachExpr: true
17+
curlSetOptArrayTypes: true

conf/config.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ parameters:
3838
rawMessageInBaseline: false
3939
reportNestedTooWideType: false
4040
assignToByRefForeachExpr: false
41+
curlSetOptArrayTypes: false
4142
fileExtensions:
4243
- php
4344
checkAdvancedIsset: false

conf/parametersSchema.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ parametersSchema:
4141
rawMessageInBaseline: bool()
4242
reportNestedTooWideType: bool()
4343
assignToByRefForeachExpr: bool()
44+
curlSetOptArrayTypes: bool()
4445
])
4546
fileExtensions: listOf(string())
4647
checkAdvancedIsset: bool()
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: 52 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,7 @@
2627
use PHPStan\Type\ArrayType;
2728
use PHPStan\Type\BooleanType;
2829
use PHPStan\Type\CallableType;
30+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
2931
use PHPStan\Type\Constant\ConstantIntegerType;
3032
use PHPStan\Type\Generic\TemplateType;
3133
use PHPStan\Type\Generic\TemplateTypeMap;
@@ -170,6 +172,56 @@ public static function selectFromArgs(
170172
}
171173
}
172174

175+
if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) {
176+
$optArrayType = $scope->getType($args[1]->value);
177+
178+
$hasTypes = false;
179+
$builder = ConstantArrayTypeBuilder::createEmpty();
180+
foreach ($optArrayType->getIterableKeyType()->getConstantScalarValues() as $optValue) {
181+
if (!is_int($optValue)) {
182+
$hasTypes = false;
183+
break;
184+
}
185+
186+
$optValueType = self::getCurlOptValueType($optValue);
187+
if ($optValueType === null) {
188+
$hasTypes = false;
189+
break;
190+
}
191+
192+
$hasTypes = true;
193+
$builder->setOffsetValueType(
194+
new ConstantIntegerType($optValue),
195+
$optValueType,
196+
);
197+
}
198+
199+
if ($hasTypes) {
200+
$acceptor = $parametersAcceptors[0];
201+
$parameters = $acceptor->getParameters();
202+
203+
$parameters[1] = new NativeParameterReflection(
204+
$parameters[1]->getName(),
205+
$parameters[1]->isOptional(),
206+
$builder->getArray(),
207+
$parameters[1]->passedByReference(),
208+
$parameters[1]->isVariadic(),
209+
$parameters[1]->getDefaultValue(),
210+
);
211+
212+
$parametersAcceptors = [
213+
new FunctionVariant(
214+
$acceptor->getTemplateTypeMap(),
215+
$acceptor->getResolvedTemplateTypeMap(),
216+
array_values($parameters),
217+
$acceptor->isVariadic(),
218+
$acceptor->getReturnType(),
219+
$acceptor instanceof ExtendedParametersAcceptor ? $acceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
220+
),
221+
];
222+
}
223+
}
224+
173225
if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) {
174226
if (isset($args[2])) {
175227
$mode = $scope->getType($args[2]->value);

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,29 @@ public function testCurlSetOpt(): void
13821382
]);
13831383
}
13841384

1385+
#[RequiresPhp('>= 8.1')]
1386+
public function testCurlSetOptArray(): void
1387+
{
1388+
$this->analyse([__DIR__ . '/data/curl-setopt-array.php'], [
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\RequestMethod::POST} given.",
1391+
30,
1392+
'Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\RequestMethod::POST.',
1393+
],
1394+
[
1395+
"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.",
1396+
42,
1397+
'Offset 10036 (non-empty-string|null) does not accept type CurlSetOptArray\BackedRequestMethod::POST.',
1398+
],
1399+
[
1400+
"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.",
1401+
54,
1402+
'Offset 19913 (bool) does not accept type string.',
1403+
],
1404+
1405+
]);
1406+
}
1407+
13851408
public function testBug8280(): void
13861409
{
13871410
$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)