Skip to content

Commit 3eeb763

Browse files
committed
Add SuperglobalAssignRule and SuperglobalAccessRule
1 parent fdfca14 commit 3eeb763

File tree

9 files changed

+505
-0
lines changed

9 files changed

+505
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This extension provides the following features:
2020
* Checks if the string argument passed to `service()` or `single_service()` function is a valid service name. This can be turned off by setting `codeigniter.checkArgumentTypeOfServices: false` in your `phpstan.neon`.
2121
* Disallows instantiating cache handlers using `new` and suggests to use the `CacheFactory` class instead.
2222
* Disallows instantiating `FrameworkException` classes using `new`.
23+
* Disallows direct re-assignment or access of `$_SERVER` and `$_GET` and suggests to use the `Superglobals` class instead.
2324

2425
## Installation
2526

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ services:
3333
arguments:
3434
additionalServices: %codeigniter.additionalServices%
3535

36+
superglobalRuleHelper:
37+
class: CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalRuleHelper
38+
3639
-
3740
class: CodeIgniter\PHPStan\Type\FactoriesFunctionReturnTypeExtension
3841
tags:
@@ -58,3 +61,5 @@ conditionalTags:
5861
rules:
5962
- CodeIgniter\PHPStan\Rules\Classes\CacheHandlerInstantiationRule
6063
- CodeIgniter\PHPStan\Rules\Classes\FrameworkExceptionInstantiationRule
64+
- CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalAccessRule
65+
- CodeIgniter\PHPStan\Rules\Superglobals\SuperglobalAssignRule
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Rules\Superglobals;
15+
16+
use PhpParser\Node;
17+
use PHPStan\Analyser\Scope;
18+
use PHPStan\Rules\Rule;
19+
use PHPStan\Rules\RuleErrorBuilder;
20+
use PHPStan\Type\VerbosityLevel;
21+
22+
/**
23+
* @implements Rule<Node\Expr\ArrayDimFetch>
24+
*/
25+
final class SuperglobalAccessRule implements Rule
26+
{
27+
public function __construct(
28+
private readonly SuperglobalRuleHelper $superglobalRuleHelper
29+
) {}
30+
31+
public function getNodeType(): string
32+
{
33+
return Node\Expr\ArrayDimFetch::class;
34+
}
35+
36+
/**
37+
* @param Node\Expr\ArrayDimFetch $node
38+
*/
39+
public function processNode(Node $node, Scope $scope): array
40+
{
41+
if ($scope->isInExpressionAssign($node)) {
42+
return [];
43+
}
44+
45+
if (! $node->var instanceof Node\Expr\Variable) {
46+
return [];
47+
}
48+
49+
$name = $node->var->name;
50+
51+
if (! is_string($name)) {
52+
return [];
53+
}
54+
55+
if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) {
56+
return [];
57+
}
58+
59+
if ($node->dim === null) {
60+
return [];
61+
}
62+
63+
$dimType = $scope->getType($node->dim);
64+
65+
if ($dimType->isString()->no()) {
66+
return [];
67+
}
68+
69+
$method = $this->superglobalRuleHelper->getSuperglobalMethodGetter($name);
70+
$errors = [];
71+
72+
if ($dimType->getConstantStrings() !== []) {
73+
foreach ($dimType->getConstantStrings() as $dimString) {
74+
$dim = $dimString->getValue();
75+
76+
$errors[] = RuleErrorBuilder::message(sprintf('Accessing offset \'%s\' directly on $%s is discouraged.', $dim, $name))
77+
->tip(sprintf('Use \\Config\\Services::superglobals()->%s(\'%s\') instead.', $method, $dim))
78+
->identifier('codeigniter.superglobalAccess')
79+
->build();
80+
}
81+
82+
return $errors;
83+
}
84+
85+
$dim = $dimType->describe(VerbosityLevel::precise());
86+
87+
return [
88+
RuleErrorBuilder::message(sprintf('Accessing offset %s directly on $%s is discouraged.', $dim, $name))
89+
->tip(sprintf('Use \\Config\\Services::superglobals()->%s() instead.', $method))
90+
->identifier('codeigniter.superglobalAccess')
91+
->build(),
92+
];
93+
}
94+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Rules\Superglobals;
15+
16+
use CodeIgniter\Superglobals;
17+
use PhpParser\Node;
18+
use PHPStan\Analyser\Scope;
19+
use PHPStan\Rules\Rule;
20+
use PHPStan\Rules\RuleError;
21+
use PHPStan\Rules\RuleErrorBuilder;
22+
use PHPStan\Type\VerbosityLevel;
23+
24+
/**
25+
* @implements Rule<Node\Expr\Assign>
26+
*/
27+
final class SuperglobalAssignRule implements Rule
28+
{
29+
public function __construct(
30+
private readonly SuperglobalRuleHelper $superglobalRuleHelper
31+
) {}
32+
33+
public function getNodeType(): string
34+
{
35+
return Node\Expr\Assign::class;
36+
}
37+
38+
/**
39+
* @param Node\Expr\Assign $node
40+
*/
41+
public function processNode(Node $node, Scope $scope): array
42+
{
43+
if ($node->var instanceof Node\Expr\ArrayDimFetch) {
44+
return $this->processArrayDimFetch($node, $scope);
45+
}
46+
47+
if ($node->var instanceof Node\Expr\Variable) {
48+
return $this->processVariableExpr($node, $scope);
49+
}
50+
51+
return [];
52+
}
53+
54+
/**
55+
* @param Node\Expr\Assign $node
56+
*
57+
* @return RuleError[]
58+
*/
59+
private function processArrayDimFetch(Node $node, Scope $scope): array
60+
{
61+
assert($node->var instanceof Node\Expr\ArrayDimFetch);
62+
63+
$arrayDimFetch = $node->var;
64+
65+
if ($arrayDimFetch->dim === null) {
66+
return [];
67+
}
68+
69+
$dimType = $scope->getType($arrayDimFetch->dim);
70+
71+
if ($dimType->isString()->no()) {
72+
return [];
73+
}
74+
75+
if (! $arrayDimFetch->var instanceof Node\Expr\Variable) {
76+
return [];
77+
}
78+
79+
$name = $arrayDimFetch->var->name;
80+
81+
if (! is_string($name)) {
82+
return [];
83+
}
84+
85+
if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) {
86+
return [];
87+
}
88+
89+
if ($scope->isInClass() && $scope->getClassReflection()->getName() === Superglobals::class) {
90+
return [];
91+
}
92+
93+
$exprType = $scope->getType($node->expr);
94+
95+
$expr = $exprType->describe(VerbosityLevel::precise());
96+
$dim = $dimType->describe(VerbosityLevel::precise());
97+
$method = $this->superglobalRuleHelper->getSuperglobalMethodSetter($name);
98+
99+
$addTip = static function (RuleErrorBuilder $ruleErrorBuilder) use ($method, $dimType, $exprType): RuleErrorBuilder {
100+
if ($dimType->getConstantStrings() !== [] && $exprType->getConstantStrings() !== []) {
101+
foreach ($dimType->getConstantStrings() as $dimString) {
102+
foreach ($exprType->getConstantStrings() as $exprString) {
103+
$ruleErrorBuilder->addTip(sprintf(
104+
'Use \\Config\\Services::superglobals()->%s(%s, %s) instead.',
105+
$method,
106+
$dimString->describe(VerbosityLevel::precise()),
107+
$exprString->describe(VerbosityLevel::precise())
108+
));
109+
}
110+
}
111+
112+
return $ruleErrorBuilder;
113+
}
114+
115+
return $ruleErrorBuilder->tip(sprintf('Use \\Config\\Services::superglobals()->%s() instead.', $method));
116+
};
117+
118+
return [
119+
$addTip(RuleErrorBuilder::message(sprintf('Assigning %s directly on offset %s of $%s is discouraged.', $expr, $dim, $name)))
120+
->identifier('codeigniter.superglobalAccessAssign')
121+
->build(),
122+
];
123+
}
124+
125+
/**
126+
* @param Node\Expr\Assign $node
127+
*
128+
* @return RuleError[]
129+
*/
130+
private function processVariableExpr(Node $node, Scope $scope): array
131+
{
132+
assert($node->var instanceof Node\Expr\Variable);
133+
134+
$name = $node->var->name;
135+
136+
if (! is_string($name)) {
137+
return [];
138+
}
139+
140+
if (! $this->superglobalRuleHelper->isHandledSuperglobal($name)) {
141+
return [];
142+
}
143+
144+
if ($name !== '_GET') {
145+
return [];
146+
}
147+
148+
$exprType = $scope->getType($node->expr);
149+
150+
if (! $exprType->isArray()->yes()) {
151+
return [
152+
RuleErrorBuilder::message(sprintf('Cannot re-assign non-arrays to $_GET, got %s.', $exprType->describe(VerbosityLevel::typeOnly())))
153+
->identifier('codeigniter.getReassignNonarray')
154+
->build(),
155+
];
156+
}
157+
158+
return [
159+
RuleErrorBuilder::message('Re-assigning arrays to $_GET directly is discouraged.')
160+
->tip('Use \\Config\\Services::superglobals()->setGetArray() instead.')
161+
->identifier('codeigniter.getReassignArray')
162+
->build(),
163+
];
164+
}
165+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\PHPStan\Rules\Superglobals;
15+
16+
use InvalidArgumentException;
17+
18+
final class SuperglobalRuleHelper
19+
{
20+
public function isHandledSuperglobal(string $name): bool
21+
{
22+
return in_array($name, ['_SERVER', '_GET'], true);
23+
}
24+
25+
/**
26+
* @throws InvalidArgumentException
27+
*/
28+
public function getSuperglobalMethodSetter(string $name): string
29+
{
30+
return match ($name) {
31+
'_SERVER' => 'setServer',
32+
'_GET' => 'setGet',
33+
default => throw new InvalidArgumentException(sprintf('Unhandled superglobal: "%s".', $name)),
34+
};
35+
}
36+
37+
/**
38+
* @throws InvalidArgumentException
39+
*/
40+
public function getSuperglobalMethodGetter(string $name): string
41+
{
42+
return match ($name) {
43+
'_SERVER' => 'server',
44+
'_GET' => 'get',
45+
default => throw new InvalidArgumentException(sprintf('Unhandled superglobal: "%s".', $name)),
46+
};
47+
}
48+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace SuperglobalAccess;
15+
16+
$foo = $_SERVER['foo'] ?? null;
17+
18+
$a = (static fn (): string => mt_rand(0, 1) ? 'a' : 'b')();
19+
$b = $_GET[$a] ?? null;
20+
21+
function bar(string $c): ?string
22+
{
23+
return $_SERVER[$c] ?? null;
24+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) 2023 CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace SuperglobalAssign;
15+
16+
$_SERVER['HTTP_HOST'] = 'https://localhost';
17+
18+
$_GET['first_name'] = 'John Doe';
19+
20+
$_SERVER[0] = 'hello';
21+
22+
function bar(string $key, string $value): void
23+
{
24+
$_SERVER[$key] = $value;
25+
26+
$_GET[$key] = $value;
27+
}
28+
29+
$_GET = 'sss';
30+
$_GET = 12_500;
31+
32+
$_GET = ['first' => 'John', 'last' => 'Doe'];

0 commit comments

Comments
 (0)