Merge branch 'feature/15' into develop

This commit is contained in:
Alejandro Celaya 2016-07-27 20:18:28 +02:00
commit a3554db1c3
20 changed files with 298 additions and 22 deletions

View File

@ -11,6 +11,10 @@ php:
- 7.1 - 7.1
- hhvm - hhvm
matrix:
allow_failures:
- hhvm
before_script: before_script:
- composer self-update - composer self-update
- composer install --no-interaction - composer install --no-interaction

View File

@ -5,7 +5,7 @@ return [
'driver' => 'pdo_mysql', 'driver' => 'pdo_mysql',
'user' => getenv('DB_USER'), 'user' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'), 'password' => getenv('DB_PASSWORD'),
'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener', 'dbname' => getenv('DB_NAME') ?: 'shlink',
'charset' => 'utf8', 'charset' => 'utf8',
'driverOptions' => [ 'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'

View File

@ -1,14 +1,14 @@
<?php <?php
use Shlinkio\Shlink\Common\Expressive\ContentBasedErrorHandler;
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
return [ return [
'services' => [ 'services' => [
'invokables' => [ 'invokables' => [
'Zend\Expressive\Whoops' => Whoops\Run::class, 'Zend\Expressive\Whoops' => Whoops\Run::class,
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class, 'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
], ],
'factories' => [
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
],
], ],
'whoops' => [ 'whoops' => [
@ -18,4 +18,12 @@ return [
'ajax_only' => true, 'ajax_only' => true,
], ],
], ],
'error_handler' => [
'plugins' => [
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
],
],
],
]; ];

View File

@ -26,11 +26,5 @@ return [
], ],
'priority' => 1, 'priority' => 1,
], ],
'error' => [
'middleware' => [],
'error' => true,
'priority' => -10000,
],
], ],
]; ];

View File

@ -1,4 +1,6 @@
<?php <?php
use Shlinkio\Shlink\Common\Expressive\ContentBasedErrorHandlerFactory;
use Shlinkio\Shlink\Rest\Expressive\JsonErrorHandler;
use Zend\Expressive; use Zend\Expressive;
use Zend\Expressive\Container; use Zend\Expressive\Container;
use Zend\Expressive\Helper; use Zend\Expressive\Helper;
@ -21,7 +23,7 @@ return [
Router\FastRouteRouter::class => InvokableFactory::class, Router\FastRouteRouter::class => InvokableFactory::class,
// View // View
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class, 'Zend\Expressive\FinalHandler' => ContentBasedErrorHandlerFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
], ],
'aliases' => [ 'aliases' => [

View File

@ -0,0 +1,22 @@
<?php
use Shlinkio\Shlink\Common\Expressive\ContentBasedErrorHandler;
use Zend\Expressive\Container\TemplatedErrorHandlerFactory;
use Zend\Stratigility\FinalHandler;
return [
'error_handler' => [
'plugins' => [
'invokables' => [
'text/plain' => FinalHandler::class,
],
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => TemplatedErrorHandlerFactory::class,
],
'aliases' => [
'application/xhtml+xml' => ContentBasedErrorHandler::DEFAULT_CONTENT,
],
],
],
];

View File

@ -0,0 +1,64 @@
<?php
namespace Shlinkio\Shlink\Common\Expressive;
use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Zend\ServiceManager\AbstractPluginManager;
use Zend\ServiceManager\Exception\InvalidServiceException;
class ContentBasedErrorHandler extends AbstractPluginManager implements ErrorHandlerInterface
{
const DEFAULT_CONTENT = 'text/html';
public function validate($instance)
{
if (is_callable($instance)) {
return;
}
throw new InvalidServiceException(sprintf(
'Only callables are valid plugins for "%s". "%s" provided',
__CLASS__,
is_object($instance) ? get_class($instance) : gettype($instance)
));
}
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null)
{
// Try to get an error handler for provided request accepted type
$errorHandler = $this->resolveErrorHandlerFromAcceptHeader($request);
return $errorHandler($request, $response, $err);
}
/**
* Tries to resolve
*
* @param Request $request
* @return callable
*/
protected function resolveErrorHandlerFromAcceptHeader(Request $request)
{
$accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT;
$accepts = explode(',', $accepts);
foreach ($accepts as $accept) {
if (! $this->has($accept)) {
continue;
}
return $this->get($accept);
}
throw new InvalidArgumentException(sprintf(
'It wasn\'t possible to find an error handler for '
));
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace Shlinkio\Shlink\Common\Expressive;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ContentBasedErrorHandlerFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['error_handler'];
$plugins = isset($config['plugins']) ? $config['plugins'] : [];
return new ContentBasedErrorHandler($container, $plugins);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Shlinkio\Shlink\Common\Expressive;
use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
interface ErrorHandlerInterface
{
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null);
}

View File

@ -67,8 +67,8 @@ class RedirectMiddleware implements MiddlewareInterface
try { try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode); $longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
// If provided shortCode does not belong to a valid long URL, dispatch next middleware, which is 404 // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger
// middleware // a not-found error
if (! isset($longUrl)) { if (! isset($longUrl)) {
return $out($request, $response); return $out($request, $response);
} }

View File

@ -0,0 +1,18 @@
<?php
use Shlinkio\Shlink\Rest\Expressive\JsonErrorHandler;
return [
'error_handler' => [
'plugins' => [
'invokables' => [
'application/json' => JsonErrorHandler::class,
],
'aliases' => [
'application/x-json' => 'application/json',
'text/json' => 'application/json',
],
],
],
];

View File

@ -12,5 +12,13 @@ return [
], ],
'priority' => 5, 'priority' => 5,
], ],
'rest-not-found' => [
'path' => '/rest',
'middleware' => [
Middleware\NotFoundMiddleware::class,
],
'priority' => -1,
],
], ],
]; ];

