diff --git a/src/Drupal/DrupalAutoloader.php b/src/Drupal/DrupalAutoloader.php index 9b681dd5..6d034289 100644 --- a/src/Drupal/DrupalAutoloader.php +++ b/src/Drupal/DrupalAutoloader.php @@ -253,6 +253,14 @@ protected function addCoreTestNamespaces(): void $this->namespaces['Drupal\\TestSite'] = $core_tests_dir . '/TestSite'; $this->namespaces['Drupal\\TestTools'] = $core_tests_dir . '/TestTools'; $this->namespaces['Drupal\\Tests\\TestSuites'] = $this->drupalRoot . '/core/tests/TestSuites'; + + $classMap = [ + '\\Drupal\\Tests\\Core\\Render\\BubblingTest' => $this->drupalRoot . '/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php', + '\\Drupal\\Tests\\Core\\Render\\PlaceholdersTest' => $this->drupalRoot . '/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php', + '\\Drupal\\Tests\\Core\\Render\\TestAccessClass' => $this->drupalRoot . '/core/tests/Drupal/Tests/Core/Render/RendererTest.php', + '\\Drupal\\Tests\\Core\\Render\\TestCallables' => $this->drupalRoot . '/core/tests/Drupal/Tests/Core/Render/RendererTest.php', + ]; + $this->autoloader->addClassMap($classMap); } protected function addModuleNamespaces(): void diff --git a/src/Rules/Drupal/RenderCallbackRule.php b/src/Rules/Drupal/RenderCallbackRule.php index 413d943e..7a38dfa4 100644 --- a/src/Rules/Drupal/RenderCallbackRule.php +++ b/src/Rules/Drupal/RenderCallbackRule.php @@ -170,8 +170,28 @@ private function doProcessNode(Node\Expr $node, Scope $scope, string $keyChecked )->line($errorLine)->build(); } elseif (!$trustedCallbackType->isSuperTypeOf(new ObjectType($matches[1]))->yes()) { $errors[] = RuleErrorBuilder::message( - sprintf("%s callback class %s at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos) + sprintf( + "%s callback class %s at key '%s' does not implement Drupal\Core\Security\TrustedCallbackInterface.", + $keyChecked, + $constantStringType->describe(VerbosityLevel::value()), + $pos + ) )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); + } else { + $object = (new ObjectType($matches[1])); + if ($object->hasMethod('trustedCallbacks')->yes()) { + $allowedMethods = $this->getTrustedCallbackValues($object); + if (!in_array($matches[2], $allowedMethods, true)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + "%s callback method '%s' is not present in 'trustedCallbacks' at key '%s'.", + $keyChecked, + $matches[2], + $pos + ) + )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); + } + } } } } @@ -237,6 +257,21 @@ function (ClassReflection $reflection) use ($typeAndMethodName) { ) )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); } + } elseif ($isTrustedCallbackInterfaceType) { + $object = $typeAndMethodName->getType(); + if ($object->hasMethod('trustedCallbacks')->yes()) { + $allowedMethods = $this->getTrustedCallbackValues($typeAndMethodName->getType()); + if (!in_array($typeAndMethodName->getMethod(), $allowedMethods, true)) { + $errors[] = RuleErrorBuilder::message( + sprintf( + "%s callback method '%s' is not present in 'trustedCallbacks' at key '%s'.", + $keyChecked, + $typeAndMethodName->getMethod(), + $pos + ) + )->line($errorLine)->tip('Change record: https://www.drupal.org/node/2966725.')->build(); + } + } } } } @@ -314,4 +349,22 @@ private function getType(Node\Expr $node, Scope $scope): Type } return $type; } + + /** + * @return string[] + */ + private function getTrustedCallbackValues(Type $type): array + { + $values = []; + foreach ($type->getObjectClassReflections() as $classReflection) { + if (!$classReflection->hasMethod('trustedCallbacks')) { + continue; + } + $values[] = $classReflection + ->getNativeReflection() + ->getMethod('trustedCallbacks') + ->invoke(null); + } + return array_merge(...$values); + } } diff --git a/tests/src/Rules/RenderCallbackRuleTest.php b/tests/src/Rules/RenderCallbackRuleTest.php index 435c58ba..c5cc8b37 100644 --- a/tests/src/Rules/RenderCallbackRuleTest.php +++ b/tests/src/Rules/RenderCallbackRuleTest.php @@ -161,7 +161,18 @@ public static function fileData(): \Generator } yield [ __DIR__ . '/data/bug-543.php', - [] + [ + [ + "#access_callback callback method 'accessResultForbiddenNotInTrustedCallbacks' is not present in 'trustedCallbacks' at key '0'.", + 58, + "Change record: https://www.drupal.org/node/2966725." + ], + [ + "#access_callback callback method 'accessResultForbiddenNotInTrustedCallbacks' is not present in 'trustedCallbacks' at key '0'.", + 103, + "Change record: https://www.drupal.org/node/2966725." + ], + ], ]; yield [ __DIR__ . '/data/bug-554.php', diff --git a/tests/src/Rules/data/bug-543.php b/tests/src/Rules/data/bug-543.php index 2eda8414..0c2eb833 100644 --- a/tests/src/Rules/data/bug-543.php +++ b/tests/src/Rules/data/bug-543.php @@ -21,10 +21,13 @@ public function providerAccessValues() { [TRUE], [AccessResult::forbidden()], [AccessResult::allowed()], + ['accessResultForbiddenNotInTrustedCallbacks'], ]; } /** + * Tests callbacks with the method names in a variable. + * * @dataProvider providerAccessValues */ public function testRenderWithAccessControllerResolved($access) { @@ -45,6 +48,10 @@ public function testRenderWithAccessControllerResolved($access) { case TRUE: $method = 'accessTrue'; break; + + case 'accessResultForbiddenNotInTrustedCallbacks': + $method = 'accessResultForbiddenNotInTrustedCallbacks'; + break; } $build = [ @@ -52,31 +59,56 @@ public function testRenderWithAccessControllerResolved($access) { ]; } + /** + * Tests callback with the actual method name. + */ public function bug543AccessResultAllowed(): void { $build = [ '#access_callback' => TestAccessClass::class . '::accessResultAllowed', ]; } + /** + * Tests callback with the actual method name. + */ public function bug543AccessResultForbidden(): void { $build = [ '#access_callback' => TestAccessClass::class . '::accessResultForbidden', ]; } + /** + * Tests callback with the actual method name. + */ public function bug543AccessFalse(): void { $build = [ '#access_callback' => TestAccessClass::class . '::accessFalse', ]; } + /** + * Tests callback with the actual method name. + */ public function bug543AccessTrue(): void { $build = [ '#access_callback' => TestAccessClass::class . '::accessTrue', ]; } + + /** + * Tests callback with the actual method name. + */ + public function bug543AccessResultForbiddenNotInTrustedCallbacks(): void { + $build = [ + '#access_callback' => TestAccessClass::class . '::accessResultForbiddenNotInTrustedCallbacks', + ]; + } + } +/** + * Test class with callbacks. + */ class TestAccessClass implements TrustedCallbackInterface { public static function accessTrue() { @@ -95,6 +127,10 @@ public static function accessResultForbidden() { return AccessResult::forbidden(); } + public static function accessResultForbiddenNotInTrustedCallbacks() { + return AccessResult::forbidden(); + } + /** * {@inheritdoc} */