Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/V1Module/presenters/GroupsPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ public function checkGetExamLocks(string $id, string $examId)
* @GET
*/
#[Path("id", new VString(), "An identifier of the related group", required: true)]
#[Path("examId", new VString(), "An identifier of the exam", required: true)]
#[Path("examId", new VInt(), "An identifier of the exam", required: true)]
public function actionGetExamLocks(string $id, string $examId)
{
$group = $this->groups->findOrThrow($id);
Expand Down
66 changes: 50 additions & 16 deletions app/V1Module/presenters/base/BasePresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,32 +214,41 @@ private function processParams(ReflectionMethod $reflection)
// use a method specialized for formats if there is a format available
$format = MetaFormatHelper::extractFormatFromAttribute($reflection);
if ($format !== null) {
$this->processParamsFormat($format);
return;
$this->requestFormatInstance = $this->processParamsFormat($format, null);
}

// otherwise use a method for loose parameters
// handle loose parameters
$paramData = MetaFormatHelper::extractRequestParamData($reflection);
$this->processParamsLoose($paramData);
}

/**
* Processes loose parameters. Request parameters are validated, no new data is created.
* @param array $paramData Parameter data to be validated.
*/
private function processParamsLoose(array $paramData)
{
// validate each param
foreach ($paramData as $param) {
///TODO: path parameters are not checked yet
if ($param->type == Type::Path) {
continue;
}

$paramValue = $this->getValueFromParamData($param);

// this throws when it does not conform
$param->conformsToDefinition($paramValue);
}
}

