Merge pull request #148 from acelaya/feature/1.9.0

Version 1.9.0
This commit is contained in:
Alejandro Celaya 2018-05-07 11:26:27 +02:00 committed by GitHub
commit 8cfb4f61ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 857 additions and 179 deletions

View file

@ -1,5 +1,15 @@
## CHANGELOG ## CHANGELOG
### 1.9.0
**Features**
* [147: Allow short URLs to be created on the fly with query param authentication](https://github.com/shlinkio/shlink/issues/147)
**Bugs:**
* [139: Make sure all core actions log exceptions](https://github.com/shlinkio/shlink/issues/139)
### 1.8.1 ### 1.8.1
**Tasks** **Tasks**

View file

@ -33,8 +33,12 @@ rm composer.*
rm LICENSE rm LICENSE
rm indocker rm indocker
rm docker-compose.yml rm docker-compose.yml
rm docker-compose.override.yml
rm docker-compose.override.yml.dist
rm func_tests_bootstrap.php
rm php* rm php*
rm README.md rm README.md
rm infection.json
rm -rf build rm -rf build
rm -ff data/database.sqlite rm -ff data/database.sqlite
rm -rf data/infra rm -rf data/infra

View file

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate; use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware; use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware; use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
@ -16,6 +16,7 @@ return [
'pre-routing' => [ 'pre-routing' => [
'middleware' => [ 'middleware' => [
ErrorHandler::class, ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
LocaleMiddleware::class, LocaleMiddleware::class,
], ],
'priority' => 11, 'priority' => 11,
@ -49,7 +50,7 @@ return [
'post-routing' => [ 'post-routing' => [
'middleware' => [ 'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class, Expressive\Router\Middleware\DispatchMiddleware::class,
NotFoundDelegate::class, NotFoundHandler::class,
], ],
'priority' => 1, 'priority' => 1,
], ],

View file

@ -0,0 +1,125 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "Create a short URL",
"description": "Creates a short URL in a single API call. Useful for third party integrations",
"parameters": [
{
"name": "apiKey",
"in": "query",
"description": "The API key used to authenticate the request",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "longUrl",
"in": "query",
"description": "The URL to be shortened",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "format",
"in": "query",
"description": "The format in which you want the response to be returned. You can also use the \"Accept\" header instead of this",
"required": false,
"schema": {
"type": "string",
"enum": [
"txt",
"json"
]
}
}
],
"responses": {
"200": {
"description": "The list of short URLs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL that has been shortened"
},
"shortUrl": {
"type": "string",
"description": "The generated short URL"
},
"shortCode": {
"type": "string",
"description": "the short code that is being used in the short URL"
}
}
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"longUrl": "https://github.com/shlinkio/shlink",
"shortUrl": "https://dom.ain/abc123",
"shortCode": "abc123"
},
"text/plain": "https://dom.ain/abc123"
}
},
"400": {
"description": "The long URL was not provided or is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"error": "INVALID_URL",
"message": "Provided URL foo is invalid. Try with a different one."
},
"text/plain": "INVALID_URL"
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"examples": {
"application/json": {
"error": "UNKNOWN_ERROR",
"message": "Unexpected error occurred"
},
"text/plain": "UNKNOWN_ERROR"
}
}
}
}
}

View file

@ -40,6 +40,9 @@
"/v1/short-codes": { "/v1/short-codes": {
"$ref": "paths/v1_short-codes.json" "$ref": "paths/v1_short-codes.json"
}, },
"/v1/short-codes/shorten": {
"$ref": "paths/v1_short-codes_shorten.json"
},
"/v1/short-codes/{shortCode}": { "/v1/short-codes/{shortCode}": {
"$ref": "paths/v1_short-codes_{shortCode}.json" "$ref": "paths/v1_short-codes_{shortCode}.json"
}, },

View file

