mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-28 09:03:07 +03:00
Merge pull request #554 from acelaya-forks/feature/problem-details
Feature/problem details
This commit is contained in:
commit
df23f20d31
130 changed files with 1597 additions and 1878 deletions
|
@ -9,7 +9,7 @@ echo 'Starting server...'
|
|||
vendor/bin/zend-expressive-swoole start -d
|
||||
sleep 2
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
|
||||
testsExitCode=$?
|
||||
|
||||
vendor/bin/zend-expressive-swoole stop
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"php": "^7.2",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"acelaya/ze-content-based-error-handler": "^3.0",
|
||||
"akrabat/ip-address-middleware": "^1.0",
|
||||
"cakephp/chronos": "^1.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
|
@ -34,7 +33,7 @@
|
|||
"phly/phly-event-dispatcher": "^1.0",
|
||||
"predis/predis": "^1.1",
|
||||
"pugx/shortid-php": "^0.5",
|
||||
"shlinkio/shlink-common": "^2.2.1",
|
||||
"shlinkio/shlink-common": "^2.3",
|
||||
"shlinkio/shlink-event-dispatcher": "^1.0",
|
||||
"shlinkio/shlink-installer": "^3.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^1.1",
|
||||
|
@ -53,20 +52,20 @@
|
|||
"zendframework/zend-expressive-swoole": "^2.4",
|
||||
"zendframework/zend-inputfilter": "^2.10",
|
||||
"zendframework/zend-paginator": "^2.8",
|
||||
"zendframework/zend-problem-details": "^1.0",
|
||||
"zendframework/zend-servicemanager": "^3.4",
|
||||
"zendframework/zend-stdlib": "^3.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"eaglewu/swoole-ide-helper": "dev-master",
|
||||
"filp/whoops": "^2.4",
|
||||
"infection/infection": "^0.14.2",
|
||||
"phpstan/phpstan": "^0.11.16",
|
||||
"phpunit/phpcov": "^6.0",
|
||||
"phpunit/phpunit": "^8.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.0.0",
|
||||
"shlinkio/shlink-test-utils": "^1.0",
|
||||
"shlinkio/shlink-test-utils": "^1.1",
|
||||
"symfony/dotenv": "^4.3",
|
||||
"symfony/var-dumper": "^4.3",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
|
|
34
config/autoload/error-handler.global.php
Normal file
34
config/autoload/error-handler.global.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common\Logger;
|
||||
use Zend\ProblemDetails\ProblemDetailsMiddleware;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
|
||||
'backwards_compatible_problem_details' => [
|
||||
'default_type_fallbacks' => [
|
||||
404 => 'NOT_FOUND',
|
||||
500 => 'INTERNAL_SERVER_ERROR',
|
||||
],
|
||||
'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
|
||||
],
|
||||
|
||||
'error_handler' => [
|
||||
'listeners' => [Logger\ErrorLogger::class],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'delegators' => [
|
||||
ErrorHandler::class => [
|
||||
Logger\ErrorHandlerListenerAttachingDelegator::class,
|
||||
],
|
||||
ProblemDetailsMiddleware::class => [
|
||||
Logger\ErrorHandlerListenerAttachingDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
|
||||
|
||||
return [
|
||||
'dependencies' => [
|
||||
'invokables' => [
|
||||
'Zend\Expressive\Whoops' => Whoops\Run::class,
|
||||
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
|
||||
],
|
||||
],
|
||||
|
||||
'whoops' => [
|
||||
'json_exceptions' => [
|
||||
'display' => true,
|
||||
'show_trace' => true,
|
||||
'ajax_only' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'error_handler' => [
|
||||
'plugins' => [
|
||||
'factories' => [
|
||||
'text/html' => WhoopsErrorResponseGeneratorFactory::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
|
@ -5,18 +5,31 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Zend\Expressive;
|
||||
use Zend\ProblemDetails;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'error-handler' => [
|
||||
'middleware' => [
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
ErrorHandler::class,
|
||||
],
|
||||
],
|
||||
'error-handler-rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\CrossDomainMiddleware::class,
|
||||
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
|
||||
ProblemDetails\ProblemDetailsMiddleware::class,
|
||||
],
|
||||
],
|
||||
|
||||
'pre-routing' => [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Common\Middleware\CloseDbConnectionMiddleware::class,
|
||||
],
|
||||
'priority' => 12,
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
'path' => '/rest',
|
||||
|
@ -24,33 +37,40 @@ return [
|
|||
Rest\Middleware\PathVersionMiddleware::class,
|
||||
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
|
||||
],
|
||||
'priority' => 11,
|
||||
],
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\RouteMiddleware::class,
|
||||
],
|
||||
'priority' => 10,
|
||||
],
|
||||
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\CrossDomainMiddleware::class,
|
||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
|
||||
'post-routing' => [
|
||||
'dispatch' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
Core\Response\NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
||||
'not-found-rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
ProblemDetails\ProblemDetailsNotFoundHandler::class,
|
||||
],
|
||||
],
|
||||
'not-found' => [
|
||||
'middleware' => [
|
||||
Core\ErrorHandler\NotFoundRedirectHandler::class,
|
||||
Core\ErrorHandler\NotFoundTemplateHandler::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -4,9 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
use Zend\ProblemDetails;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
|
@ -16,7 +16,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Expressive\Plates\ConfigProvider::class,
|
||||
Expressive\Swoole\ConfigProvider::class,
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": ["type", "title", "detail", "status"],
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "A machine unique code"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A unique title"
|
||||
},
|
||||
"detail": {
|
||||
"type": "string",
|
||||
"description": "A human-friendly error description"
|
||||
},
|
||||
"status": {
|
||||
"type": "number",
|
||||
"description": "HTTP response status code"
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "**[Deprecated] Use type instead. Not returned for v2 of the REST API** A machine unique code",
|
||||
"deprecated": true
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "A human-friendly error message"
|
||||
"description": "**[Deprecated] Use detail instead. Not returned for v2 of the REST API** A human-friendly error message",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
13
docs/swagger/parameters/version.json
Normal file
13
docs/swagger/parameters/version.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "version",
|
||||
"description": "The API version to be consumed",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"2",
|
||||
"1"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@
|
|||
"summary": "List short URLs",
|
||||
"description": "Returns the list of short URLs.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
|
@ -150,7 +153,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -175,6 +178,11 @@
|
|||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
|
@ -256,11 +264,43 @@
|
|||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The long URL was not provided or is invalid.",
|
||||
"description": "Some of provided data is invalid. Check extra fields to know exactly what.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"invalidElements": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"validSince",
|
||||
"validUntil",
|
||||
"customSlug",
|
||||
"maxVisits",
|
||||
"findIfExists",
|
||||
"domain"
|
||||
]
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "A URL that could not be verified, if the error type is INVALID_URL"
|
||||
},
|
||||
"customSlug": {
|
||||
"type": "string",
|
||||
"description": "Provided custom slug when the error type is INVALID_SLUG"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +308,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"summary": "Create a short URL",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "apiKey",
|
||||
"in": "query",
|
||||
|
@ -77,7 +80,7 @@
|
|||
"400": {
|
||||
"description": "The long URL was not provided or is invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -89,9 +92,12 @@
|
|||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "INVALID_URL",
|
||||
"message": "Provided URL foo is invalid. Try with a different one."
|
||||
"application/problem+json": {
|
||||
"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": "INVALID_URL"
|
||||
}
|
||||
|
@ -99,7 +105,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -111,11 +117,11 @@
|
|||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "UNKNOWN_ERROR",
|
||||
"application/problem+json": {
|
||||
"error": "INTERNAL_SERVER_ERROR",
|
||||
"message": "Unexpected error occurred"
|
||||
},
|
||||
"text/plain": "UNKNOWN_ERROR"
|
||||
"text/plain": "INTERNAL_SERVER_ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"summary": "Parse short code",
|
||||
"description": "Get the long URL behind a short URL's short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -62,20 +65,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -85,7 +78,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -103,6 +96,9 @@
|
|||
"summary": "Edit short URL",
|
||||
"description": "Update certain meta arguments from an existing short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -153,9 +149,31 @@
|
|||
"400": {
|
||||
"description": "Provided meta arguments are invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["invalidElements"],
|
||||
"properties": {
|
||||
"invalidElements": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"validSince",
|
||||
"validUntil",
|
||||
"maxVisits"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -163,7 +181,7 @@
|
|||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -173,7 +191,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -192,6 +210,9 @@
|
|||
"summary": "[DEPRECATED] Edit short URL",
|
||||
"description": "**[DEPRECATED]** Use [editShortUrl](#/Short_URLs/getShortUrl) instead",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -242,7 +263,7 @@
|
|||
"400": {
|
||||
"description": "Provided meta arguments are invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -252,7 +273,7 @@
|
|||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -262,7 +283,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -280,6 +301,9 @@
|
|||
"summary": "Delete short URL",
|
||||
"description": "Deletes the short URL for provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -302,26 +326,28 @@
|
|||
"204": {
|
||||
"description": "The short URL has been properly deleted."
|
||||
},
|
||||
"400": {
|
||||
"422": {
|
||||
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "INVALID_SHORTCODE_DELETION",
|
||||
"message": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits."
|
||||
"application/problem+json": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORTCODE_DELETION",
|
||||
"detail": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits.",
|
||||
"status": 422
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -331,7 +357,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"summary": "Edit tags on short URL",
|
||||
"description": "Edit the tags on URL identified by provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -78,7 +81,7 @@
|
|||
"400": {
|
||||
"description": "The request body does not contain a \"tags\" param with array type.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
"summary": "List visits for short URL",
|
||||
"description": "Get the list of visits on the short URL behind provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
|
@ -132,7 +135,7 @@
|
|||
"404": {
|
||||
"description": "The short code does not belong to any short URL.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -142,7 +145,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
|
@ -53,7 +58,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -78,6 +83,11 @@
|
|||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
|
@ -140,7 +150,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -165,6 +175,11 @@
|
|||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
|
@ -197,7 +212,7 @@
|
|||
"400": {
|
||||
"description": "You have not provided either the oldName or the newName params.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -207,7 +222,7 @@
|
|||
"404": {
|
||||
"description": "There's no tag found with the name provided in oldName param.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -217,7 +232,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
@ -235,6 +250,9 @@
|
|||
"summary": "Delete tags",
|
||||
"description": "Deletes provided list of tags",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
|
@ -263,7 +281,7 @@
|
|||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
|
|
|
@ -20,16 +20,6 @@
|
|||
"responses": {
|
||||
"302": {
|
||||
"description": "Visit properly tracked and redirected"
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,16 +29,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,16 +40,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,16 +28,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,24 +71,24 @@
|
|||
],
|
||||
|
||||
"paths": {
|
||||
"/rest/v1/short-urls": {
|
||||
"/rest/v{version}/short-urls": {
|
||||
"$ref": "paths/v1_short-urls.json"
|
||||
},
|
||||
"/rest/v1/short-urls/shorten": {
|
||||
"/rest/v{version}/short-urls/shorten": {
|
||||
"$ref": "paths/v1_short-urls_shorten.json"
|
||||
},
|
||||
"/rest/v1/short-urls/{shortCode}": {
|
||||
"/rest/v{version}/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/rest/v1/short-urls/{shortCode}/tags": {
|
||||
"/rest/v{version}/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/rest/v1/tags": {
|
||||
"/rest/v{version}/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/rest/v1/short-urls/{shortCode}/visits": {
|
||||
"/rest/v{version}/short-urls/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||
},
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
@ -45,7 +45,7 @@ class DisableKeyCommand extends Command
|
|||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$io->error(sprintf('API key "%s" does not exist.', $apiKey));
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,22 +55,17 @@ class DeleteShortUrlCommand extends Command
|
|||
try {
|
||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
} catch (Exception\ShortUrlNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
return $this->retry($io, $shortCode, $e);
|
||||
return $this->retry($io, $shortCode, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): int
|
||||
private function retry(SymfonyStyle $io, string $shortCode, string $warningMsg): int
|
||||
{
|
||||
$warningMsg = sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
|
||||
$shortCode,
|
||||
$e->getVisitsThreshold()
|
||||
);
|
||||
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
||||
$io->writeln(sprintf('<bg=yellow>%s</>', $warningMsg));
|
||||
$forceDelete = $io->confirm('Do you want to delete it anyway?', false);
|
||||
|
||||
if ($forceDelete) {
|
||||
|
|
|
@ -141,13 +141,8 @@ class GenerateShortUrlCommand extends Command
|
|||
sprintf('Generated short URL: <info>%s</info>', $shortUrl->toString($this->domainConfig)),
|
||||
]);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidUrlException $e) {
|
||||
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$io->error(
|
||||
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
|
||||
);
|
||||
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
@ -65,11 +64,8 @@ class ResolveUrlCommand extends Command
|
|||
$url = $this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
@ -13,8 +13,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:rename';
|
||||
|
@ -47,8 +45,8 @@ class RenameTagCommand extends Command
|
|||
$this->tagService->renameTag($oldName, $newName);
|
||||
$io->success('Tag properly renamed.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(sprintf('A tag with name "%s" was not found', $oldName));
|
||||
} catch (TagNotFoundException $e) {
|
||||
$io->error($e->getMessage());
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,20 +12,16 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||
/** @var bool */
|
||||
private $olderDbExists;
|
||||
|
||||
public function __construct(bool $olderDbExists, string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
$this->olderDbExists = $olderDbExists;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function create(bool $olderDbExists, ?Throwable $prev = null): self
|
||||
{
|
||||
return new self(
|
||||
$olderDbExists,
|
||||
$e = new self(
|
||||
'An error occurred while updating geolocation database, and an older version could not be found',
|
||||
0,
|
||||
$prev
|
||||
);
|
||||
$e->olderDbExists = $olderDbExists;
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public function olderDbExists(): bool
|
||||
|
|
|
@ -10,7 +10,6 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
|||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use Throwable;
|
||||
|
||||
class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
{
|
||||
|
@ -40,8 +39,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
|
||||
try {
|
||||
$this->downloadIfNeeded($mustBeUpdated, $handleProgress);
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ class DisableKeyCommandTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function providedApiKeyIsDisabled()
|
||||
public function providedApiKeyIsDisabled(): void
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
|
||||
|
@ -43,17 +43,18 @@ class DisableKeyCommandTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function errorIsReturnedIfServiceThrowsException()
|
||||
public function errorIsReturnedIfServiceThrowsException(): void
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$disable = $this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class);
|
||||
$expectedMessage = 'API key "abcd1234" does not exist.';
|
||||
$disable = $this->apiKeyService->disable($apiKey)->willThrow(new InvalidArgumentException($expectedMessage));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('API key "abcd1234" does not exist.', $output);
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
$disable->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,13 +58,13 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
Exception\InvalidShortCodeException::class
|
||||
Exception\ShortUrlNotFoundException::fromNotFoundShortCode($shortCode)
|
||||
);
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
||||
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
@ -79,11 +79,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
): void {
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
|
||||
function (array $args) {
|
||||
function (array $args) use ($shortCode) {
|
||||
$ignoreThreshold = array_pop($args);
|
||||
|
||||
if (!$ignoreThreshold) {
|
||||
throw new Exception\DeleteShortUrlException(10);
|
||||
throw Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -93,7 +93,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||
$shortCode
|
||||
), $output);
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
|
@ -112,7 +112,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
new Exception\DeleteShortUrlException(10)
|
||||
Exception\DeleteShortUrlException::fromVisitsThreshold(10, $shortCode)
|
||||
);
|
||||
$this->commandTester->setInputs(['no']);
|
||||
|
||||
|
@ -120,7 +120,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString(sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
|
||||
$shortCode
|
||||
), $output);
|
||||
$this->assertStringContainsString('Short URL was not deleted.', $output);
|
||||
|
|
|
@ -59,21 +59,22 @@ class GenerateShortUrlCommandTest extends TestCase
|
|||
/** @test */
|
||||
public function exceptionWhileParsingLongUrlOutputsError(): void
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||
$url = 'http://domain.com/invalid';
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid']);
|
||||
$this->commandTester->execute(['longUrl' => $url]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
|
||||
$this->assertStringContainsString('Provided URL "http://domain.com/invalid" is invalid.', $output);
|
||||
$this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function providingNonUniqueSlugOutputsError(): void
|
||||
{
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
|
||||
NonUniqueSlugException::class
|
||||
NonUniqueSlugException::fromSlug('my-slug')
|
||||
);
|
||||
|
||||
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
|
||||
|
|
|
@ -8,12 +8,13 @@ use PHPUnit\Framework\TestCase;
|
|||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
|
@ -51,23 +52,12 @@ class ResolveUrlCommandTest extends TestCase
|
|||
public function incorrectShortCodeOutputsErrorMessage(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(EntityDoesNotExistException::class)
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, null)
|
||||
->willThrow(ShortUrlNotFoundException::fromNotFoundShortCode($shortCode))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Provided short code "' . $shortCode . '" could not be found.', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function wrongShortCodeFormatOutputsErrorMessage(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, null)->willThrow(new InvalidShortCodeException())
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertStringContainsString('Provided short code "' . $shortCode . '" has an invalid format.', $output);
|
||||
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase;
|
|||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
@ -34,11 +34,11 @@ class RenameTagCommandTest extends TestCase
|
|||
}
|
||||
|
||||
/** @test */
|
||||
public function errorIsPrintedIfExceptionIsThrown()
|
||||
public function errorIsPrintedIfExceptionIsThrown(): void
|
||||
{
|
||||
$oldName = 'foo';
|
||||
$newName = 'bar';
|
||||
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class);
|
||||
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(TagNotFoundException::fromTag('foo'));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'oldName' => $oldName,
|
||||
|
@ -46,12 +46,12 @@ class RenameTagCommandTest extends TestCase
|
|||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('A tag with name "foo" was not found', $output);
|
||||
$this->assertStringContainsString('Tag with name "foo" could not be found', $output);
|
||||
$renameTag->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function successIsPrintedIfNoErrorOccurs()
|
||||
public function successIsPrintedIfNoErrorOccurs(): void
|
||||
{
|
||||
$oldName = 'foo';
|
||||
$newName = 'bar';
|
||||
|
|
|
@ -12,47 +12,6 @@ use Throwable;
|
|||
|
||||
class GeolocationDbUpdateFailedExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideOlderDbExists
|
||||
*/
|
||||
public function constructCreatesExceptionWithDefaultArgs(bool $olderDbExists): void
|
||||
{
|
||||
$e = new GeolocationDbUpdateFailedException($olderDbExists);
|
||||
|
||||
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||
$this->assertEquals('', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideOlderDbExists(): iterable
|
||||
{
|
||||
yield 'with older DB' => [true];
|
||||
yield 'without older DB' => [false];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideConstructorArgs
|
||||
*/
|
||||
public function constructCreatesException(bool $olderDbExists, string $message, int $code, ?Throwable $prev): void
|
||||
{
|
||||
$e = new GeolocationDbUpdateFailedException($olderDbExists, $message, $code, $prev);
|
||||
|
||||
$this->assertEquals($olderDbExists, $e->olderDbExists());
|
||||
$this->assertEquals($message, $e->getMessage());
|
||||
$this->assertEquals($code, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideConstructorArgs(): iterable
|
||||
{
|
||||
yield [true, 'This is a nice error message', 99, new Exception('prev')];
|
||||
yield [false, 'Another message', 0, new RuntimeException('prev')];
|
||||
yield [true, 'An yet another message', -50, null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideCreateArgs
|
||||
|
|
|
@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\Core;
|
|||
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
|
||||
use Zend\Expressive\Router\RouterInterface;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
@ -17,7 +17,8 @@ return [
|
|||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
NotFoundHandler::class => ConfigAbstractFactory::class,
|
||||
ErrorHandler\NotFoundRedirectHandler::class => ConfigAbstractFactory::class,
|
||||
ErrorHandler\NotFoundTemplateHandler::class => ConfigAbstractFactory::class,
|
||||
|
||||
Options\AppOptions::class => ConfigAbstractFactory::class,
|
||||
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
|
||||
|
@ -43,11 +44,8 @@ return [
|
|||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
NotFoundHandler::class => [
|
||||
TemplateRendererInterface::class,
|
||||
NotFoundRedirectOptions::class,
|
||||
'config.router.base_path',
|
||||
],
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [NotFoundRedirectOptions::class, 'config.router.base_path'],
|
||||
ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class],
|
||||
|
||||
Options\AppOptions::class => ['config.app_options'],
|
||||
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],
|
||||
|
|
|
@ -11,8 +11,7 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
|
@ -72,7 +71,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
|
|||
}
|
||||
|
||||
return $this->createSuccessResp($this->buildUrlToRedirectTo($url, $query, $disableTrackParam));
|
||||
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
$this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]);
|
||||
return $this->createErrorResp($request, $handler);
|
||||
}
|
||||
|
|
|
@ -11,8 +11,7 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface;
|
||||
|
@ -56,7 +55,7 @@ class PreviewAction implements MiddlewareInterface
|
|||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
$imagePath = $this->previewGenerator->generatePreview($url->getLongUrl());
|
||||
return $this->generateImageResponse($imagePath);
|
||||
} catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
|
||||
} catch (ShortUrlNotFoundException | PreviewGenerationException $e) {
|
||||
$this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]);
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Zend\Expressive\Router\Exception\RuntimeException;
|
||||
use Zend\Expressive\Router\RouterInterface;
|
||||
|
@ -60,7 +59,7 @@ class QrCodeAction implements MiddlewareInterface
|
|||
|
||||
try {
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
||||
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
$this->logger->warning('An error occurred while creating QR code. {e}', ['e' => $e]);
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
|
|
@ -2,78 +2,40 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Response;
|
||||
namespace Shlinkio\Shlink\Core\ErrorHandler;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function Functional\contains;
|
||||
use function rtrim;
|
||||
|
||||
class NotFoundHandler implements RequestHandlerInterface
|
||||
class NotFoundRedirectHandler implements MiddlewareInterface
|
||||
{
|
||||
public const NOT_FOUND_TEMPLATE = 'ShlinkCore::error/404';
|
||||
public const INVALID_SHORT_CODE_TEMPLATE = 'ShlinkCore::invalid-short-code';
|
||||
|
||||
/** @var TemplateRendererInterface */
|
||||
private $renderer;
|
||||
/** @var NotFoundRedirectOptions */
|
||||
private $redirectOptions;
|
||||
/** @var string */
|
||||
private $shlinkBasePath;
|
||||
|
||||
public function __construct(
|
||||
TemplateRendererInterface $renderer,
|
||||
NotFoundRedirectOptions $redirectOptions,
|
||||
string $shlinkBasePath
|
||||
) {
|
||||
$this->renderer = $renderer;
|
||||
public function __construct(NotFoundRedirectOptions $redirectOptions, string $shlinkBasePath)
|
||||
{
|
||||
$this->redirectOptions = $redirectOptions;
|
||||
$this->shlinkBasePath = $shlinkBasePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the next available middleware and return the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
/** @var RouteResult $routeResult */
|
||||
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
||||
$redirectResponse = $this->createRedirectResponse($routeResult, $request->getUri());
|
||||
if ($redirectResponse !== null) {
|
||||
return $redirectResponse;
|
||||
}
|
||||
|
||||
$accepts = explode(',', $request->getHeaderLine('Accept'));
|
||||
$accept = array_shift($accepts);
|
||||
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
|
||||
// If the first accepted type is json, return a json response
|
||||
if (contains(['application/json', 'text/json', 'application/x-json'], $accept)) {
|
||||
return new Response\JsonResponse([
|
||||
'error' => 'NOT_FOUND',
|
||||
'message' => 'Not found',
|
||||
], $status);
|
||||
}
|
||||
|
||||
$template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE;
|
||||
return new Response\HtmlResponse($this->renderer->render($template), $status);
|
||||
return $redirectResponse ?? $handler->handle($request);
|
||||
}
|
||||
|
||||
private function createRedirectResponse(RouteResult $routeResult, UriInterface $uri): ?ResponseInterface
|
46
module/Core/src/ErrorHandler/NotFoundTemplateHandler.php
Normal file
46
module/Core/src/ErrorHandler/NotFoundTemplateHandler.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ErrorHandler;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
class NotFoundTemplateHandler implements RequestHandlerInterface
|
||||
{
|
||||
public const NOT_FOUND_TEMPLATE = 'ShlinkCore::error/404';
|
||||
public const INVALID_SHORT_CODE_TEMPLATE = 'ShlinkCore::invalid-short-code';
|
||||
|
||||
/** @var TemplateRendererInterface */
|
||||
private $renderer;
|
||||
|
||||
public function __construct(TemplateRendererInterface $renderer)
|
||||
{
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch the next available middleware and return the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var RouteResult $routeResult */
|
||||
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
||||
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
|
||||
$template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE;
|
||||
return new Response\HtmlResponse($this->renderer->render($template), $status);
|
||||
}
|
||||
}
|
|
@ -4,32 +4,41 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlException extends RuntimeException
|
||||
class DeleteShortUrlException extends DomainException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
/** @var int */
|
||||
private $visitsThreshold;
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
public function __construct(int $visitsThreshold, string $message = '', int $code = 0, ?Throwable $previous = null)
|
||||
{
|
||||
$this->visitsThreshold = $visitsThreshold;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
private const TITLE = 'Cannot delete short URL';
|
||||
private const TYPE = 'INVALID_SHORTCODE_DELETION'; // FIXME Should be INVALID_SHORT_URL_DELETION
|
||||
|
||||
public static function fromVisitsThreshold(int $threshold, string $shortCode): self
|
||||
{
|
||||
return new self($threshold, sprintf(
|
||||
$e = new self(sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.',
|
||||
$shortCode,
|
||||
$threshold
|
||||
));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_UNPROCESSABLE_ENTITY;
|
||||
$e->additional = [
|
||||
'shortCode' => $shortCode,
|
||||
'threshold' => $threshold,
|
||||
];
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public function getVisitsThreshold(): int
|
||||
{
|
||||
return $this->visitsThreshold;
|
||||
return $this->additional['threshold'];
|
||||
}
|
||||
}
|
||||
|
|
11
module/Core/src/Exception/DomainException.php
Normal file
11
module/Core/src/Exception/DomainException.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use DomainException as SplDomainException;
|
||||
|
||||
class DomainException extends SplDomainException implements ExceptionInterface
|
||||
{
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class EntityDoesNotExistException extends RuntimeException
|
||||
{
|
||||
public static function createFromEntityAndConditions($entityName, array $conditions)
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Entity of type %s with params [%s] does not exist',
|
||||
$entityName,
|
||||
static::serializeParams($conditions)
|
||||
));
|
||||
}
|
||||
|
||||
private static function serializeParams(array $params)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$result[] = sprintf('"%s" => "%s"', $key, $value);
|
||||
}
|
||||
|
||||
return implode(', ', $result);
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class InvalidShortCodeException extends RuntimeException
|
||||
{
|
||||
public static function fromCharset(string $shortCode, string $charSet, ?Throwable $previous = null): self
|
||||
{
|
||||
$code = $previous !== null ? $previous->getCode() : -1;
|
||||
return new static(
|
||||
sprintf('Provided short code "%s" does not match the char set "%s"', $shortCode, $charSet),
|
||||
$code,
|
||||
$previous
|
||||
);
|
||||
}
|
||||
|
||||
public static function fromNotFoundShortCode(string $shortCode): self
|
||||
{
|
||||
return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||
}
|
||||
}
|
|
@ -4,15 +4,31 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Throwable;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class InvalidUrlException extends RuntimeException
|
||||
class InvalidUrlException extends DomainException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Invalid URL';
|
||||
private const TYPE = 'INVALID_URL';
|
||||
|
||||
public static function fromUrl(string $url, ?Throwable $previous = null): self
|
||||
{
|
||||
$code = $previous !== null ? $previous->getCode() : -1;
|
||||
return new static(sprintf('Provided URL "%s" is not an existing and valid URL', $url), $code, $previous);
|
||||
$status = StatusCodeInterface::STATUS_BAD_REQUEST;
|
||||
$e = new self(sprintf('Provided URL %s is invalid. Try with a different one.', $url), $status, $previous);
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = $status;
|
||||
$e->additional = ['url' => $url];
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,34 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class NonUniqueSlugException extends InvalidArgumentException
|
||||
class NonUniqueSlugException extends InvalidArgumentException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
public static function fromSlug(string $slug, ?string $domain): self
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Invalid custom slug';
|
||||
private const TYPE = 'INVALID_SLUG';
|
||||
|
||||
public static function fromSlug(string $slug, ?string $domain = null): self
|
||||
{
|
||||
$suffix = '';
|
||||
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
|
||||
$e = new self(sprintf('Provided slug "%s" is already in use%s.', $slug, $suffix));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_BAD_REQUEST;
|
||||
$e->additional = ['customSlug' => $slug];
|
||||
|
||||
if ($domain !== null) {
|
||||
$suffix = sprintf(' for domain "%s"', $domain);
|
||||
$e->additional['domain'] = $domain;
|
||||
}
|
||||
|
||||
return new self(sprintf('Provided slug "%s" is not unique%s.', $slug, $suffix));
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
|
37
module/Core/src/Exception/ShortUrlNotFoundException.php
Normal file
37
module/Core/src/Exception/ShortUrlNotFoundException.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ShortUrlNotFoundException extends DomainException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Short URL not found';
|
||||
private const TYPE = 'INVALID_SHORTCODE';
|
||||
|
||||
public static function fromNotFoundShortCode(string $shortCode, ?string $domain = null): self
|
||||
{
|
||||
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
|
||||
$e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
$e->additional = ['shortCode' => $shortCode];
|
||||
|
||||
if ($domain !== null) {
|
||||
$e->additional['domain'] = $domain;
|
||||
}
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
32
module/Core/src/Exception/TagNotFoundException.php
Normal file
32
module/Core/src/Exception/TagNotFoundException.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class TagNotFoundException extends DomainException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Tag not found';
|
||||
private const TYPE = 'TAG_NOT_FOUND';
|
||||
|
||||
public static function fromTag(string $tag): self
|
||||
{
|
||||
$e = new self(sprintf('Tag with name "%s" could not be found', $tag));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
$e->additional = ['tag' => $tag];
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
|
@ -4,9 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Throwable;
|
||||
use Zend\InputFilter\InputFilterInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function array_keys;
|
||||
use function Functional\reduce_left;
|
||||
use function is_array;
|
||||
use function print_r;
|
||||
|
@ -14,44 +18,59 @@ use function sprintf;
|
|||
|
||||
use const PHP_EOL;
|
||||
|
||||
class ValidationException extends RuntimeException
|
||||
class ValidationException extends InvalidArgumentException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Invalid data';
|
||||
private const TYPE = 'INVALID_ARGUMENT';
|
||||
|
||||
/** @var array */
|
||||
private $invalidElements;
|
||||
|
||||
public function __construct(
|
||||
string $message = '',
|
||||
array $invalidElements = [],
|
||||
int $code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
$this->invalidElements = $invalidElements;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public static function fromInputFilter(InputFilterInterface $inputFilter, ?Throwable $prev = null): self
|
||||
{
|
||||
return static::fromArray($inputFilter->getMessages(), $prev);
|
||||
}
|
||||
|
||||
private static function fromArray(array $invalidData, ?Throwable $prev = null): self
|
||||
public static function fromArray(array $invalidData, ?Throwable $prev = null): self
|
||||
{
|
||||
return new self(
|
||||
sprintf(
|
||||
'Provided data is not valid. These are the messages:%s%s%s',
|
||||
$status = StatusCodeInterface::STATUS_BAD_REQUEST;
|
||||
$e = new self('Provided data is not valid', $status, $prev);
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_BAD_REQUEST;
|
||||
$e->invalidElements = $invalidData;
|
||||
$e->additional = ['invalidElements' => array_keys($invalidData)];
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public function getInvalidElements(): array
|
||||
{
|
||||
return $this->invalidElements;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s %s in %s:%s%s%sStack trace:%s%s',
|
||||
__CLASS__,
|
||||
$this->getMessage(),
|
||||
$this->getFile(),
|
||||
$this->getLine(),
|
||||
$this->invalidElementsToString(),
|
||||
PHP_EOL,
|
||||
self::formMessagesToString($invalidData),
|
||||
PHP_EOL
|
||||
),
|
||||
$invalidData,
|
||||
-1,
|
||||
$prev
|
||||
PHP_EOL,
|
||||
$this->getTraceAsString()
|
||||
);
|
||||
}
|
||||
|
||||
private static function formMessagesToString(array $messages = []): string
|
||||
private function invalidElementsToString(): string
|
||||
{
|
||||
return reduce_left($messages, function ($messageSet, $name, $_, string $acc) {
|
||||
return reduce_left($this->getInvalidElements(), function ($messageSet, string $name, $_, string $acc) {
|
||||
return $acc . sprintf(
|
||||
"\n '%s' => %s",
|
||||
$name,
|
||||
|
@ -59,9 +78,4 @@ class ValidationException extends RuntimeException
|
|||
);
|
||||
}, '');
|
||||
}
|
||||
|
||||
public function getInvalidElements(): array
|
||||
{
|
||||
return $this->invalidElements;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\ShortUrlNotFoundException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void
|
||||
|
|
|
@ -9,7 +9,7 @@ use Shlinkio\Shlink\Core\Exception;
|
|||
interface DeleteShortUrlServiceInterface
|
||||
{
|
||||
/**
|
||||
* @throws Exception\InvalidShortCodeException
|
||||
* @throws Exception\ShortUrlNotFoundException
|
||||
* @throws Exception\DeleteShortUrlException
|
||||
*/
|
||||
public function deleteByShortCode(string $shortCode, bool $ignoreThreshold = false): void;
|
||||
|
|
|
@ -6,14 +6,14 @@ namespace Shlinkio\Shlink\Core\Service\ShortUrl;
|
|||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
|
||||
trait FindShortCodeTrait
|
||||
{
|
||||
/**
|
||||
* @param string $shortCode
|
||||
* @return ShortUrl
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
private function findByShortCode(EntityManagerInterface $em, string $shortCode): ShortUrl
|
||||
{
|
||||
|
@ -22,7 +22,7 @@ trait FindShortCodeTrait
|
|||
'shortCode' => $shortCode,
|
||||
]);
|
||||
if ($shortUrl === null) {
|
||||
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Service;
|
|||
|
||||
use Doctrine\ORM;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
|
@ -45,7 +45,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
|
@ -20,12 +20,12 @@ interface ShortUrlServiceInterface
|
|||
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function setTagsByShortCode(string $shortCode, array $tags = []): ShortUrl;
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function updateMetadataByShortCode(string $shortCode, ShortUrlMeta $shortUrlMeta): ShortUrl;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace Shlinkio\Shlink\Core\Service\Tag;
|
|||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
|
||||
|
@ -35,8 +35,7 @@ class TagService implements TagServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $tagNames
|
||||
* @return void
|
||||
* @param string[] $tagNames
|
||||
*/
|
||||
public function deleteTags(array $tagNames): void
|
||||
{
|
||||
|
@ -60,23 +59,17 @@ class TagService implements TagServiceInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
* @return Tag
|
||||
* @throws EntityDoesNotExistException
|
||||
* @throws ORM\OptimisticLockException
|
||||
* @throws TagNotFoundException
|
||||
*/
|
||||
public function renameTag($oldName, $newName): Tag
|
||||
public function renameTag(string $oldName, string $newName): Tag
|
||||
{
|
||||
$criteria = ['name' => $oldName];
|
||||
/** @var Tag|null $tag */
|
||||
$tag = $this->em->getRepository(Tag::class)->findOneBy($criteria);
|
||||
$tag = $this->em->getRepository(Tag::class)->findOneBy(['name' => $oldName]);
|
||||
if ($tag === null) {
|
||||
throw EntityDoesNotExistException::createFromEntityAndConditions(Tag::class, $criteria);
|
||||
throw TagNotFoundException::fromTag($oldName);
|
||||
}
|
||||
|
||||
$tag->rename($newName);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $tag;
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Service\Tag;
|
|||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
|
||||
interface TagServiceInterface
|
||||
{
|
||||
|
@ -17,23 +17,17 @@ interface TagServiceInterface
|
|||
|
||||
/**
|
||||
* @param string[] $tagNames
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTags(array $tagNames): void;
|
||||
|
||||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
*
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames): Collection;
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
* @return Tag
|
||||
* @throws EntityDoesNotExistException
|
||||
* @throws TagNotFoundException
|
||||
*/
|
||||
public function renameTag($oldName, $newName): Tag;
|
||||
public function renameTag(string $oldName, string $newName): Tag;
|
||||
}
|
||||
|
|
|
@ -8,10 +8,9 @@ use Doctrine\ORM\EntityManagerInterface;
|
|||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Resolver\PersistenceDomainResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
|
@ -130,8 +129,8 @@ class UrlShortener implements UrlShortenerInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws EntityDoesNotExistException
|
||||
* @throws ShortUrlNotFoundException
|
||||
* @fixme Move this method to a different service
|
||||
*/
|
||||
public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl
|
||||
{
|
||||
|
@ -139,10 +138,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||
$shortUrl = $shortUrlRepo->findOneByShortCode($shortCode, $domain);
|
||||
if ($shortUrl === null) {
|
||||
throw EntityDoesNotExistException::createFromEntityAndConditions(ShortUrl::class, [
|
||||
'shortCode' => $shortCode,
|
||||
'domain' => $domain,
|
||||
]);
|
||||
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain);
|
||||
}
|
||||
|
||||
return $shortUrl;
|
||||
|
|
|
@ -6,11 +6,9 @@ namespace Shlinkio\Shlink\Core\Service;
|
|||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
|
||||
interface UrlShortenerInterface
|
||||
|
@ -19,13 +17,11 @@ interface UrlShortenerInterface
|
|||
* @param string[] $tags
|
||||
* @throws NonUniqueSlugException
|
||||
* @throws InvalidUrlException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function urlToShortCode(UriInterface $url, array $tags, ShortUrlMeta $meta): ShortUrl;
|
||||
|
||||
/**
|
||||
* @throws InvalidShortCodeException
|
||||
* @throws EntityDoesNotExistException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function shortCodeToUrl(string $shortCode, ?string $domain = null): ShortUrl;
|
||||
}
|
||||
|
|
|
@ -9,15 +9,13 @@ use Psr\EventDispatcher\EventDispatcherInterface;
|
|||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\ShortUrlVisited;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Paginator\Adapter\VisitsPaginatorAdapter;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class VisitsTracker implements VisitsTrackerInterface
|
||||
{
|
||||
/** @var ORM\EntityManagerInterface */
|
||||
|
@ -53,14 +51,14 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||
* Returns the visits on certain short code
|
||||
*
|
||||
* @return Visit[]|Paginator
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function info(string $shortCode, VisitsParams $params): Paginator
|
||||
{
|
||||
/** @var ORM\EntityRepository $repo */
|
||||
$repo = $this->em->getRepository(ShortUrl::class);
|
||||
if ($repo->count(['shortCode' => $shortCode]) < 1) {
|
||||
throw new InvalidArgumentException(sprintf('Short code "%s" not found', $shortCode));
|
||||
throw ShortUrlNotFoundException::fromNotFoundShortCode($shortCode);
|
||||
}
|
||||
|
||||
/** @var VisitRepository $repo */
|
||||
|
|
|
@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
@ -21,7 +21,7 @@ interface VisitsTrackerInterface
|
|||
* Returns the visits on certain short code
|
||||
*
|
||||
* @return Visit[]|Paginator
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ShortUrlNotFoundException
|
||||
*/
|
||||
public function info(string $shortCode, VisitsParams $params): Paginator;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,7 @@ use Prophecy\Prophecy\ObjectProphecy;
|
|||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\PreviewAction;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
|
||||
use Zend\Diactoros\Response;
|
||||
|
@ -38,19 +37,6 @@ class PreviewActionTest extends TestCase
|
|||
$this->action = new PreviewAction($this->previewGenerator->reveal(), $this->urlShortener->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function invalidShortCodeFallsBackToNextMiddleware(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$delegate->handle(Argument::cetera())->shouldBeCalledOnce()
|
||||
->willReturn(new Response());
|
||||
|
||||
$this->action->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function correctShortCodeReturnsImageResponse(): void
|
||||
{
|
||||
|
@ -74,7 +60,7 @@ class PreviewActionTest extends TestCase
|
|||
public function invalidShortCodeExceptionFallsBackToNextMiddleware(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(ShortUrlNotFoundException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
|
|
@ -11,8 +11,7 @@ use Psr\Http\Server\RequestHandlerInterface;
|
|||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||
use Shlinkio\Shlink\Core\Action\QrCodeAction;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
|
@ -39,7 +38,7 @@ class QrCodeActionTest extends TestCase
|
|||
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(EntityDoesNotExistException::class)
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
@ -53,7 +52,7 @@ class QrCodeActionTest extends TestCase
|
|||
public function anInvalidShortCodeWillReturnNotFoundResponse(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(InvalidShortCodeException::class)
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$delegate = $this->prophesize(RequestHandlerInterface::class);
|
||||
$process = $delegate->handle(Argument::any())->willReturn(new Response());
|
||||
|
|
|
@ -10,7 +10,7 @@ use Prophecy\Prophecy\ObjectProphecy;
|
|||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||
|
@ -76,7 +76,7 @@ class RedirectActionTest extends TestCase
|
|||
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(EntityDoesNotExistException::class)
|
||||
$this->urlShortener->shortCodeToUrl($shortCode, '')->willThrow(ShortUrlNotFoundException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$this->visitTracker->track(Argument::cetera())->shouldNotBeCalled();
|
||||
|
||||
|
|
101
module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php
Normal file
101
module/Core/test/ErrorHandler/NotFoundRedirectHandlerTest.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ErrorHandler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundRedirectHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\Expressive\Router\Route;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
|
||||
class NotFoundRedirectHandlerTest extends TestCase
|
||||
{
|
||||
/** @var NotFoundRedirectHandler */
|
||||
private $middleware;
|
||||
/** @var NotFoundRedirectOptions */
|
||||
private $redirectOptions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->redirectOptions = new NotFoundRedirectOptions();
|
||||
$this->middleware = new NotFoundRedirectHandler($this->redirectOptions, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRedirects
|
||||
*/
|
||||
public function expectedRedirectionIsReturnedDependingOnTheCase(
|
||||
ServerRequestInterface $request,
|
||||
string $expectedRedirectTo
|
||||
): void {
|
||||
$this->redirectOptions->invalidShortUrl = 'invalidShortUrl';
|
||||
$this->redirectOptions->regular404 = 'regular404';
|
||||
$this->redirectOptions->baseUrl = 'baseUrl';
|
||||
|
||||
$next = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $next->handle($request)->willReturn(new Response());
|
||||
|
||||
$resp = $this->middleware->process($request, $next->reveal());
|
||||
|
||||
$this->assertInstanceOf(Response\RedirectResponse::class, $resp);
|
||||
$this->assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location'));
|
||||
$handle->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideRedirects(): iterable
|
||||
{
|
||||
yield 'base URL with trailing slash' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('/')),
|
||||
'baseUrl',
|
||||
];
|
||||
yield 'base URL without trailing slash' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('')),
|
||||
'baseUrl',
|
||||
];
|
||||
yield 'regular 404' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo/bar')),
|
||||
'regular404',
|
||||
];
|
||||
yield 'invalid short URL' => [
|
||||
ServerRequestFactory::fromGlobals()
|
||||
->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(
|
||||
new Route(
|
||||
'',
|
||||
$this->prophesize(MiddlewareInterface::class)->reveal(),
|
||||
['GET'],
|
||||
RedirectAction::class
|
||||
)
|
||||
)
|
||||
)
|
||||
->withUri(new Uri('/abc123')),
|
||||
'invalidShortUrl',
|
||||
];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function nextMiddlewareIsInvokedWhenNotRedirectNeedsToOccur(): void
|
||||
{
|
||||
$req = ServerRequestFactory::fromGlobals();
|
||||
$resp = new Response();
|
||||
|
||||
$next = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $next->handle($req)->willReturn($resp);
|
||||
|
||||
$result = $this->middleware->process($req, $next->reveal());
|
||||
|
||||
$this->assertSame($resp, $result);
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ErrorHandler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundTemplateHandler;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Expressive\Router\Route;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
class NotFoundTemplateHandlerTest extends TestCase
|
||||
{
|
||||
/** @var NotFoundTemplateHandler */
|
||||
private $handler;
|
||||
/** @var ObjectProphecy */
|
||||
private $renderer;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
|
||||
$this->handler = new NotFoundTemplateHandler($this->renderer->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTemplates
|
||||
*/
|
||||
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
|
||||
{
|
||||
$request = $request->withHeader('Accept', 'text/html');
|
||||
$render = $this->renderer->render($expectedTemplate)->willReturn('');
|
||||
|
||||
$resp = $this->handler->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response\HtmlResponse::class, $resp);
|
||||
$render->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideTemplates(): iterable
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals();
|
||||
|
||||
yield [$request, NotFoundTemplateHandler::NOT_FOUND_TEMPLATE];
|
||||
yield [
|
||||
$request->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('', $this->prophesize(MiddlewareInterface::class)->reveal()))
|
||||
),
|
||||
NotFoundTemplateHandler::INVALID_SHORT_CODE_TEMPLATE,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -5,17 +5,15 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Exception\DeleteShortUrlException;
|
||||
|
||||
use function Functional\map;
|
||||
use function range;
|
||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlExceptionTest extends TestCase
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideThresholds
|
||||
|
@ -29,29 +27,12 @@ class DeleteShortUrlExceptionTest extends TestCase
|
|||
|
||||
$this->assertEquals($threshold, $e->getVisitsThreshold());
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideThresholds
|
||||
*/
|
||||
public function visitsThresholdIsProperlyReturned(int $threshold): void
|
||||
{
|
||||
$e = new DeleteShortUrlException($threshold);
|
||||
|
||||
$this->assertEquals($threshold, $e->getVisitsThreshold());
|
||||
$this->assertEquals('', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideThresholds(): array
|
||||
{
|
||||
return map(range(5, 50, 5), function (int $number) {
|
||||
$shortCode = $this->generateRandomString(6);
|
||||
return [$number, $shortCode, sprintf(
|
||||
return [$number, $shortCode = generateRandomShortCode(6), sprintf(
|
||||
'Impossible to delete short URL with short code "%s" since it has more than "%s" visits.',
|
||||
$shortCode,
|
||||
$number
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Throwable;
|
||||
|
||||
class InvalidShortCodeExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider providePrevious
|
||||
*/
|
||||
public function properlyCreatesExceptionFromCharset(?Throwable $prev): void
|
||||
{
|
||||
$e = InvalidShortCodeException::fromCharset('abc123', 'def456', $prev);
|
||||
|
||||
$this->assertEquals('Provided short code "abc123" does not match the char set "def456"', $e->getMessage());
|
||||
$this->assertEquals($prev !== null ? $prev->getCode() : -1, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function providePrevious(): iterable
|
||||
{
|
||||
yield 'null previous' => [null];
|
||||
yield 'instance previous' => [new Exception('Previous error', 10)];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properlyCreatesExceptionFromNotFoundShortCode(): void
|
||||
{
|
||||
$e = InvalidShortCodeException::fromNotFoundShortCode('abc123');
|
||||
|
||||
$this->assertEquals('Provided short code "abc123" does not belong to a short URL', $e->getMessage());
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use Exception;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Throwable;
|
||||
|
@ -19,8 +20,8 @@ class InvalidUrlExceptionTest extends TestCase
|
|||
{
|
||||
$e = InvalidUrlException::fromUrl('http://the_url.com', $prev);
|
||||
|
||||
$this->assertEquals('Provided URL "http://the_url.com" is not an existing and valid URL', $e->getMessage());
|
||||
$this->assertEquals($prev !== null ? $prev->getCode() : -1, $e->getCode());
|
||||
$this->assertEquals('Provided URL http://the_url.com is invalid. Try with a different one.', $e->getMessage());
|
||||
$this->assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
}
|
||||
|
||||
|
|
|
@ -22,12 +22,12 @@ class NonUniqueSlugExceptionTest extends TestCase
|
|||
public function provideMessages(): iterable
|
||||
{
|
||||
yield 'without domain' => [
|
||||
'Provided slug "foo" is not unique.',
|
||||
'Provided slug "foo" is already in use.',
|
||||
'foo',
|
||||
null,
|
||||
];
|
||||
yield 'with domain' => [
|
||||
'Provided slug "baz" is not unique for domain "bar".',
|
||||
'Provided slug "baz" is already in use for domain "bar".',
|
||||
'baz',
|
||||
'bar',
|
||||
];
|
||||
|
|
38
module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php
Normal file
38
module/Core/test/Exception/ShortUrlNotFoundExceptionTest.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
|
||||
class ShortUrlNotFoundExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideMessages
|
||||
*/
|
||||
public function properlyCreatesExceptionFromNotFoundShortCode(
|
||||
string $expectedMessage,
|
||||
string $shortCode,
|
||||
?string $domain
|
||||
): void {
|
||||
$e = ShortUrlNotFoundException::fromNotFoundShortCode($shortCode, $domain);
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
}
|
||||
|
||||
public function provideMessages(): iterable
|
||||
{
|
||||
yield 'without domain' => [
|
||||
'No URL found with short code "abc123"',
|
||||
'abc123',
|
||||
null,
|
||||
];
|
||||
yield 'with domain' => [
|
||||
'No URL found with short code "bar" for domain "foo"',
|
||||
'bar',
|
||||
'foo',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace ShlinkioTest\Shlink\Core\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use LogicException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
@ -11,39 +12,11 @@ use Shlinkio\Shlink\Core\Exception\ValidationException;
|
|||
use Throwable;
|
||||
use Zend\InputFilter\InputFilterInterface;
|
||||
|
||||
use function array_keys;
|
||||
use function print_r;
|
||||
use function random_int;
|
||||
|
||||
class ValidationExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExceptionData
|
||||
*/
|
||||
public function createsExceptionWrappingExpectedData(
|
||||
array $args,
|
||||
string $expectedMessage,
|
||||
array $expectedInvalidElements,
|
||||
int $expectedCode,
|
||||
?Throwable $expectedPrev
|
||||
): void {
|
||||
$e = new ValidationException(...$args);
|
||||
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
$this->assertEquals($expectedInvalidElements, $e->getInvalidElements());
|
||||
$this->assertEquals($expectedCode, $e->getCode());
|
||||
$this->assertEquals($expectedPrev, $e->getPrevious());
|
||||
}
|
||||
|
||||
public function provideExceptionData(): iterable
|
||||
{
|
||||
yield 'empty args' => [[], '', [], 0, null];
|
||||
yield 'with message' => [['something'], 'something', [], 0, null];
|
||||
yield 'with elements' => [['something_else', [1, 2, 3]], 'something_else', [1, 2, 3], 0, null];
|
||||
yield 'with code' => [['foo', [], $foo = random_int(-100, 100)], 'foo', [], $foo, null];
|
||||
yield 'with prev' => [['bar', [], 8, $e = new RuntimeException()], 'bar', [], 8, $e];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideExceptions
|
||||
|
@ -55,12 +28,9 @@ class ValidationExceptionTest extends TestCase
|
|||
'something' => ['baz', 'foo'],
|
||||
];
|
||||
$barValue = print_r(['baz', 'foo'], true);
|
||||
$expectedMessage = <<<EOT
|
||||
Provided data is not valid. These are the messages:
|
||||
|
||||
$expectedStringRepresentation = <<<EOT
|
||||
'foo' => bar
|
||||
'something' => {$barValue}
|
||||
|
||||
EOT;
|
||||
|
||||
$inputFilter = $this->prophesize(InputFilterInterface::class);
|
||||
|
@ -69,9 +39,11 @@ EOT;
|
|||
$e = ValidationException::fromInputFilter($inputFilter->reveal());
|
||||
|
||||
$this->assertEquals($invalidData, $e->getInvalidElements());
|
||||
$this->assertEquals($expectedMessage, $e->getMessage());
|
||||
$this->assertEquals(-1, $e->getCode());
|
||||
$this->assertEquals(['invalidElements' => array_keys($invalidData)], $e->getAdditionalData());
|
||||
$this->assertEquals('Provided data is not valid', $e->getMessage());
|
||||
$this->assertEquals(StatusCodeInterface::STATUS_BAD_REQUEST, $e->getCode());
|
||||
$this->assertEquals($prev, $e->getPrevious());
|
||||
$this->assertStringContainsString($expectedStringRepresentation, (string) $e);
|
||||
$getMessages->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Response;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\ServerRequest;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\Expressive\Router\Route;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
|
||||
class NotFoundHandlerTest extends TestCase
|
||||
{
|
||||
/** @var NotFoundHandler */
|
||||
private $delegate;
|
||||
/** @var ObjectProphecy */
|
||||
private $renderer;
|
||||
/** @var NotFoundRedirectOptions */
|
||||
private $redirectOptions;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
|
||||
$this->redirectOptions = new NotFoundRedirectOptions();
|
||||
|
||||
$this->delegate = new NotFoundHandler($this->renderer->reveal(), $this->redirectOptions, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideResponses
|
||||
*/
|
||||
public function properResponseTypeIsReturned(string $expectedResponse, string $accept, int $renderCalls): void
|
||||
{
|
||||
$request = (new ServerRequest())->withHeader('Accept', $accept);
|
||||
$render = $this->renderer->render(Argument::cetera())->willReturn('');
|
||||
|
||||
$resp = $this->delegate->handle($request);
|
||||
|
||||
$this->assertInstanceOf($expectedResponse, $resp);
|
||||
$render->shouldHaveBeenCalledTimes($renderCalls);
|
||||
}
|
||||
|
||||
public function provideResponses(): iterable
|
||||
{
|
||||
yield 'application/json' => [Response\JsonResponse::class, 'application/json', 0];
|
||||
yield 'text/json' => [Response\JsonResponse::class, 'text/json', 0];
|
||||
yield 'application/x-json' => [Response\JsonResponse::class, 'application/x-json', 0];
|
||||
yield 'text/html' => [Response\HtmlResponse::class, 'text/html', 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRedirects
|
||||
*/
|
||||
public function expectedRedirectionIsReturnedDependingOnTheCase(
|
||||
ServerRequestInterface $request,
|
||||
string $expectedRedirectTo
|
||||
): void {
|
||||
$this->redirectOptions->invalidShortUrl = 'invalidShortUrl';
|
||||
$this->redirectOptions->regular404 = 'regular404';
|
||||
$this->redirectOptions->baseUrl = 'baseUrl';
|
||||
|
||||
$resp = $this->delegate->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response\RedirectResponse::class, $resp);
|
||||
$this->assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location'));
|
||||
$this->renderer->render(Argument::cetera())->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideRedirects(): iterable
|
||||
{
|
||||
yield 'base URL with trailing slash' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('/')),
|
||||
'baseUrl',
|
||||
];
|
||||
yield 'base URL without trailing slash' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('')),
|
||||
'baseUrl',
|
||||
];
|
||||
yield 'regular 404' => [
|
||||
ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo/bar')),
|
||||
'regular404',
|
||||
];
|
||||
yield 'invalid short URL' => [
|
||||
ServerRequestFactory::fromGlobals()
|
||||
->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(
|
||||
new Route(
|
||||
'',
|
||||
$this->prophesize(MiddlewareInterface::class)->reveal(),
|
||||
['GET'],
|
||||
RedirectAction::class
|
||||
)
|
||||
)
|
||||
)
|
||||
->withUri(new Uri('/abc123')),
|
||||
'invalidShortUrl',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTemplates
|
||||
*/
|
||||
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
|
||||
{
|
||||
$request = $request->withHeader('Accept', 'text/html');
|
||||
$render = $this->renderer->render($expectedTemplate)->willReturn('');
|
||||
|
||||
$resp = $this->delegate->handle($request);
|
||||
|
||||
$this->assertInstanceOf(Response\HtmlResponse::class, $resp);
|
||||
$render->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
public function provideTemplates(): iterable
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals();
|
||||
|
||||
yield [$request, NotFoundHandler::NOT_FOUND_TEMPLATE];
|
||||
yield [
|
||||
$request->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('', $this->prophesize(MiddlewareInterface::class)->reveal()))
|
||||
),
|
||||
NotFoundHandler::INVALID_SHORT_CODE_TEMPLATE,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ use Prophecy\Argument;
|
|||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
|
@ -62,7 +62,7 @@ class ShortUrlServiceTest extends TestCase
|
|||
->shouldBeCalledOnce();
|
||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->expectException(InvalidShortCodeException::class);
|
||||
$this->expectException(ShortUrlNotFoundException::class);
|
||||
$this->service->setTagsByShortCode($shortCode);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ use PHPUnit\Framework\TestCase;
|
|||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
|
||||
|
@ -83,7 +83,7 @@ class TagServiceTest extends TestCase
|
|||
|
||||
$find->shouldBeCalled();
|
||||
$getRepo->shouldBeCalled();
|
||||
$this->expectException(EntityDoesNotExistException::class);
|
||||
$this->expectException(TagNotFoundException::class);
|
||||
|
||||
$this->service->renameTag('foo', 'bar');
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ return [
|
|||
Middleware\AuthenticationMiddleware::class => [
|
||||
Authentication\RequestToHttpAuthPlugin::class,
|
||||
'config.auth.routes_whitelist',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
],
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
|
@ -20,15 +21,15 @@ return [
|
|||
ApiKeyService::class => ConfigAbstractFactory::class,
|
||||
|
||||
Action\AuthenticateAction::class => ConfigAbstractFactory::class,
|
||||
Action\HealthAction::class => Action\HealthActionFactory::class,
|
||||
Action\HealthAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\EditShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\DeleteShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ResolveShortUrlAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\ListShortUrlsAction::class => ConfigAbstractFactory::class,
|
||||
Action\ShortUrl\EditShortUrlTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
|
||||
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
|
||||
|
@ -38,6 +39,7 @@ return [
|
|||
Middleware\BodyParserMiddleware::class => InvokableFactory::class,
|
||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
|
||||
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => ConfigAbstractFactory::class,
|
||||
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
|
||||
Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class,
|
||||
],
|
||||
|
@ -48,6 +50,7 @@ return [
|
|||
ApiKeyService::class => ['em'],
|
||||
|
||||
Action\AuthenticateAction::class => [ApiKeyService::class, Authentication\JWTService::class, 'Logger_Shlink'],
|
||||
Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'],
|
||||
Action\ShortUrl\CreateShortUrlAction::class => [
|
||||
Service\UrlShortener::class,
|
||||
'config.url_shortener.domain',
|
||||
|
@ -73,6 +76,11 @@ return [
|
|||
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
|
||||
Action\Tag\CreateTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
|
||||
Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
|
||||
|
||||
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => [
|
||||
'config.backwards_compatible_problem_details.default_type_fallbacks',
|
||||
'config.backwards_compatible_problem_details.json_flags',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Rest\ErrorHandler\JsonErrorResponseGenerator;
|
||||
|
||||
return [
|
||||
|
||||
'error_handler' => [
|
||||
'plugins' => [
|
||||
'invokables' => [
|
||||
'application/json' => JsonErrorResponseGenerator::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'application/x-json' => 'application/json',
|
||||
'text/json' => 'application/json',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
|
@ -10,7 +10,6 @@ use Psr\Log\LoggerInterface;
|
|||
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
/** @deprecated */
|
||||
|
@ -44,7 +43,7 @@ class AuthenticateAction extends AbstractRestAction
|
|||
$authData = $request->getParsedBody();
|
||||
if (! isset($authData['apiKey'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'error' => 'INVALID_ARGUMENT',
|
||||
'message' => 'You have to provide a valid API key under the "apiKey" param name.',
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
@ -53,7 +52,7 @@ class AuthenticateAction extends AbstractRestAction
|
|||
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
|
||||
if ($apiKey === null || ! $apiKey->isValid()) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_API_KEY_ERROR,
|
||||
'error' => 'INVALID_API_KEY',
|
||||
'message' => 'Provided API key does not exist or is invalid.',
|
||||
], self::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ use Zend\Diactoros\Response\JsonResponse;
|
|||
class HealthAction extends AbstractRestAction
|
||||
{
|
||||
private const HEALTH_CONTENT_TYPE = 'application/health+json';
|
||||
private const PASS_STATUS = 'pass';
|
||||
private const FAIL_STATUS = 'fail';
|
||||
private const STATUS_PASS = 'pass';
|
||||
private const STATUS_FAIL = 'fail';
|
||||
|
||||
protected const ROUTE_PATH = '/health';
|
||||
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
|
||||
|
@ -48,7 +48,7 @@ class HealthAction extends AbstractRestAction
|
|||
|
||||
$statusCode = $connected ? self::STATUS_OK : self::STATUS_SERVICE_UNAVAILABLE;
|
||||
return new JsonResponse([
|
||||
'status' => $connected ? self::PASS_STATUS : self::FAIL_STATUS,
|
||||
'status' => $connected ? self::STATUS_PASS : self::STATUS_FAIL,
|
||||
'version' => $this->options->getVersion(),
|
||||
'links' => [
|
||||
'about' => 'https://shlink.io',
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Action;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
|
||||
class HealthActionFactory
|
||||
{
|
||||
public function __invoke(ContainerInterface $container)
|
||||
{
|
||||
$em = $container->get(EntityManager::class);
|
||||
$options = $container->get(AppOptions::class);
|
||||
$logger = $container->get('Logger_Shlink');
|
||||
return new HealthAction($em->getConnection(), $options, $logger);
|
||||
}
|
||||
}
|
|
@ -7,19 +7,13 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Throwable;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
abstract class AbstractCreateShortUrlAction extends AbstractRestAction
|
||||
{
|
||||
/** @var UrlShortenerInterface */
|
||||
|
@ -37,56 +31,23 @@ abstract class AbstractCreateShortUrlAction extends AbstractRestAction
|
|||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$shortUrlData = $this->buildShortUrlData($request);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->warning('Provided data is invalid. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => $e->getMessage(),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$longUrl = $shortUrlData->getLongUrl();
|
||||
$tags = $shortUrlData->getTags();
|
||||
$shortUrlMeta = $shortUrlData->getMeta();
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlShortener->urlToShortCode($longUrl, $shortUrlData->getTags(), $shortUrlMeta);
|
||||
$shortUrl = $this->urlShortener->urlToShortCode($longUrl, $tags, $shortUrlMeta);
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
return new JsonResponse($transformer->transform($shortUrl));
|
||||
} catch (InvalidUrlException $e) {
|
||||
$this->logger->warning('Provided Invalid URL. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided URL %s is invalid. Try with a different one.', $longUrl),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$customSlug = $shortUrlMeta->getCustomSlug();
|
||||
$this->logger->warning('Provided non-unique slug. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided slug %s is already in use. Try with a different one.', $customSlug),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error('Unexpected error creating short url. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occurred',
|
||||
], self::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return CreateShortUrlData
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
abstract protected function buildShortUrlData(Request $request): CreateShortUrlData;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
|
@ -20,17 +19,17 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
|
|||
/**
|
||||
* @param Request $request
|
||||
* @return CreateShortUrlData
|
||||
* @throws InvalidArgumentException
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function buildShortUrlData(Request $request): CreateShortUrlData
|
||||
{
|
||||
$postData = (array) $request->getParsedBody();
|
||||
if (! isset($postData['longUrl'])) {
|
||||
throw new InvalidArgumentException('A URL was not provided');
|
||||
throw ValidationException::fromArray([
|
||||
'longUrl' => 'A URL was not provided',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$meta = ShortUrlMeta::createFromParams(
|
||||
$this->getOptionalDate($postData, 'validSince'),
|
||||
$this->getOptionalDate($postData, 'validUntil'),
|
||||
|
@ -41,9 +40,6 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
|
|||
);
|
||||
|
||||
return new CreateShortUrlData(new Uri($postData['longUrl']), (array) ($postData['tags'] ?? []), $meta);
|
||||
} catch (ValidationException $e) {
|
||||
throw new InvalidArgumentException('Provided meta data is not valid', -1, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function getOptionalDate(array $postData, string $fieldName): ?Chronos
|
||||
|
|
|
@ -7,14 +7,9 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlAction extends AbstractRestAction
|
||||
{
|
||||
|
@ -30,34 +25,10 @@ class DeleteShortUrlAction extends AbstractRestAction
|
|||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the request and return a response.
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode', '');
|
||||
|
||||
try {
|
||||
$this->deleteShortUrlService->deleteByShortCode($shortCode);
|
||||
return new EmptyResponse();
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$this->logger->warning(
|
||||
'Provided short code {shortCode} does not belong to any URL. {e}',
|
||||
['e' => $e, 'shortCode' => $shortCode]
|
||||
);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('No URL found for short code "%s"', $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
$this->logger->warning('Provided data is invalid. {e}', ['e' => $e]);
|
||||
$messagePlaceholder =
|
||||
'It is not possible to delete URL with short code "%s" because it has reached more than "%s" visits.';
|
||||
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf($messagePlaceholder, $shortCode, $e->getVisitsThreshold()),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,15 +7,10 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class EditShortUrlAction extends AbstractRestAction
|
||||
{
|
||||
|
@ -31,38 +26,12 @@ class EditShortUrlAction extends AbstractRestAction
|
|||
$this->shortUrlService = $shortUrlService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$postData = (array) $request->getParsedBody();
|
||||
$shortCode = $request->getAttribute('shortCode', '');
|
||||
|
||||
try {
|
||||
$this->shortUrlService->updateMetadataByShortCode(
|
||||
$shortCode,
|
||||
ShortUrlMeta::createFromRawData($postData)
|
||||
);
|
||||
$this->shortUrlService->updateMetadataByShortCode($shortCode, ShortUrlMeta::createFromRawData($postData));
|
||||
return new EmptyResponse();
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$this->logger->warning('Provided data is invalid. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('No URL found for short code "%s"', $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
} catch (Exception\ValidationException $e) {
|
||||
$this->logger->warning('Provided data is invalid. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => 'Provided data is invalid.',
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,14 +7,11 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class EditShortUrlTagsAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/short-urls/{shortCode}/tags';
|
||||
|
@ -29,32 +26,19 @@ class EditShortUrlTagsAction extends AbstractRestAction
|
|||
$this->shortUrlService = $shortUrlService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
$bodyParams = $request->getParsedBody();
|
||||
|
||||
if (! isset($bodyParams['tags'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => 'A list of tags was not provided',
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
throw ValidationException::fromArray([
|
||||
'tags' => 'List of tags has to be provided',
|
||||
]);
|
||||
}
|
||||
$tags = $bodyParams['tags'];
|
||||
|
||||
try {
|
||||
$shortUrl = $this->shortUrlService->setTagsByShortCode($shortCode, $tags);
|
||||
return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]);
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('No URL found for short code "%s"', $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -12,7 +12,6 @@ use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
|||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class ListShortUrlsAction extends AbstractRestAction
|
||||
|
@ -40,23 +39,15 @@ class ListShortUrlsAction extends AbstractRestAction
|
|||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
try {
|
||||
$params = $this->queryToListParams($request->getQueryParams());
|
||||
$shortUrls = $this->shortUrlService->listShortUrls(...$params);
|
||||
return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer(
|
||||
$this->domainConfig
|
||||
))]);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Unexpected error while listing short URLs. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occurred',
|
||||
], self::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,20 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
||||
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ResolveShortUrlAction extends AbstractRestAction
|
||||
{
|
||||
protected const ROUTE_PATH = '/short-urls/{shortCode}';
|
||||
|
@ -41,7 +36,7 @@ class ResolveShortUrlAction extends AbstractRestAction
|
|||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
|
@ -49,27 +44,7 @@ class ResolveShortUrlAction extends AbstractRestAction
|
|||
$domain = $request->getQueryParams()['domain'] ?? null;
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
try {
|
||||
$url = $this->urlShortener->shortCodeToUrl($shortCode, $domain);
|
||||
return new JsonResponse($transformer->transform($url));
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$this->logger->warning('Provided short code with invalid format. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided short code "%s" has an invalid format', $shortCode),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$this->logger->warning('Provided short code couldn\'t be found. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => sprintf('No URL found for short code "%s"', $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
} catch (Exception $e) {
|
||||
$this->logger->error('Unexpected error while resolving the URL behind a short code. {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::UNKNOWN_ERROR,
|
||||
'message' => 'Unexpected error occurred',
|
||||
], self::STATUS_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
|
|||
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\CreateShortUrlData;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
|
@ -33,19 +33,22 @@ class SingleStepCreateShortUrlAction extends AbstractCreateShortUrlAction
|
|||
/**
|
||||
* @param Request $request
|
||||
* @return CreateShortUrlData
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws InvalidArgumentException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
protected function buildShortUrlData(Request $request): CreateShortUrlData
|
||||
{
|
||||
$query = $request->getQueryParams();
|
||||
|
||||
if (! $this->apiKeyService->check($query['apiKey'] ?? '')) {
|
||||
throw new InvalidArgumentException('No API key was provided or it is not valid');
|
||||
throw ValidationException::fromArray([
|
||||
'apiKey' => 'No API key was provided or it is not valid',
|
||||
]);
|
||||
}
|
||||
|
||||
if (! isset($query['longUrl'])) {
|
||||
throw new InvalidArgumentException('A URL was not provided');
|
||||
throw ValidationException::fromArray([
|
||||
'longUrl' => 'A URL was not provided',
|
||||
]);
|
||||
}
|
||||
|
||||
return new CreateShortUrlData(new Uri($query['longUrl']));
|
||||
|
|
|
@ -7,14 +7,10 @@ namespace Shlinkio\Shlink\Rest\Action\Tag;
|
|||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class UpdateTagAction extends AbstractRestAction
|
||||
{
|
||||
|
@ -43,21 +39,13 @@ class UpdateTagAction extends AbstractRestAction
|
|||
{
|
||||
$body = $request->getParsedBody();
|
||||
if (! isset($body['oldName'], $body['newName'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' =>
|
||||
'You have to provide both \'oldName\' and \'newName\' params in order to properly rename the tag',
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
throw ValidationException::fromArray([
|
||||
'oldName' => 'oldName is required',
|
||||
'newName' => 'newName is required',
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($body['oldName'], $body['newName']);
|
||||
return new EmptyResponse();
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::NOT_FOUND_ERROR,
|
||||
'message' => sprintf('It was not possible to find a tag with name %s', $body['oldName']),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,16 +7,12 @@ namespace Shlinkio\Shlink\Rest\Action\Visit;
|
|||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GetVisitsAction extends AbstractRestAction
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
@ -33,27 +29,13 @@ class GetVisitsAction extends AbstractRestAction
|
|||
$this->visitsTracker = $visitsTracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
|
||||
try {
|
||||
$visits = $this->visitsTracker->info($shortCode, VisitsParams::fromRawData($request->getQueryParams()));
|
||||
|
||||
return new JsonResponse([
|
||||
'visits' => $this->serializePaginator($visits),
|
||||
]);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$this->logger->warning('Provided nonexistent short code {e}', ['e' => $e]);
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||
'message' => sprintf('Provided short code %s does not exist', $shortCode),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ use UnexpectedValueException;
|
|||
|
||||
use function time;
|
||||
|
||||
/** @deprecated */
|
||||
class JWTService implements JWTServiceInterface
|
||||
{
|
||||
/** @var AppOptions */
|
||||
|
|
|
@ -8,7 +8,6 @@ use Psr\Http\Message\ResponseInterface;
|
|||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
|
||||
class ApiKeyHeaderPlugin implements AuthenticationPluginInterface
|
||||
{
|
||||
|
@ -28,14 +27,9 @@ class ApiKeyHeaderPlugin implements AuthenticationPluginInterface
|
|||
public function verify(ServerRequestInterface $request): void
|
||||
{
|
||||
$apiKey = $request->getHeaderLine(self::HEADER_NAME);
|
||||
if ($this->apiKeyService->check($apiKey)) {
|
||||
return;
|
||||
if (! $this->apiKeyService->check($apiKey)) {
|
||||
throw VerifyAuthenticationException::forInvalidApiKey();
|
||||
}
|
||||
|
||||
throw VerifyAuthenticationException::withError(
|
||||
RestUtils::INVALID_API_KEY_ERROR,
|
||||
'Provided API key does not exist or is invalid.'
|
||||
);
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
|
|
|
@ -8,7 +8,6 @@ use Psr\Http\Message\ResponseInterface;
|
|||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Throwable;
|
||||
|
||||
use function count;
|
||||
|
@ -16,6 +15,7 @@ use function explode;
|
|||
use function sprintf;
|
||||
use function strtolower;
|
||||
|
||||
/** @deprecated */
|
||||
class AuthorizationHeaderPlugin implements AuthenticationPluginInterface
|
||||
{
|
||||
public const HEADER_NAME = 'Authorization';
|
||||
|
@ -37,19 +37,13 @@ class AuthorizationHeaderPlugin implements AuthenticationPluginInterface
|
|||
$authToken = $request->getHeaderLine(self::HEADER_NAME);
|
||||
$authTokenParts = explode(' ', $authToken);
|
||||
if (count($authTokenParts) === 1) {
|
||||
throw VerifyAuthenticationException::withError(
|
||||
RestUtils::INVALID_AUTHORIZATION_ERROR,
|
||||
sprintf('You need to provide the Bearer type in the %s header.', self::HEADER_NAME)
|
||||
);
|
||||
throw VerifyAuthenticationException::forMissingAuthType();
|
||||
}
|
||||
|
||||
// Make sure the authorization type is Bearer
|
||||
[$authType, $jwt] = $authTokenParts;
|
||||
if (strtolower($authType) !== 'bearer') {
|
||||
throw VerifyAuthenticationException::withError(
|
||||
RestUtils::INVALID_AUTHORIZATION_ERROR,
|
||||
sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $authType)
|
||||
);
|
||||
throw VerifyAuthenticationException::forInvalidAuthType($authType);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -57,21 +51,13 @@ class AuthorizationHeaderPlugin implements AuthenticationPluginInterface
|
|||
throw $this->createInvalidTokenError();
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
throw $this->createInvalidTokenError($e);
|
||||
throw $this->createInvalidTokenError();
|
||||
}
|
||||
}
|
||||
|
||||
private function createInvalidTokenError(?Throwable $prev = null): VerifyAuthenticationException
|
||||
private function createInvalidTokenError(): VerifyAuthenticationException
|
||||
{
|
||||
return VerifyAuthenticationException::withError(
|
||||
RestUtils::INVALID_AUTH_TOKEN_ERROR,
|
||||
sprintf(
|
||||
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
|
||||
. 'token on every new request on the %s header',
|
||||
self::HEADER_NAME
|
||||
),
|
||||
$prev
|
||||
);
|
||||
return VerifyAuthenticationException::forInvalidAuthToken();
|
||||
}
|
||||
|
||||
public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
|
|
|
@ -4,9 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Container;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_reduce;
|
||||
|
@ -30,16 +29,12 @@ class RequestToHttpAuthPlugin implements RequestToHttpAuthPluginInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @throws Container\ContainerExceptionInterface
|
||||
* @throws NoAuthenticationException
|
||||
* @throws MissingAuthenticationException
|
||||
*/
|
||||
public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface
|
||||
{
|
||||
if (! $this->hasAnySupportedHeader($request)) {
|
||||
throw NoAuthenticationException::fromExpectedTypes([
|
||||
Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
|
||||
Plugin\AuthorizationHeaderPlugin::HEADER_NAME,
|
||||
]);
|
||||
throw MissingAuthenticationException::fromExpectedTypes(self::SUPPORTED_AUTH_HEADERS);
|
||||
}
|
||||
|
||||
return $this->authPluginManager->get($this->getFirstAvailableHeader($request));
|
||||
|
|
|
@ -4,15 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||
|
||||
use Psr\Container;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
|
||||
|
||||
interface RequestToHttpAuthPluginInterface
|
||||
{
|
||||
/**
|
||||
* @throws Container\ContainerExceptionInterface
|
||||
* @throws NoAuthenticationException
|
||||
* @throws MissingAuthenticationException
|
||||
*/
|
||||
public function fromRequest(ServerRequestInterface $request): Plugin\AuthenticationPluginInterface;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use function sprintf;
|
|||
|
||||
class ConfigProvider
|
||||
{
|
||||
private const ROUTES_PREFIX = '/rest/v{version:1}';
|
||||
private const ROUTES_PREFIX = '/rest/v{version:1|2}';
|
||||
|
||||
public function __invoke()
|
||||
{
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\ErrorHandler;
|
||||
|
||||
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ErrorResponseGeneratorInterface;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Throwable;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
use function str_replace;
|
||||
use function strtoupper;
|
||||
|
||||
class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, StatusCodeInterface
|
||||
{
|
||||
/**
|
||||
* Final handler for an application.
|
||||
*
|
||||
* @param \Throwable|\Exception $e
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function __invoke(?Throwable $e, Request $request, Response $response)
|
||||
{
|
||||
$status = $response->getStatusCode();
|
||||
$responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
|
||||
$status = $status < 400 ? self::STATUS_INTERNAL_SERVER_ERROR : $status;
|
||||
|
||||
return new JsonResponse([
|
||||
'error' => $this->responsePhraseToCode($responsePhrase),
|
||||
'message' => $responsePhrase,
|
||||
], $status);
|
||||
}
|
||||
|
||||
private function responsePhraseToCode(string $responsePhrase): string
|
||||
{
|
||||
return strtoupper(str_replace(' ', '_', $responsePhrase));
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Rest\Exception;
|
|||
|
||||
use Throwable;
|
||||
|
||||
/** @deprecated */
|
||||
class AuthenticationException extends RuntimeException
|
||||
{
|
||||
public static function expiredJWT(?Throwable $prev = null): self
|
||||
|
|
36
module/Rest/src/Exception/MissingAuthenticationException.php
Normal file
36
module/Rest/src/Exception/MissingAuthenticationException.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Exception;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class MissingAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
private const TITLE = 'Invalid authorization';
|
||||
private const TYPE = 'INVALID_AUTHORIZATION';
|
||||
|
||||
public static function fromExpectedTypes(array $expectedTypes): self
|
||||
{
|
||||
$e = new self(sprintf(
|
||||
'Expected one of the following authentication headers, but none were provided, ["%s"]',
|
||||
implode('", "', $expectedTypes)
|
||||
));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = self::TITLE;
|
||||
$e->type = self::TYPE;
|
||||
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
|
||||
$e->additional = ['expectedTypes' => $expectedTypes];
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Rest\Exception;
|
||||
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class NoAuthenticationException extends RuntimeException
|
||||
{
|
||||
public static function fromExpectedTypes(array $expectedTypes): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'None of the valid authentication mechanisms where provided. Expected one of ["%s"]',
|
||||
implode('", "', $expectedTypes)
|
||||
));
|
||||
}
|
||||
}
|
|
@ -4,47 +4,67 @@ declare(strict_types=1);
|
|||
|
||||
namespace Shlinkio\Shlink\Rest\Exception;
|
||||
|
||||
use Throwable;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
|
||||
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class VerifyAuthenticationException extends RuntimeException
|
||||
class VerifyAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface
|
||||
{
|
||||
/** @var string */
|
||||
private $errorCode;
|
||||
/** @var string */
|
||||
private $publicMessage;
|
||||
use CommonProblemDetailsExceptionTrait;
|
||||
|
||||
public function __construct(
|
||||
string $errorCode,
|
||||
string $publicMessage,
|
||||
string $message = '',
|
||||
int $code = 0,
|
||||
?Throwable $previous = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->errorCode = $errorCode;
|
||||
$this->publicMessage = $publicMessage;
|
||||
public static function forInvalidApiKey(): self
|
||||
{
|
||||
$e = new self('Provided API key does not exist or is invalid.');
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = 'Invalid API key';
|
||||
$e->type = 'INVALID_API_KEY';
|
||||
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public static function withError(string $errorCode, string $publicMessage, ?Throwable $prev = null): self
|
||||
/** @deprecated */
|
||||
public static function forInvalidAuthToken(): self
|
||||
{
|
||||
return new self(
|
||||
$errorCode,
|
||||
$publicMessage,
|
||||
sprintf('Authentication verification failed with the public message "%s"', $publicMessage),
|
||||
0,
|
||||
$prev
|
||||
$e = new self(
|
||||
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
|
||||
. 'token on every new request on the Authorization header'
|
||||
);
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = 'Invalid auth token';
|
||||
$e->type = 'INVALID_AUTH_TOKEN';
|
||||
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public function getErrorCode(): string
|
||||
/** @deprecated */
|
||||
public static function forMissingAuthType(): self
|
||||
{
|
||||
return $this->errorCode;
|
||||
$e = new self('You need to provide the Bearer type in the Authorization header.');
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = 'Invalid authorization';
|
||||
$e->type = 'INVALID_AUTHORIZATION';
|
||||
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
|
||||
|
||||
return $e;
|
||||
}
|
||||
|
||||
public function getPublicMessage(): string
|
||||
/** @deprecated */
|
||||
public static function forInvalidAuthType(string $providedType): self
|
||||
{
|
||||
return $this->publicMessage;
|
||||
$e = new self(sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $providedType));
|
||||
|
||||
$e->detail = $e->getMessage();
|
||||
$e->title = 'Invalid authorization';
|
||||
$e->type = 'INVALID_AUTHORIZATION';
|
||||
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
|
||||
|
||||
return $e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,54 +6,28 @@ namespace Shlinkio\Shlink\Rest\Middleware;
|
|||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin;
|
||||
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPluginInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\NoAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Expressive\Router\RouteResult;
|
||||
|
||||
use function Functional\contains;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface, RequestMethodInterface
|
||||
{
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
/** @var array */
|
||||
private $routesWhitelist;
|
||||
/** @var RequestToHttpAuthPluginInterface */
|
||||
private $requestToAuthPlugin;
|
||||
|
||||
public function __construct(
|
||||
RequestToHttpAuthPluginInterface $requestToAuthPlugin,
|
||||
array $routesWhitelist,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
public function __construct(RequestToHttpAuthPluginInterface $requestToAuthPlugin, array $routesWhitelist)
|
||||
{
|
||||
$this->routesWhitelist = $routesWhitelist;
|
||||
$this->requestToAuthPlugin = $requestToAuthPlugin;
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param RequestHandlerInterface $handler
|
||||
*
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
{
|
||||
/** @var RouteResult|null $routeResult */
|
||||
|
@ -67,33 +41,10 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
|
|||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = $this->requestToAuthPlugin->fromRequest($request);
|
||||
} catch (ContainerExceptionInterface | NoAuthenticationException $e) {
|
||||
$this->logger->warning('Invalid or no authentication provided. {e}', ['e' => $e]);
|
||||
return $this->createErrorResponse(sprintf(
|
||||
'Expected one of the following authentication headers, but none were provided, ["%s"]',
|
||||
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
|
||||
));
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin->verify($request);
|
||||
$response = $handler->handle($request);
|
||||
return $plugin->update($request, $response);
|
||||
} catch (VerifyAuthenticationException $e) {
|
||||
$this->logger->warning('Authentication verification failed. {e}', ['e' => $e]);
|
||||
return $this->createErrorResponse($e->getPublicMessage(), $e->getErrorCode());
|
||||
}
|
||||
}
|
||||
|
||||
private function createErrorResponse(
|
||||
string $message,
|
||||
string $errorCode = RestUtils::INVALID_AUTHORIZATION_ERROR
|
||||
): JsonResponse {
|
||||
return new JsonResponse([
|
||||
'error' => $errorCode,
|
||||
'message' => $message,
|
||||
], self::STATUS_UNAUTHORIZED);
|
||||
return $plugin->update($request, $response);
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue