From e3de403c6cd88c38397bd804e305fbbaf754865a Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sat, 17 Feb 2024 12:02:57 +0100 Subject: [PATCH] Remove support to validate long URLs during short URL creation/edition --- UPGRADE.md | 2 +- docs/swagger/definitions/ShortUrlEdition.json | 5 - docs/swagger/paths/v1_short-urls.json | 22 --- docs/swagger/paths/v1_short-urls_shorten.json | 43 ----- .../ShortUrl/CreateShortUrlCommand.php | 11 +- .../ShortUrl/CreateShortUrlCommandTest.php | 25 +-- module/Core/config/dependencies.config.php | 4 +- .../src/Exception/InvalidUrlException.php | 35 ---- module/Core/src/ShortUrl/Entity/ShortUrl.php | 1 - .../Helper/ShortUrlTitleResolutionHelper.php | 85 +++++++-- ...ShortUrlTitleResolutionHelperInterface.php | 6 +- .../Helper/TitleResolutionModelInterface.php | 3 - .../src/ShortUrl/Model/ShortUrlCreation.php | 10 - .../src/ShortUrl/Model/ShortUrlEdition.php | 10 - .../Model/Validation/ShortUrlInputFilter.php | 5 +- module/Core/src/ShortUrl/ShortUrlService.php | 4 +- .../src/ShortUrl/ShortUrlServiceInterface.php | 2 - module/Core/src/ShortUrl/UrlShortener.php | 4 +- .../src/ShortUrl/UrlShortenerInterface.php | 2 - module/Core/src/Util/UrlValidator.php | 116 ------------ .../Core/src/Util/UrlValidatorInterface.php | 23 --- .../Exception/InvalidUrlExceptionTest.php | 41 ---- .../ShortUrlTitleResolutionHelperTest.php | 131 ++++++++++--- .../test/ShortUrl/ShortUrlServiceTest.php | 6 +- .../Core/test/ShortUrl/UrlShortenerTest.php | 6 +- module/Core/test/Util/UrlValidatorTest.php | 176 ------------------ .../test-api/Action/CreateShortUrlTest.php | 21 --- .../Rest/test-api/Action/EditShortUrlTest.php | 18 +- 28 files changed, 198 insertions(+), 619 deletions(-) delete mode 100644 module/Core/src/Exception/InvalidUrlException.php delete mode 100644 module/Core/src/Util/UrlValidator.php delete mode 100644 module/Core/src/Util/UrlValidatorInterface.php delete mode 100644 module/Core/test/Exception/InvalidUrlExceptionTest.php delete mode 100644 module/Core/test/Util/UrlValidatorTest.php diff --git a/UPGRADE.md b/UPGRADE.md index f76be20b..ab6085ad 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -13,6 +13,7 @@ * The short URLs `loosely` mode is no longer supported, as it was a typo. Use `loose` mode instead. * QR codes URLs now work by default, even for short URLs that cannot be visited due to max visits or date range limitations. If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option. +* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition. ### Changes in REST API @@ -21,7 +22,6 @@ * `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion` * `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found` * `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation` - * `INVALID_URL` -> `https://shlink.io/api/error/invalid-url` * `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug` * `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found` * `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict` diff --git a/docs/swagger/definitions/ShortUrlEdition.json b/docs/swagger/definitions/ShortUrlEdition.json index dda213ca..baef4f52 100644 --- a/docs/swagger/definitions/ShortUrlEdition.json +++ b/docs/swagger/definitions/ShortUrlEdition.json @@ -20,11 +20,6 @@ "description": "The maximum number of allowed visits for this short code", "type": ["number", "null"] }, - "validateUrl": { - "deprecated": true, - "description": "**[DEPRECATED]** Tells if the long URL should or should not be validated as a reachable URL. Defaults to `false`", - "type": "boolean" - }, "tags": { "type": "array", "items": { diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index c226046f..08e08b67 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -388,10 +388,6 @@ ] } }, - "url": { - "type": "string", - "description": "A URL that could not be verified, if the error type is https://shlink.io/api/error/invalid-url" - }, "customSlug": { "type": "string", "description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug" @@ -408,15 +404,6 @@ "Invalid arguments with API v3 and newer": { "$ref": "../examples/short-url-invalid-args-v3.json" }, - "Invalid long URL with API v3 and newer": { - "value": { - "title": "Invalid URL", - "type": "https://shlink.io/api/error/invalid-url", - "detail": "Provided URL foo is invalid. Try with a different one.", - "status": 400, - "url": "https://invalid-url.com" - } - }, "Non-unique slug with API v3 and newer": { "value": { "title": "Invalid custom slug", @@ -429,15 +416,6 @@ "Invalid arguments previous to API v3": { "$ref": "../examples/short-url-invalid-args-v2.json" }, - "Invalid long URL previous to API v3": { - "value": { - "title": "Invalid URL", - "type": "INVALID_URL", - "detail": "Provided URL foo is invalid. Try with a different one.", - "status": 400, - "url": "https://invalid-url.com" - } - }, "Non-unique slug previous to API v3": { "value": { "title": "Invalid custom slug", diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json index cacb00bb..5c16482c 100644 --- a/docs/swagger/paths/v1_short-urls_shorten.json +++ b/docs/swagger/paths/v1_short-urls_shorten.json @@ -88,49 +88,6 @@ } } }, - "400": { - "description": "The long URL was not provided or is invalid.", - "content": { - "application/problem+json": { - "schema": { - "$ref": "../definitions/Error.json" - }, - "examples": { - "API v3 and newer": { - "value": { - "title": "Invalid URL", - "type": "https://shlink.io/api/error/invalid-url", - "detail": "Provided URL foo is invalid. Try with a different one.", - "status": 400, - "url": "https://invalid-url.com" - } - }, - "Previous to API v3": { - "value": { - "title": "Invalid URL", - "type": "INVALID_URL", - "detail": "Provided URL foo is invalid. Try with a different one.", - "status": 400, - "url": "https://invalid-url.com" - } - } - } - }, - "text/plain": { - "schema": { - "type": "string" - }, - "examples": { - "API v3 and newer": { - "value": "https://shlink.io/api/error/invalid-url" - }, - "Previous to API v3": { - "value": "INVALID_URL" - } - } - } - } - }, "default": { "description": "Unexpected error.", "content": { diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php index 3377e649..118ad201 100644 --- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\CLI\Command\ShortUrl; use Shlinkio\Shlink\CLI\Util\ExitCode; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; @@ -95,12 +94,6 @@ class CreateShortUrlCommand extends Command InputOption::VALUE_REQUIRED, 'The length for generated short code (it will be ignored if --custom-slug was provided).', ) - ->addOption( - 'validate-url', - null, - InputOption::VALUE_NONE, - '[DEPRECATED] Makes the URL to be validated as publicly accessible.', - ) ->addOption( 'crawlable', 'r', @@ -148,7 +141,6 @@ class CreateShortUrlCommand extends Command $customSlug = $input->getOption('custom-slug'); $maxVisits = $input->getOption('max-visits'); $shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength; - $doValidateUrl = $input->getOption('validate-url'); try { $result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([ @@ -160,7 +152,6 @@ class CreateShortUrlCommand extends Command ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'), ShortUrlInputFilter::DOMAIN => $input->getOption('domain'), ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength, - ShortUrlInputFilter::VALIDATE_URL => $doValidateUrl, ShortUrlInputFilter::TAGS => $tags, ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'), ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'), @@ -176,7 +167,7 @@ class CreateShortUrlCommand extends Command sprintf('Generated short URL: %s', $this->stringifier->stringify($result->shortUrl)), ]); return ExitCode::EXIT_SUCCESS; - } catch (InvalidUrlException | NonUniqueSlugException $e) { + } catch (NonUniqueSlugException $e) { $io->error($e->getMessage()); return ExitCode::EXIT_FAILURE; } diff --git a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php index de0fe26b..33031c6b 100644 --- a/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/CreateShortUrlCommandTest.php @@ -12,7 +12,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand; use Shlinkio\Shlink\CLI\Util\ExitCode; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; @@ -68,22 +67,6 @@ class CreateShortUrlCommandTest extends TestCase self::assertStringNotContainsString('but the real-time updates cannot', $output); } - #[Test] - public function exceptionWhileParsingLongUrlOutputsError(): void - { - $url = 'http://domain.com/invalid'; - $this->urlShortener->expects($this->once())->method('shorten')->withAnyParameters()->willThrowException( - InvalidUrlException::fromUrl($url), - ); - $this->stringifier->method('stringify')->with($this->isInstanceOf(ShortUrl::class))->willReturn(''); - - $this->commandTester->execute(['longUrl' => $url]); - $output = $this->commandTester->getDisplay(); - - self::assertEquals(ExitCode::EXIT_FAILURE, $this->commandTester->getStatusCode()); - self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output); - } - #[Test] public function providingNonUniqueSlugOutputsError(): void { @@ -148,12 +131,12 @@ class CreateShortUrlCommandTest extends TestCase } #[Test, DataProvider('provideFlags')] - public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedValidateUrl): void + public function urlValidationHasExpectedValueBasedOnProvidedFlags(array $options, ?bool $expectedCrawlable): void { $shortUrl = ShortUrl::createFake(); $this->urlShortener->expects($this->once())->method('shorten')->with( - $this->callback(function (ShortUrlCreation $meta) use ($expectedValidateUrl) { - Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl()); + $this->callback(function (ShortUrlCreation $meta) use ($expectedCrawlable) { + Assert::assertEquals($expectedCrawlable, $meta->crawlable); return true; }), )->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl)); @@ -166,7 +149,7 @@ class CreateShortUrlCommandTest extends TestCase public static function provideFlags(): iterable { yield 'no flags' => [[], null]; - yield 'validate-url' => [['--validate-url' => true], true]; + yield 'crawlable' => [['--crawlable' => true], true]; } /** diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php index 6b6be190..5f9ae565 100644 --- a/module/Core/config/dependencies.config.php +++ b/module/Core/config/dependencies.config.php @@ -75,7 +75,6 @@ return [ Visit\Entity\Visit::class, ], - Util\UrlValidator::class => ConfigAbstractFactory::class, Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class, Util\RedirectResponseHelper::class => ConfigAbstractFactory::class, @@ -153,7 +152,6 @@ return [ ShortUrl\Helper\ShortCodeUniquenessHelper::class => ['em', Options\UrlShortenerOptions::class], Domain\DomainService::class => ['em', 'config.url_shortener.domain.hostname'], - Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class], Util\DoctrineBatchHelper::class => ['em'], Util\RedirectResponseHelper::class => [Options\RedirectOptions::class], @@ -180,7 +178,7 @@ return [ Lock\LockFactory::class, ], ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'], - ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class], + ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ['httpClient', Options\UrlShortenerOptions::class], ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::class], ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class], ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => [ diff --git a/module/Core/src/Exception/InvalidUrlException.php b/module/Core/src/Exception/InvalidUrlException.php deleted file mode 100644 index 200914c2..00000000 --- a/module/Core/src/Exception/InvalidUrlException.php +++ /dev/null @@ -1,35 +0,0 @@ -detail = $e->getMessage(); - $e->title = self::TITLE; - $e->type = toProblemDetailsType(self::ERROR_CODE); - $e->status = $status; - $e->additional = ['url' => $url]; - - return $e; - } -} diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php index e53e9afa..ee2c5920 100644 --- a/module/Core/src/ShortUrl/Entity/ShortUrl.php +++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php @@ -120,7 +120,6 @@ class ShortUrl extends AbstractEntity ?ShortUrlRelationResolverInterface $relationResolver = null, ): self { $meta = [ - ShortUrlInputFilter::VALIDATE_URL => false, ShortUrlInputFilter::LONG_URL => $url->longUrl, ShortUrlInputFilter::DOMAIN => $url->domain, ShortUrlInputFilter::TAGS => $url->tags, diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php index 71963437..6a539f58 100644 --- a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php +++ b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelper.php @@ -4,31 +4,90 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Helper; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; -use Shlinkio\Shlink\Core\Util\UrlValidatorInterface; +use Fig\Http\Message\RequestMethodInterface; +use GuzzleHttp\ClientInterface; +use GuzzleHttp\RequestOptions; +use Psr\Http\Message\ResponseInterface; +use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; +use Throwable; -class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface +use function html_entity_decode; +use function preg_match; +use function str_contains; +use function str_starts_with; +use function strtolower; +use function trim; + +use const Shlinkio\Shlink\TITLE_TAG_VALUE; + +readonly class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface { - public function __construct(private readonly UrlValidatorInterface $urlValidator) - { + private const MAX_REDIRECTS = 15; + private const CHROME_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) ' + . 'Chrome/121.0.0.0 Safari/537.36'; + + public function __construct( + private ClientInterface $httpClient, + private UrlShortenerOptions $options, + ) { } /** - * @deprecated TODO Rename to processTitle once URL validation is removed with Shlink 4.0.0 - * Move relevant logic from URL validator here. * @template T of TitleResolutionModelInterface * @param T $data * @return T - * @throws InvalidUrlException */ - public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface + public function processTitle(TitleResolutionModelInterface $data): TitleResolutionModelInterface { - if ($data->hasTitle()) { - $this->urlValidator->validateUrl($data->getLongUrl(), $data->doValidateUrl()); + if (! $this->options->autoResolveTitles || $data->hasTitle()) { return $data; } - $title = $this->urlValidator->validateUrlWithTitle($data->getLongUrl(), $data->doValidateUrl()); - return $title === null ? $data : $data->withResolvedTitle($title); + $response = $this->fetchUrl($data->getLongUrl()); + if ($response === null) { + return $data; + } + + $contentType = strtolower($response->getHeaderLine('Content-Type')); + if (! str_starts_with($contentType, 'text/html')) { + return $data; + } + + $title = $this->tryToResolveTitle($response); + return $title !== null ? $data->withResolvedTitle($title) : $data; + } + + private function fetchUrl(string $url): ?ResponseInterface + { + try { + return $this->httpClient->request(RequestMethodInterface::METHOD_GET, $url, [ + // TODO Add a sensible timeout that prevents hanging here forever + // Prevent potential infinite redirection loops + RequestOptions::ALLOW_REDIRECTS => ['max' => self::MAX_REDIRECTS], + RequestOptions::IDN_CONVERSION => true, + // Making the request with a browser's user agent results in responses closer to a real user + RequestOptions::HEADERS => ['User-Agent' => self::CHROME_USER_AGENT], + RequestOptions::STREAM => true, // This ensures large files are not fully downloaded if not needed + ]); + } catch (Throwable) { + return null; + } + } + + private function tryToResolveTitle(ResponseInterface $response): ?string + { + $collectedBody = ''; + $body = $response->getBody(); + // With streaming enabled, we can walk the body until the tag is found, and then stop + while (! str_contains($collectedBody, '') && ! $body->eof()) { + $collectedBody .= $body->read(1024); + } + preg_match(TITLE_TAG_VALUE, $collectedBody, $matches); + return isset($matches[1]) ? $this->normalizeTitle($matches[1]) : null; + } + + private function normalizeTitle(string $title): string + { + return html_entity_decode(trim($title)); } } diff --git a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelperInterface.php b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelperInterface.php index 1861b451..6641460a 100644 --- a/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelperInterface.php +++ b/module/Core/src/ShortUrl/Helper/ShortUrlTitleResolutionHelperInterface.php @@ -4,16 +4,12 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl\Helper; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; - interface ShortUrlTitleResolutionHelperInterface { /** - * @deprecated TODO Rename to processTitle once URL validation is removed with Shlink 4.0.0 * @template T of TitleResolutionModelInterface * @param T $data * @return T - * @throws InvalidUrlException */ - public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface; + public function processTitle(TitleResolutionModelInterface $data): TitleResolutionModelInterface; } diff --git a/module/Core/src/ShortUrl/Helper/TitleResolutionModelInterface.php b/module/Core/src/ShortUrl/Helper/TitleResolutionModelInterface.php index 4c56bfc1..cecd83e1 100644 --- a/module/Core/src/ShortUrl/Helper/TitleResolutionModelInterface.php +++ b/module/Core/src/ShortUrl/Helper/TitleResolutionModelInterface.php @@ -10,8 +10,5 @@ interface TitleResolutionModelInterface public function getLongUrl(): string; - /** @deprecated */ - public function doValidateUrl(): bool; - public function withResolvedTitle(string $title): static; } diff --git a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php index 43b39874..1a1673ac 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlCreation.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlCreation.php @@ -35,8 +35,6 @@ final class ShortUrlCreation implements TitleResolutionModelInterface public readonly bool $findIfExists = false, public readonly ?string $domain = null, public readonly int $shortCodeLength = 5, - /** @deprecated */ - public readonly bool $validateUrl = false, public readonly ?ApiKey $apiKey = null, public readonly array $tags = [], public readonly ?string $title = null, @@ -75,7 +73,6 @@ final class ShortUrlCreation implements TitleResolutionModelInterface $inputFilter, ShortUrlInputFilter::SHORT_CODE_LENGTH, ) ?? DEFAULT_SHORT_CODES_LENGTH, - validateUrl: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false, apiKey: $inputFilter->getValue(ShortUrlInputFilter::API_KEY), tags: $inputFilter->getValue(ShortUrlInputFilter::TAGS), title: $inputFilter->getValue(ShortUrlInputFilter::TITLE), @@ -97,7 +94,6 @@ final class ShortUrlCreation implements TitleResolutionModelInterface findIfExists: $this->findIfExists, domain: $this->domain, shortCodeLength: $this->shortCodeLength, - validateUrl: $this->validateUrl, apiKey: $this->apiKey, tags: $this->tags, title: $title, @@ -137,12 +133,6 @@ final class ShortUrlCreation implements TitleResolutionModelInterface return $this->domain !== null; } - /** @deprecated */ - public function doValidateUrl(): bool - { - return $this->validateUrl; - } - public function hasTitle(): bool { return $this->title !== null; diff --git a/module/Core/src/ShortUrl/Model/ShortUrlEdition.php b/module/Core/src/ShortUrl/Model/ShortUrlEdition.php index fe92fae8..7a0000de 100644 --- a/module/Core/src/ShortUrl/Model/ShortUrlEdition.php +++ b/module/Core/src/ShortUrl/Model/ShortUrlEdition.php @@ -38,8 +38,6 @@ final class ShortUrlEdition implements TitleResolutionModelInterface private readonly bool $titlePropWasProvided = false, public readonly ?string $title = null, public readonly bool $titleWasAutoResolved = false, - /** @deprecated */ - public readonly bool $validateUrl = false, private readonly bool $crawlablePropWasProvided = false, public readonly bool $crawlable = false, private readonly bool $forwardQueryPropWasProvided = false, @@ -76,7 +74,6 @@ final class ShortUrlEdition implements TitleResolutionModelInterface tags: $inputFilter->getValue(ShortUrlInputFilter::TAGS), titlePropWasProvided: array_key_exists(ShortUrlInputFilter::TITLE, $data), title: $inputFilter->getValue(ShortUrlInputFilter::TITLE), - validateUrl: getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false, crawlablePropWasProvided: array_key_exists(ShortUrlInputFilter::CRAWLABLE, $data), crawlable: $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE), forwardQueryPropWasProvided: array_key_exists(ShortUrlInputFilter::FORWARD_QUERY, $data), @@ -102,7 +99,6 @@ final class ShortUrlEdition implements TitleResolutionModelInterface titlePropWasProvided: $this->titlePropWasProvided, title: $title, titleWasAutoResolved: true, - validateUrl: $this->validateUrl, crawlablePropWasProvided: $this->crawlablePropWasProvided, crawlable: $this->crawlable, forwardQueryPropWasProvided: $this->forwardQueryPropWasProvided, @@ -155,12 +151,6 @@ final class ShortUrlEdition implements TitleResolutionModelInterface return $this->titleWasAutoResolved; } - /** @deprecated */ - public function doValidateUrl(): bool - { - return $this->validateUrl; - } - public function crawlableWasProvided(): bool { return $this->crawlablePropWasProvided; diff --git a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php index 23ac8a2f..325b400f 100644 --- a/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php +++ b/module/Core/src/ShortUrl/Model/Validation/ShortUrlInputFilter.php @@ -36,8 +36,6 @@ class ShortUrlInputFilter extends InputFilter public const SHORT_CODE_LENGTH = 'shortCodeLength'; public const LONG_URL = 'longUrl'; public const DEVICE_LONG_URLS = 'deviceLongUrls'; - /** @deprecated */ - public const VALIDATE_URL = 'validateUrl'; public const API_KEY = 'apiKey'; public const TAGS = 'tags'; public const TITLE = 'title'; @@ -97,9 +95,8 @@ class ShortUrlInputFilter extends InputFilter $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); - // These cannot be defined as a boolean inputs, because they can actually have 3 values: true, false and null. + // This cannot be defined as a boolean inputs, because they can actually have 3 values: true, false and null. // Defining them as boolean will make null fall back to false, which is not the desired behavior. - $this->add($this->createInput(self::VALIDATE_URL, false)); $this->add($this->createInput(self::FORWARD_QUERY, false)); $domain = $this->createInput(self::DOMAIN, false); diff --git a/module/Core/src/ShortUrl/ShortUrlService.php b/module/Core/src/ShortUrl/ShortUrlService.php index 95561fc5..1c3e9295 100644 --- a/module/Core/src/ShortUrl/ShortUrlService.php +++ b/module/Core/src/ShortUrl/ShortUrlService.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl; use Doctrine\ORM; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface; @@ -26,7 +25,6 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @throws ShortUrlNotFoundException - * @throws InvalidUrlException */ public function updateShortUrl( ShortUrlIdentifier $identifier, @@ -34,7 +32,7 @@ class ShortUrlService implements ShortUrlServiceInterface ?ApiKey $apiKey = null, ): ShortUrl { if ($shortUrlEdit->longUrlWasProvided()) { - $shortUrlEdit = $this->titleResolutionHelper->processTitleAndValidateUrl($shortUrlEdit); + $shortUrlEdit = $this->titleResolutionHelper->processTitle($shortUrlEdit); } $shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey); diff --git a/module/Core/src/ShortUrl/ShortUrlServiceInterface.php b/module/Core/src/ShortUrl/ShortUrlServiceInterface.php index 3365374e..c7892f55 100644 --- a/module/Core/src/ShortUrl/ShortUrlServiceInterface.php +++ b/module/Core/src/ShortUrl/ShortUrlServiceInterface.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition; @@ -15,7 +14,6 @@ interface ShortUrlServiceInterface { /** * @throws ShortUrlNotFoundException - * @throws InvalidUrlException */ public function updateShortUrl( ShortUrlIdentifier $identifier, diff --git a/module/Core/src/ShortUrl/UrlShortener.php b/module/Core/src/ShortUrl/UrlShortener.php index 0305f936..4a908c78 100644 --- a/module/Core/src/ShortUrl/UrlShortener.php +++ b/module/Core/src/ShortUrl/UrlShortener.php @@ -8,7 +8,6 @@ use Doctrine\ORM\EntityManagerInterface; use Psr\Container\ContainerExceptionInterface; use Psr\EventDispatcher\EventDispatcherInterface; use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface; @@ -31,7 +30,6 @@ class UrlShortener implements UrlShortenerInterface /** * @throws NonUniqueSlugException - * @throws InvalidUrlException */ public function shorten(ShortUrlCreation $creation): UrlShorteningResult { @@ -41,7 +39,7 @@ class UrlShortener implements UrlShortenerInterface return UrlShorteningResult::withoutErrorOnEventDispatching($existingShortUrl); } - $creation = $this->titleResolutionHelper->processTitleAndValidateUrl($creation); + $creation = $this->titleResolutionHelper->processTitle($creation); /** @var ShortUrl $newShortUrl */ $newShortUrl = $this->em->wrapInTransaction(function () use ($creation): ShortUrl { diff --git a/module/Core/src/ShortUrl/UrlShortenerInterface.php b/module/Core/src/ShortUrl/UrlShortenerInterface.php index 70896ec1..7da0aaef 100644 --- a/module/Core/src/ShortUrl/UrlShortenerInterface.php +++ b/module/Core/src/ShortUrl/UrlShortenerInterface.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\ShortUrl; -use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult; @@ -13,7 +12,6 @@ interface UrlShortenerInterface { /** * @throws NonUniqueSlugException - * @throws InvalidUrlException */ public function shorten(ShortUrlCreation $creation): UrlShorteningResult; } diff --git a/module/Core/src/Util/UrlValidator.php b/module/Core/src/Util/UrlValidator.php deleted file mode 100644 index 1ab3e8f8..00000000 --- a/module/Core/src/Util/UrlValidator.php +++ /dev/null @@ -1,116 +0,0 @@ -validateUrlAndGetResponse($url); - } - - /** - * @deprecated - * @throws InvalidUrlException - */ - public function validateUrlWithTitle(string $url, bool $doValidate): ?string - { - if (! $doValidate && ! $this->options->autoResolveTitles) { - return null; - } - - if (! $this->options->autoResolveTitles) { - $this->validateUrlAndGetResponse($url, self::METHOD_HEAD); - return null; - } - - $response = $doValidate ? $this->validateUrlAndGetResponse($url) : $this->getResponse($url); - if ($response === null) { - return null; - } - - $contentType = strtolower($response->getHeaderLine('Content-Type')); - if (! str_starts_with($contentType, 'text/html')) { - return null; - } - - $collectedBody = ''; - $body = $response->getBody(); - // With streaming enabled, we can walk the body until the tag is found, and then stop - while (! str_contains($collectedBody, '') && ! $body->eof()) { - $collectedBody .= $body->read(1024); - } - preg_match(TITLE_TAG_VALUE, $collectedBody, $matches); - return isset($matches[1]) ? $this->normalizeTitle($matches[1]) : null; - } - - /** - * @param self::METHOD_GET|self::METHOD_HEAD $method - * @throws InvalidUrlException - */ - private function validateUrlAndGetResponse(string $url, string $method = self::METHOD_GET): ResponseInterface - { - try { - return $this->httpClient->request($method, $url, [ - RequestOptions::ALLOW_REDIRECTS => ['max' => self::MAX_REDIRECTS], - RequestOptions::IDN_CONVERSION => true, - // Making the request with a browser's user agent makes the validation closer to a real user - RequestOptions::HEADERS => ['User-Agent' => self::CHROME_USER_AGENT], - RequestOptions::STREAM => true, // This ensures large files are not fully downloaded if not needed - ]); - } catch (GuzzleException $e) { - throw InvalidUrlException::fromUrl($url, $e); - } - } - - private function getResponse(string $url): ?ResponseInterface - { - try { - return $this->validateUrlAndGetResponse($url); - } catch (Throwable) { - return null; - } - } - - private function normalizeTitle(string $title): string - { - return html_entity_decode(trim($title)); - } -} diff --git a/module/Core/src/Util/UrlValidatorInterface.php b/module/Core/src/Util/UrlValidatorInterface.php deleted file mode 100644 index cb38dc42..00000000 --- a/module/Core/src/Util/UrlValidatorInterface.php +++ /dev/null @@ -1,23 +0,0 @@ -getMessage()); - self::assertEquals($expectedMessage, $e->getDetail()); - self::assertEquals('Invalid URL', $e->getTitle()); - self::assertEquals('https://shlink.io/api/error/invalid-url', $e->getType()); - self::assertEquals(['url' => $url], $e->getAdditionalData()); - self::assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode()); - self::assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getStatus()); - self::assertEquals($prev, $e->getPrevious()); - } - - public static function providePrevious(): iterable - { - yield 'null previous' => [null]; - yield 'instance previous' => [new Exception('Previous error', 10)]; - } -} diff --git a/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php b/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php index ae89fa6f..d5c9f833 100644 --- a/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php +++ b/module/Core/test/ShortUrl/Helper/ShortUrlTitleResolutionHelperTest.php @@ -4,46 +4,131 @@ declare(strict_types=1); namespace ShlinkioTest\Shlink\Core\ShortUrl\Helper; -use PHPUnit\Framework\Attributes\DataProvider; +use Exception; +use GuzzleHttp\ClientInterface; +use Laminas\Diactoros\Response; +use Laminas\Diactoros\Response\JsonResponse; +use Laminas\Diactoros\Stream; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Shlinkio\Shlink\Core\Options\UrlShortenerOptions; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelper; use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation; -use Shlinkio\Shlink\Core\Util\UrlValidatorInterface; class ShortUrlTitleResolutionHelperTest extends TestCase { - private ShortUrlTitleResolutionHelper $helper; - private MockObject & UrlValidatorInterface $urlValidator; + private MockObject & ClientInterface $httpClient; protected function setUp(): void { - $this->urlValidator = $this->createMock(UrlValidatorInterface::class); - $this->helper = new ShortUrlTitleResolutionHelper($this->urlValidator); + $this->httpClient = $this->createMock(ClientInterface::class); } - #[Test, DataProvider('provideTitles')] - public function urlIsProperlyShortened(?string $title, int $validateWithTitleCallsNum, int $validateCallsNum): void + #[Test] + public function dataIsReturnedAsIsWhenResolvingTitlesIsDisabled(): void { - $longUrl = 'http://foobar.com/12345/hello?foo=bar'; - $this->urlValidator->expects($this->exactly($validateWithTitleCallsNum))->method('validateUrlWithTitle')->with( - $longUrl, - $this->isFalse(), - ); - $this->urlValidator->expects($this->exactly($validateCallsNum))->method('validateUrl')->with( - $longUrl, - $this->isFalse(), - ); + $data = ShortUrlCreation::fromRawData(['longUrl' => 'http://foobar.com/12345/hello?foo=bar']); + $this->httpClient->expects($this->never())->method('request'); - $this->helper->processTitleAndValidateUrl( - ShortUrlCreation::fromRawData(['longUrl' => $longUrl, 'title' => $title]), - ); + $result = $this->helper()->processTitle($data); + + self::assertSame($data, $result); } - public static function provideTitles(): iterable + #[Test] + public function dataIsReturnedAsIsWhenItAlreadyHasTitle(): void { - yield 'no title' => [null, 1, 0]; - yield 'title' => ['link title', 0, 1]; + $data = ShortUrlCreation::fromRawData([ + 'longUrl' => 'http://foobar.com/12345/hello?foo=bar', + 'title' => 'foo', + ]); + $this->httpClient->expects($this->never())->method('request'); + + $result = $this->helper(autoResolveTitles: true)->processTitle($data); + + self::assertSame($data, $result); + } + + #[Test] + public function dataIsReturnedAsIsWhenFetchingFails(): void + { + $data = ShortUrlCreation::fromRawData([ + 'longUrl' => 'http://foobar.com/12345/hello?foo=bar', + ]); + $this->httpClient->expects($this->once())->method('request')->willThrowException(new Exception('Error')); + + $result = $this->helper(autoResolveTitles: true)->processTitle($data); + + self::assertSame($data, $result); + } + + #[Test] + public function dataIsReturnedAsIsWhenResponseIsNotHtml(): void + { + $data = ShortUrlCreation::fromRawData([ + 'longUrl' => 'http://foobar.com/12345/hello?foo=bar', + ]); + $this->httpClient->expects($this->once())->method('request')->willReturn(new JsonResponse(['foo' => 'bar'])); + + $result = $this->helper(autoResolveTitles: true)->processTitle($data); + + self::assertSame($data, $result); + } + + #[Test] + public function dataIsReturnedAsIsWhenTitleCannotBeResolvedFromResponse(): void + { + $data = ShortUrlCreation::fromRawData([ + 'longUrl' => 'http://foobar.com/12345/hello?foo=bar', + ]); + $this->httpClient->expects($this->once())->method('request')->willReturn($this->respWithoutTitle()); + + $result = $this->helper(autoResolveTitles: true)->processTitle($data); + + self::assertSame($data, $result); + } + + #[Test] + public function titleIsUpdatedWhenItCanBeResolvedFromResponse(): void + { + $data = ShortUrlCreation::fromRawData([ + 'longUrl' => 'http://foobar.com/12345/hello?foo=bar', + ]); + $this->httpClient->expects($this->once())->method('request')->willReturn($this->respWithTitle()); + + $result = $this->helper(autoResolveTitles: true)->processTitle($data); + + self::assertNotSame($data, $result); + self::assertEquals('Resolved "title"', $result->title); + } + + private function respWithoutTitle(): Response + { + $body = $this->createStreamWithContent('No title'); + return new Response($body, 200, ['Content-Type' => 'text/html']); + } + + private function respWithTitle(): Response + { + $body = $this->createStreamWithContent(' Resolved "title" '); + return new Response($body, 200, ['Content-Type' => 'TEXT/html; charset=utf-8']); + } + + private function createStreamWithContent(string $content): Stream + { + $body = new Stream('php://temp', 'wr'); + $body->write($content); + $body->rewind(); + + return $body; + } + + private function helper(bool $autoResolveTitles = false): ShortUrlTitleResolutionHelper + { + return new ShortUrlTitleResolutionHelper( + $this->httpClient, + new UrlShortenerOptions(autoResolveTitles: $autoResolveTitles), + ); } } diff --git a/module/Core/test/ShortUrl/ShortUrlServiceTest.php b/module/Core/test/ShortUrl/ShortUrlServiceTest.php index 67b10720..dfbf7d75 100644 --- a/module/Core/test/ShortUrl/ShortUrlServiceTest.php +++ b/module/Core/test/ShortUrl/ShortUrlServiceTest.php @@ -63,7 +63,7 @@ class ShortUrlServiceTest extends TestCase )->willReturn($shortUrl); $this->titleResolutionHelper->expects($expectedValidateCalls) - ->method('processTitleAndValidateUrl') + ->method('processTitle') ->with($shortUrlEdit) ->willReturn($shortUrlEdit); @@ -102,10 +102,6 @@ class ShortUrlServiceTest extends TestCase 'maxVisits' => 10, 'longUrl' => 'https://modifiedLongUrl', ]), ApiKey::create()]; - yield 'long URL with validation' => [new InvokedCount(1), ShortUrlEdition::fromRawData([ - 'longUrl' => 'https://modifiedLongUrl', - 'validateUrl' => true, - ]), null]; yield 'device redirects' => [new InvokedCount(0), ShortUrlEdition::fromRawData([ 'deviceLongUrls' => [ DeviceType::IOS->value => 'https://iosLongUrl', diff --git a/module/Core/test/ShortUrl/UrlShortenerTest.php b/module/Core/test/ShortUrl/UrlShortenerTest.php index a442abb3..b332afd2 100644 --- a/module/Core/test/ShortUrl/UrlShortenerTest.php +++ b/module/Core/test/ShortUrl/UrlShortenerTest.php @@ -57,7 +57,7 @@ class UrlShortenerTest extends TestCase { $longUrl = 'http://foobar.com/12345/hello?foo=bar'; $meta = ShortUrlCreation::fromRawData(['longUrl' => $longUrl]); - $this->titleResolutionHelper->expects($this->once())->method('processTitleAndValidateUrl')->with( + $this->titleResolutionHelper->expects($this->once())->method('processTitle')->with( $meta, )->willReturnArgument(0); $this->shortCodeHelper->method('ensureShortCodeUniqueness')->willReturn(true); @@ -90,7 +90,7 @@ class UrlShortenerTest extends TestCase ); $this->shortCodeHelper->expects($this->once())->method('ensureShortCodeUniqueness')->willReturn(false); - $this->titleResolutionHelper->expects($this->once())->method('processTitleAndValidateUrl')->with( + $this->titleResolutionHelper->expects($this->once())->method('processTitle')->with( $meta, )->willReturnArgument(0); @@ -105,7 +105,7 @@ class UrlShortenerTest extends TestCase $repo = $this->createMock(ShortUrlRepository::class); $repo->expects($this->once())->method('findOneMatching')->willReturn($expected); $this->em->expects($this->once())->method('getRepository')->with(ShortUrl::class)->willReturn($repo); - $this->titleResolutionHelper->expects($this->never())->method('processTitleAndValidateUrl'); + $this->titleResolutionHelper->expects($this->never())->method('processTitle'); $this->shortCodeHelper->method('ensureShortCodeUniqueness')->willReturn(true); $result = $this->urlShortener->shorten($meta); diff --git a/module/Core/test/Util/UrlValidatorTest.php b/module/Core/test/Util/UrlValidatorTest.php deleted file mode 100644 index 233d69bd..00000000 --- a/module/Core/test/Util/UrlValidatorTest.php +++ /dev/null @@ -1,176 +0,0 @@ -httpClient = $this->createMock(ClientInterface::class); - } - - #[Test] - public function exceptionIsThrownWhenUrlIsInvalid(): void - { - $this->httpClient->expects($this->once())->method('request')->willThrowException($this->clientException()); - $this->expectException(InvalidUrlException::class); - - $this->urlValidator()->validateUrl('http://foobar.com/12345/hello?foo=bar', true); - } - - #[Test] - public function expectedUrlIsCalledWhenTryingToVerify(): void - { - $expectedUrl = 'http://foobar.com'; - - $this->httpClient->expects($this->once())->method('request')->with( - RequestMethodInterface::METHOD_GET, - $expectedUrl, - $this->callback(function (array $options) { - Assert::assertArrayHasKey(RequestOptions::ALLOW_REDIRECTS, $options); - Assert::assertEquals(['max' => 15], $options[RequestOptions::ALLOW_REDIRECTS]); - Assert::assertArrayHasKey(RequestOptions::IDN_CONVERSION, $options); - Assert::assertTrue($options[RequestOptions::IDN_CONVERSION]); - Assert::assertArrayHasKey(RequestOptions::HEADERS, $options); - Assert::assertArrayHasKey('User-Agent', $options[RequestOptions::HEADERS]); - - return true; - }), - )->willReturn(new Response()); - - $this->urlValidator()->validateUrl($expectedUrl, true); - } - - #[Test] - public function noCheckIsPerformedWhenUrlValidationIsDisabled(): void - { - $this->httpClient->expects($this->never())->method('request'); - $this->urlValidator()->validateUrl('', false); - } - - #[Test] - public function validateUrlWithTitleReturnsNullWhenRequestFailsAndValidationIsDisabled(): void - { - $this->httpClient->expects($this->once())->method('request')->willThrowException($this->clientException()); - - $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); - - self::assertNull($result); - } - - #[Test] - public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabled(): void - { - $this->httpClient->expects($this->never())->method('request'); - - $result = $this->urlValidator()->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false); - - self::assertNull($result); - } - - #[Test] - public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabledAndValidationIsEnabled(): void - { - $this->httpClient->expects($this->once())->method('request')->with( - RequestMethodInterface::METHOD_HEAD, - $this->anything(), - $this->anything(), - )->willReturn($this->respWithTitle()); - - $result = $this->urlValidator()->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); - - self::assertNull($result); - } - - #[Test] - public function validateUrlWithTitleResolvesTitleWhenAutoResolutionIsEnabled(): void - { - $this->httpClient->expects($this->once())->method('request')->with( - RequestMethodInterface::METHOD_GET, - $this->anything(), - $this->anything(), - )->willReturn($this->respWithTitle()); - - $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); - - self::assertEquals('Resolved "title"', $result); - } - - #[Test] - public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsEnabledAndReturnedContentTypeIsInvalid(): void - { - $this->httpClient->expects($this->once())->method('request')->with( - RequestMethodInterface::METHOD_GET, - $this->anything(), - $this->anything(), - )->willReturn(new Response('php://memory', 200, ['Content-Type' => 'application/octet-stream'])); - - $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); - - self::assertNull($result); - } - - #[Test] - public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsEnabledAndBodyDoesNotContainTitle(): void - { - $this->httpClient->expects($this->once())->method('request')->with( - RequestMethodInterface::METHOD_GET, - $this->anything(), - $this->anything(), - )->willReturn( - new Response($this->createStreamWithContent('No title'), 200, ['Content-Type' => 'text/html']), - ); - - $result = $this->urlValidator(true)->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true); - - self::assertNull($result); - } - - private function respWithTitle(): Response - { - $body = $this->createStreamWithContent(' Resolved "title" '); - return new Response($body, 200, ['Content-Type' => 'TEXT/html; charset=utf-8']); - } - - private function createStreamWithContent(string $content): Stream - { - $body = new Stream('php://temp', 'wr'); - $body->write($content); - $body->rewind(); - - return $body; - } - - private function clientException(): ClientException - { - return new ClientException( - '', - new Request(RequestMethodInterface::METHOD_GET, ''), - new Response(), - ); - } - - public function urlValidator(bool $autoResolveTitles = false): UrlValidator - { - return new UrlValidator($this->httpClient, new UrlShortenerOptions(autoResolveTitles: $autoResolveTitles)); - } -} diff --git a/module/Rest/test-api/Action/CreateShortUrlTest.php b/module/Rest/test-api/Action/CreateShortUrlTest.php index efd70666..41a4559b 100644 --- a/module/Rest/test-api/Action/CreateShortUrlTest.php +++ b/module/Rest/test-api/Action/CreateShortUrlTest.php @@ -224,27 +224,6 @@ class CreateShortUrlTest extends ApiTestCase yield ['http://téstb.shlink.io']; // Redirects to http://tést.shlink.io } - #[Test, DataProvider('provideInvalidUrls')] - public function failsToCreateShortUrlWithInvalidLongUrl(string $url, string $version, string $expectedType): void - { - $expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url); - - [$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url, 'validateUrl' => true], version: $version); - - self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode); - self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']); - self::assertEquals($expectedType, $payload['type']); - self::assertEquals($expectedDetail, $payload['detail']); - self::assertEquals('Invalid URL', $payload['title']); - self::assertEquals($url, $payload['url']); - } - - public static function provideInvalidUrls(): iterable - { - yield 'API version 2' => ['https://this-has-to-be-invalid.com', '2', 'https://shlink.io/api/error/invalid-url']; - yield 'API version 3' => ['https://this-has-to-be-invalid.com', '3', 'https://shlink.io/api/error/invalid-url']; - } - #[Test, DataProvider('provideInvalidArgumentApiVersions')] public function failsToCreateShortUrlWithoutLongUrl(array $payload, string $version, string $expectedType): void { diff --git a/module/Rest/test-api/Action/EditShortUrlTest.php b/module/Rest/test-api/Action/EditShortUrlTest.php index 89055adb..07fe84d3 100644 --- a/module/Rest/test-api/Action/EditShortUrlTest.php +++ b/module/Rest/test-api/Action/EditShortUrlTest.php @@ -75,28 +75,16 @@ class EditShortUrlTest extends ApiTestCase return $matchingShortUrl['meta'] ?? []; } - #[Test, DataProvider('provideLongUrls')] - public function longUrlCanBeEditedIfItIsValid(string $longUrl, int $expectedStatus, ?string $expectedError): void + public function longUrlCanBeEdited(): void { $shortCode = 'abc123'; $url = sprintf('/short-urls/%s', $shortCode); $resp = $this->callApiWithKey(self::METHOD_PATCH, $url, [RequestOptions::JSON => [ - 'longUrl' => $longUrl, - 'validateUrl' => true, + 'longUrl' => 'https://shlink.io', ]]); - self::assertEquals($expectedStatus, $resp->getStatusCode()); - if ($expectedError !== null) { - $payload = $this->getJsonResponsePayload($resp); - self::assertEquals($expectedError, $payload['type']); - } - } - - public static function provideLongUrls(): iterable - { - yield 'valid URL' => ['https://shlink.io', self::STATUS_OK, null]; - yield 'invalid URL' => ['http://foo', self::STATUS_BAD_REQUEST, 'https://shlink.io/api/error/invalid-url']; + self::assertEquals(self::STATUS_OK, $resp->getStatusCode()); } #[Test, DataProviderExternal(ApiTestDataProviders::class, 'invalidUrlsProvider')]