@ -6,7 +6,7 @@ use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Action; use Shlinkio\Shlink\Core\Action;
use Shlinkio\Shlink\Core\Middleware; use Shlinkio\Shlink\Core\Middleware;
use Shlinkio\Shlink\Core\Options; use Shlinkio\Shlink\Core\Options;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate; use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Service;
use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface; use Zend\Expressive\Template\TemplateRendererInterface;
@ -17,7 +17,7 @@ return [
'dependencies' => [ 'dependencies' => [
'factories' => [ 'factories' => [
Options\AppOptions::class => Options\AppOptionsFactory::class, Options\AppOptions::class => Options\AppOptionsFactory::class,
NotFoundDelegate::class => ConfigAbstractFactory::class, NotFoundHandler::class => ConfigAbstractFactory::class,
// Services // Services
Service\UrlShortener::class => ConfigAbstractFactory::class, Service\UrlShortener::class => ConfigAbstractFactory::class,
@ -33,14 +33,10 @@ return [
Action\PreviewAction::class => ConfigAbstractFactory::class, Action\PreviewAction::class => ConfigAbstractFactory::class,
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class, Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
], ],
'aliases' => [
'Zend\Expressive\Delegate\DefaultDelegate' => NotFoundDelegate::class,
],
], ],
ConfigAbstractFactory::class => [ ConfigAbstractFactory::class => [
NotFoundDelegate::class => [TemplateRendererInterface::class], NotFoundHandler::class => [TemplateRendererInterface::class],
// Services // Services
Service\UrlShortener::class => [ Service\UrlShortener::class => [
@ -60,14 +56,16 @@ return [
Service\UrlShortener::class, Service\UrlShortener::class,
Service\VisitsTracker::class, Service\VisitsTracker::class,
Options\AppOptions::class, Options\AppOptions::class,
'Logger_Shlink',
], ],
Action\PixelAction::class => [ Action\PixelAction::class => [
Service\UrlShortener::class, Service\UrlShortener::class,
Service\VisitsTracker::class, Service\VisitsTracker::class,
Options\AppOptions::class, Options\AppOptions::class,
'Logger_Shlink',
], ],
Action\QrCodeAction::class => [RouterInterface::class, Service\UrlShortener::class, 'Logger_Shlink'], Action\QrCodeAction::class => [RouterInterface::class, Service\UrlShortener::class, 'Logger_Shlink'],
Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class], Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class, 'Logger_Shlink'],
Middleware\QrCodeCacheMiddleware::class => [Cache::class], Middleware\QrCodeCacheMiddleware::class => [Cache::class],
], ],

View file

@ -7,6 +7,8 @@ use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait; use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
@ -30,15 +32,21 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
* @var AppOptions * @var AppOptions
*/ */
private $appOptions; private $appOptions;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct( public function __construct(
UrlShortenerInterface $urlShortener, UrlShortenerInterface $urlShortener,
VisitsTrackerInterface $visitTracker, VisitsTrackerInterface $visitTracker,
AppOptions $appOptions AppOptions $appOptions,
LoggerInterface $logger = null
) { ) {
$this->urlShortener = $urlShortener; $this->urlShortener = $urlShortener;
$this->visitTracker = $visitTracker; $this->visitTracker = $visitTracker;
$this->appOptions = $appOptions; $this->appOptions = $appOptions;
$this->logger = $logger ?: new NullLogger();
} }
/** /**
@ -66,6 +74,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface
return $this->createResp($longUrl); return $this->createResp($longUrl);
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) { } catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('An error occurred while tracking short code.' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler); return $this->buildErrorResponse($request, $handler);
} }
} }

View file

@ -7,6 +7,8 @@ use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException; use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface; use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait; use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
@ -28,11 +30,19 @@ class PreviewAction implements MiddlewareInterface
* @var UrlShortenerInterface * @var UrlShortenerInterface
*/ */
private $urlShortener; private $urlShortener;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(PreviewGeneratorInterface $previewGenerator, UrlShortenerInterface $urlShortener) public function __construct(
{ PreviewGeneratorInterface $previewGenerator,
UrlShortenerInterface $urlShortener,
LoggerInterface $logger = null
) {
$this->previewGenerator = $previewGenerator; $this->previewGenerator = $previewGenerator;
$this->urlShortener = $urlShortener; $this->urlShortener = $urlShortener;
$this->logger = $logger ?: new NullLogger();
} }
/** /**
@ -53,6 +63,7 @@ class PreviewAction implements MiddlewareInterface
$imagePath = $this->previewGenerator->generatePreview($url); $imagePath = $this->previewGenerator->generatePreview($url);
return $this->generateImageResponse($imagePath); return $this->generateImageResponse($imagePath);
} catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) { } catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
$this->logger->warning('An error occurred while generating preview image.' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler); return $this->buildErrorResponse($request, $handler);
} }
} }

View file

@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Zend\Expressive\Router\Exception\RuntimeException;
use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Router\RouterInterface;
class QrCodeAction implements MiddlewareInterface class QrCodeAction implements MiddlewareInterface
@ -52,6 +53,8 @@ class QrCodeAction implements MiddlewareInterface
* @param RequestHandlerInterface $handler * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
* @throws \InvalidArgumentException
* @throws RuntimeException
*/ */
public function process(Request $request, RequestHandlerInterface $handler): Response public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
@ -59,11 +62,8 @@ class QrCodeAction implements MiddlewareInterface
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
try { try {
$this->urlShortener->shortCodeToUrl($shortCode); $this->urlShortener->shortCodeToUrl($shortCode);
} catch (InvalidShortCodeException $e) { } catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e); $this->logger->warning('An error occurred while creating QR code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler);
} catch (EntityDoesNotExistException $e) {
$this->logger->warning('Tried to create a QR code with a not found short code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler); return $this->buildErrorResponse($request, $handler);
} }

View file

@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Core\Action\Util;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate; use Shlinkio\Shlink\Core\Response\NotFoundHandler;
trait ErrorResponseBuilderTrait trait ErrorResponseBuilderTrait
{ {
@ -14,7 +14,7 @@ trait ErrorResponseBuilderTrait
ServerRequestInterface $request, ServerRequestInterface $request,
RequestHandlerInterface $handler RequestHandlerInterface $handler
): ResponseInterface { ): ResponseInterface {
$request = $request->withAttribute(NotFoundDelegate::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code'); $request = $request->withAttribute(NotFoundHandler::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code');
return $handler->handle($request); return $handler->handle($request);
} }
} }

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\UriInterface;
final class CreateShortCodeData
{
/**
* @var UriInterface
*/
private $longUrl;
/**
* @var array
*/
private $tags;
/**
* @var ShortUrlMeta
*/
private $meta;
public function __construct(
UriInterface $longUrl,
array $tags = [],
ShortUrlMeta $meta = null
) {
$this->longUrl = $longUrl;
$this->tags = $tags;
$this->meta = $meta ?? ShortUrlMeta::createFromParams();
}
/**
* @return UriInterface
*/
public function getLongUrl(): UriInterface
{
return $this->longUrl;
}
/**
* @return array
*/
public function getTags(): array
{
return $this->tags;
}
/**
* @return ShortUrlMeta
*/
public function getMeta(): ShortUrlMeta
{
return $this->meta;
}
}

View file

