use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
+use wcf\data\DatabaseObject;
use wcf\event\endpoint\ControllerCollecting;
use wcf\http\attribute\AllowHttpMethod;
use wcf\system\cache\builder\ApiEndpointCacheBuilder;
+use wcf\system\endpoint\HydrateFromRequestParameter;
use wcf\system\endpoint\IController;
use wcf\system\endpoint\RequestFailure;
use wcf\system\endpoint\RequestType;
$controller = $result->handler;
try {
+ $this->hydrateFromRequestParameters($controller, $result->variables);
+
return $controller($request, $result->variables);
} catch (MappingError $e) {
return $this->toErrorResponse(RequestFailure::ValidationFailed, 'mapping_error', $e->getMessage());
return $endpoint;
}
+ private function hydrateFromRequestParameters(
+ IController $controller,
+ /** @var array<string, string> */
+ array $variables
+ ): void {
+ $reflectionClass = new \ReflectionClass($controller);
+ $properties = $reflectionClass->getProperties(\ReflectionProperty::IS_PUBLIC);
+ foreach ($properties as $property) {
+ $attribute = $property->getAttributes(HydrateFromRequestParameter::class)[0] ?? false;
+ if ($attribute === false) {
+ continue;
+ }
+
+ $propertyName = \sprintf(
+ '%s::$%s',
+ $reflectionClass->getName(),
+ $property->getName(),
+ );
+
+ $propertyType = $property->getType();
+ if ($propertyType === null) {
+ throw new \RuntimeException("Cannot determine the type of {$propertyName}.");
+ }
+
+ if (
+ !($propertyType instanceof \ReflectionNamedType)
+ || !\is_subclass_of($propertyType->getName(), DatabaseObject::class)
+ ) {
+ throw new \RuntimeException(
+ \sprintf(
+ "Only types deriving from %s are permitted for %s.",
+ DatabaseObject::class,
+ $propertyName,
+ ),
+ );
+ }
+
+ $variableName = $attribute->newInstance()->parameterName;
+ if (!isset($variables[$variableName])) {
+ throw new \RuntimeException(
+ \sprintf(
+ "The variable '%s' for %s does not appear in the request variables, please check its spelling and if it appears in the route definition.",
+ $variableName,
+ $propertyName,
+ ),
+ );
+ }
+
+ if ($property->isReadOnly()) {
+ throw new \RuntimeException("{$propertyName} must not be declared as readonly.");
+ }
+
+ $className = $propertyType->getName();
+ $dbo = new $className($variables[$variableName]);
+ \assert($dbo instanceof DatabaseObject);
+
+ if (!$dbo->getObjectID()) {
+ throw new UserInputException(
+ $dbo->getDatabaseTableIndexName(),
+ );
+ }
+
+ $controller->{$property->getName()} = $dbo;
+ }
+ }
+
private function toErrorResponse(
RequestFailure $reason,
string $code,
use wcf\data\file\FileEditor;
use wcf\data\file\temporary\FileTemporary;
use wcf\data\file\temporary\FileTemporaryEditor;
+use wcf\system\endpoint\HydrateFromRequestParameter;
use wcf\system\endpoint\IController;
use wcf\system\endpoint\PostRequest;
use wcf\system\exception\UserInputException;
#[PostRequest('/core/files/upload/{identifier}/chunk/{sequenceNo:\d+}')]
final class PostChunk implements IController
{
+ #[HydrateFromRequestParameter('identifier')]
+ public FileTemporary $fileTemporary;
+
/**
* Read data in chunks to avoid hitting the memory limit.
* See https://stackoverflow.com/a/61997147
throw new UserInputException('chunk-checksum-sha256');
}
- $identifier = $variables['identifier'];
$sequenceNo = $variables['sequenceNo'];
- $fileTemporary = new FileTemporary($identifier);
- if (!$fileTemporary->identifier) {
- throw new UserInputException('identifier');
- }
-
// Check if this is a valid sequence no.
- if ($sequenceNo >= $fileTemporary->getChunkCount()) {
+ if ($sequenceNo >= $this->fileTemporary->getChunkCount()) {
throw new UserInputException('sequenceNo', 'outOfRange');
}
// Check if this chunk has already been written.
- if ($fileTemporary->hasChunk($sequenceNo)) {
+ if ($this->fileTemporary->hasChunk($sequenceNo)) {
throw new UserInputException('sequenceNo', 'alreadyExists');
}
// Validate the chunk size.
- $chunkSize = $fileTemporary->getChunkSize();
+ $chunkSize = $this->fileTemporary->getChunkSize();
$stream = $request->getBody();
$receivedSize = $stream->getSize();
if ($receivedSize !== null && $receivedSize > $chunkSize) {
throw new UserInputException('payload', 'tooLarge');
}
- $tmpPath = $fileTemporary->getPath();
+ $tmpPath = $this->fileTemporary->getPath();
if (!\is_dir($tmpPath)) {
\mkdir($tmpPath, recursive: true);
}
- $file = new File($tmpPath . $fileTemporary->getFilename(), 'cb+');
+ $file = new File($tmpPath . $this->fileTemporary->getFilename(), 'cb+');
$file->lock(\LOCK_EX);
$file->seek($sequenceNo * $chunkSize);
}
// Mark the chunk as written.
- $chunks = $fileTemporary->chunks;
+ $chunks = $this->fileTemporary->chunks;
$chunks[$sequenceNo] = '1';
- (new FileTemporaryEditor($fileTemporary))->update([
+ (new FileTemporaryEditor($this->fileTemporary))->update([
'chunks' => $chunks,
]);
// Check if we have all chunks.
- if ($chunks === \str_repeat('1', $fileTemporary->getChunkCount())) {
+ if ($chunks === \str_repeat('1', $this->fileTemporary->getChunkCount())) {
// Check if the final result matches the expected checksum.
- $checksum = \hash_file('sha256', $tmpPath . $fileTemporary->getFilename());
- if ($checksum !== $fileTemporary->fileHash) {
+ $checksum = \hash_file('sha256', $tmpPath . $this->fileTemporary->getFilename());
+ if ($checksum !== $this->fileTemporary->fileHash) {
throw new UserInputException('file', 'checksum');
}
- $file = FileEditor::createFromTemporary($fileTemporary);
+ $file = FileEditor::createFromTemporary($this->fileTemporary);
- $context = $fileTemporary->getContext();
- (new FileTemporaryEditor($fileTemporary))->delete();
- unset($fileTemporary);
+ $context = $this->fileTemporary->getContext();
+ (new FileTemporaryEditor($this->fileTemporary))->delete();
+ unset($this->fileTemporary);
$processor = $file->getProcessor();
$processor?->adopt($file, $context);