Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x
  2. RenderCallbackRule.php

function RenderCallbackRule::doProcessNode

Return value

(string|\PHPStan\Rules\RuleError)[] errors

1 call to RenderCallbackRule::doProcessNode()
RenderCallbackRule::processNode in vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RenderCallbackRule.php

File

vendor/mglaman/phpstan-drupal/src/Rules/Drupal/RenderCallbackRule.php, line 138

Class

RenderCallbackRule
@implements Rule<Node\Expr\ArrayItem>

Namespace

mglaman\PHPStanDrupal\Rules\Drupal

Code

private function doProcessNode(Node\Expr $node, Scope $scope, string $keyChecked, int $pos) : array {
    $checkIsCallable = true;
    $trustedCallbackType = new UnionType([
        new ObjectType(TrustedCallbackInterface::class),
        new ObjectType(RenderCallbackInterface::class),
    ]);
    $errors = [];
    $errorLine = $node->getStartLine();
    $type = $this->getType($node, $scope);
    foreach ($type->getConstantStrings() as $constantStringType) {
        if (!$constantStringType->isCallable()
            ->yes()) {
            $errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
                ->line($errorLine)
                ->build();
        }
        elseif ($this->reflectionProvider
            ->hasFunction(new Name($constantStringType->getValue()), null)) {
            // We can determine if the callback is callable through the type system. However, we cannot determine
            // if it is just a function or a static class call (MyClass::staticFunc).
            $errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not trusted.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
                ->line($errorLine)
                ->tip('Change record: https://www.drupal.org/node/2966725.')
                ->build();
        }
        else {
            // @see \PHPStan\Type\Constant\ConstantStringType::isCallable
            preg_match('#^([a-zA-Z_\\x7f-\\xff\\\\][a-zA-Z0-9_\\x7f-\\xff\\\\]*)::([a-zA-Z_\\x7f-\\xff][a-zA-Z0-9_\\x7f-\\xff]*)\\z#', $constantStringType->getValue(), $matches);
            if (count($matches) === 0) {
                $errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantStringType->describe(VerbosityLevel::value()), $pos))
                    ->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))
                    ->line($errorLine)
                    ->tip('Change record: https://www.drupal.org/node/2966725.')
                    ->build();
            }
        }
    }
    foreach ($type->getConstantArrays() as $constantArrayType) {
        if (!$constantArrayType->isCallable()
            ->yes()) {
            // If the right-hand side of the array is a variable, we cannot
            // determine if it is callable. Bail now.
            $itemType = $constantArrayType->getItemType();
            if ($itemType instanceof UnionType) {
                $unionConstantStrings = array_merge(...array_map(static function (Type $type) {
                    return $type->getConstantStrings();
                }, $itemType->getTypes()));
                if (count($unionConstantStrings) === 0) {
                    // Right-hand side of UnionType is not a constant string. We cannot determine if the dynamic
                    // value is callable or not.
                    $checkIsCallable = false;
                    break;
                }
            }
            $errors[] = RuleErrorBuilder::message(sprintf("%s callback %s at key '%s' is not callable.", $keyChecked, $constantArrayType->describe(VerbosityLevel::value()), $pos))
                ->line($errorLine)
                ->build();
            continue;
        }
        $typeAndMethodNames = $constantArrayType->findTypeAndMethodNames();
        if ($typeAndMethodNames === []) {
            continue;
        }
        foreach ($typeAndMethodNames as $typeAndMethodName) {
            $isTrustedCallbackAttribute = TrinaryLogic::createNo()->lazyOr($typeAndMethodName->getType()
                ->getObjectClassReflections(), function (ClassReflection $reflection) use ($typeAndMethodName) {
                if (!class_exists(TrustedCallback::class)) {
                    return TrinaryLogic::createNo();
                }
                $hasAttribute = $reflection->getNativeReflection()
                    ->getMethod($typeAndMethodName->getMethod())
                    ->getAttributes(TrustedCallback::class);
                return TrinaryLogic::createFromBoolean(count($hasAttribute) > 0);
            });
            $isTrustedCallbackInterfaceType = $trustedCallbackType->isSuperTypeOf($typeAndMethodName->getType())
                ->yes();
            if (!$isTrustedCallbackInterfaceType && !$isTrustedCallbackAttribute->yes()) {
                if (class_exists(TrustedCallback::class)) {
                    $errors[] = RuleErrorBuilder::message(sprintf("%s callback method '%s' at key '%s' does not implement attribute \\Drupal\\Core\\Security\\Attribute\\TrustedCallback.", $keyChecked, $constantArrayType->describe(VerbosityLevel::value()), $pos))
                        ->line($errorLine)
                        ->tip('Change record: https://www.drupal.org/node/3349470')
                        ->build();
                }
                else {
                    $errors[] = RuleErrorBuilder::message(sprintf("%s callback class '%s' at key '%s' does not implement Drupal\\Core\\Security\\TrustedCallbackInterface.", $keyChecked, $typeAndMethodName->getType()
                        ->describe(VerbosityLevel::value()), $pos))
                        ->line($errorLine)
                        ->tip('Change record: https://www.drupal.org/node/2966725.')
                        ->build();
                }
            }
        }
    }
    // @todo move to its own rule for 1.2.0, FormClosureSerializationRule.
    if ($type instanceof ClosureType && $scope->isInClass()) {
        $classReflection = $scope->getClassReflection();
        $classType = new ObjectType($classReflection->getName());
        $formType = new ObjectType('\\Drupal\\Core\\Form\\FormInterface');
        if ($formType->isSuperTypeOf($classType)
            ->yes()) {
            $errors[] = RuleErrorBuilder::message(sprintf("%s may not contain a closure at key '%s' as forms may be serialized and serialization of closures is not allowed.", $keyChecked, $pos))->line($errorLine)
                ->build();
        }
    }
    if (count($errors) === 0 && ($checkIsCallable && !$type->isCallable()
        ->yes())) {
        $errors[] = RuleErrorBuilder::message(sprintf("%s value '%s' at key '%s' is invalid.", $keyChecked, $type->describe(VerbosityLevel::value()), $pos))
            ->line($errorLine)
            ->build();
    }
    return $errors;
}

API Navigation

  • Drupal Core 11.1.x
  • Topics
  • Classes
  • Functions
  • Constants
  • Globals
  • Files
  • Namespaces
  • Deprecated
  • Services
RSS feed
Powered by Drupal