mirror of
https://github.com/shlinkio/shlink.git
synced 2026-10-13 13:31:59 -05:00
Merge pull request #688 from acelaya-forks/feature/edit-long-url
Feature/edit long url
This commit is contained in:
16 files changed
+267
-84
No files matched your search
@@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* [#556](https://github.com/shlinkio/shlink/issues/556) Short code lengths can now be customized, both globally and on a per-short URL basis.
|
||||
* [#541](https://github.com/shlinkio/shlink/issues/541) Added a request ID that is returned on `X-Request-Id` header, can be provided from outside and is set in log entries.
|
||||
* [#642](https://github.com/shlinkio/shlink/issues/642) IP geolocation is now performed over the non-anonymized IP address when using swoole.
|
||||
* [#521](https://github.com/shlinkio/shlink/issues/521) The long URL for any existing short URL can now be edited using the `PATCH /short-urls/{shortCode}` endpoint.
|
||||
|
||||
#### Changed
|
||||
|
||||
|
||||
@@ -112,6 +112,10 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"description": "The long URL this short URL will redirect to",
|
||||
"type": "string"
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
@@ -157,6 +161,7 @@
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"longUrl",
|
||||
"validSince",
|
||||
"validUntil",
|
||||
"maxVisits"
|
||||
|
||||
@@ -9,6 +9,7 @@ use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Mezzio\Router\RouterInterface;
|
||||
use Mezzio\Template\TemplateRendererInterface;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
|
||||
@@ -39,6 +40,8 @@ return [
|
||||
Action\QrCodeAction::class => ConfigAbstractFactory::class,
|
||||
|
||||
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
|
||||
|
||||
Resolver\PersistenceDomainResolver::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -51,9 +54,9 @@ return [
|
||||
Options\NotFoundRedirectOptions::class => ['config.not_found_redirects'],
|
||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||
|
||||
Service\UrlShortener::class => [Util\UrlValidator::class, 'em', Options\UrlShortenerOptions::class],
|
||||
Service\UrlShortener::class => [Util\UrlValidator::class, 'em', Resolver\PersistenceDomainResolver::class],
|
||||
Service\VisitsTracker::class => ['em', EventDispatcherInterface::class],
|
||||
Service\ShortUrlService::class => ['em', Service\ShortUrl\ShortUrlResolver::class],
|
||||
Service\ShortUrlService::class => ['em', Service\ShortUrl\ShortUrlResolver::class, Util\UrlValidator::class],
|
||||
Service\VisitService::class => ['em'],
|
||||
Service\Tag\TagService::class => ['em'],
|
||||
Service\ShortUrl\DeleteShortUrlService::class => [
|
||||
@@ -63,7 +66,7 @@ return [
|
||||
],
|
||||
Service\ShortUrl\ShortUrlResolver::class => ['em'],
|
||||
|
||||
Util\UrlValidator::class => ['httpClient'],
|
||||
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
|
||||
|
||||
Action\RedirectAction::class => [
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
@@ -84,6 +87,8 @@ return [
|
||||
],
|
||||
|
||||
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
|
||||
|
||||
Resolver\PersistenceDomainResolver::class => ['em'],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -12,6 +12,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
|
||||
use function array_reduce;
|
||||
@@ -93,16 +94,19 @@ class ShortUrl extends AbstractEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updateMeta(ShortUrlMeta $shortCodeMeta): void
|
||||
public function update(ShortUrlEdit $shortUrlEdit): void
|
||||
{
|
||||
if ($shortCodeMeta->hasValidSince()) {
|
||||
$this->validSince = $shortCodeMeta->getValidSince();
|
||||
if ($shortUrlEdit->hasValidSince()) {
|
||||
$this->validSince = $shortUrlEdit->validSince();
|
||||
}
|
||||
if ($shortCodeMeta->hasValidUntil()) {
|
||||
$this->validUntil = $shortCodeMeta->getValidUntil();
|
||||
if ($shortUrlEdit->hasValidUntil()) {
|
||||
$this->validUntil = $shortUrlEdit->validUntil();
|
||||
}
|
||||
if ($shortCodeMeta->hasMaxVisits()) {
|
||||
$this->maxVisits = $shortCodeMeta->getMaxVisits();
|
||||
if ($shortUrlEdit->hasMaxVisits()) {
|
||||
$this->maxVisits = $shortUrlEdit->maxVisits();
|
||||
}
|
||||
if ($shortUrlEdit->hasLongUrl()) {
|
||||
$this->longUrl = $shortUrlEdit->longUrl();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
use function array_key_exists;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
final class ShortUrlEdit
|
||||
{
|
||||
private bool $longUrlPropWasProvided = false;
|
||||
private ?string $longUrl = null;
|
||||
private bool $validSincePropWasProvided = false;
|
||||
private ?Chronos $validSince = null;
|
||||
private bool $validUntilPropWasProvided = false;
|
||||
private ?Chronos $validUntil = null;
|
||||
private bool $maxVisitsPropWasProvided = false;
|
||||
private ?int $maxVisits = null;
|
||||
|
||||
// Enforce named constructors
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public static function fromRawData(array $data): self
|
||||
{
|
||||
$instance = new self();
|
||||
$instance->validateAndInit($data);
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ValidationException
|
||||
*/
|
||||
private function validateAndInit(array $data): void
|
||||
{
|
||||
$inputFilter = new ShortUrlMetaInputFilter($data);
|
||||
if (! $inputFilter->isValid()) {
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
$this->longUrlPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::LONG_URL, $data);
|
||||
$this->validSincePropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_SINCE, $data);
|
||||
$this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data);
|
||||
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data);
|
||||
|
||||
$this->longUrl = $inputFilter->getValue(ShortUrlMetaInputFilter::LONG_URL);
|
||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
}
|
||||
|
||||
private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
}
|
||||
|
||||
public function longUrl(): ?string
|
||||
{
|
||||
return $this->longUrl;
|
||||
}
|
||||
|
||||
public function hasLongUrl(): bool
|
||||
{
|
||||
return $this->longUrlPropWasProvided && $this->longUrl !== null;
|
||||
}
|
||||
|
||||
public function validSince(): ?Chronos
|
||||
{
|
||||
return $this->validSince;
|
||||
}
|
||||
|
||||
public function hasValidSince(): bool
|
||||
{
|
||||
return $this->validSincePropWasProvided;
|
||||
}
|
||||
|
||||
public function validUntil(): ?Chronos
|
||||
{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
public function hasValidUntil(): bool
|
||||
{
|
||||
return $this->validUntilPropWasProvided;
|
||||
}
|
||||
|
||||
public function maxVisits(): ?int
|
||||
{
|
||||
return $this->maxVisits;
|
||||
}
|
||||
|
||||
public function hasMaxVisits(): bool
|
||||
{
|
||||
return $this->maxVisitsPropWasProvided;
|
||||
}
|
||||
}
|
||||
@@ -8,25 +8,21 @@ use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
|
||||
use function array_key_exists;
|
||||
use function Shlinkio\Shlink\Core\parseDateField;
|
||||
|
||||
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
final class ShortUrlMeta
|
||||
{
|
||||
private bool $validSincePropWasProvided = false;
|
||||
private ?Chronos $validSince = null;
|
||||
private bool $validUntilPropWasProvided = false;
|
||||
private ?Chronos $validUntil = null;
|
||||
private ?string $customSlug = null;
|
||||
private bool $maxVisitsPropWasProvided = false;
|
||||
private ?int $maxVisits = null;
|
||||
private ?bool $findIfExists = null;
|
||||
private ?string $domain = null;
|
||||
private int $shortCodeLength = 5;
|
||||
|
||||
// Force named constructors
|
||||
// Enforce named constructors
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
@@ -57,12 +53,9 @@ final class ShortUrlMeta
|
||||
}
|
||||
|
||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||
$this->validSincePropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_SINCE, $data);
|
||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data);
|
||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
||||
$this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data);
|
||||
$this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS);
|
||||
$this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN);
|
||||
$this->shortCodeLength = $this->getOptionalIntFromInputFilter(
|
||||
@@ -84,7 +77,7 @@ final class ShortUrlMeta
|
||||
|
||||
public function hasValidSince(): bool
|
||||
{
|
||||
return $this->validSincePropWasProvided;
|
||||
return $this->validSince !== null;
|
||||
}
|
||||
|
||||
public function getValidUntil(): ?Chronos
|
||||
@@ -94,7 +87,7 @@ final class ShortUrlMeta
|
||||
|
||||
public function hasValidUntil(): bool
|
||||
{
|
||||
return $this->validUntilPropWasProvided;
|
||||
return $this->validUntil !== null;
|
||||
}
|
||||
|
||||
public function getCustomSlug(): ?string
|
||||
@@ -114,7 +107,7 @@ final class ShortUrlMeta
|
||||
|
||||
public function hasMaxVisits(): bool
|
||||
{
|
||||
return $this->maxVisitsPropWasProvided;
|
||||
return $this->maxVisits !== null;
|
||||
}
|
||||
|
||||
public function findIfExists(): bool
|
||||
|
||||
@@ -7,14 +7,16 @@ namespace Shlinkio\Shlink\Core\Service;
|
||||
use Doctrine\ORM;
|
||||
use Laminas\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
@@ -22,11 +24,16 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
|
||||
private ORM\EntityManagerInterface $em;
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
|
||||
public function __construct(ORM\EntityManagerInterface $em, ShortUrlResolverInterface $urlResolver)
|
||||
{
|
||||
public function __construct(
|
||||
ORM\EntityManagerInterface $em,
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
UrlValidatorInterface $urlValidator
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->urlResolver = $urlResolver;
|
||||
$this->urlValidator = $urlValidator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,11 +66,16 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
|
||||
/**
|
||||
* @throws ShortUrlNotFoundException
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlMeta $shortUrlMeta): ShortUrl
|
||||
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlEdit $shortUrlEdit): ShortUrl
|
||||
{
|
||||
if ($shortUrlEdit->hasLongUrl()) {
|
||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl());
|
||||
}
|
||||
|
||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier);
|
||||
$shortUrl->updateMeta($shortUrlMeta);
|
||||
$shortUrl->update($shortUrlEdit);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Laminas\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
|
||||
interface ShortUrlServiceInterface
|
||||
@@ -26,6 +27,7 @@ interface ShortUrlServiceInterface
|
||||
|
||||
/**
|
||||
* @throws ShortUrlNotFoundException
|
||||
* @throws InvalidUrlException
|
||||
*/
|
||||
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlMeta $shortUrlMeta): ShortUrl;
|
||||
public function updateMetadataByShortCode(ShortUrlIdentifier $identifier, ShortUrlEdit $shortUrlEdit): ShortUrl;
|
||||
}
|
||||
@@ -6,12 +6,11 @@ namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
@@ -24,17 +23,17 @@ class UrlShortener implements UrlShortenerInterface
|
||||
use TagManagerTrait;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
private UrlShortenerOptions $options;
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
private DomainResolverInterface $domainResolver;
|
||||
|
||||
public function __construct(
|
||||
UrlValidatorInterface $urlValidator,
|
||||
EntityManagerInterface $em,
|
||||
UrlShortenerOptions $options
|
||||
DomainResolverInterface $domainResolver
|
||||
) {
|
||||
$this->urlValidator = $urlValidator;
|
||||
$this->em = $em;
|
||||
$this->options = $options;
|
||||
$this->domainResolver = $domainResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,13 +52,9 @@ class UrlShortener implements UrlShortenerInterface
|
||||
return $existingShortUrl;
|
||||
}
|
||||
|
||||
// If the URL validation is enabled, check that the URL actually exists
|
||||
if ($this->options->isUrlValidationEnabled()) {
|
||||
$this->urlValidator->validateUrl($url);
|
||||
}
|
||||
|
||||
$this->urlValidator->validateUrl($url);
|
||||
$this->em->beginTransaction();
|
||||
$shortUrl = new ShortUrl($url, $meta, new PersistenceDomainResolver($this->em));
|
||||
$shortUrl = new ShortUrl($url, $meta, $this->domainResolver);
|
||||
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,16 +9,19 @@ use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
||||
class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
||||
{
|
||||
private const MAX_REDIRECTS = 15;
|
||||
|
||||
private ClientInterface $httpClient;
|
||||
private UrlShortenerOptions $options;
|
||||
|
||||
public function __construct(ClientInterface $httpClient)
|
||||
public function __construct(ClientInterface $httpClient, UrlShortenerOptions $options)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,6 +29,11 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
||||
*/
|
||||
public function validateUrl(string $url): void
|
||||
{
|
||||
// If the URL validation is not enabled, skip check
|
||||
if (! $this->options->isUrlValidationEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->httpClient->request(self::METHOD_GET, $url, [
|
||||
RequestOptions::ALLOW_REDIRECTS => ['max' => self::MAX_REDIRECTS],
|
||||
|
||||
@@ -23,6 +23,7 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
public const FIND_IF_EXISTS = 'findIfExists';
|
||||
public const DOMAIN = 'domain';
|
||||
public const SHORT_CODE_LENGTH = 'shortCodeLength';
|
||||
public const LONG_URL = 'longUrl';
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
@@ -32,6 +33,8 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
$this->add($this->createInput(self::LONG_URL, false));
|
||||
|
||||
$validSince = $this->createInput(self::VALID_SINCE, false);
|
||||
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTime::ATOM]));
|
||||
$this->add($validSince);
|
||||
|
||||
@@ -12,12 +12,13 @@ use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
use function count;
|
||||
|
||||
@@ -26,6 +27,7 @@ class ShortUrlServiceTest extends TestCase
|
||||
private ShortUrlService $service;
|
||||
private ObjectProphecy $em;
|
||||
private ObjectProphecy $urlResolver;
|
||||
private ObjectProphecy $urlValidator;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
@@ -34,8 +36,13 @@ class ShortUrlServiceTest extends TestCase
|
||||
$this->em->flush()->willReturn(null);
|
||||
|
||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||
$this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
|
||||
|
||||
$this->service = new ShortUrlService($this->em->reveal(), $this->urlResolver->reveal());
|
||||
$this->service = new ShortUrlService(
|
||||
$this->em->reveal(),
|
||||
$this->urlResolver->reveal(),
|
||||
$this->urlValidator->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -74,27 +81,47 @@ class ShortUrlServiceTest extends TestCase
|
||||
$this->service->setTagsByShortCode(new ShortUrlIdentifier($shortCode), ['foo', 'bar']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function updateMetadataByShortCodeUpdatesProvidedData(): void
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideShortUrlEdits
|
||||
*/
|
||||
public function updateMetadataByShortCodeUpdatesProvidedData(
|
||||
int $expectedValidateCalls,
|
||||
ShortUrlEdit $shortUrlEdit
|
||||
): void {
|
||||
$originalLongUrl = 'originalLongUrl';
|
||||
$shortUrl = new ShortUrl($originalLongUrl);
|
||||
|
||||
$findShortUrl = $this->urlResolver->resolveShortUrl(new ShortUrlIdentifier('abc123'))->willReturn($shortUrl);
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$result = $this->service->updateMetadataByShortCode(new ShortUrlIdentifier('abc123'), ShortUrlMeta::fromRawData(
|
||||
$result = $this->service->updateMetadataByShortCode(new ShortUrlIdentifier('abc123'), $shortUrlEdit);
|
||||
|
||||
$this->assertSame($shortUrl, $result);
|
||||
$this->assertEquals($shortUrlEdit->validSince(), $shortUrl->getValidSince());
|
||||
$this->assertEquals($shortUrlEdit->validUntil(), $shortUrl->getValidUntil());
|
||||
$this->assertEquals($shortUrlEdit->maxVisits(), $shortUrl->getMaxVisits());
|
||||
$this->assertEquals($shortUrlEdit->longUrl() ?? $originalLongUrl, $shortUrl->getLongUrl());
|
||||
$findShortUrl->shouldHaveBeenCalled();
|
||||
$flush->shouldHaveBeenCalled();
|
||||
$this->urlValidator->validateUrl($shortUrlEdit->longUrl())->shouldHaveBeenCalledTimes($expectedValidateCalls);
|
||||
}
|
||||
|
||||
public function provideShortUrlEdits(): iterable
|
||||
{
|
||||
yield 'no long URL' => [0, ShortUrlEdit::fromRawData(
|
||||
[
|
||||
'validSince' => Chronos::parse('2017-01-01 00:00:00')->toAtomString(),
|
||||
'validUntil' => Chronos::parse('2017-01-05 00:00:00')->toAtomString(),
|
||||
'maxVisits' => 5,
|
||||
],
|
||||
));
|
||||
|
||||
$this->assertSame($shortUrl, $result);
|
||||
$this->assertEquals(Chronos::parse('2017-01-01 00:00:00'), $shortUrl->getValidSince());
|
||||
$this->assertEquals(Chronos::parse('2017-01-05 00:00:00'), $shortUrl->getValidUntil());
|
||||
$this->assertEquals(5, $shortUrl->getMaxVisits());
|
||||
$findShortUrl->shouldHaveBeenCalled();
|
||||
$flush->shouldHaveBeenCalled();
|
||||
)];
|
||||
yield 'long URL' => [1, ShortUrlEdit::fromRawData(
|
||||
[
|
||||
'validSince' => Chronos::parse('2017-01-01 00:00:00')->toAtomString(),
|
||||
'maxVisits' => 10,
|
||||
'longUrl' => 'modifiedLongUrl',
|
||||
],
|
||||
)];
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ use Laminas\Diactoros\Uri;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
@@ -33,6 +33,10 @@ class UrlShortenerTest extends TestCase
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->urlValidator = $this->prophesize(UrlValidatorInterface::class);
|
||||
$this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar')->will(
|
||||
function (): void {
|
||||
},
|
||||
);
|
||||
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$conn = $this->prophesize(Connection::class);
|
||||
@@ -50,15 +54,10 @@ class UrlShortenerTest extends TestCase
|
||||
$repo->shortCodeIsInUse(Argument::cetera())->willReturn(false);
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->setUrlShortener(false);
|
||||
}
|
||||
|
||||
private function setUrlShortener(bool $urlValidationEnabled): void
|
||||
{
|
||||
$this->urlShortener = new UrlShortener(
|
||||
$this->urlValidator->reveal(),
|
||||
$this->em->reveal(),
|
||||
new UrlShortenerOptions(['validate_url' => $urlValidationEnabled]),
|
||||
new SimpleDomainResolver(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,24 +118,6 @@ class UrlShortenerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function validatorIsCalledWhenUrlValidationIsEnabled(): void
|
||||
{
|
||||
$this->setUrlShortener(true);
|
||||
$validateUrl = $this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar')->will(
|
||||
function (): void {
|
||||
},
|
||||
);
|
||||
|
||||
$this->urlShortener->urlToShortCode(
|
||||
new Uri('http://foobar.com/12345/hello?foo=bar'),
|
||||
[],
|
||||
ShortUrlMeta::createEmpty(),
|
||||
);
|
||||
|
||||
$validateUrl->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function exceptionIsThrownWhenNonUniqueSlugIsProvided(): void
|
||||
{
|
||||
@@ -175,6 +156,7 @@ class UrlShortenerTest extends TestCase
|
||||
$findExisting->shouldHaveBeenCalledOnce();
|
||||
$getRepo->shouldHaveBeenCalledOnce();
|
||||
$this->em->persist(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->urlValidator->validateUrl(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,20 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Util\UrlValidator;
|
||||
|
||||
class UrlValidatorTest extends TestCase
|
||||
{
|
||||
private UrlValidator $urlValidator;
|
||||
private ObjectProphecy $httpClient;
|
||||
private UrlShortenerOptions $options;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->httpClient = $this->prophesize(ClientInterface::class);
|
||||
$this->urlValidator = new UrlValidator($this->httpClient->reveal());
|
||||
$this->options = new UrlShortenerOptions(['validate_url' => true]);
|
||||
$this->urlValidator = new UrlValidator($this->httpClient->reveal(), $this->options);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -52,4 +55,15 @@ class UrlValidatorTest extends TestCase
|
||||
|
||||
$request->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function noCheckIsPerformedWhenUrlValidationIsDisabled(): void
|
||||
{
|
||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||
$this->options->validateUrl = false;
|
||||
|
||||
$this->urlValidator->validateUrl('');
|
||||
|
||||
$request->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
|
||||
@@ -28,10 +28,10 @@ class EditShortUrlAction extends AbstractRestAction
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$postData = (array) $request->getParsedBody();
|
||||
$shortUrlEdit = ShortUrlEdit::fromRawData((array) $request->getParsedBody());
|
||||
$identifier = ShortUrlIdentifier::fromApiRequest($request);
|
||||
|
||||
$this->shortUrlService->updateMetadataByShortCode($identifier, ShortUrlMeta::fromRawData($postData));
|
||||
$this->shortUrlService->updateMetadataByShortCode($identifier, $shortUrlEdit);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,32 @@ class EditShortUrlActionTest extends ApiTestCase
|
||||
return $matchingShortUrl['meta'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideLongUrls
|
||||
*/
|
||||
public function longUrlCanBeEditedIfItIsValid(string $longUrl, int $expectedStatus, ?string $expectedError): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$url = sprintf('/short-urls/%s', $shortCode);
|
||||
|
||||
$resp = $this->callApiWithKey(self::METHOD_PATCH, $url, [RequestOptions::JSON => [
|
||||
'longUrl' => $longUrl,
|
||||
]]);
|
||||
|
||||
$this->assertEquals($expectedStatus, $resp->getStatusCode());
|
||||
if ($expectedError !== null) {
|
||||
$payload = $this->getJsonResponsePayload($resp);
|
||||
$this->assertEquals($expectedError, $payload['type']);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideLongUrls(): iterable
|
||||
{
|
||||
yield 'valid URL' => ['https://shlink.io', self::STATUS_NO_CONTENT, null];
|
||||
yield 'invalid URL' => ['htt:foo', self::STATUS_BAD_REQUEST, 'INVALID_URL'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideInvalidUrls
|
||||
|
||||
Reference in new issue
Block a user