Skip to content

Commit

Permalink
Merge pull request #378 from brambaud/issue/315
Browse files Browse the repository at this point in the history
Report missing explicit access check on entity queries
  • Loading branch information
mglaman authored Apr 6, 2022
2 parents 479217b + 693c356 commit 82d08d6
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 6 deletions.
4 changes: 4 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ rules:
- mglaman\PHPStanDrupal\Rules\Deprecations\PluginAnnotationContextDefinitionsRule
- mglaman\PHPStanDrupal\Rules\Drupal\ModuleLoadInclude
- mglaman\PHPStanDrupal\Rules\Drupal\LoadIncludes
- mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule
services:
-
class: mglaman\PHPStanDrupal\Drupal\ServiceMap
Expand Down Expand Up @@ -286,6 +287,9 @@ services:
-
class: mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
class: mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryAccessCheckDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
-
class: mglaman\PHPStanDrupal\Type\UrlToStringDynamicReturnTypeExtension
tags: [phpstan.broker.dynamicMethodReturnTypeExtension]
Expand Down
47 changes: 47 additions & 0 deletions src/Rules/Drupal/EntityQuery/EntityQueryHasAccessCheckRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery;

use mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryExecuteWithoutAccessCheckCountType;
use mglaman\PHPStanDrupal\Type\EntityQuery\EntityQueryExecuteWithoutAccessCheckType;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;

final class EntityQueryHasAccessCheckRule implements Rule
{
public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!$node instanceof Node\Expr\MethodCall) {
return [];
}

$name = $node->name;
if (!$name instanceof Node\Identifier) {
return [];
}
if ($name->toString() !== 'execute') {
return [];
}

$type = $scope->getType($node);

if (!$type instanceof EntityQueryExecuteWithoutAccessCheckCountType && !$type instanceof EntityQueryExecuteWithoutAccessCheckType) {
return [];
}

return [
RuleErrorBuilder::message(
'Missing explicit access check on entity query.'
)->tip('See https://www.drupal.org/node/3201242')->build(),
];
}
}
2 changes: 1 addition & 1 deletion src/Type/EntityQuery/ConfigEntityQueryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* Type used to represent an entity query instance for config entity query.
*/
final class ConfigEntityQueryType extends ObjectType
final class ConfigEntityQueryType extends EntityQueryType
{

}
2 changes: 1 addition & 1 deletion src/Type/EntityQuery/ContentEntityQueryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* Type used to represent an entity query instance for content entity query.
*/
final class ContentEntityQueryType extends ObjectType
final class ContentEntityQueryType extends EntityQueryType
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type\EntityQuery;

use Drupal\Core\Entity\Query\QueryInterface;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Type;

class EntityQueryAccessCheckDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return QueryInterface::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
return 'accessCheck' === $methodReflection->getName();
}

public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$varType = $scope->getType($methodCall->var);

if (!$varType instanceof EntityQueryType) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}

return $varType->withAccessCheck();
}
}
2 changes: 1 addition & 1 deletion src/Type/EntityQuery/EntityQueryCountType.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
/**
* Type used to represent an entity query instance as count query.
*/
final class EntityQueryCountType extends ObjectType
final class EntityQueryCountType extends EntityQueryType
{

}
12 changes: 9 additions & 3 deletions src/Type/EntityQuery/EntityQueryDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ public function getTypeFromMethodCall(

if ($methodName === 'execute') {
if ($varType instanceof EntityQueryCountType) {
return new IntegerType();
return $varType->hasAccessCheck()
? new IntegerType()
: new EntityQueryExecuteWithoutAccessCheckCountType();
}
if ($varType instanceof ConfigEntityQueryType) {
return new ArrayType(new StringType(), new StringType());
return $varType->hasAccessCheck()
? new ArrayType(new StringType(), new StringType())
: new EntityQueryExecuteWithoutAccessCheckType(new StringType(), new StringType());
}
if ($varType instanceof ContentEntityQueryType) {
return new ArrayType(new IntegerType(), new StringType());
return $varType->hasAccessCheck()
? new ArrayType(new IntegerType(), new StringType())
: new EntityQueryExecuteWithoutAccessCheckType(new IntegerType(), new StringType());
}
return $defaultReturnType;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type\EntityQuery;

use PHPStan\Type\IntegerType;

final class EntityQueryExecuteWithoutAccessCheckCountType extends IntegerType
{

}
14 changes: 14 additions & 0 deletions src/Type/EntityQuery/EntityQueryExecuteWithoutAccessCheckType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type\EntityQuery;

use PHPStan\Type\ArrayType;

use PHPStan\Type\StringType;

final class EntityQueryExecuteWithoutAccessCheckType extends ArrayType
{

}
25 changes: 25 additions & 0 deletions src/Type/EntityQuery/EntityQueryType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Type\EntityQuery;

use PHPStan\Type\ObjectType;

class EntityQueryType extends ObjectType
{
private bool $hasAccessCheck = false;

public function hasAccessCheck(): bool
{
return $this->hasAccessCheck;
}

public function withAccessCheck(): self
{
$type = clone $this;
$type->hasAccessCheck = true;

return $type;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace fixtures\drupal\modules\phpstan_fixtures\src;

use function PHPStan\dumpType;

final class EntityQueryHasAccessRule
{
public function foo(): void
{
\Drupal::entityTypeManager()->getStorage('node')
->getQuery()
->accessCheck(FALSE)
->condition('field_test', 'foo', '=')
->execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace fixtures\drupal\modules\phpstan_fixtures\src;

final class EntityQueryWithAccessRule
{
public function foo(): void
{
\Drupal::entityTypeManager()->getStorage('node')
->getQuery()
->accessCheck(FALSE)
->condition('field_test', 'foo', '=')
->execute();
}

public function bar(): void
{
\Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('field_test', 'foo', '=')
->execute();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace fixtures\drupal\modules\phpstan_fixtures\src;

final class EntityQueryWithoutAccessRule
{
public function foo(): void
{
\Drupal::entityTypeManager()->getStorage('node')
->getQuery()
->condition('field_test', 'foo', '=')
->execute();
}

public function bar(): void
{
\Drupal::entityQuery('node')
->condition('field_test', 'foo', '=')
->execute();
}

}
48 changes: 48 additions & 0 deletions tests/src/Rules/EntityQueryHasAccessCheckRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace mglaman\PHPStanDrupal\Tests\Rules;

use mglaman\PHPStanDrupal\Rules\Drupal\EntityQuery\EntityQueryHasAccessCheckRule;
use mglaman\PHPStanDrupal\Tests\DrupalRuleTestCase;

final class EntityQueryHasAccessCheckRuleTest extends DrupalRuleTestCase
{
protected function getRule(): \PHPStan\Rules\Rule
{
return new EntityQueryHasAccessCheckRule();
}

/**
* @dataProvider cases
*/
public function test(array $files, array $errors): void
{
$this->analyse($files, $errors);
}

public function cases(): \Generator
{
yield [
[__DIR__.'/../../fixtures/drupal/modules/phpstan_fixtures/src/EntityQueryWithAccessRule.php'],
[],
];

yield [
[__DIR__.'/../../fixtures/drupal/modules/phpstan_fixtures/src/EntityQueryWithoutAccessRule.php'],
[
[
'Missing explicit access check on entity query.',
11,
'See https://www.drupal.org/node/3201242',
],
[
'Missing explicit access check on entity query.',
19,
'See https://www.drupal.org/node/3201242',
],
],
];
}
}

0 comments on commit 82d08d6

Please sign in to comment.