@ -71,7 +71,7 @@ final class ShortUrlMeta
* @param array $data * @param array $data
* @throws ValidationException * @throws ValidationException
*/ */
private function validate(array $data) private function validate(array $data): void
{ {
$inputFilter = new ShortUrlMetaInputFilter($data); $inputFilter = new ShortUrlMetaInputFilter($data);
if (! $inputFilter->isValid()) { if (! $inputFilter->isValid()) {

View file

@ -6,13 +6,13 @@ namespace Shlinkio\Shlink\Core\Response;
use Fig\Http\Message\StatusCodeInterface; use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface; use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Expressive\Template\TemplateRendererInterface; use Zend\Expressive\Template\TemplateRendererInterface;
class NotFoundDelegate implements DelegateInterface class NotFoundHandler implements RequestHandlerInterface
{ {
const NOT_FOUND_TEMPLATE = 'notFoundTemplate'; public const NOT_FOUND_TEMPLATE = 'notFoundTemplate';
/** /**
* @var TemplateRendererInterface * @var TemplateRendererInterface

View file

@ -7,15 +7,15 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate; use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Template\TemplateRendererInterface; use Zend\Expressive\Template\TemplateRendererInterface;
class NotFoundDelegateTest extends TestCase class NotFoundHandlerTest extends TestCase
{ {
/** /**
* @var NotFoundDelegate * @var NotFoundHandler
*/ */
private $delegate; private $delegate;
/** /**
@ -26,7 +26,7 @@ class NotFoundDelegateTest extends TestCase
public function setUp() public function setUp()
{ {
$this->renderer = $this->prophesize(TemplateRendererInterface::class); $this->renderer = $this->prophesize(TemplateRendererInterface::class);
$this->delegate = new NotFoundDelegate($this->renderer->reveal()); $this->delegate = new NotFoundHandler($this->renderer->reveal());
} }
/** /**

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest;
return [
'auth' => [
'routes_whitelist' => [
Action\AuthenticateAction::class,
Action\ShortCode\SingleStepCreateShortCodeAction::class,
],
],
];

View file

@ -20,12 +20,13 @@ return [
ApiKeyService::class => ConfigAbstractFactory::class, ApiKeyService::class => ConfigAbstractFactory::class,
Action\AuthenticateAction::class => ConfigAbstractFactory::class, Action\AuthenticateAction::class => ConfigAbstractFactory::class,
Action\CreateShortcodeAction::class => ConfigAbstractFactory::class, Action\ShortCode\CreateShortCodeAction::class => ConfigAbstractFactory::class,
Action\EditShortCodeAction::class => ConfigAbstractFactory::class, Action\ShortCode\SingleStepCreateShortCodeAction::class => ConfigAbstractFactory::class,
Action\ResolveUrlAction::class => ConfigAbstractFactory::class, Action\ShortCode\EditShortCodeAction::class => ConfigAbstractFactory::class,
Action\GetVisitsAction::class => ConfigAbstractFactory::class, Action\ShortCode\ResolveUrlAction::class => ConfigAbstractFactory::class,
Action\ListShortcodesAction::class => ConfigAbstractFactory::class, Action\Visit\GetVisitsAction::class => ConfigAbstractFactory::class,
Action\EditShortcodeTagsAction::class => ConfigAbstractFactory::class, Action\ShortCode\ListShortCodesAction::class => ConfigAbstractFactory::class,
Action\ShortCode\EditShortCodeTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class, Action\Tag\ListTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class, Action\Tag\DeleteTagsAction::class => ConfigAbstractFactory::class,
Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class, Action\Tag\CreateTagsAction::class => ConfigAbstractFactory::class,
@ -35,6 +36,7 @@ return [
Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class, Middleware\PathVersionMiddleware::class => InvokableFactory::class,
Middleware\CheckAuthenticationMiddleware::class => ConfigAbstractFactory::class, Middleware\CheckAuthenticationMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class => InvokableFactory::class,
], ],
], ],
@ -43,23 +45,39 @@ return [
ApiKeyService::class => ['em'], ApiKeyService::class => ['em'],
Action\AuthenticateAction::class => [ApiKeyService::class, JWTService::class, 'translator', 'Logger_Shlink'], Action\AuthenticateAction::class => [ApiKeyService::class, JWTService::class, 'translator', 'Logger_Shlink'],
Action\CreateShortcodeAction::class => [ Action\ShortCode\CreateShortCodeAction::class => [
Service\UrlShortener::class, Service\UrlShortener::class,
'translator', 'translator',
'config.url_shortener.domain', 'config.url_shortener.domain',
'Logger_Shlink', 'Logger_Shlink',
], ],
Action\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',], Action\ShortCode\SingleStepCreateShortCodeAction::class => [
Action\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'], Service\UrlShortener::class,
Action\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'], 'translator',
Action\ListShortcodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'], ApiKeyService::class,
Action\EditShortcodeTagsAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'], 'config.url_shortener.domain',
'Logger_Shlink',
],
Action\ShortCode\EditShortCodeAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink',],
Action\ShortCode\ResolveUrlAction::class => [Service\UrlShortener::class, 'translator'],
Action\Visit\GetVisitsAction::class => [Service\VisitsTracker::class, 'translator', 'Logger_Shlink'],
Action\ShortCode\ListShortCodesAction::class => [Service\ShortUrlService::class, 'translator', 'Logger_Shlink'],
Action\ShortCode\EditShortCodeTagsAction::class => [
Service\ShortUrlService::class,
'translator',
'Logger_Shlink',
],
Action\Tag\ListTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Action\Tag\ListTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\CreateTagsAction::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, Translator::class, LoggerInterface::class], Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, Translator::class, LoggerInterface::class],
Middleware\CheckAuthenticationMiddleware::class => [JWTService::class, 'translator', 'Logger_Shlink'], Middleware\CheckAuthenticationMiddleware::class => [
JWTService::class,
'translator',
'config.auth.routes_whitelist',
'Logger_Shlink',
],
], ],
]; ];

View file

@ -1,84 +1,35 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Fig\Http\Message\RequestMethodInterface as RequestMethod; namespace Shlinkio\Shlink\Rest;
use Shlinkio\Shlink\Rest\Action; use Shlinkio\Shlink\Rest\Action;
return [ return [
'routes' => [ 'routes' => [
[ Action\AuthenticateAction::getRouteDef(),
'name' => Action\AuthenticateAction::class,
'path' => '/authenticate',
'middleware' => Action\AuthenticateAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
// Short codes // Short codes
[ Action\ShortCode\CreateShortCodeAction::getRouteDef([
'name' => Action\CreateShortcodeAction::class, Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class,
'path' => '/short-codes', ]),
'middleware' => Action\CreateShortcodeAction::class, Action\ShortCode\SingleStepCreateShortCodeAction::getRouteDef([
'allowed_methods' => [RequestMethod::METHOD_POST], Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware::class,
], ]),
[ Action\ShortCode\EditShortCodeAction::getRouteDef(),
'name' => Action\EditShortCodeAction::class, Action\ShortCode\ResolveUrlAction::getRouteDef(),
'path' => '/short-codes/{shortCode}', Action\ShortCode\ListShortCodesAction::getRouteDef(),
'middleware' => Action\EditShortCodeAction::class, Action\ShortCode\EditShortCodeTagsAction::getRouteDef(),
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
[
'name' => Action\ResolveUrlAction::class,
'path' => '/short-codes/{shortCode}',
'middleware' => Action\ResolveUrlAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\ListShortcodesAction::class,
'path' => '/short-codes',
'middleware' => Action\ListShortcodesAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\EditShortcodeTagsAction::class,
'path' => '/short-codes/{shortCode}/tags',
'middleware' => Action\EditShortcodeTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
// Visits // Visits
[ Action\Visit\GetVisitsAction::getRouteDef(),
'name' => Action\GetVisitsAction::class,
'path' => '/short-codes/{shortCode}/visits',
'middleware' => Action\GetVisitsAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
// Tags // Tags
[ Action\Tag\ListTagsAction::getRouteDef(),
'name' => Action\Tag\ListTagsAction::class, Action\Tag\DeleteTagsAction::getRouteDef(),
'path' => '/tags', Action\Tag\CreateTagsAction::getRouteDef(),
'middleware' => Action\Tag\ListTagsAction::class, Action\Tag\UpdateTagAction::getRouteDef(),
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => Action\Tag\DeleteTagsAction::class,
'path' => '/tags',
'middleware' => Action\Tag\DeleteTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_DELETE],
],
[
'name' => Action\Tag\CreateTagsAction::class,
'path' => '/tags',
'middleware' => Action\Tag\CreateTagsAction::class,
'allowed_methods' => [RequestMethod::METHOD_POST],
],
[
'name' => Action\Tag\UpdateTagAction::class,
'path' => '/tags',
'middleware' => Action\Tag\UpdateTagAction::class,
'allowed_methods' => [RequestMethod::METHOD_PUT],
],
], ],
]; ];

Binary file not shown.

View file

@ -1,15 +1,15 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Shlink 1.0\n" "Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2018-01-21 09:40+0100\n" "POT-Creation-Date: 2018-05-06 12:34+0200\n"
"PO-Revision-Date: 2018-01-21 09:40+0100\n" "PO-Revision-Date: 2018-05-06 12:35+0200\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n" "Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: es_ES\n" "Language: es_ES\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.4\n" "X-Generator: Poedit 2.0.6\n"
"X-Poedit-Basepath: ..\n" "X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
@ -25,9 +25,6 @@ msgstr ""
msgid "Provided API key does not exist or is invalid." msgid "Provided API key does not exist or is invalid."
msgstr "La clave de API proporcionada no existe o es inválida." msgstr "La clave de API proporcionada no existe o es inválida."
msgid "A URL was not provided"
msgstr "No se ha proporcionado una URL"
#, php-format #, php-format
msgid "Provided URL %s is invalid. Try with a different one." msgid "Provided URL %s is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" es inválida. Prueba con una diferente." msgstr "La URL proporcionada \"%s\" es inválida. Prueba con una diferente."
@ -39,6 +36,9 @@ msgstr "El slug proporcionado \"%s\" ya está en uso. Prueba con uno diferente."
msgid "Unexpected error occurred" msgid "Unexpected error occurred"
msgstr "Ocurrió un error inesperado" msgstr "Ocurrió un error inesperado"
msgid "A URL was not provided"
msgstr "No se ha proporcionado una URL"
#, php-format #, php-format
msgid "No URL found for short code \"%s\"" msgid "No URL found for short code \"%s\""
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\"" msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
@ -49,14 +49,13 @@ msgstr "Los datos proporcionados son inválidos."
msgid "A list of tags was not provided" msgid "A list of tags was not provided"
msgstr "No se ha proporcionado una lista de etiquetas" msgstr "No se ha proporcionado una lista de etiquetas"
#, php-format
msgid "Provided short code %s does not exist"
msgstr "El código corto \"%s\" proporcionado no existe"
#, php-format #, php-format
msgid "Provided short code \"%s\" has an invalid format" msgid "Provided short code \"%s\" has an invalid format"
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido" msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
msgid "No API key was provided or it is not valid"
msgstr "No se ha proporcionado una clave de API o esta es inválida"
msgid "" msgid ""
"You have to provide both 'oldName' and 'newName' params in order to properly " "You have to provide both 'oldName' and 'newName' params in order to properly "
"rename the tag" "rename the tag"
@ -68,6 +67,10 @@ msgstr ""
msgid "It wasn't possible to find a tag with name '%s'" msgid "It wasn't possible to find a tag with name '%s'"
msgstr "No fue posible encontrar una etiqueta con el nombre '%s'" msgstr "No fue posible encontrar una etiqueta con el nombre '%s'"
#, php-format
msgid "Provided short code %s does not exist"
msgstr "El código corto \"%s\" proporcionado no existe"
#, php-format #, php-format
msgid "You need to provide the Bearer type in the %s header." msgid "You need to provide the Bearer type in the %s header."
msgstr "Debes proporcionar el typo Bearer en la cabecera %s." msgstr "Debes proporcionar el typo Bearer en la cabecera %s."

View file

@ -11,6 +11,9 @@ use Psr\Log\NullLogger;
abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface
{ {
protected const ROUTE_PATH = '';
protected const ROUTE_ALLOWED_METHODS = [];
/** /**
* @var LoggerInterface * @var LoggerInterface
*/ */
@ -20,4 +23,14 @@ abstract class AbstractRestAction implements RequestHandlerInterface, RequestMet
{ {
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
public static function getRouteDef(array $prevMiddleware = [], array $postMiddleware = []): array
{
return [
'name' => static::class,
'middleware' => \array_merge($prevMiddleware, [static::class], $postMiddleware),
'path' => static::ROUTE_PATH,
'allowed_methods' => static::ROUTE_ALLOWED_METHODS,
];
}
} }

View file

@ -15,6 +15,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class AuthenticateAction extends AbstractRestAction class AuthenticateAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/authenticate';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */

View file

@ -1,20 +1,23 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\Uri; use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class CreateShortcodeAction extends AbstractRestAction abstract class AbstractCreateShortCodeAction extends AbstractRestAction
{ {
/** /**
* @var UrlShortenerInterface * @var UrlShortenerInterface
@ -27,7 +30,7 @@ class CreateShortcodeAction extends AbstractRestAction
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
private $translator; protected $translator;
public function __construct( public function __construct(
UrlShortenerInterface $urlShortener, UrlShortenerInterface $urlShortener,
@ -48,31 +51,34 @@ class CreateShortcodeAction extends AbstractRestAction
*/ */
public function handle(Request $request): Response public function handle(Request $request): Response
{ {
$postData = (array) $request->getParsedBody(); try {
if (! isset($postData['longUrl'])) { $shortCodeData = $this->buildUrlToShortCodeData($request);
$shortCodeMeta = $shortCodeData->getMeta();
$longUrl = $shortCodeData->getLongUrl();
$customSlug = $shortCodeMeta->getCustomSlug();
} catch (InvalidArgumentException $e) {
$this->logger->warning('Provided data is invalid.' . PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR, 'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => $this->translator->translate('A URL was not provided'), 'message' => $e->getMessage(),
], self::STATUS_BAD_REQUEST); ], self::STATUS_BAD_REQUEST);
} }
$longUrl = $postData['longUrl'];
$customSlug = $postData['customSlug'] ?? null;
try { try {
$shortCode = $this->urlShortener->urlToShortCode( $shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl), $longUrl,
(array) ($postData['tags'] ?? []), $shortCodeData->getTags(),
$this->getOptionalDate($postData, 'validSince'), $shortCodeMeta->getValidSince(),
$this->getOptionalDate($postData, 'validUntil'), $shortCodeMeta->getValidUntil(),
$customSlug, $customSlug,
isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null $shortCodeMeta->getMaxVisits()
); );
$shortUrl = (new Uri())->withPath($shortCode) $shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema']) ->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']); ->withHost($this->domainConfig['hostname']);
return new JsonResponse([ return new JsonResponse([
'longUrl' => $longUrl, 'longUrl' => (string) $longUrl,
'shortUrl' => (string) $shortUrl, 'shortUrl' => (string) $shortUrl,
'shortCode' => $shortCode, 'shortCode' => $shortCode,
]); ]);
@ -80,7 +86,7 @@ class CreateShortcodeAction extends AbstractRestAction
$this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e); $this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e), 'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf( 'message' => \sprintf(
$this->translator->translate('Provided URL %s is invalid. Try with a different one.'), $this->translator->translate('Provided URL %s is invalid. Try with a different one.'),
$longUrl $longUrl
), ),
@ -89,7 +95,7 @@ class CreateShortcodeAction extends AbstractRestAction
$this->logger->warning('Provided non-unique slug.' . PHP_EOL . $e); $this->logger->warning('Provided non-unique slug.' . PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e), 'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf( 'message' => \sprintf(
$this->translator->translate('Provided slug %s is already in use. Try with a different one.'), $this->translator->translate('Provided slug %s is already in use. Try with a different one.'),
$customSlug $customSlug
), ),
@ -103,8 +109,10 @@ class CreateShortcodeAction extends AbstractRestAction
} }
} }
private function getOptionalDate(array $postData, string $fieldName) /**
{ * @param Request $request
return isset($postData[$fieldName]) ? new \DateTime($postData[$fieldName]) : null; * @return CreateShortCodeData
} * @throws InvalidArgumentException
*/
abstract protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData;
} }

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Zend\Diactoros\Uri;
class CreateShortCodeAction extends AbstractCreateShortCodeAction
{
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/**
* @param Request $request
* @return CreateShortCodeData
* @throws InvalidArgumentException
* @throws \InvalidArgumentException
*/
protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData
{
$postData = (array) $request->getParsedBody();
if (! isset($postData['longUrl'])) {
throw new InvalidArgumentException($this->translator->translate('A URL was not provided'));
}
return new CreateShortCodeData(
new Uri($postData['longUrl']),
(array) ($postData['tags'] ?? []),
ShortUrlMeta::createFromParams(
$this->getOptionalDate($postData, 'validSince'),
$this->getOptionalDate($postData, 'validUntil'),
$postData['customSlug'] ?? null,
isset($postData['maxVisits']) ? (int) $postData['maxVisits'] : null
)
);
}
private function getOptionalDate(array $postData, string $fieldName)
{
return isset($postData[$fieldName]) ? new \DateTime($postData[$fieldName]) : null;
}
}

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@ -9,6 +9,7 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
@ -16,6 +17,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class EditShortCodeAction extends AbstractRestAction class EditShortCodeAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */

