Skip to content

Commit 63d5c62

Browse files
Merge pull request #493 from ReCodEx/validation-bugfixes
Validation bugfixes, File type addition, and Performance improvements
2 parents 6cc69f0 + 5ca772a commit 63d5c62

File tree

15 files changed

+522
-22
lines changed

15 files changed

+522
-22
lines changed

app/V1Module/presenters/UploadedFilesPresenter.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace App\V1Module\Presenters;
44

5+
use App\Helpers\MetaFormats\Attributes\File;
56
use App\Helpers\MetaFormats\Attributes\Post;
67
use App\Helpers\MetaFormats\Attributes\Query;
78
use App\Helpers\MetaFormats\Attributes\Path;
9+
use App\Helpers\MetaFormats\FileRequestType;
810
use App\Helpers\MetaFormats\Validators\VInt;
911
use App\Helpers\MetaFormats\Validators\VString;
1012
use App\Helpers\MetaFormats\Validators\VUuid;
@@ -321,6 +323,7 @@ public function checkUpload()
321323
* @throws CannotReceiveUploadedFileException
322324
* @throws InternalServerException
323325
*/
326+
#[File(FileRequestType::FormData, "The whole file to be uploaded")]
324327
public function actionUpload()
325328
{
326329
$user = $this->getCurrentUser();
@@ -440,6 +443,7 @@ public function checkAppendPartial(string $id)
440443
*/
441444
#[Query("offset", new VInt(), "Offset of the chunk for verification", required: true)]
442445
#[Path("id", new VUuid(), "Identifier of the partial file", required: true)]
446+
#[File(FileRequestType::OctetStream, "A chunk of the uploaded file", required: false)]
443447
public function actionAppendPartial(string $id, int $offset)
444448
{
445449
$partialFile = $this->uploadedPartialFiles->findOrThrow($id);

app/V1Module/presenters/base/BasePresenter.php

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use App\Responses\StorageFileResponse;
2727
use App\Responses\ZipFilesResponse;
2828
use Nette\Application\Application;
29+
use Nette\Http\FileUpload;
2930
use Nette\Http\IResponse;
3031
use Tracy\ILogger;
3132
use ReflectionClass;
@@ -213,7 +214,14 @@ private function processParams(ReflectionMethod $reflection)
213214
}
214215

215216
// handle loose parameters
216-
$paramData = MetaFormatHelper::extractRequestParamData($reflection);
217+
218+
// cache the data from the loose attributes to improve performance
219+
$actionPath = get_class($this) . $reflection->name;
220+
if (!FormatCache::looseParametersCached($actionPath)) {
221+
$newParamData = MetaFormatHelper::extractRequestParamData($reflection);
222+
FormatCache::cacheLooseParameters($actionPath, $newParamData);
223+
}
224+
$paramData = FormatCache::getLooseParameters($actionPath);
217225
$this->processParamsLoose($paramData);
218226
}
219227

@@ -279,7 +287,7 @@ private function processParamsFormat(string $format, ?array $valueDictionary): M
279287
}
280288

281289
// this throws if the value is invalid
282-
$formatInstance->checkedAssign($fieldName, $value);
290+
$formatInstance->checkedAssignWithSchema($requestParamData, $fieldName, $value);
283291
}
284292

285293
// validate structural constraints
@@ -305,6 +313,8 @@ private function getValueFromParamData(RequestParamData $paramData): mixed
305313
return $this->getQueryField($paramData->name, required: $paramData->required);
306314
case Type::Path:
307315
return $this->getPathField($paramData->name);
316+
case Type::File:
317+
return $this->getFileField(required: $paramData->required);
308318
default:
309319
throw new InternalServerException("Unknown parameter type: {$paramData->type->name}");
310320
}
@@ -338,6 +348,30 @@ private function getPostField($param, $required = true)
338348
}
339349
}
340350

