feat(DI): Abort querying if infinite loop is detected

Signed-off-by: provokateurin <kate@provokateurin.de>
This commit is contained in:
provokateurin 2025-11-05 11:55:01 +01:00
parent 6911a33d50
commit 3dbf848ee9
No known key found for this signature in database
3 changed files with 53 additions and 27 deletions

View file

@ -310,21 +310,29 @@ class DIContainer extends SimpleContainer implements IAppContainer {
return false;
}
public function query(string $name, bool $autoload = true) {
/**
* @inheritDoc
* @param list<class-string> $chain
*/
public function query(string $name, bool $autoload = true, array $chain = []) {
if ($name === 'AppName' || $name === 'appName') {
return $this->appName;
}
$isServerClass = str_starts_with($name, 'OCP\\') || str_starts_with($name, 'OC\\');
if ($isServerClass && !$this->has($name)) {
return $this->getServer()->query($name, $autoload);
/** @var ServerContainer $server */
$server = $this->getServer();
return $server->query($name, $autoload, $chain);
}
try {
return $this->queryNoFallback($name);
return $this->queryNoFallback($name, $chain);
} catch (QueryException $firstException) {
try {
return $this->getServer()->query($name, $autoload);
/** @var ServerContainer $server */
$server = $this->getServer();
return $server->query($name, $autoload, $chain);
} catch (QueryException $secondException) {
if ($firstException->getCode() === 1) {
throw $secondException;
@ -339,23 +347,23 @@ class DIContainer extends SimpleContainer implements IAppContainer {
* @return mixed
* @throws QueryException if the query could not be resolved
*/
public function queryNoFallback($name) {
public function queryNoFallback($name, array $chain) {
$name = $this->sanitizeName($name);
if ($this->offsetExists($name)) {
return parent::query($name);
return parent::query($name, chain: $chain);
} elseif ($this->appName === 'settings' && str_starts_with($name, 'OC\\Settings\\')) {
return parent::query($name);
return parent::query($name, chain: $chain);
} elseif ($this->appName === 'core' && str_starts_with($name, 'OC\\Core\\')) {
return parent::query($name);
return parent::query($name, chain: $chain);
} elseif (str_starts_with($name, \OC\AppFramework\App::buildAppNamespace($this->appName) . '\\')) {
return parent::query($name);
return parent::query($name, chain: $chain);
} elseif (
str_starts_with($name, 'OC\\AppFramework\\Services\\')
|| str_starts_with($name, 'OC\\AppFramework\\Middleware\\')
) {
/* AppFramework services are scoped to the application */
return parent::query($name);
return parent::query($name, chain: $chain);
}
throw new QueryException('Could not resolve ' . $name . '!'

View file

@ -19,6 +19,7 @@ use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use RuntimeException;
use function class_exists;
/**
@ -52,10 +53,11 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
/**
* @param ReflectionClass $class the class to instantiate
* @param list<class-string> $chain
* @return object the created class
* @suppress PhanUndeclaredClassInstanceof
*/
private function buildClass(ReflectionClass $class): object {
private function buildClass(ReflectionClass $class, array $chain): object {
$constructor = $class->getConstructor();
if ($constructor === null) {
/* No constructor, return a instance directly */
@ -64,17 +66,20 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
if (PHP_VERSION_ID >= 80400 && self::$useLazyObjects && !$class->isInternal()) {
/* For PHP>=8.4, use a lazy ghost to delay constructor and dependency resolving */
/** @psalm-suppress UndefinedMethod */
return $class->newLazyGhost(function (object $object) use ($constructor): void {
return $class->newLazyGhost(function (object $object) use ($constructor, $chain): void {
/** @psalm-suppress DirectConstructorCall For lazy ghosts we have to call the constructor directly */
$object->__construct(...$this->buildClassConstructorParameters($constructor));
$object->__construct(...$this->buildClassConstructorParameters($constructor, $chain));
});
} else {
return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor));
return $class->newInstanceArgs($this->buildClassConstructorParameters($constructor, $chain));
}
}
private function buildClassConstructorParameters(\ReflectionMethod $constructor): array {
return array_map(function (ReflectionParameter $parameter) {
/**
* @param list<class-string> $chain
*/
private function buildClassConstructorParameters(\ReflectionMethod $constructor, array $chain): array {
return array_map(function (ReflectionParameter $parameter) use ($chain) {
$parameterType = $parameter->getType();
$resolveName = $parameter->getName();
@ -87,7 +92,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
try {
$builtIn = $parameterType !== null && ($parameterType instanceof ReflectionNamedType)
&& $parameterType->isBuiltin();
return $this->query($resolveName, !$builtIn);
return $this->query($resolveName, !$builtIn, $chain);
} catch (ContainerExceptionInterface $e) {
// Service not found, use the default value when available
if ($parameter->isDefaultValueAvailable()) {
@ -97,7 +102,7 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
if ($parameterType !== null && ($parameterType instanceof ReflectionNamedType) && !$parameterType->isBuiltin()) {
$resolveName = $parameter->getName();
try {
return $this->query($resolveName);
return $this->query($resolveName, chain: $chain);
} catch (ContainerExceptionInterface $e2) {
// Pass null if typed and nullable
if ($parameter->allowsNull() && ($parameterType instanceof ReflectionNamedType)) {
@ -114,12 +119,16 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
}, $constructor->getParameters());
}
public function resolve($name) {
/**
* @inheritDoc
* @param list<class-string> $chain
*/
public function resolve($name, array $chain = []) {
$baseMsg = 'Could not resolve ' . $name . '!';
try {
$class = new ReflectionClass($name);
if ($class->isInstantiable()) {
return $this->buildClass($class);
return $this->buildClass($class, $chain);
} else {
throw new QueryException($baseMsg
. ' Class can not be instantiated');
@ -130,14 +139,22 @@ class SimpleContainer implements ArrayAccess, ContainerInterface, IContainer {
}
}
public function query(string $name, bool $autoload = true) {
/**
* @inheritDoc
* @param list<class-string> $chain
*/
public function query(string $name, bool $autoload = true, array $chain = []) {
$name = $this->sanitizeName($name);
if (isset($this->container[$name])) {
return $this->container[$name];
}
if ($autoload) {
$object = $this->resolve($name);
if (in_array($name, $chain, true)) {
throw new RuntimeException('Tried to query ' . $name . ', but it is already in the chain: ' . implode(', ', $chain));
}
$object = $this->resolve($name, array_merge($chain, [$name]));
$this->registerService($name, function () use ($object) {
return $object;
});

View file

@ -111,6 +111,7 @@ class ServerContainer extends SimpleContainer {
/**
* @template T
* @param class-string<T>|string $name
* @param list<class-string> $chain
* @return T|mixed
* @psalm-template S as class-string<T>|string
* @psalm-param S $name
@ -118,13 +119,13 @@ class ServerContainer extends SimpleContainer {
* @throws QueryException
* @deprecated 20.0.0 use \Psr\Container\ContainerInterface::get
*/
public function query(string $name, bool $autoload = true) {
public function query(string $name, bool $autoload = true, array $chain = []) {
$name = $this->sanitizeName($name);
if (str_starts_with($name, 'OCA\\')) {
// Skip server container query for app namespace classes
try {
return parent::query($name, false);
return parent::query($name, false, $chain);
} catch (QueryException $e) {
// Continue with general autoloading then
}
@ -132,7 +133,7 @@ class ServerContainer extends SimpleContainer {
// the apps container first.
if (($appContainer = $this->getAppContainerForService($name)) !== null) {
try {
return $appContainer->queryNoFallback($name);
return $appContainer->queryNoFallback($name, $chain);
} catch (QueryException $e) {
// Didn't find the service or the respective app container
// In this case the service won't be part of the core container,
@ -144,14 +145,14 @@ class ServerContainer extends SimpleContainer {
$segments = explode('\\', $name);
try {
$appContainer = $this->getAppContainer(strtolower($segments[1]), $segments[1]);
return $appContainer->queryNoFallback($name);
return $appContainer->queryNoFallback($name, $chain);
} catch (QueryException $e) {
// Didn't find the service or the respective app container,
// ignore it and fall back to the core container.
}
}
return parent::query($name, $autoload);
return parent::query($name, $autoload, $chain);
}
/**