private function processParamsFormat(string $format)
/**
* Processes parameters defined by a format. Request parameters are validated and a format instance with
* parameter values created.
* @param string $format The format defining the parameters.
* @param ?array $valueDictionary If not null, a nested format instance will be created. The values will be taken
* from here instead of the request object. Format validation ignores parameter type (path, query or post).
* A top-level format will be created if null.
* @throws \App\Exceptions\InternalServerException Thrown when the format definition is corrupted/absent.
* @throws \App\Exceptions\BadRequestException Thrown when the request parameter values do not conform to the definition.
* @return MetaFormat Returns a format instance with values filled from the request object.
*/
private function processParamsFormat(string $format, ?array $valueDictionary): MetaFormat
{
// get the parsed attribute data from the format fields
$formatToFieldDefinitionsMap = FormatCache::getFormatToFieldDefinitionsMap();
Expand All @@ -250,15 +259,29 @@ private function processParamsFormat(string $format)
// maps field names to their attribute data
$nameToFieldDefinitionsMap = $formatToFieldDefinitionsMap[$format];

///TODO: handle nested MetaFormat creation
$formatInstance = MetaFormatHelper::createFormatInstance($format);
foreach ($nameToFieldDefinitionsMap as $fieldName => $requestParamData) {
///TODO: path parameters are not checked yet
if ($requestParamData->type == Type::Path) {
continue;
$value = null;
// top-level format
if ($valueDictionary === null) {
$value = $this->getValueFromParamData($requestParamData);
// nested format
} else {
// Instead of retrieving the values with the getRequest call, use the provided $valueDictionary.
// This makes the nested format ignore the parameter type (path, query, post) which is intended.
// The data for this nested format cannot be spread across multiple param types, but it could be
// if this was not a nested format but the top level format.
if (array_key_exists($requestParamData->name, $valueDictionary)) {
$value = $valueDictionary[$requestParamData->name];
}
}

$value = $this->getValueFromParamData($requestParamData);
// handle nested format creation
// replace the value dictionary stored in $value with a format instance
$nestedFormatName = $requestParamData->getFormatName();
if ($nestedFormatName !== null) {
$value = $this->processParamsFormat($nestedFormatName, $value);
}

// this throws if the value is invalid
$formatInstance->checkedAssign($fieldName, $value);
Expand All @@ -269,11 +292,11 @@ private function processParamsFormat(string $format)
throw new BadRequestException("All request fields are valid but additional structural constraints failed.");
}

$this->requestFormatInstance = $formatInstance;
return $formatInstance;
}

/**
* Calls either getPostField or getQueryField based on the provided metadata.
* Calls either getPostField, getQueryField or getPathField based on the provided metadata.
* @param \App\Helpers\MetaFormats\RequestParamData $paramData Metadata of the request parameter.
* @throws \App\Exceptions\InternalServerException Thrown when an unexpected parameter location was set.
* @return mixed Returns the value from the request.
Expand All @@ -285,6 +308,8 @@ private function getValueFromParamData(RequestParamData $paramData): mixed
return $this->getPostField($paramData->name, required: $paramData->required);
case Type::Query:
return $this->getQueryField($paramData->name, required: $paramData->required);
case Type::Path:
return $this->getPathField($paramData->name);
default:
throw new InternalServerException("Unknown parameter type: {$paramData->type->name}");
}
Expand Down Expand Up @@ -327,6 +352,15 @@ private function getQueryField($param, $required = true)
return $value;
}

private function getPathField($param)
{
$value = $this->getParameter($param);
if ($value === null) {
throw new BadRequestException("Missing required path field $param");
}
return $value;
}

protected function logUserAction($code = IResponse::S200_OK)
{
if ($this->getUser()->isLoggedIn()) {
Expand Down
12 changes: 11 additions & 1 deletion app/helpers/MetaFormats/Attributes/FormatParameterAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Exceptions\InternalServerException;
use App\Helpers\MetaFormats\Type;
use App\Helpers\MetaFormats\Validators\BaseValidator;
use Attribute;

/**
Expand All @@ -13,10 +14,12 @@
class FormatParameterAttribute
{
public Type $type;
/**
* @var BaseValidator[]
*/
public array $validators;
public string $description;
public bool $required;
// there is not an easy way to check whether a property has the nullability flag set
public bool $nullable;

/**
Expand Down Expand Up @@ -50,5 +53,12 @@ public function __construct(
}
$this->validators = $validators;
}

// remove strict type checking for query and path parameters
if ($type === Type::Path || $type === Type::Query) {
foreach ($this->validators as $validator) {
$validator->setStrict(false);
}
}
}
}
40 changes: 31 additions & 9 deletions app/helpers/MetaFormats/RequestParamData.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use App\Exceptions\InternalServerException;
use App\Exceptions\InvalidArgumentException;
use App\Helpers\MetaFormats\Validators\BaseValidator;
use App\Helpers\MetaFormats\Validators\VArray;
use App\Helpers\MetaFormats\Validators\VObject;
use App\Helpers\Swagger\AnnotationParameterData;
use Exception;

Expand All @@ -17,6 +19,9 @@ class RequestParamData
public string $name;
public string $description;
public bool $required;
/**
* @var BaseValidator[]
*/
public array $validators;
public bool $nullable;

Expand Down Expand Up @@ -76,12 +81,21 @@ public function conformsToDefinition(mixed $value)
}
}