351+
/**
352+
* @param bool $required Whether the file field is required.
353+
* @throws BadRequestException Thrown when the number of files is not 1 (and the field is required).
354+
* @return FileUpload|null Returns a FileUpload object or null if the file was optional and not sent.
355+
*/
356+
private function getFileField(bool $required = true): FileUpload | null
357+
{
358+
$req = $this->getRequest();
359+
$files = $req->getFiles();
360+
361+
if (count($files) === 0) {
362+
if ($required) {
363+
throw new BadRequestException("No file was uploaded");
364+
} else {
365+
return null;
366+
}
367+
} elseif (count($files) > 1) {
368+
throw new BadRequestException("Too many files were uploaded");
369+
}
370+
371+
$file = array_pop($files);
372+
return $file;
373+
}
374+
341375
private function getQueryField($param, $required = true)
342376
{
343377
$value = $this->getRequest()->getParameter($param);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Helpers\MetaFormats\Attributes;
4+
5+
use App\Helpers\MetaFormats\FileRequestType;
6+
use App\Helpers\MetaFormats\Type;
7+
use App\Helpers\MetaFormats\Validators\VFile;
8+
use Attribute;
9+
10+
/**
11+
* Attribute used to annotate format definition properties representing a file parameter.
12+
*/
13+
#[Attribute(Attribute::TARGET_PROPERTY)]
14+
class FFile extends FormatParameterAttribute
15+
{
16+
/**
17+
* @param FileRequestType $fileRequestType How will the file be transmitted in the request.
18+
* @param string $description The description of the request parameter.
19+
* @param bool $required Whether the request parameter is required.
20+
*/
21+
public function __construct(
22+
FileRequestType $fileRequestType,
23+
string $description = "",
24+
bool $required = true,
25+
) {
26+
parent::__construct(Type::File, new VFile($fileRequestType), $description, $required, false);
27+
}
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace App\Helpers\MetaFormats\Attributes;
4+
5+
use App\Helpers\MetaFormats\FileRequestType;
6+
use App\Helpers\MetaFormats\Type;
7+
use App\Helpers\MetaFormats\Validators\VFile;
8+
use Attribute;
9+
10+
/**
11+
* Attribute used to specify that an endpoint expects a file.
12+
*/
13+
#[Attribute(Attribute::TARGET_METHOD)]
14+
class File extends Param
15+
{
16+
/**
17+
* @param FileRequestType $fileRequestType How will the file be transmitted in the request.
18+
* @param string $description The description of the request parameter.
19+
* @param bool $required Whether the request parameter is required.
20+
*/
21+
public function __construct(
22+
FileRequestType $fileRequestType,
23+
string $description = "",
24+
bool $required = true,
25+
) {
26+
parent::__construct(Type::File, "file", new VFile($fileRequestType), $description, $required, false);
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace App\Helpers\MetaFormats;
4+
5+
// @codingStandardsIgnoreStart
6+
/**
7+
* An enumeration of types how files can be transmitted.
8+
*/
9+
enum FileRequestType
10+
{
11+
case OctetStream;
12+
case FormData;
13+
}
14+
// @codingStandardsIgnoreEnd

app/helpers/MetaFormats/FormatCache.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,37 @@ class FormatCache
1616
private static ?array $formatNamesHashSet = null;
1717
private static ?array $formatToFieldFormatsMap = null;
1818

19+
// this array caches loose attribute data which are added over time by the presenters
20+
private static array $actionToRequestParamDataMap = [];
21+
22+
/**
23+
* @param string $actionPath The presenter class name joined with the name of the action method.
24+
* @return bool Returns whether the loose parameters of the action are cached.
25+
*/
26+
public static function looseParametersCached(string $actionPath): bool
27+
{
28+
return array_key_exists($actionPath, self::$actionToRequestParamDataMap);
29+
}
30+
31+
/**
32+
* @param string $actionPath The presenter class name joined with the name of the action method.
33+
* @return array Returns the cached RequestParamData array of the loose attributes.
34+
*/
35+
public static function getLooseParameters(string $actionPath): array
36+
{
37+
return self::$actionToRequestParamDataMap[$actionPath];
38+
}
39+
40+
/**
41+
* Caches a RequestParamData array from the loose attributes of an action.
42+
* @param string $actionPath The presenter class name joined with the name of the action method.
43+
* @param array $data The RequestParamData array to be cached.
44+
*/
45+
public static function cacheLooseParameters(string $actionPath, array $data): void
46+
{
47+
self::$actionToRequestParamDataMap[$actionPath] = $data;
48+
}
49+
1950
/**
2051
* @return array Returns a dictionary of dictionaries: [<formatName> => [<fieldName> => RequestParamData, ...], ...]
2152
* mapping formats to their fields and field metadata.

app/helpers/MetaFormats/MetaFormat.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@ public function checkedAssign(string $fieldName, mixed $value)
4141
$this->$fieldName = $value;
4242
}
4343

44+
/**
45+
* Tries to assign a value to a field. If the value does not conform to the provided schema, an exception is thrown.
46+
* The exception details why the value does not conform to the format.
47+
* More performant version of checkedAssign.
48+
* @param RequestParamData $requestParamData The schema of the request parameter.
49+
* @param string $fieldName The name of the field.
50+
* @param mixed $value The value to be assigned.
51+
* @throws InvalidApiArgumentException Thrown when the value is not assignable.
52+
*/
53+
public function checkedAssignWithSchema(RequestParamData $requestParamData, string $fieldName, mixed $value)
54+
{
55+
$requestParamData->conformsToDefinition($value);
56+
$this->$fieldName = $value;
57+
}
58+
4459
/**
4560
* Validates the given format.
4661
* @throws InvalidApiArgumentException Thrown when a value is not assignable.

app/helpers/MetaFormats/MetaFormatHelper.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace App\Helpers\MetaFormats;
44

55
use App\Exceptions\InternalServerException;
6+
use App\Helpers\MetaFormats\Attributes\FFile;
7+
use App\Helpers\MetaFormats\Attributes\File;
68
use App\Helpers\MetaFormats\Attributes\Format;
79
use App\Helpers\MetaFormats\Attributes\FormatParameterAttribute;
810
use App\Helpers\MetaFormats\Attributes\FPath;
@@ -50,8 +52,9 @@ public static function getEndpointAttributes(ReflectionMethod $reflectionMethod)
5052
$path = $reflectionMethod->getAttributes(name: Path::class);
5153
$query = $reflectionMethod->getAttributes(name: Query::class);
5254
$post = $reflectionMethod->getAttributes(name: Post::class);
55+
$file = $reflectionMethod->getAttributes(name: File::class);
5356
$param = $reflectionMethod->getAttributes(name: Param::class);
54-
return array_merge($path, $query, $post, $param);
57+
return array_merge($path, $query, $post, $file, $param);
5558
}
5659

5760
/**
@@ -91,7 +94,14 @@ public static function extractFormatParameterData(ReflectionProperty $reflection
9194
$pathAttributes = $reflectionObject->getAttributes(FPath::class);
9295
$queryAttributes = $reflectionObject->getAttributes(FQuery::class);
9396
$postAttributes = $reflectionObject->getAttributes(FPost::class);
94-
$requestAttributes = array_merge($longAttributes, $pathAttributes, $queryAttributes, $postAttributes);
97+
$fileAttributes = $reflectionObject->getAttributes(FFile::class);
98+
$requestAttributes = array_merge(
99+
$longAttributes,
100+
$pathAttributes,
101+
$queryAttributes,
102+
$postAttributes,
103+
$fileAttributes
104+
);
95105

96106
// there should be only one attribute
97107
if (count($requestAttributes) == 0) {

app/helpers/MetaFormats/RequestParamData.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use App\Exceptions\InvalidApiArgumentException;
77
use App\Helpers\MetaFormats\Validators\BaseValidator;
88
use App\Helpers\MetaFormats\Validators\VArray;
9+
use App\Helpers\MetaFormats\Validators\VFile;
910
use App\Helpers\MetaFormats\Validators\VObject;
1011
use App\Helpers\Swagger\AnnotationParameterData;
1112

@@ -143,6 +144,12 @@ public function toAnnotationParameterData()
143144
}, $nestedRequestParmData);
144145
}
145146

147+
// get file request type if file
148+
$fileRequestType = null;
149+
if ($this->validators[0] instanceof VFile) {
150+
$fileRequestType = $this->validators[0]->fileRequestType;
151+
}
152+
146153
return new AnnotationParameterData(
147154
$swaggerType,
148155
$this->name,
@@ -155,6 +162,7 @@ public function toAnnotationParameterData()
155162
$arrayDepth,
156163
$nestedObjectParameterData,
157164
$constraints,
165+
$fileRequestType,
158166
);
159167
}
160168
}

app/helpers/MetaFormats/Type.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ enum Type
1111
case Post;
1212
case Query;
1313
case Path;
14+
case File;
1415
}
1516
// @codingStandardsIgnoreEnd

0 commit comments

Comments
 (0)