mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-22 08:56:42 -06:00
Extract device long URL validation to its own validation class
This commit is contained in:
parent
822652cac3
commit
3e26f1113d
@ -6,16 +6,23 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Entity;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
|
||||||
|
|
||||||
class DeviceLongUrl extends AbstractEntity
|
class DeviceLongUrl extends AbstractEntity
|
||||||
{
|
{
|
||||||
public function __construct(
|
private ShortUrl $shortUrl; // @phpstan-ignore-line
|
||||||
public readonly ShortUrl $shortUrl,
|
|
||||||
|
private function __construct(
|
||||||
public readonly DeviceType $deviceType,
|
public readonly DeviceType $deviceType,
|
||||||
private string $longUrl,
|
private string $longUrl,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function fromPair(DeviceLongUrlPair $pair): self
|
||||||
|
{
|
||||||
|
return new self($pair->deviceType, $pair->longUrl);
|
||||||
|
}
|
||||||
|
|
||||||
public function longUrl(): string
|
public function longUrl(): string
|
||||||
{
|
{
|
||||||
return $this->longUrl;
|
return $this->longUrl;
|
||||||
|
@ -4,14 +4,21 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||||
|
|
||||||
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
||||||
{
|
{
|
||||||
public function __construct(private UrlValidatorInterface $urlValidator)
|
public function __construct(private readonly UrlValidatorInterface $urlValidator)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of TitleResolutionModelInterface
|
||||||
|
* @param T $data
|
||||||
|
* @return T
|
||||||
|
* @throws InvalidUrlException
|
||||||
|
*/
|
||||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
||||||
{
|
{
|
||||||
if ($data->hasTitle()) {
|
if ($data->hasTitle()) {
|
||||||
|
@ -9,6 +9,9 @@ use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
|||||||
interface ShortUrlTitleResolutionHelperInterface
|
interface ShortUrlTitleResolutionHelperInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
* @template T of TitleResolutionModelInterface
|
||||||
|
* @param T $data
|
||||||
|
* @return T
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface;
|
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface;
|
||||||
|
@ -12,5 +12,5 @@ interface TitleResolutionModelInterface
|
|||||||
|
|
||||||
public function doValidateUrl(): bool;
|
public function doValidateUrl(): bool;
|
||||||
|
|
||||||
public function withResolvedTitle(string $title): self;
|
public function withResolvedTitle(string $title): static;
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withResolvedTitle(string $title): self
|
public function withResolvedTitle(string $title): static
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
longUrl: $this->longUrl,
|
longUrl: $this->longUrl,
|
||||||
|
@ -76,7 +76,7 @@ final class ShortUrlEdition implements TitleResolutionModelInterface
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withResolvedTitle(string $title): self
|
public function withResolvedTitle(string $title): static
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
longUrlPropWasProvided: $this->longUrlPropWasProvided,
|
longUrlPropWasProvided: $this->longUrlPropWasProvided,
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
|
||||||
|
|
||||||
|
use Laminas\Validator\AbstractValidator;
|
||||||
|
use Laminas\Validator\ValidatorChain;
|
||||||
|
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||||
|
|
||||||
|
use function array_keys;
|
||||||
|
use function array_values;
|
||||||
|
use function Functional\contains;
|
||||||
|
use function Functional\every;
|
||||||
|
use function is_array;
|
||||||
|
use function Shlinkio\Shlink\Core\enumValues;
|
||||||
|
|
||||||
|
class DeviceLongUrlsValidator extends AbstractValidator
|
||||||
|
{
|
||||||
|
private const NOT_ARRAY = 'NOT_ARRAY';
|
||||||
|
private const INVALID_DEVICE = 'INVALID_DEVICE';
|
||||||
|
private const INVALID_LONG_URL = 'INVALID_LONG_URL';
|
||||||
|
|
||||||
|
protected array $messageTemplates = [
|
||||||
|
self::NOT_ARRAY => 'Provided value is not an array.',
|
||||||
|
self::INVALID_DEVICE => 'You have provided at least one invalid device identifier.',
|
||||||
|
self::INVALID_LONG_URL => 'At least one of the long URLs are invalid.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(private readonly ValidatorChain $longUrlValidators)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isValid(mixed $value): bool
|
||||||
|
{
|
||||||
|
if (! is_array($value)) {
|
||||||
|
$this->error(self::NOT_ARRAY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validValues = enumValues(DeviceType::class);
|
||||||
|
$keys = array_keys($value);
|
||||||
|
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
|
||||||
|
$this->error(self::INVALID_DEVICE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$longUrls = array_values($value);
|
||||||
|
$result = every($longUrls, $this->longUrlValidators->isValid(...));
|
||||||
|
if (! $result) {
|
||||||
|
$this->error(self::INVALID_LONG_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@ -10,16 +10,9 @@ use Laminas\InputFilter\InputFilter;
|
|||||||
use Laminas\Validator;
|
use Laminas\Validator;
|
||||||
use Shlinkio\Shlink\Common\Validation;
|
use Shlinkio\Shlink\Common\Validation;
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
use function array_keys;
|
|
||||||
use function array_values;
|
|
||||||
use function Functional\contains;
|
|
||||||
use function Functional\every;
|
|
||||||
use function is_array;
|
|
||||||
use function is_string;
|
use function is_string;
|
||||||
use function Shlinkio\Shlink\Core\enumValues;
|
|
||||||
use function str_replace;
|
use function str_replace;
|
||||||
use function substr;
|
use function substr;
|
||||||
use function trim;
|
use function trim;
|
||||||
@ -64,37 +57,20 @@ class ShortUrlInputFilter extends InputFilter
|
|||||||
|
|
||||||
private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
|
private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
|
||||||
{
|
{
|
||||||
$notEmptyValidator = new Validator\NotEmpty([
|
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
||||||
|
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
|
||||||
Validator\NotEmpty::OBJECT,
|
Validator\NotEmpty::OBJECT,
|
||||||
Validator\NotEmpty::SPACE,
|
Validator\NotEmpty::SPACE,
|
||||||
Validator\NotEmpty::NULL,
|
Validator\NotEmpty::NULL,
|
||||||
Validator\NotEmpty::EMPTY_ARRAY,
|
Validator\NotEmpty::EMPTY_ARRAY,
|
||||||
Validator\NotEmpty::BOOLEAN,
|
Validator\NotEmpty::BOOLEAN,
|
||||||
Validator\NotEmpty::STRING,
|
Validator\NotEmpty::STRING,
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
|
|
||||||
$longUrlInput->getValidatorChain()->attach($notEmptyValidator);
|
|
||||||
$this->add($longUrlInput);
|
$this->add($longUrlInput);
|
||||||
|
|
||||||
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
|
$deviceLongUrlsInput = $this->createInput(self::DEVICE_LONG_URLS, false);
|
||||||
$deviceLongUrlsInput->getValidatorChain()->attach( // TODO Extract callback to own validator
|
$deviceLongUrlsInput->getValidatorChain()->attach(
|
||||||
new Validator\Callback(function (mixed $value) use ($notEmptyValidator): bool {
|
new DeviceLongUrlsValidator($longUrlInput->getValidatorChain()),
|
||||||
if (! is_array($value)) {
|
|
||||||
// TODO Set proper error: Not array
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$validValues = enumValues(DeviceType::class);
|
|
||||||
$keys = array_keys($value);
|
|
||||||
if (! every($keys, static fn ($key) => contains($validValues, $key))) {
|
|
||||||
// TODO Set proper error: Provided invalid device type
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$longUrls = array_values($value);
|
|
||||||
return every($longUrls, $notEmptyValidator->isValid(...));
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
$this->add($deviceLongUrlsInput);
|
$this->add($deviceLongUrlsInput);
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInt
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $tags
|
* @param string[] $tags
|
||||||
* @return Collection|Tag[]
|
* @return Collection<int, Tag>
|
||||||
*/
|
*/
|
||||||
public function resolveTags(array $tags): Collections\Collection
|
public function resolveTags(array $tags): Collections\Collection
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ interface ShortUrlRelationResolverInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $tags
|
* @param string[] $tags
|
||||||
* @return Collection|Tag[]
|
* @return Collection<int, Tag>
|
||||||
*/
|
*/
|
||||||
public function resolveTags(array $tags): Collection;
|
public function resolveTags(array $tags): Collection;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ class SimpleShortUrlRelationResolver implements ShortUrlRelationResolverInterfac
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string[] $tags
|
* @param string[] $tags
|
||||||
* @return Collection|Tag[]
|
* @return Collection<int, Tag>
|
||||||
*/
|
*/
|
||||||
public function resolveTags(array $tags): Collections\Collection
|
public function resolveTags(array $tags): Collections\Collection
|
||||||
{
|
{
|
||||||
|
@ -34,7 +34,6 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||||||
?ApiKey $apiKey = null,
|
?ApiKey $apiKey = null,
|
||||||
): ShortUrl {
|
): ShortUrl {
|
||||||
if ($shortUrlEdit->longUrlWasProvided()) {
|
if ($shortUrlEdit->longUrlWasProvided()) {
|
||||||
/** @var ShortUrlEdition $shortUrlEdit */
|
|
||||||
$shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit);
|
$shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,22 +31,21 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
* @throws NonUniqueSlugException
|
* @throws NonUniqueSlugException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function shorten(ShortUrlCreation $meta): ShortUrl
|
public function shorten(ShortUrlCreation $creation): ShortUrl
|
||||||
{
|
{
|
||||||
// First, check if a short URL exists for all provided params
|
// First, check if a short URL exists for all provided params
|
||||||
$existingShortUrl = $this->findExistingShortUrlIfExists($meta);
|
$existingShortUrl = $this->findExistingShortUrlIfExists($creation);
|
||||||
if ($existingShortUrl !== null) {
|
if ($existingShortUrl !== null) {
|
||||||
return $existingShortUrl;
|
return $existingShortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var \Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation $meta */
|
$creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation);
|
||||||
$meta = $this->titleResolutionHelper->processTitleAndValidateUrl($meta);
|
|
||||||
|
|
||||||
/** @var ShortUrl $newShortUrl */
|
/** @var ShortUrl $newShortUrl */
|
||||||
$newShortUrl = $this->em->wrapInTransaction(function () use ($meta) {
|
$newShortUrl = $this->em->wrapInTransaction(function () use ($creation): ShortUrl {
|
||||||
$shortUrl = ShortUrl::create($meta, $this->relationResolver);
|
$shortUrl = ShortUrl::create($creation, $this->relationResolver);
|
||||||
|
|
||||||
$this->verifyShortCodeUniqueness($meta, $shortUrl);
|
$this->verifyShortCodeUniqueness($creation, $shortUrl);
|
||||||
$this->em->persist($shortUrl);
|
$this->em->persist($shortUrl);
|
||||||
|
|
||||||
return $shortUrl;
|
return $shortUrl;
|
||||||
|
@ -15,5 +15,5 @@ interface UrlShortenerInterface
|
|||||||
* @throws NonUniqueSlugException
|
* @throws NonUniqueSlugException
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function shorten(ShortUrlCreation $meta): ShortUrl;
|
public function shorten(ShortUrlCreation $creation): ShortUrl;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ class ApiKey extends AbstractEntity
|
|||||||
private string $key;
|
private string $key;
|
||||||
private ?Chronos $expirationDate = null;
|
private ?Chronos $expirationDate = null;
|
||||||
private bool $enabled;
|
private bool $enabled;
|
||||||
/** @var Collection|ApiKeyRole[] */
|
/** @var Collection<string, ApiKeyRole> */
|
||||||
private Collection $roles;
|
private Collection $roles;
|
||||||
private ?string $name = null;
|
private ?string $name = null;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user