View File

@ -23,7 +23,7 @@ return [
'allowed_methods' => ['GET', 'OPTIONS'], 'allowed_methods' => ['GET', 'OPTIONS'],
], ],
[ [
'name' => 'rest-lActionist-shortened-url', 'name' => 'rest-list-shortened-url',
'path' => '/rest/short-codes', 'path' => '/rest/short-codes',
'middleware' => Action\ListShortcodesMiddleware::class, 'middleware' => Action\ListShortcodesMiddleware::class,
'allowed_methods' => ['GET'], 'allowed_methods' => ['GET'],

View File

@ -19,6 +19,7 @@ return [
Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
Middleware\NotFoundMiddleware::class => AnnotatedFactory::class,
], ],
], ],

Binary file not shown.

View File

@ -1,9 +1,9 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Shlink 1.0\n" "Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-07-21 16:39+0200\n" "POT-Creation-Date: 2016-07-27 08:53+0200\n"
"PO-Revision-Date: 2016-07-21 16:40+0200\n" "PO-Revision-Date: 2016-07-27 08:53+0200\n"
"Last-Translator: \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"
@ -52,9 +52,12 @@ msgid ""
"Missing or invalid auth token provided. Perform a new authentication request " "Missing or invalid auth token provided. Perform a new authentication request "
"and send provided token on every new request on the \"%s\" header" "and send provided token on every new request on the \"%s\" header"
msgstr "" msgstr ""
"No se ha proporcionado token de autenticación o este es inválido. Realia una " "No se ha proporcionado token de autenticación o este es inválido. Realiza "
"nueva petición de autenticación y envía el token proporcionado en cada nueva " "una nueva petición de autenticación y envía el token proporcionado en cada "
"petición en la cabecera \"%s\"" "nueva petición en la cabecera \"%s\""
msgid "Requested route does not exist."
msgstr "La ruta solicitada no existe."
#~ msgid "RestToken not found for token \"%s\"" #~ msgid "RestToken not found for token \"%s\""
#~ msgstr "No se ha encontrado un RestToken para el token \"%s\"" #~ msgstr "No se ha encontrado un RestToken para el token \"%s\""

View File

@ -0,0 +1,39 @@
<?php
namespace Shlinkio\Shlink\Rest\Expressive;
use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Shlinkio\Shlink\Common\Expressive\ErrorHandlerInterface;
use Zend\Diactoros\Response\JsonResponse;
class JsonErrorHandler implements ErrorHandlerInterface
{
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null)
{
$status = $response->getStatusCode();
$responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
$status = $status < 400 ? 500 : $status;
return new JsonResponse([
'error' => $this->responsePhraseToCode($responsePhrase),
'message' => $responsePhrase,
], $status);
}
/**
* @param string $responsePhrase
* @return string
*/
protected function responsePhraseToCode($responsePhrase)
{
return strtoupper(str_replace(' ', '_', $responsePhrase));
}
}

View File

@ -69,7 +69,9 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
// If current route is the authenticate route or an OPTIONS request, continue to the next middleware // If current route is the authenticate route or an OPTIONS request, continue to the next middleware
/** @var RouteResult $routeResult */ /** @var RouteResult $routeResult */
$routeResult = $request->getAttribute(RouteResult::class); $routeResult = $request->getAttribute(RouteResult::class);
if ((isset($routeResult) && $routeResult->getMatchedRouteName() === 'rest-authenticate') if (! isset($routeResult)
|| $routeResult->isFailure()
|| $routeResult->getMatchedRouteName() === 'rest-authenticate'
|| $request->getMethod() === 'OPTIONS' || $request->getMethod() === 'OPTIONS'
) { ) {
return $out($request, $response); return $out($request, $response);

View File

@ -0,0 +1,62 @@
<?php
namespace Shlinkio\Shlink\Rest\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface;
use Zend\Stratigility\MiddlewareInterface;
class NotFoundMiddleware implements MiddlewareInterface
{
/**
* @var TranslatorInterface
*/
private $translator;
/**
* NotFoundMiddleware constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* Process an incoming request and/or response.
*
* Accepts a server-side request and a response instance, and does
* something with them.
*
* If the response is not complete and/or further processing would not
* interfere with the work done in the middleware, or if the middleware
* wants to delegate to another process, it can use the `$out` callable
* if present.
*
* If the middleware does not return a value, execution of the current
* request is considered complete, and the response instance provided will
* be considered the response to return.
*
* Alternately, the middleware may return a response instance.
*
* Often, middleware will `return $out();`, with the assumption that a
* later middleware will return a response.
*
* @param Request $request
* @param Response $response
* @param null|callable $out
* @return null|Response
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
return new JsonResponse([
'error' => RestUtils::NOT_FOUND_ERROR,
'message' => $this->translator->translate('Requested route does not exist.'),
], 404);
}
}

View File

@ -11,7 +11,8 @@ class RestUtils
const INVALID_URL_ERROR = 'INVALID_URL'; const INVALID_URL_ERROR = 'INVALID_URL';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN_ERROR'; const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
const NOT_FOUND_ERROR = 'NOT_FOUND';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public static function getRestErrorCodeFromException(Common\ExceptionInterface $e) public static function getRestErrorCodeFromException(Common\ExceptionInterface $e)