View file

@ -1,19 +1,23 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class EditShortcodeTagsAction extends AbstractRestAction class EditShortCodeTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */

View file

@ -1,21 +1,25 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesAction extends AbstractRestAction class ListShortCodesAction extends AbstractRestAction
{ {
use PaginatorUtilsTrait; use PaginatorUtilsTrait;
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
@ -9,12 +9,16 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlAction extends AbstractRestAction class ResolveUrlAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var UrlShortenerInterface * @var UrlShortenerInterface
*/ */

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\CreateShortCodeData;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class SingleStepCreateShortCodeAction extends AbstractCreateShortCodeAction
{
protected const ROUTE_PATH = '/short-codes/shorten';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
ApiKeyServiceInterface $apiKeyService,
array $domainConfig,
LoggerInterface $logger = null
) {
parent::__construct($urlShortener, $translator, $domainConfig, $logger);
$this->apiKeyService = $apiKeyService;
}
/**
* @param Request $request
* @return CreateShortCodeData
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
protected function buildUrlToShortCodeData(Request $request): CreateShortCodeData
{
$query = $request->getQueryParams();
// Check provided API key
$apiKey = $this->apiKeyService->getByKey($query['apiKey'] ?? '');
if ($apiKey === null || ! $apiKey->isValid()) {
throw new InvalidArgumentException(
$this->translator->translate('No API key was provided or it is not valid')
);
}
if (! isset($query['longUrl'])) {
throw new InvalidArgumentException($this->translator->translate('A URL was not provided'));
}
return new CreateShortCodeData(new Uri($query['longUrl']));
}
}

View file

@ -12,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class CreateTagsAction extends AbstractRestAction class CreateTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */

View file

@ -12,6 +12,9 @@ use Zend\Diactoros\Response\EmptyResponse;
class DeleteTagsAction extends AbstractRestAction class DeleteTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_DELETE];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */

View file

@ -12,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class ListTagsAction extends AbstractRestAction class ListTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */

View file

@ -16,6 +16,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class UpdateTagAction extends AbstractRestAction class UpdateTagAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\Visit;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
@ -9,12 +9,16 @@ use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsAction extends AbstractRestAction class GetVisitsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}/visits';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var VisitsTrackerInterface * @var VisitsTrackerInterface
*/ */