private function hasValidators(): bool
/**
* Returns the format name if the parameter should be interpreted as a format and not as a primitive type.
* @return ?string Returns the format name or null if the param represents a primitive type.
*/
public function getFormatName(): ?string
{
if (is_array($this->validators)) {
return count($this->validators) > 0;
// all format params have to have a VObject validator
foreach ($this->validators as $validator) {
if ($validator instanceof VObject) {
return $validator->format;
}
}
return $this->validators !== null;

// return null for primitive types
return null;
}

/**
Expand All @@ -91,7 +105,7 @@ private function hasValidators(): bool
*/
public function toAnnotationParameterData()
{
if (!$this->hasValidators()) {
if (count($this->validators) === 0) {
throw new InternalServerException(
"No validator found for parameter {$this->name}, description: {$this->description}."
);
Expand All @@ -105,10 +119,17 @@ public function toAnnotationParameterData()
$nestedArraySwaggerType = $this->validators[0]->getElementSwaggerType();
}

// retrieve the example value from the getExampleValue method if present
$exampleValue = null;
if (method_exists(get_class($this->validators[0]), "getExampleValue")) {
$exampleValue = $this->validators[0]->getExampleValue();
// get example value from the first validator
$exampleValue = $this->validators[0]->getExampleValue();

// add nested parameter data if this is an object
$format = $this->getFormatName();
$nestedObjectParameterData = null;
if ($format !== null) {
$nestedRequestParmData = FormatCache::getFieldDefinitions($format);
$nestedObjectParameterData = array_map(function (RequestParamData $data) {
return $data->toAnnotationParameterData();
}, $nestedRequestParmData);
}

return new AnnotationParameterData(
Expand All @@ -120,6 +141,7 @@ public function toAnnotationParameterData()
$this->nullable,
$exampleValue,
$nestedArraySwaggerType,
$nestedObjectParameterData,
);
}
}
56 changes: 56 additions & 0 deletions app/helpers/MetaFormats/Validators/BaseValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace App\Helpers\MetaFormats\Validators;

/**
* Base class for all validators.
*/
class BaseValidator
{
public function __construct(bool $strict = true)
{
$this->strict = $strict;
}

/**
* @var string One of the valid swagger types (https://swagger.io/docs/specification/v3_0/data-models/data-types/).
*/
public const SWAGGER_TYPE = "invalid";

/**
* @var bool Whether strict type checking is done in validation.
*/
protected bool $strict;

/**
* Sets the strict flag.
* Expected to be changed by Attributes containing validators to change their behavior based on the Attribute type.
* @param bool $strict Whether validation type checking should be done.
* When false, the validation step will no longer enforce the correct type of the value.
*/
public function setStrict(bool $strict)
{
$this->strict = $strict;
}

/**
* @return string Returns a sample expected value to be validated by the validator.
* This value will be used in generated swagger documents.
* Can return null, signalling to the swagger generator to omit the example field.
*/
public function getExampleValue(): string | null
{
return null;
}

/**
* Validates a value with the configured validation strictness.
* @param mixed $value The value to be validated.
* @return bool Whether the value passed the test.
*/
public function validate(mixed $value): bool
{
// return false by default to enforce overriding in derived types
return false;
}
}
27 changes: 20 additions & 7 deletions app/helpers/MetaFormats/Validators/VArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,27 @@
/**
* Validates arrays and their nested elements.
*/
class VArray
class VArray extends BaseValidator
{
public const SWAGGER_TYPE = "array";

// validator used for elements
private mixed $nestedValidator;
private ?BaseValidator $nestedValidator;

/**
* Creates an array validator.
* @param mixed $nestedValidator A validator that will be applied on all elements
* @param ?BaseValidator $nestedValidator A validator that will be applied on all elements
* (validator arrays are not supported).
*/
public function __construct(mixed $nestedValidator = null)
public function __construct(?BaseValidator $nestedValidator = null, bool $strict = true)
{
parent::__construct($strict);
$this->nestedValidator = $nestedValidator;
}

public function getExampleValue()
public function getExampleValue(): string | null
{
if ($this->nestedValidator !== null && method_exists(get_class($this->nestedValidator), "getExampleValue")) {
if ($this->nestedValidator !== null) {
return $this->nestedValidator->getExampleValue();
}

Expand All @@ -43,7 +44,19 @@ public function getElementSwaggerType(): mixed
return $this->nestedValidator::SWAGGER_TYPE;
}

public function validate(mixed $value)
/**
* Sets the strict flag for this validator and the element validator if present.
* Expected to be changed by Attributes containing validators to change their behavior based on the Attribute type.
* @param bool $strict Whether validation type checking should be done.
* When false, the validation step will no longer enforce the correct type of the value.
*/
public function setStrict(bool $strict)
{
parent::setStrict($strict);
$this->nestedValidator?->setStrict($strict);
}

public function validate(mixed $value): bool
{
if (!is_array($value)) {
return false;
Expand Down
Loading