View file

@ -10,7 +10,6 @@ use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException; use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
@ -35,14 +34,20 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
* @var LoggerInterface * @var LoggerInterface
*/ */
private $logger; private $logger;
/**
* @var array
*/
private $routesWhitelist;
public function __construct( public function __construct(
JWTServiceInterface $jwtService, JWTServiceInterface $jwtService,
TranslatorInterface $translator, TranslatorInterface $translator,
array $routesWhitelist,
LoggerInterface $logger = null LoggerInterface $logger = null
) { ) {
$this->translator = $translator; $this->translator = $translator;
$this->jwtService = $jwtService; $this->jwtService = $jwtService;
$this->routesWhitelist = $routesWhitelist;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
@ -64,8 +69,8 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
$routeResult = $request->getAttribute(RouteResult::class); $routeResult = $request->getAttribute(RouteResult::class);
if ($routeResult === null if ($routeResult === null
|| $routeResult->isFailure() || $routeResult->isFailure()
|| $routeResult->getMatchedRouteName() === AuthenticateAction::class
|| $request->getMethod() === 'OPTIONS' || $request->getMethod() === 'OPTIONS'
|| \in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
) { ) {
return $handler->handle($request); return $handler->handle($request);
} }

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware\ShortCode;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
class CreateShortCodeContentNegotiationMiddleware implements MiddlewareInterface
{
private const PLAIN_TEXT = 'text';
private const JSON = 'json';
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
* @throws \RuntimeException
* @throws \InvalidArgumentException
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
// If the response is not JSON, return it as is
if (! $response instanceof JsonResponse) {
return $response;
}
$query = $request->getQueryParams();
$acceptedType = isset($query['format'])
? $this->determineAcceptTypeFromQuery($query)
: $this->determineAcceptTypeFromHeader($request->getHeaderLine('Accept'));
// If JSON was requested, return the response from next handler as is
if ($acceptedType === self::JSON) {
return $response;
}
// If requested, return a plain text response containing the short URL only
$resp = (new Response())->withHeader('Content-Type', 'text/plain');
$body = $resp->getBody();
$body->write($this->determineBody($response));
$body->rewind();
return $resp;
}
private function determineAcceptTypeFromQuery(array $query): string
{
if (! isset($query['format'])) {
return self::JSON;
}
$format = \strtolower((string) $query['format']);
return $format === 'txt' ? self::PLAIN_TEXT : self::JSON;
}
private function determineAcceptTypeFromHeader(string $acceptValue): string
{
$accepts = \explode(',', $acceptValue);
$accept = \strtolower(\array_shift($accepts));
return \strpos($accept, 'text/plain') !== false ? self::PLAIN_TEXT : self::JSON;
}
private function determineBody(JsonResponse $resp): string
{
$payload = $resp->getPayload();
return $payload['shortUrl'] ?? $payload['error'] ?? '';
}
}

View file

@ -28,7 +28,7 @@ class ApiKeyService implements ApiKeyServiceInterface
public function create(\DateTime $expirationDate = null) public function create(\DateTime $expirationDate = null)
{ {
$key = new ApiKey(); $key = new ApiKey();
if (isset($expirationDate)) { if ($expirationDate !== null) {
$key->setExpirationDate($expirationDate); $key->setExpirationDate($expirationDate);
} }
@ -44,7 +44,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param string $key * @param string $key
* @return bool * @return bool
*/ */
public function check($key) public function check(string $key)
{ {
/** @var ApiKey|null $apiKey */ /** @var ApiKey|null $apiKey */
$apiKey = $this->getByKey($key); $apiKey = $this->getByKey($key);
@ -58,7 +58,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @return ApiKey * @return ApiKey
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function disable($key) public function disable(string $key)
{ {
/** @var ApiKey|null $apiKey */ /** @var ApiKey|null $apiKey */
$apiKey = $this->getByKey($key); $apiKey = $this->getByKey($key);
@ -77,7 +77,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param bool $enabledOnly Tells if only enabled keys should be returned * @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[] * @return ApiKey[]
*/ */
public function listKeys($enabledOnly = false) public function listKeys(bool $enabledOnly = false)
{ {
$conditions = $enabledOnly ? ['enabled' => true] : []; $conditions = $enabledOnly ? ['enabled' => true] : [];
return $this->em->getRepository(ApiKey::class)->findBy($conditions); return $this->em->getRepository(ApiKey::class)->findBy($conditions);
@ -89,7 +89,7 @@ class ApiKeyService implements ApiKeyServiceInterface
* @param string $key * @param string $key
* @return ApiKey|null * @return ApiKey|null
*/ */
public function getByKey($key) public function getByKey(string $key)
{ {
/** @var ApiKey|null $apiKey */ /** @var ApiKey|null $apiKey */
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([ $apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([

View file

@ -22,7 +22,7 @@ interface ApiKeyServiceInterface
* @param string $key * @param string $key
* @return bool * @return bool
*/ */
public function check($key); public function check(string $key);
/** /**
* Disables provided api key * Disables provided api key
@ -31,7 +31,7 @@ interface ApiKeyServiceInterface
* @return ApiKey * @return ApiKey
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function disable($key); public function disable(string $key);
/** /**
* Lists all existing api keys * Lists all existing api keys
@ -39,7 +39,7 @@ interface ApiKeyServiceInterface
* @param bool $enabledOnly Tells if only enabled keys should be returned * @param bool $enabledOnly Tells if only enabled keys should be returned
* @return ApiKey[] * @return ApiKey[]
*/ */
public function listKeys($enabledOnly = false); public function listKeys(bool $enabledOnly = false);
/** /**
* Tries to find one API key by its key string * Tries to find one API key by its key string
@ -47,5 +47,5 @@ interface ApiKeyServiceInterface
* @param string $key * @param string $key
* @return ApiKey|null * @return ApiKey|null
*/ */
public function getByKey($key); public function getByKey(string $key);
} }

View file

@ -30,6 +30,7 @@ class RestUtils
case $e instanceof Core\NonUniqueSlugException: case $e instanceof Core\NonUniqueSlugException:
return self::INVALID_SLUG_ERROR; return self::INVALID_SLUG_ERROR;
case $e instanceof Common\InvalidArgumentException: case $e instanceof Common\InvalidArgumentException:
case $e instanceof Core\InvalidArgumentException:
case $e instanceof Core\ValidationException: case $e instanceof Core\ValidationException:
return self::INVALID_ARGUMENT_ERROR; return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException: case $e instanceof Rest\AuthenticationException:

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@ -9,16 +9,16 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\CreateShortcodeAction; use Shlinkio\Shlink\Rest\Action\ShortCode\CreateShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Uri; use Zend\Diactoros\Uri;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
class CreateShortcodeActionTest extends TestCase class CreateShortCodeActionTest extends TestCase
{ {
/** /**
* @var CreateShortcodeAction * @var CreateShortCodeAction
*/ */
protected $action; protected $action;
/** /**
@ -29,7 +29,7 @@ class CreateShortcodeActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->urlShortener = $this->prophesize(UrlShortener::class); $this->urlShortener = $this->prophesize(UrlShortener::class);
$this->action = new CreateShortcodeAction($this->urlShortener->reveal(), Translator::factory([]), [ $this->action = new CreateShortCodeAction($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http', 'schema' => 'http',
'hostname' => 'foo.com', 'hostname' => 'foo.com',
]); ]);

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@ -9,7 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\EditShortCodeAction; use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;

View file

@ -1,21 +1,21 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\EditShortcodeTagsAction; use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeTagsAction;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
class EditShortcodeTagsActionTest extends TestCase class EditShortCodeTagsActionTest extends TestCase
{ {
/** /**
* @var EditShortcodeTagsAction * @var EditShortCodeTagsAction
*/ */
protected $action; protected $action;
/** /**
@ -26,7 +26,7 @@ class EditShortcodeTagsActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->shortUrlService = $this->prophesize(ShortUrlService::class); $this->shortUrlService = $this->prophesize(ShortUrlService::class);
$this->action = new EditShortcodeTagsAction($this->shortUrlService->reveal(), Translator::factory([])); $this->action = new EditShortCodeTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
} }
/** /**

View file

@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\ListShortcodesAction; use Shlinkio\Shlink\Rest\Action\ShortCode\ListShortCodesAction;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter; use Zend\Paginator\Adapter\ArrayAdapter;
@ -15,7 +15,7 @@ use Zend\Paginator\Paginator;
class ListShortCodesActionTest extends TestCase class ListShortCodesActionTest extends TestCase
{ {
/** /**
* @var ListShortcodesAction * @var ListShortCodesAction
*/ */
protected $action; protected $action;
/** /**
@ -26,7 +26,7 @@ class ListShortCodesActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->service = $this->prophesize(ShortUrlService::class); $this->service = $this->prophesize(ShortUrlService::class);
$this->action = new ListShortcodesAction($this->service->reveal(), Translator::factory([])); $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]));
} }
/** /**

View file

@ -1,14 +1,14 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\ResolveUrlAction; use Shlinkio\Shlink\Rest\Action\ShortCode\ResolveUrlAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;

View file

@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortCode\SingleStepCreateShortCodeAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator;
class SingleStepCreateShortCodeActionTest extends TestCase
{
/**
* @var SingleStepCreateShortCodeAction
*/
private $action;
/**
* @var ObjectProphecy
*/
private $urlShortener;
/**
* @var ObjectProphecy
*/
private $apiKeyService;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortenerInterface::class);
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$this->action = new SingleStepCreateShortCodeAction(
$this->urlShortener->reveal(),
Translator::factory([]),
$this->apiKeyService->reveal(),
[
'schema' => 'http',
'hostname' => 'foo.com',
]
);
}
/**
* @test
* @dataProvider provideInvalidApiKeys
*/
public function errorResponseIsReturnedIfInvalidApiKeyIsProvided(?ApiKey $apiKey)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn($apiKey);
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals('INVALID_ARGUMENT', $payload['error']);
$this->assertEquals('No API key was provided or it is not valid', $payload['message']);
$findApiKey->shouldHaveBeenCalled();
}
public function provideInvalidApiKeys(): array
{
return [
[null],
[(new ApiKey())->disable()],
];
}
/**
* @test
*/
public function errorResponseIsReturnedIfNoUrlIsProvided()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['apiKey' => 'abc123']);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
/** @var JsonResponse $resp */
$resp = $this->action->handle($request);
$payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode());
$this->assertEquals('INVALID_ARGUMENT', $payload['error']);
$this->assertEquals('A URL was not provided', $payload['message']);
$findApiKey->shouldHaveBeenCalled();
}
/**
* @test
*/
public function properDataIsPassedWhenGeneratingShortCode()
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams([
'apiKey' => 'abc123',
'longUrl' => 'http://foobar.com',
]);
$findApiKey = $this->apiKeyService->getByKey('abc123')->willReturn(new ApiKey());
$generateShortCode = $this->urlShortener->urlToShortCode(
Argument::that(function (UriInterface $argument) {
Assert::assertEquals('http://foobar.com', (string) $argument);
return $argument;
}),
[],
null,
null,
null,
null
);
$resp = $this->action->handle($request);
$this->assertEquals(200, $resp->getStatusCode());
$findApiKey->shouldHaveBeenCalled();
$generateShortCode->shouldHaveBeenCalled();
}
}

View file

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@ -9,7 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Rest\Action\GetVisitsAction; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;

View file

@ -37,9 +37,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function setUp() public function setUp()
{ {
$this->jwtService = $this->prophesize(JWTService::class); $this->jwtService = $this->prophesize(JWTService::class);
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([])); $this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]), [
$this->dummyMiddleware = middleware(function ($request, $handler) { AuthenticateAction::class,
return new Response\EmptyResponse; ]);
$this->dummyMiddleware = middleware(function () {
return new Response\EmptyResponse();
}); });
} }

View file

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware\ShortCode;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\ShortCode\CreateShortCodeContentNegotiationMiddleware;
use Zend\Diactoros\Response;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory;
class CreateShortCodeContentNegotiationMiddlewareTest extends TestCase
{
/**
* @var CreateShortCodeContentNegotiationMiddleware
*/
private $middleware;
/**
* @var RequestHandlerInterface
*/
private $requestHandler;
public function setUp()
{
$this->middleware = new CreateShortCodeContentNegotiationMiddleware();
$this->requestHandler = $this->prophesize(RequestHandlerInterface::class);
}
/**
* @test
*/
public function whenNoJsonResponseIsReturnedNoFurtherOperationsArePerformed()
{
$expectedResp = new Response();
$this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn($expectedResp);
$resp = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->requestHandler->reveal());
$this->assertSame($expectedResp, $resp);
}
/**
* @test
* @dataProvider provideData
* @param array $query
*/
public function properResponseIsReturned(?string $accept, array $query, string $expectedContentType)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams($query);
if ($accept !== null) {
$request = $request->withHeader('Accept', $accept);
}
$handle = $this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn(
new JsonResponse(['shortUrl' => 'http://doma.in/foo'])
);
$response = $this->middleware->process($request, $this->requestHandler->reveal());
$this->assertEquals($expectedContentType, $response->getHeaderLine('Content-type'));
$handle->shouldHaveBeenCalled();
}
public function provideData(): array
{
return [
[null, [], 'application/json'],
[null, ['format' => 'json'], 'application/json'],
[null, ['format' => 'invalid'], 'application/json'],
[null, ['format' => 'txt'], 'text/plain'],
['application/json', [], 'application/json'],
['application/xml', [], 'application/json'],
['text/plain', [], 'text/plain'],
['application/json', ['format' => 'txt'], 'text/plain'],
];
}
/**
* @test
* @dataProvider provideTextBodies
* @param array $json
*/
public function properBodyIsReturnedInPlainTextResponses(array $json, string $expectedBody)
{
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['format' => 'txt']);
$handle = $this->requestHandler->handle(Argument::type(ServerRequestInterface::class))->willReturn(
new JsonResponse($json)
);
$response = $this->middleware->process($request, $this->requestHandler->reveal());
$this->assertEquals($expectedBody, (string) $response->getBody());
$handle->shouldHaveBeenCalled();
}
public function provideTextBodies(): array
{
return [
[['shortUrl' => 'foobar'], 'foobar'],
[['error' => 'FOO_BAR'], 'FOO_BAR'],
[[], ''],
];
}
}