Compare commits

...

33 Commits

Author SHA1 Message Date
Alejandro Celaya
b53e51de33 Merge branch 'develop' 2017-03-25 10:13:11 +01:00
Alejandro Celaya
9478c71af1 Merge pull request #90 from acelaya/feature/expressive-2
Feature/expressive 2
2017-03-25 10:11:46 +01:00
Alejandro Celaya
a2c4eebec8 Updated CHANGELOG 2017-03-25 10:09:30 +01:00
Alejandro Celaya
2e5a7d76df Migrated rest actions to psr-15 middleware 2017-03-25 10:04:48 +01:00
Alejandro Celaya
288249d0b8 Renamed JsonErrorHandler to JsonErrorResponseGenerator 2017-03-25 09:46:29 +01:00
Alejandro Celaya
cd47aae902 Migrated CrossDomainMiddleware to psr-15 middleware 2017-03-25 09:44:34 +01:00
Alejandro Celaya
9bd18ee041 Migrated CheckAuthenticationMiddleware to psr-15 middleware 2017-03-25 09:37:13 +01:00
Alejandro Celaya
22c76df8e6 Migrated BodyParserMiddleware to psr-15 middleware 2017-03-25 09:22:00 +01:00
Alejandro Celaya
6c87436a96 Migrated QrCodeCacheMiddleware to psr-15 middleware 2017-03-24 23:34:17 +01:00
Alejandro Celaya
734dac9456 Migrated RedirectAction to psr-15 middleware 2017-03-24 23:24:11 +01:00
Alejandro Celaya
85ca366893 Migrated QrCodeAction to psr-15 middleware 2017-03-24 23:19:42 +01:00
Alejandro Celaya
46db736af8 Migrated PreviewAction to psr-15 middleware 2017-03-24 22:07:28 +01:00
Alejandro Celaya
7530048fbd Removed exception catch that used to return a 500, and now returns a 404 due to a behavior change 2017-03-24 21:59:45 +01:00
Alejandro Celaya
c3c03a3a3b Migrated LocaleMiddleware to psr-15 middleware 2017-03-24 21:49:31 +01:00
Alejandro Celaya
d1018b6da7 Fixed tests 2017-03-24 21:38:43 +01:00
Alejandro Celaya
fe7928ae0e Fixed JsonErrorHandler and prevented AuthorizationMiddleware to eat exceptions 2017-03-24 21:31:55 +01:00
Alejandro Celaya
f6c39285c9 Updated to expressive 2 and used new error handling system 2017-03-24 21:10:25 +01:00
Alejandro Celaya
0e2a289f9f Updated to phpunit 6 2017-03-24 20:34:18 +01:00
Alejandro Celaya
f7424da16b Merge branch 'develop' 2017-01-22 11:37:35 +01:00
Alejandro Celaya
2e10ee66b7 Merge pull request #87 from acelaya/feature/1.3.1
Feature/1.3.1
2017-01-22 11:36:04 +01:00
Alejandro Celaya
7e442671c3 Updated build script 2017-01-22 11:27:56 +01:00
Alejandro Celaya
ca646ec2b7 Updated changelog including v1.3.1 2017-01-22 11:17:44 +01:00
Alejandro Celaya
4df1af5fd8 Fixed searching short URLs list not querying tag names 2017-01-22 11:14:25 +01:00
Alejandro Celaya
b4548f3401 Added configs to enable fastroute cache 2017-01-22 11:07:18 +01:00
Alejandro Celaya
1819481710 Updated license year 2017-01-22 10:59:35 +01:00
Alejandro Celaya
e59ae654c0 Increased number of followed redirects to 15 2017-01-22 10:53:41 +01:00
Alejandro Celaya
a8bf699f2d Fixed error with non-registered service on latest expressive-twig-renderer version 2017-01-21 20:19:30 +01:00
Alejandro Celaya
de9d9d8667 Updated PathVersionMiddleware so that it is only applied to rest routes 2017-01-21 20:12:12 +01:00
Alejandro Celaya
869865f22a Added option to customize database hostname and port 2017-01-21 13:45:28 +01:00
Alejandro Celaya
29fd313337 Added memcached to php docker image 2017-01-21 13:33:51 +01:00
Alejandro Celaya
7781f07352 Added local entity manager config that allows db host name to be set 2017-01-21 09:16:00 +01:00
Alejandro Celaya
072371d459 Created docker-related files 2017-01-19 22:56:45 +01:00
Alejandro Celaya
51bf948458 Fixed schema definition on order by argument 2016-10-29 12:42:36 +02:00
100 changed files with 893 additions and 850 deletions

View File

@@ -1,20 +1,41 @@
## CHANGELOG ## CHANGELOG
### 1.4.0
**Enhancements:**
* [89: Update to expressive 2](https://github.com/shlinkio/shlink/issues/89)
### 1.3.1
**Tasks**
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82)
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
**Bugs**
* [83: Short codes list: search in tags when filtering by query string](https://github.com/shlinkio/shlink/issues/83)
* [79: Increase the number of followed redirects](https://github.com/shlinkio/shlink/issues/79)
* [75: Apply PathVersionMiddleware only to rest routes defining it by configuration instead of code](https://github.com/shlinkio/shlink/issues/75)
* [77: Allow defining database server hostname and port](https://github.com/shlinkio/shlink/issues/77)
### 1.3.0 ### 1.3.0
**Enhancements:** **Enhancements:**
* [67: Allow to order the short codes list](https://github.com/acelaya/url-shortener/issues/67) * [67: Allow to order the short codes list](https://github.com/shlinkio/shlink/issues/67)
* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/acelaya/url-shortener/issues/60) * [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/shlinkio/shlink/issues/60)
* [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/acelaya/url-shortener/issues/72) * [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/shlinkio/shlink/issues/72)
* [58: Allow to filter short URLs by tag](https://github.com/acelaya/url-shortener/issues/58) * [58: Allow to filter short URLs by tag](https://github.com/shlinkio/shlink/issues/58)
* [69: Allow to filter short codes by text query](https://github.com/acelaya/url-shortener/issues/69) * [69: Allow to filter short codes by text query](https://github.com/shlinkio/shlink/issues/69)
**Tasks** **Tasks**
* [73: Tag endpoints in swagger file](https://github.com/acelaya/url-shortener/issues/73) * [73: Tag endpoints in swagger file](https://github.com/shlinkio/shlink/issues/73)
* [71: Separate swagger docs into multiple files](https://github.com/acelaya/url-shortener/issues/71) * [71: Separate swagger docs into multiple files](https://github.com/shlinkio/shlink/issues/71)
* [63: Add path versioning to REST API routes](https://github.com/acelaya/url-shortener/issues/63) * [63: Add path versioning to REST API routes](https://github.com/shlinkio/shlink/issues/63)
### 1.2.2 ### 1.2.2
@@ -26,91 +47,91 @@
**Bugs** **Bugs**
* [62: Fix cross-domain requests in REST API](https://github.com/acelaya/url-shortener/issues/62) * [62: Fix cross-domain requests in REST API](https://github.com/shlinkio/shlink/issues/62)
### 1.2.0 ### 1.2.0
**Features** **Features**
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/acelaya/url-shortener/issues/45) * [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/shlinkio/shlink/issues/45)
* [7: Add website previews while listing available URLs](https://github.com/acelaya/url-shortener/issues/7) * [7: Add website previews while listing available URLs](https://github.com/shlinkio/shlink/issues/7)
**Enhancements:** **Enhancements:**
* [57: Add database migrations system to improve updating between versions](https://github.com/acelaya/url-shortener/issues/57) * [57: Add database migrations system to improve updating between versions](https://github.com/shlinkio/shlink/issues/57)
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/acelaya/url-shortener/issues/31) * [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/shlinkio/shlink/issues/31)
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/acelaya/url-shortener/issues/51) * [51: Generate build process to paquetize the app and ease distribution](https://github.com/shlinkio/shlink/issues/51)
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/acelaya/url-shortener/issues/38) * [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/shlinkio/shlink/issues/38)
**Tasks** **Tasks**
* [55: Create update script which does not try to create a new database](https://github.com/acelaya/url-shortener/issues/55) * [55: Create update script which does not try to create a new database](https://github.com/shlinkio/shlink/issues/55)
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/acelaya/url-shortener/issues/54) * [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/shlinkio/shlink/issues/54)
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/acelaya/url-shortener/issues/29) * [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/shlinkio/shlink/issues/29)
**Bugs** **Bugs**
* [53: Fix entities database interoperability](https://github.com/acelaya/url-shortener/issues/53) * [53: Fix entities database interoperability](https://github.com/shlinkio/shlink/issues/53)
* [52: Add missing htaccess file for apache environments](https://github.com/acelaya/url-shortener/issues/52) * [52: Add missing htaccess file for apache environments](https://github.com/shlinkio/shlink/issues/52)
### 1.1.0 ### 1.1.0
**Features** **Features**
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/acelaya/url-shortener/issues/46) * [46: Define a route that returns a QR code representing the shortened URL](https://github.com/shlinkio/shlink/issues/46)
**Enhancements:** **Enhancements:**
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/acelaya/url-shortener/issues/32) * [32: Add support for other cache adapters by improving the Cache factory](https://github.com/shlinkio/shlink/issues/32)
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/acelaya/url-shortener/issues/14) * [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/shlinkio/shlink/issues/14)
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/acelaya/url-shortener/issues/41) * [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/shlinkio/shlink/issues/41)
* [13: Improve REST authentication](https://github.com/acelaya/url-shortener/issues/13) * [13: Improve REST authentication](https://github.com/shlinkio/shlink/issues/13)
**Tasks** **Tasks**
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/acelaya/url-shortener/issues/39) * [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/shlinkio/shlink/issues/39)
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/acelaya/url-shortener/issues/42) * [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/shlinkio/shlink/issues/42)
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/acelaya/url-shortener/issues/35) * [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/shlinkio/shlink/issues/35)
**Bugs** **Bugs**
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/acelaya/url-shortener/issues/40) * [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/shlinkio/shlink/issues/40)
### 1.0.0 ### 1.0.0
**Enhancements:** **Enhancements:**
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33) * [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/shlinkio/shlink/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15) * [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/shlinkio/shlink/issues/15)
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23) * [23: Translate application literals](https://github.com/shlinkio/shlink/issues/23)
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21) * [21: Allow to filter visits by date range](https://github.com/shlinkio/shlink/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22) * [22: Save visits locations data on a visit_locations table](https://github.com/shlinkio/shlink/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20) * [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/shlinkio/shlink/issues/20)
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11) * [11: Separate code into multiple modules](https://github.com/shlinkio/shlink/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18) * [18: Group routable middleware in an Action namespace](https://github.com/shlinkio/shlink/issues/18)
**Tasks** **Tasks**
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36) * [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/shlinkio/shlink/issues/36)
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4) * [4: Installation steps](https://github.com/shlinkio/shlink/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6) * [6: Remove dependency on expressive helpers package](https://github.com/shlinkio/shlink/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30) * [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/shlinkio/shlink/issues/30)
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12) * [12: Improve code coverage](https://github.com/shlinkio/shlink/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25) * [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/shlinkio/shlink/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19) * [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/shlinkio/shlink/issues/19)
**Bugs** **Bugs**
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24) * [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/shlinkio/shlink/issues/24)
### 0.2.0 ### 0.2.0
**Enhancements:** **Enhancements:**
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/acelaya/url-shortener/issues/9) * [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/shlinkio/shlink/issues/9)
* [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8) * [8: Create a REST API](https://github.com/shlinkio/shlink/issues/8)
* [10: Add more CLI functionality](https://github.com/acelaya/url-shortener/issues/10) * [10: Add more CLI functionality](https://github.com/shlinkio/shlink/issues/10)
**Tasks** **Tasks**
* [5: Create CHANGELOG file](https://github.com/acelaya/url-shortener/issues/5) * [5: Create CHANGELOG file](https://github.com/shlinkio/shlink/issues/5)

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Alejandro Celaya Copyright (c) 2017 Alejandro Celaya
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -15,6 +15,7 @@ projectdir=$(pwd)
echo 'Copying project files...' echo 'Copying project files...'
rm -rf "${builtcontent}" rm -rf "${builtcontent}"
mkdir "${builtcontent}" mkdir "${builtcontent}"
sudo chmod -R 777 "${projectdir}"/data/infra/{database,nginx}
cp -R "${projectdir}"/* "${builtcontent}" cp -R "${projectdir}"/* "${builtcontent}"
cd "${builtcontent}" cd "${builtcontent}"
@@ -22,7 +23,7 @@ cd "${builtcontent}"
rm -r vendor rm -r vendor
rm composer.lock rm composer.lock
composer self-update composer self-update
composer install --no-dev --optimize-autoloader composer install --no-dev --optimize-autoloader --no-progress --no-interaction
# Delete development files # Delete development files
echo 'Deleting dev files...' echo 'Deleting dev files...'
@@ -34,6 +35,7 @@ rm php*
rm README.md rm README.md
rm -r build rm -r build
rm -f data/database.sqlite rm -f data/database.sqlite
rm -rf data/infra
rm -rf data/{cache,log,proxies}/{*,.gitignore} rm -rf data/{cache,log,proxies}/{*,.gitignore}
rm -rf config/params/{*,.gitignore} rm -rf config/params/{*,.gitignore}
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore} rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}

View File

@@ -1,29 +1,29 @@
{ {
"name": "shlinkio/shlink", "name": "shlinkio/shlink",
"type": "project", "type": "project",
"homepage": "http://shlink.io", "homepage": "https://shlink.io",
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces", "description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Alejandro Celaya Alastrué", "name": "Alejandro Celaya Alastrué",
"homepage": "http://www.alejandrocelaya.com", "homepage": "https://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com" "email": "alejandro@alejandrocelaya.com"
} }
], ],
"require": { "require": {
"php": "^5.6 || ^7.0", "php": "^5.6 || ^7.0",
"zendframework/zend-expressive": "^1.0", "zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^1.1", "zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-twigrenderer": "^1.0", "zendframework/zend-expressive-twigrenderer": "^1.4",
"zendframework/zend-stdlib": "^2.7", "zendframework/zend-stdlib": "^3.0",
"zendframework/zend-servicemanager": "^3.0", "zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6", "zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6", "zendframework/zend-config": "^3.0",
"zendframework/zend-i18n": "^2.7", "zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4", "zendframework/zend-config-aggregator": "^0.1",
"acelaya/zsm-annotated-services": "^0.2.0", "acelaya/zsm-annotated-services": "^1.0",
"acelaya/ze-content-based-error-handler": "^1.0", "acelaya/ze-content-based-error-handler": "^2.0",
"doctrine/orm": "^2.5", "doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2", "guzzlehttp/guzzle": "^6.2",
"symfony/console": "^3.0", "symfony/console": "^3.0",
@@ -37,13 +37,12 @@
"doctrine/migrations": "^1.4" "doctrine/migrations": "^1.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^5.0", "phpunit/phpunit": "^5.7 || ^6.0",
"squizlabs/php_codesniffer": "^2.3", "squizlabs/php_codesniffer": "^2.3",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"filp/whoops": "^2.0", "filp/whoops": "^2.0",
"symfony/var-dumper": "^3.0", "symfony/var-dumper": "^3.0",
"vlucas/phpdotenv": "^2.2", "vlucas/phpdotenv": "^2.2"
"phly/changelog-generator": "^2.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@@ -4,18 +4,17 @@ use Zend\Expressive\Container;
use Zend\Expressive\Router; use Zend\Expressive\Router;
use Zend\Expressive\Template; use Zend\Expressive\Template;
use Zend\Expressive\Twig; use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory; use Zend\Stratigility\Middleware\ErrorHandler;
return [ return [
'dependencies' => [ 'dependencies' => [
'factories' => [ 'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class, Expressive\Application::class => Container\ApplicationFactory::class,
Router\FastRouteRouter::class => InvokableFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class, Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
], \Twig_Environment::class => Twig\TwigEnvironmentFactory::class,
'aliases' => [ Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
Router\RouterInterface::class => Router\FastRouteRouter::class, ErrorHandler::class => Container\ErrorHandlerFactory::class,
], ],
], ],

View File

@@ -6,14 +6,10 @@ return [
'proxies_dir' => 'data/proxies', 'proxies_dir' => 'data/proxies',
], ],
'connection' => [ 'connection' => [
'driver' => 'pdo_mysql',
'user' => env('DB_USER'), 'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'), 'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'), 'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8', 'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
], ],
], ],

View File

@@ -0,0 +1,14 @@
<?php
return [
'entity_manager' => [
'connection' => [
'driver' => 'pdo_mysql',
'host' => 'shlink_db',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
],
],
];

View File

@@ -1,6 +1,5 @@
<?php <?php
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ContentBasedErrorHandler; use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
return [ return [
'dependencies' => [ 'dependencies' => [
@@ -21,7 +20,7 @@ return [
'error_handler' => [ 'error_handler' => [
'plugins' => [ 'plugins' => [
'factories' => [ 'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class, 'text/html' => WhoopsErrorResponseGeneratorFactory::class,
], ],
], ],
], ],

View File

@@ -1,9 +1,30 @@
<?php <?php
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Expressive\Container\ApplicationFactory; use Zend\Expressive\Container\ApplicationFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [ return [
'middleware_pipeline' => [ 'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
LocaleMiddleware::class,
],
'priority' => 11,
],
'pre-routing-rest' => [
'path' => '/rest',
'middleware' => [
PathVersionMiddleware::class,
],
'priority' => 11,
],
'routing' => [ 'routing' => [
'middleware' => [ 'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE, ApplicationFactory::ROUTING_MIDDLEWARE,
@@ -11,6 +32,16 @@ return [
'priority' => 10, 'priority' => 10,
], ],
'rest' => [
'path' => '/rest',
'middleware' => [
CrossDomainMiddleware::class,
BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [ 'post-routing' => [
'middleware' => [ 'middleware' => [
ApplicationFactory::DISPATCH_MIDDLEWARE, ApplicationFactory::DISPATCH_MIDDLEWARE,

View File

@@ -0,0 +1,13 @@
<?php
use Zend\Expressive\Router\FastRouteRouter;
return [
'router' => [
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
use Zend\Expressive\Router\FastRouteRouter;
return [
'router' => [
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
],
],
];

View File

@@ -4,7 +4,7 @@ use Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common; use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core; use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest; use Shlinkio\Shlink\Rest;
use Zend\Expressive\ConfigManager; use Zend\ConfigAggregator;
/** /**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``. * Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
@@ -15,11 +15,11 @@ use Zend\Expressive\ConfigManager;
* Obviously, if you use closures in your config you can't cache it. * Obviously, if you use closures in your config you can't cache it.
*/ */
return (new ConfigManager\ConfigManager([ return (new ConfigAggregator\ConfigAggregator([
ExpressiveErrorHandler\ConfigProvider::class, ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class, Common\ConfigProvider::class,
Core\ConfigProvider::class, Core\ConfigProvider::class,
CLI\ConfigProvider::class, CLI\ConfigProvider::class,
Rest\ConfigProvider::class, Rest\ConfigProvider::class,
new ConfigManager\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'), new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
], 'data/cache/app_config.php'))->getMergedConfig(); ], 'data/cache/app_config.php'))->getMergedConfig();

2
data/infra/database/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

6
data/infra/db.Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM mysql:5.7
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Enable remote access (default is localhost only, we change this
# otherwise our database would not be reachable from outside the container)
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf

View File

@@ -0,0 +1,5 @@
FROM nginx:1.11.6-alpine
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Delete default nginx vhost
RUN rm /etc/nginx/conf.d/default.conf

2
data/infra/nginx/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

87
data/infra/php.Dockerfile Normal file
View File

@@ -0,0 +1,87 @@
FROM php:7.1-fpm-alpine
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
RUN apk update
# Install common php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar
RUN apk add --no-cache --virtual sqlite-libs
RUN apk add --no-cache --virtual sqlite-dev
RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/php7.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
RUN docker-php-ext-configure redis\
&& docker-php-ext-install redis
# cleanup
RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
RUN docker-php-ext-configure memcached\
&& docker-php-ext-install memcached
# cleanup
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu\
&& docker-php-ext-install apcu
# cleanup
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu-bc\
&& docker-php-ext-install apcu-bc
# cleanup
RUN rm /tmp/apcu_bc.tar.gz
# Load APCU.ini before APC.ini
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install xdebug
ADD https://pecl.php.net/get/xdebug-2.5.0 /tmp/xdebug.tar.gz
RUN mkdir -p /usr/src/php/ext/xdebug\
&& tar xf /tmp/xdebug.tar.gz -C /usr/src/php/ext/xdebug --strip-components=1
# configure and install
RUN docker-php-ext-configure xdebug\
&& docker-php-ext-install xdebug
# cleanup
RUN rm /tmp/xdebug.tar.gz
# Install composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer

1
data/infra/php.ini Normal file
View File

@@ -0,0 +1 @@
date.timezone = Europe/Madrid

21
data/infra/vhost.conf Normal file
View File

@@ -0,0 +1,21 @@
server {
listen 80 default_server;
server_name shlink.local;
root /home/shlink/www/public;
index index.php;
charset utf-8;
error_log /home/shlink/www/data/infra/nginx/shlink.error.log;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
root /home/shlink/www/public;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass shlink_php:9000;
fastcgi_index index.php;
include fastcgi.conf;
}
}

0
data/proxies/.gitignore vendored Normal file → Executable file
View File

41
docker-compose.yml Normal file
View File

@@ -0,0 +1,41 @@
version: '2'
services:
shlink_nginx:
container_name: shlink_nginx
build:
context: .
dockerfile: ./data/infra/nginx.Dockerfile
ports:
- "8000:80"
volumes:
- ./:/home/shlink/www
- ./docs:/home/shlink/www/public/docs
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
links:
- shlink_php
shlink_php:
container_name: shlink_php
build:
context: .
dockerfile: ./data/infra/php.Dockerfile
volumes:
- ./:/home/shlink/www
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
links:
- shlink_db
shlink_db:
container_name: shlink_db
build:
context: .
dockerfile: ./data/infra/db.Dockerfile
ports:
- "3307:3306"
volumes:
- ./:/home/shlink/www
- ./data/infra/database:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: shlink

View File

@@ -26,10 +26,8 @@
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)", "description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
"required": false, "required": false,
"type": "array", "type": "array",
"schema": { "items": {
"items": { "type": "string"
"type": "string"
}
} }
}, },
{ {

2
indocker Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"

View File

@@ -135,11 +135,18 @@ class InstallCommand extends Command
$params['NAME'] = $this->ask('Database name', 'shlink'); $params['NAME'] = $this->ask('Database name', 'shlink');
$params['USER'] = $this->ask('Database username'); $params['USER'] = $this->ask('Database username');
$params['PASSWORD'] = $this->ask('Database password'); $params['PASSWORD'] = $this->ask('Database password');
$params['HOST'] = $this->ask('Database host', 'localhost');
$params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
} }
return $params; return $params;
} }
protected function getDefaultDbPort($driver)
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
protected function askUrlShortener() protected function askUrlShortener()
{ {
$this->printTitle('URL SHORTENER'); $this->printTitle('URL SHORTENER');
@@ -272,6 +279,14 @@ class InstallCommand extends Command
$config['entity_manager']['connection']['user'] = $params['DATABASE']['USER']; $config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
$config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD']; $config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
$config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME']; $config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
$config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST'];
$config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT'];
if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') {
$config['entity_manager']['connection']['driverOptions'] = [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
];
}
} }
return $config; return $config;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Api; namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand; use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Api; namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand; use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Api; namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand; use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Config; namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand; use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Install; namespace ShlinkioTest\Shlink\CLI\Command\Install;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand; use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
@@ -47,12 +47,14 @@ class InstallCommandTest extends TestCase
protected function createInputStream() protected function createInputStream()
{ {
$stream = fopen('php://memory', 'r+', false); $stream = fopen('php://memory', 'rb+', false);
fputs($stream, <<<CLI_INPUT fwrite($stream, <<<CLI_INPUT
shlink_db shlink_db
alejandro alejandro
1234 1234
0 0
doma.in doma.in
abc123BCA abc123BCA
@@ -69,7 +71,7 @@ CLI_INPUT
/** /**
* @test * @test
*/ */
public function testInputIsProperlyParsed() public function inputIsProperlyParsed()
{ {
$this->configWriter->toFile(Argument::any(), [ $this->configWriter->toFile(Argument::any(), [
'app_options' => [ 'app_options' => [
@@ -81,6 +83,11 @@ CLI_INPUT
'dbname' => 'shlink_db', 'dbname' => 'shlink_db',
'user' => 'alejandro', 'user' => 'alejandro',
'password' => '1234', 'password' => '1234',
'host' => 'localhost',
'port' => '3306',
'driverOptions' => [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
]
], ],
], ],
'translator' => [ 'translator' => [

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand; use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand; use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand; use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand; use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode; namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand; use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Command\Visit; namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand; use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI; namespace ShlinkioTest\Shlink\CLI;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider; use Shlinkio\Shlink\CLI\ConfigProvider;
class ConfigProviderTest extends TestCase class ConfigProviderTest extends TestCase

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\CLI\Factory; namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory; use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;

View File

@@ -1,15 +0,0 @@
<?php
use Shlinkio\Shlink\Common\Middleware;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
Middleware\LocaleMiddleware::class,
],
'priority' => 5,
],
],
];

View File

@@ -2,10 +2,11 @@
namespace Shlinkio\Shlink\Common\Middleware; namespace Shlinkio\Shlink\Common\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
use Zend\Stratigility\MiddlewareInterface;
class LocaleMiddleware implements MiddlewareInterface class LocaleMiddleware implements MiddlewareInterface
{ {
@@ -25,40 +26,26 @@ class LocaleMiddleware implements MiddlewareInterface
$this->translator = $translator; $this->translator = $translator;
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
if (! $request->hasHeader('Accept-Language')) { if (! $request->hasHeader('Accept-Language')) {
return $out($request, $response); return $delegate->process($request);
} }
$locale = $request->getHeaderLine('Accept-Language'); $locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale)); $this->translator->setLocale($this->normalizeLocale($locale));
return $out($request, $response); return $delegate->process($request);
} }
/** /**

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common; namespace ShlinkioTest\Shlink\Common;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\ConfigProvider; use Shlinkio\Shlink\Common\ConfigProvider;
class ConfigProviderTest extends TestCase class ConfigProviderTest extends TestCase
@@ -23,7 +23,6 @@ class ConfigProviderTest extends TestCase
{ {
$config = $this->configProvider->__invoke(); $config = $this->configProvider->__invoke();
$this->assertArrayHasKey('middleware_pipeline', $config);
$this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('twig', $config); $this->assertArrayHasKey('twig', $config);
} }

View File

@@ -6,7 +6,7 @@ use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\FilesystemCache; use Doctrine\Common\Cache\FilesystemCache;
use Doctrine\Common\Cache\MemcachedCache; use Doctrine\Common\Cache\MemcachedCache;
use Doctrine\Common\Cache\RedisCache; use Doctrine\Common\Cache\RedisCache;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Factory\CacheFactory; use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Common\Factory; namespace ShlinkioTest\Shlink\Common\Factory;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory; use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Common\Factory; namespace ShlinkioTest\Shlink\Common\Factory;
use Monolog\Logger; use Monolog\Logger;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Factory\LoggerFactory; use Shlinkio\Shlink\Common\Factory\LoggerFactory;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Factory; namespace ShlinkioTest\Shlink\Common\Factory;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory; use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Image; namespace ShlinkioTest\Shlink\Common\Image;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Image\ImageBuilder; use Shlinkio\Shlink\Common\Image\ImageBuilder;
use Shlinkio\Shlink\Common\Image\ImageBuilderFactory; use Shlinkio\Shlink\Common\Image\ImageBuilderFactory;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Common\Image; namespace ShlinkioTest\Shlink\Common\Image;
use mikehaertl\wkhtmlto\Image; use mikehaertl\wkhtmlto\Image;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Image\ImageFactory; use Shlinkio\Shlink\Common\Image\ImageFactory;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -1,9 +1,9 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Middleware; namespace ShlinkioTest\Shlink\Common\Middleware;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -30,9 +30,7 @@ class LocaleMiddlewareTest extends TestCase
public function whenNoHeaderIsPresentLocaleIsNotChanged() public function whenNoHeaderIsPresentLocaleIsNotChanged()
{ {
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());
$this->middleware->__invoke(ServerRequestFactory::fromGlobals(), new Response(), function ($req, $resp) { $this->middleware->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal());
return $resp;
});
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());
} }
@@ -43,9 +41,7 @@ class LocaleMiddlewareTest extends TestCase
{ {
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es'); $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) { $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
return $resp;
});
$this->assertEquals('es', $this->translator->getLocale()); $this->assertEquals('es', $this->translator->getLocale());
} }
@@ -54,18 +50,16 @@ class LocaleMiddlewareTest extends TestCase
*/ */
public function localeGetsNormalized() public function localeGetsNormalized()
{ {
$delegate = TestUtils::createDelegateMock();
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es_ES'); $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'es_ES');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) { $this->middleware->process($request, $delegate->reveal());
return $resp;
});
$this->assertEquals('es', $this->translator->getLocale()); $this->assertEquals('es', $this->translator->getLocale());
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'en-US'); $request = ServerRequestFactory::fromGlobals()->withHeader('Accept-Language', 'en-US');
$this->middleware->__invoke($request, new Response(), function ($req, $resp) { $this->middleware->process($request, $delegate->reveal());
return $resp;
});
$this->assertEquals('en', $this->translator->getLocale()); $this->assertEquals('en', $this->translator->getLocale());
} }
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Paginator; namespace ShlinkioTest\Shlink\Common\Paginator;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter; use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface; use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;

View File

@@ -4,7 +4,7 @@ namespace ShlinkioTest\Shlink\Common\Service;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException; use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Service\IpLocationResolver; use Shlinkio\Shlink\Common\Service\IpLocationResolver;

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Common\Service; namespace ShlinkioTest\Shlink\Common\Service;
use mikehaertl\wkhtmlto\Image; use mikehaertl\wkhtmlto\Image;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Image\ImageBuilder; use Shlinkio\Shlink\Common\Image\ImageBuilder;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Twig\Extension; namespace ShlinkioTest\Shlink\Common\Twig\Extension;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension; use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Common\Util; namespace ShlinkioTest\Shlink\Common\Util;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
class DateRangeTest extends TestCase class DateRangeTest extends TestCase

View File

@@ -0,0 +1,35 @@
<?php
namespace ShlinkioTest\Shlink\Common\Util;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Prophecy\Argument;
use Prophecy\Prophet;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response;
class TestUtils
{
private static $prophet;
public static function createDelegateMock(ResponseInterface $response = null, RequestInterface $request = null)
{
$argument = $request ?: Argument::any();
$delegate = static::getProphet()->prophesize(DelegateInterface::class);
$delegate->process($argument)->willReturn($response ?: new Response());
return $delegate;
}
/**
* @return Prophet
*/
private static function getProphet()
{
if (static::$prophet === null) {
static::$prophet = new Prophet();
}
return static::$prophet;
}
}

View File

@@ -2,16 +2,16 @@
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator; use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface; use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait; use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
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\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Zend\Stratigility\MiddlewareInterface;
class PreviewAction implements MiddlewareInterface class PreviewAction implements MiddlewareInterface
{ {
@@ -40,46 +40,28 @@ class PreviewAction implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
try { try {
$url = $this->urlShortener->shortCodeToUrl($shortCode); $url = $this->urlShortener->shortCodeToUrl($shortCode);
if (! isset($url)) { if (! isset($url)) {
return $out($request, $response->withStatus(404), 'Not found'); return $delegate->process($request);
} }
$imagePath = $this->previewGenerator->generatePreview($url); $imagePath = $this->previewGenerator->generatePreview($url);
return $this->generateImageResponse($imagePath); return $this->generateImageResponse($imagePath);
} catch (InvalidShortCodeException $e) { } catch (InvalidShortCodeException $e) {
return $out($request, $response->withStatus(404), 'Not found'); return $delegate->process($request);
} catch (PreviewGenerationException $e) {
return $out($request, $response->withStatus(500), 'Preview generation error');
} }
} }
} }

View File

@@ -3,6 +3,8 @@ namespace Shlinkio\Shlink\Core\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Endroid\QrCode\QrCode; use Endroid\QrCode\QrCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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;
@@ -12,7 +14,6 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Router\RouterInterface;
use Zend\Stratigility\MiddlewareInterface;
class QrCodeAction implements MiddlewareInterface class QrCodeAction implements MiddlewareInterface
{ {
@@ -48,42 +49,26 @@ class QrCodeAction implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
// Make sure the short URL exists for this short code // Make sure the short URL exists for this short code
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
try { try {
$shortUrl = $this->urlShortener->shortCodeToUrl($shortCode); $shortUrl = $this->urlShortener->shortCodeToUrl($shortCode);
if (! isset($shortUrl)) { if ($shortUrl === null) {
return $out($request, $response->withStatus(404), 'Not Found'); return $delegate->process($request);
} }
} catch (InvalidShortCodeException $e) { } catch (InvalidShortCodeException $e) {
$this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e); $this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e);
return $out($request, $response->withStatus(404), 'Not Found'); return $delegate->process($request);
} }
$path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]); $path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]);
@@ -101,13 +86,11 @@ class QrCodeAction implements MiddlewareInterface
*/ */
protected function getSizeParam(Request $request) protected function getSizeParam(Request $request)
{ {
$size = intval($request->getAttribute('size', 300)); $size = (int) $request->getAttribute('size', 300);
if ($size < 50) { if ($size < 50) {
return 50; return 50;
} elseif ($size > 1000) {
return 1000;
} }
return $size; return $size > 1000 ? 1000 : $size;
} }
} }

View File

@@ -2,6 +2,8 @@
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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;
@@ -11,7 +13,6 @@ use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Zend\Diactoros\Response\RedirectResponse; use Zend\Diactoros\Response\RedirectResponse;
use Zend\Stratigility\MiddlewareInterface;
class RedirectAction implements MiddlewareInterface class RedirectAction implements MiddlewareInterface
{ {
@@ -47,31 +48,15 @@ class RedirectAction implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode', ''); $shortCode = $request->getAttribute('shortCode', '');
@@ -80,8 +65,8 @@ class RedirectAction implements MiddlewareInterface
// If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger // If provided shortCode does not belong to a valid long URL, dispatch next middleware, which will trigger
// a not-found error // a not-found error
if (! isset($longUrl)) { if ($longUrl === null) {
return $this->notFoundResponse($request, $response, $out); return $delegate->process($request);
} }
// Track visit to this short code // Track visit to this short code
@@ -93,18 +78,7 @@ class RedirectAction implements MiddlewareInterface
} catch (\Exception $e) { } catch (\Exception $e) {
// In case of error, dispatch 404 error // In case of error, dispatch 404 error
$this->logger->error('Error redirecting to long URL.' . PHP_EOL . $e); $this->logger->error('Error redirecting to long URL.' . PHP_EOL . $e);
return $this->notFoundResponse($request, $response, $out); return $delegate->process($request);
} }
} }
/**
* @param Request $request
* @param Response $response
* @param callable $out
* @return Response
*/
protected function notFoundResponse(Request $request, Response $response, callable $out)
{
return $out($request, $response->withStatus(404), 'Not Found');
}
} }

View File

@@ -3,9 +3,11 @@ namespace Shlinkio\Shlink\Core\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Doctrine\Common\Cache\Cache; use Doctrine\Common\Cache\Cache;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Zend\Stratigility\MiddlewareInterface; use Zend\Diactoros\Response as DiactResp;
class QrCodeCacheMiddleware implements MiddlewareInterface class QrCodeCacheMiddleware implements MiddlewareInterface
{ {
@@ -26,44 +28,29 @@ class QrCodeCacheMiddleware implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
$cacheKey = $request->getUri()->getPath(); $cacheKey = $request->getUri()->getPath();
// If this QR code is already cached, just return it // If this QR code is already cached, just return it
if ($this->cache->contains($cacheKey)) { if ($this->cache->contains($cacheKey)) {
$qrData = $this->cache->fetch($cacheKey); $qrData = $this->cache->fetch($cacheKey);
$response = new DiactResp();
$response->getBody()->write($qrData['body']); $response->getBody()->write($qrData['body']);
return $response->withHeader('Content-Type', $qrData['content-type']); return $response->withHeader('Content-Type', $qrData['content-type']);
} }
// If not, call the next middleware and cache it // If not, call the next middleware and cache it
/** @var Response $resp */ /** @var Response $resp */
$resp = $out($request, $response); $resp = $delegate->process($request);
$this->cache->save($cacheKey, [ $this->cache->save($cacheKey, [
'body' => $resp->getBody()->__toString(), 'body' => $resp->getBody()->__toString(),
'content-type' => $resp->getHeaderLine('Content-Type'), 'content-type' => $resp->getHeaderLine('Content-Type'),

View File

@@ -21,15 +21,15 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
$qb->select('s'); $qb->select('s');
// Set limit and offset // Set limit and offset
if (isset($limit)) { if ($limit !== null) {
$qb->setMaxResults($limit); $qb->setMaxResults($limit);
} }
if (isset($offset)) { if ($offset !== null) {
$qb->setFirstResult($offset); $qb->setFirstResult($offset);
} }
// In case the ordering has been specified, the query could be more complex. Process it // In case the ordering has been specified, the query could be more complex. Process it
if (isset($orderBy)) { if ($orderBy !== null) {
return $this->processOrderByForList($qb, $orderBy); return $this->processOrderByForList($qb, $orderBy);
} }
@@ -47,7 +47,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
'visits', 'visits',
'visitsCount', 'visitsCount',
'visitCount', 'visitCount',
])) { ], true)) {
$qb->addSelect('COUNT(v) AS totalVisits') $qb->addSelect('COUNT(v) AS totalVisits')
->leftJoin('s.visits', 'v') ->leftJoin('s.visits', 'v')
->groupBy('s') ->groupBy('s')
@@ -58,7 +58,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
'originalUrl', 'originalUrl',
'shortCode', 'shortCode',
'dateCreated', 'dateCreated',
])) { ], true)) {
$qb->orderBy('s.' . $fieldName, $order); $qb->orderBy('s.' . $fieldName, $order);
} }
@@ -93,9 +93,12 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
// Apply search term to every searchable field if not empty // Apply search term to every searchable field if not empty
if (! empty($searchTerm)) { if (! empty($searchTerm)) {
$qb->join('s.tags', 't');
$conditions = [ $conditions = [
$qb->expr()->like('s.originalUrl', ':searchPattern'), $qb->expr()->like('s.originalUrl', ':searchPattern'),
$qb->expr()->like('s.shortCode', ':searchPattern'), $qb->expr()->like('s.shortCode', ':searchPattern'),
$qb->expr()->like('t.name', ':searchPattern'),
]; ];
// Unpack and apply search conditions // Unpack and apply search conditions

View File

@@ -117,7 +117,9 @@ class UrlShortener implements UrlShortenerInterface
protected function checkUrlExists(UriInterface $url) protected function checkUrlExists(UriInterface $url)
{ {
try { try {
$this->httpClient->request('GET', $url); $this->httpClient->request('GET', $url, ['allow_redirects' => [
'max' => 15,
]]);
} catch (GuzzleException $e) { } catch (GuzzleException $e) {
throw InvalidUrlException::fromUrl($url, $e); throw InvalidUrlException::fromUrl($url, $e);
} }

View File

@@ -1,14 +1,15 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator; use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Action\PreviewAction; use Shlinkio\Shlink\Core\Action\PreviewAction;
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 Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
class PreviewActionTest extends TestCase class PreviewActionTest extends TestCase
@@ -36,20 +37,17 @@ class PreviewActionTest extends TestCase
/** /**
* @test * @test
*/ */
public function invalidShortCodeFallbacksToNextMiddlewareWithStatusNotFound() public function invalidShortCodeFallsBackToNextMiddleware()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1); $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$delegate->process(Argument::cetera())->shouldBeCalledTimes(1);
$resp = $this->action->__invoke( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(), $delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertEquals(404, $resp->getStatusCode());
} }
/** /**
@@ -63,9 +61,9 @@ class PreviewActionTest extends TestCase
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($url)->shouldBeCalledTimes(1); $this->urlShortener->shortCodeToUrl($shortCode)->willReturn($url)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1); $this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1);
$resp = $this->action->__invoke( $resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length')); $this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
@@ -75,40 +73,18 @@ class PreviewActionTest extends TestCase
/** /**
* @test * @test
*/ */
public function invalidShortcodeExceptionReturnsNotFound() public function invalidShortCodeExceptionFallsBackToNextMiddleware()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$resp = $this->action->__invoke( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(), $delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertEquals(404, $resp->getStatusCode()); $delegate->process(Argument::any())->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function previewExceptionReturnsNotFound()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(PreviewGenerationException::class)
->shouldBeCalledTimes(1);
$resp = $this->action->__invoke(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(),
function ($req, $resp) {
return $resp;
}
);
$this->assertEquals(500, $resp->getStatusCode());
} }
} }

View File

@@ -1,7 +1,8 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Common\Response\QrCodeResponse;
@@ -9,7 +10,6 @@ use Shlinkio\Shlink\Core\Action\QrCodeAction;
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\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Router\RouterInterface;
@@ -37,19 +37,18 @@ class QrCodeActionTest extends TestCase
/** /**
* @test * @test
*/ */
public function aNonexistentShortCodeWillReturnNotFoundResponse() public function aNotFoundShortCodeWillDelegateIntoNextMiddleware()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1); $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$resp = $this->action->__invoke( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(), $delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertEquals(404, $resp->getStatusCode());
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(1);
} }
/** /**
@@ -60,15 +59,14 @@ class QrCodeActionTest extends TestCase
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$resp = $this->action->__invoke( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(), $delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertEquals(404, $resp->getStatusCode());
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(1);
} }
/** /**
@@ -78,16 +76,15 @@ class QrCodeActionTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(new ShortUrl())->shouldBeCalledTimes(1); $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(new ShortUrl())->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$resp = $this->action->__invoke( $resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response(), $delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertInstanceOf(QrCodeResponse::class, $resp); $this->assertInstanceOf(QrCodeResponse::class, $resp);
$this->assertEquals(200, $resp->getStatusCode()); $this->assertEquals(200, $resp->getStatusCode());
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(0);
} }
} }

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -42,7 +42,7 @@ class RedirectActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response); $this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
@@ -53,52 +53,32 @@ class RedirectActionTest extends TestCase
/** /**
* @test * @test
*/ */
public function nextErrorMiddlewareIsInvokedIfLongUrlIsNotFound() public function nextMiddlewareIsInvokedIfLongUrlIsNotFound()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null) $this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$originalResponse = new Response(); $this->action->process($request, $delegate->reveal());
$test = $this;
$this->action->__invoke($request, $originalResponse, function ( $delegate->process($request)->shouldHaveBeenCalledTimes(1);
ServerRequestInterface $req,
ResponseInterface $resp,
$error
) use (
$test,
$request
) {
$test->assertSame($request, $req);
$test->assertEquals(404, $resp->getStatusCode());
$test->assertEquals('Not Found', $error);
});
} }
/** /**
* @test * @test
*/ */
public function nextErrorMiddlewareIsInvokedIfAnExceptionIsThrown() public function nextMiddlewareIsInvokedIfAnExceptionIsThrown()
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(\Exception::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$originalResponse = new Response(); $this->action->process($request, $delegate->reveal());
$test = $this;
$this->action->__invoke($request, $originalResponse, function ( $delegate->process($request)->shouldHaveBeenCalledTimes(1);
ServerRequestInterface $req,
ResponseInterface $resp,
$error
) use (
$test,
$request
) {
$test->assertSame($request, $req);
$test->assertEquals(404, $resp->getStatusCode());
$test->assertEquals('Not Found', $error);
});
} }
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core; namespace ShlinkioTest\Shlink\Core;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\ConfigProvider; use Shlinkio\Shlink\Core\ConfigProvider;
class ConfigProviderTest extends TestCase class ConfigProviderTest extends TestCase

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core\Entity; namespace ShlinkioTest\Shlink\Core\Entity;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Entity\Tag;
class TagTest extends TestCase class TagTest extends TestCase

View File

@@ -3,7 +3,9 @@ namespace ShlinkioTest\Shlink\Core\Middleware;
use Doctrine\Common\Cache\ArrayCache; use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache; use Doctrine\Common\Cache\Cache;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware; use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -29,18 +31,16 @@ class QrCodeCacheMiddlewareTest extends TestCase
/** /**
* @test * @test
*/ */
public function noCachedPathFallbacksToNextMiddleware() public function noCachedPathFallsBackToNextMiddleware()
{ {
$isCalled = false; $delegate = $this->prophesize(DelegateInterface::class);
$this->middleware->__invoke( $delegate->process(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1);
ServerRequestFactory::fromGlobals(),
new Response(), $this->middleware->process(ServerRequestFactory::fromGlobals()->withUri(
function ($req, $resp) use (&$isCalled) { new Uri('/foo/bar')
$isCalled = true; ), $delegate->reveal());
return $resp;
} $this->assertTrue($this->cache->contains('/foo/bar'));
);
$this->assertTrue($isCalled);
} }
/** /**
@@ -51,19 +51,17 @@ class QrCodeCacheMiddlewareTest extends TestCase
$isCalled = false; $isCalled = false;
$uri = (new Uri())->withPath('/foo'); $uri = (new Uri())->withPath('/foo');
$this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']); $this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']);
$delegate = $this->prophesize(DelegateInterface::class);
$resp = $this->middleware->__invoke( $resp = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withUri($uri), ServerRequestFactory::fromGlobals()->withUri($uri),
new Response(), $delegate->reveal()
function ($req, $resp) use (&$isCalled) {
$isCalled = true;
return $resp;
}
); );
$this->assertFalse($isCalled); $this->assertFalse($isCalled);
$resp->getBody()->rewind(); $resp->getBody()->rewind();
$this->assertEquals('the body', $resp->getBody()->getContents()); $this->assertEquals('the body', $resp->getBody()->getContents());
$this->assertEquals('image/png', $resp->getHeaderLine('Content-Type')); $this->assertEquals('image/png', $resp->getHeaderLine('Content-Type'));
$delegate->process(Argument::any())->shouldHaveBeenCalledTimes(0);
} }
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Core\Options; namespace ShlinkioTest\Shlink\Core\Options;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Options\AppOptionsFactory; use Shlinkio\Shlink\Core\Options\AppOptionsFactory;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;

View File

@@ -3,7 +3,7 @@ namespace ShlinkioTest\Shlink\Core\Service;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;

View File

@@ -10,7 +10,7 @@ use Doctrine\ORM\ORMException;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Request;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Core\Service; namespace ShlinkioTest\Shlink\Core\Service;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\Visit; use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Repository\VisitRepository; use Shlinkio\Shlink\Core\Repository\VisitRepository;

View File

@@ -3,7 +3,7 @@ namespace ShlinkioTest\Shlink\Core\Service;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;

View File

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

View File

@@ -1,24 +0,0 @@
<?php
use Shlinkio\Shlink\Rest\Middleware;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
Middleware\PathVersionMiddleware::class,
],
'priority' => 11,
],
'rest' => [
'path' => '/rest',
'middleware' => [
Middleware\CrossDomainMiddleware::class,
Middleware\BodyParserMiddleware::class,
Middleware\CheckAuthenticationMiddleware::class,
],
'priority' => 5,
],
],
];

View File

@@ -1,13 +1,17 @@
<?php <?php
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Fig\Http\Message\RequestMethodInterface;
use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Zend\Stratigility\MiddlewareInterface; use Zend\Diactoros\Response\EmptyResponse;
abstract class AbstractRestAction implements MiddlewareInterface abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface
{ {
/** /**
* @var LoggerInterface * @var LoggerInterface
@@ -20,44 +24,27 @@ abstract class AbstractRestAction implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
if ($request->getMethod() === 'OPTIONS') { if ($request->getMethod() === self::METHOD_OPTIONS) {
return $response; return new EmptyResponse();
} }
return $this->dispatch($request, $response, $out); return $this->dispatch($request, $delegate);
} }
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
abstract protected function dispatch(Request $request, Response $response, callable $out = null); abstract protected function dispatch(Request $request, DelegateInterface $delegate);
} }

View File

@@ -2,9 +2,10 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Firebase\JWT\JWT; use Interop\Http\ServerMiddleware\DelegateInterface;
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 Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
@@ -33,14 +34,17 @@ class AuthenticateAction extends AbstractRestAction
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService * @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
* @param JWTServiceInterface|JWTService $jwtService * @param JWTServiceInterface|JWTService $jwtService
* @param TranslatorInterface $translator * @param TranslatorInterface $translator
* @param LoggerInterface|null $logger
* *
* @Inject({ApiKeyService::class, JWTService::class, "translator"}) * @Inject({ApiKeyService::class, JWTService::class, "translator", "Logger_Shlink"})
*/ */
public function __construct( public function __construct(
ApiKeyServiceInterface $apiKeyService, ApiKeyServiceInterface $apiKeyService,
JWTServiceInterface $jwtService, JWTServiceInterface $jwtService,
TranslatorInterface $translator TranslatorInterface $translator,
LoggerInterface $logger = null
) { ) {
parent::__construct($logger);
$this->translator = $translator; $this->translator = $translator;
$this->apiKeyService = $apiKeyService; $this->apiKeyService = $apiKeyService;
$this->jwtService = $jwtService; $this->jwtService = $jwtService;
@@ -48,11 +52,10 @@ class AuthenticateAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
public function dispatch(Request $request, Response $response, callable $out = null) public function dispatch(Request $request, DelegateInterface $delegate)
{ {
$authData = $request->getParsedBody(); $authData = $request->getParsedBody();
if (! isset($authData['apiKey'])) { if (! isset($authData['apiKey'])) {
@@ -61,7 +64,7 @@ class AuthenticateAction extends AbstractRestAction
'message' => $this->translator->translate( 'message' => $this->translator->translate(
'You have to provide a valid API key under the "apiKey" param name.' 'You have to provide a valid API key under the "apiKey" param name.'
), ),
], 400); ], self::STATUS_BAD_REQUEST);
} }
// Authenticate using provided API key // Authenticate using provided API key
@@ -70,7 +73,7 @@ class AuthenticateAction extends AbstractRestAction
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_API_KEY_ERROR, 'error' => RestUtils::INVALID_API_KEY_ERROR,
'message' => $this->translator->translate('Provided API key does not exist or is invalid.'), 'message' => $this->translator->translate('Provided API key does not exist or is invalid.'),
], 401); ], self::STATUS_UNAUTHORIZED);
} }
// Generate a JSON Web Token that will be used for authorization in next requests // Generate a JSON Web Token that will be used for authorization in next requests

View File

@@ -2,6 +2,7 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
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;
@@ -52,18 +53,17 @@ class CreateShortcodeAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
public function dispatch(Request $request, Response $response, callable $out = null) public function dispatch(Request $request, DelegateInterface $delegate)
{ {
$postData = $request->getParsedBody(); $postData = $request->getParsedBody();
if (! isset($postData['longUrl'])) { if (! isset($postData['longUrl'])) {
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' => $this->translator->translate('A URL was not provided'),
], 400); ], self::STATUS_BAD_REQUEST);
} }
$longUrl = $postData['longUrl']; $longUrl = $postData['longUrl'];
$tags = isset($postData['tags']) && is_array($postData['tags']) ? $postData['tags'] : []; $tags = isset($postData['tags']) && is_array($postData['tags']) ? $postData['tags'] : [];
@@ -87,13 +87,13 @@ class CreateShortcodeAction extends AbstractRestAction
$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
), ),
], 400); ], self::STATUS_BAD_REQUEST);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Unexpected error creating shortcode.' . PHP_EOL . $e); $this->logger->error('Unexpected error creating shortcode.' . PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR, 'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'), 'message' => $this->translator->translate('Unexpected error occurred'),
], 500); ], self::STATUS_INTERNAL_SERVER_ERROR);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
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;
@@ -43,11 +44,10 @@ class EditTagsAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
protected function dispatch(Request $request, Response $response, callable $out = null) protected function dispatch(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
$bodyParams = $request->getParsedBody(); $bodyParams = $request->getParsedBody();
@@ -56,7 +56,7 @@ class EditTagsAction extends AbstractRestAction
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR, 'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => $this->translator->translate('A list of tags was not provided'), 'message' => $this->translator->translate('A list of tags was not provided'),
], 400); ], self::STATUS_BAD_REQUEST);
} }
$tags = $bodyParams['tags']; $tags = $bodyParams['tags'];
@@ -67,7 +67,7 @@ class EditTagsAction extends AbstractRestAction
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::getRestErrorCodeFromException($e), 'error' => RestUtils::getRestErrorCodeFromException($e),
'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), 'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
], 404); ], self::STATUS_NOT_FOUND);
} }
} }
} }

View File

@@ -2,6 +2,7 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
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;
@@ -44,11 +45,10 @@ class GetVisitsAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
public function dispatch(Request $request, Response $response, callable $out = null) public function dispatch(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
$startDate = $this->getDateQueryParam($request, 'startDate'); $startDate = $this->getDateQueryParam($request, 'startDate');
@@ -70,13 +70,13 @@ class GetVisitsAction extends AbstractRestAction
$this->translator->translate('Provided short code %s does not exist'), $this->translator->translate('Provided short code %s does not exist'),
$shortCode $shortCode
), ),
], 404); ], self::STATUS_NOT_FOUND);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Unexpected error while parsing short code'. PHP_EOL . $e); $this->logger->error('Unexpected error while parsing short code'. PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR, 'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'), 'message' => $this->translator->translate('Unexpected error occurred'),
], 500); ], self::STATUS_INTERNAL_SERVER_ERROR);
} }
} }

View File

@@ -2,6 +2,7 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
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;
@@ -46,11 +47,10 @@ class ListShortcodesAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
public function dispatch(Request $request, Response $response, callable $out = null) public function dispatch(Request $request, DelegateInterface $delegate)
{ {
try { try {
$params = $this->queryToListParams($request->getQueryParams()); $params = $this->queryToListParams($request->getQueryParams());
@@ -61,7 +61,7 @@ class ListShortcodesAction extends AbstractRestAction
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR, 'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'), 'message' => $this->translator->translate('Unexpected error occurred'),
], 500); ], self::STATUS_INTERNAL_SERVER_ERROR);
} }
} }

View File

@@ -2,6 +2,7 @@
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Interop\Http\ServerMiddleware\DelegateInterface;
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;
@@ -43,11 +44,10 @@ class ResolveUrlAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param callable|null $out
* @return null|Response * @return null|Response
*/ */
public function dispatch(Request $request, Response $response, callable $out = null) public function dispatch(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
@@ -57,7 +57,7 @@ class ResolveUrlAction extends AbstractRestAction
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_ARGUMENT_ERROR, 'error' => RestUtils::INVALID_ARGUMENT_ERROR,
'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), 'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
], 404); ], self::STATUS_NOT_FOUND);
} }
return new JsonResponse([ return new JsonResponse([
@@ -71,13 +71,13 @@ class ResolveUrlAction extends AbstractRestAction
$this->translator->translate('Provided short code "%s" has an invalid format'), $this->translator->translate('Provided short code "%s" has an invalid format'),
$shortCode $shortCode
), ),
], 400); ], self::STATUS_BAD_REQUEST);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Unexpected error while resolving the URL behind a short code.' . PHP_EOL . $e); $this->logger->error('Unexpected error while resolving the URL behind a short code.' . PHP_EOL . $e);
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::UNKNOWN_ERROR, 'error' => RestUtils::UNKNOWN_ERROR,
'message' => $this->translator->translate('Unexpected error occurred'), 'message' => $this->translator->translate('Unexpected error occurred'),
], 500); ], self::STATUS_INTERNAL_SERVER_ERROR);
} }
} }
} }

View File

@@ -1,34 +1,28 @@
<?php <?php
namespace Shlinkio\Shlink\Rest\ErrorHandler; namespace Shlinkio\Shlink\Rest\ErrorHandler;
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ErrorHandlerInterface; use Acelaya\ExpressiveErrorHandler\ErrorHandler\ErrorResponseGeneratorInterface;
use Fig\Http\Message\StatusCodeInterface;
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 Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouteResult; use Zend\Expressive\Router\RouteResult;
class JsonErrorHandler implements ErrorHandlerInterface class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, StatusCodeInterface
{ {
/** /**
* Final handler for an application. * Final handler for an application.
* *
* @param \Throwable|\Exception $e
* @param Request $request * @param Request $request
* @param Response $response * @param Response $response
* @param null|mixed $err
* @return Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, $err = null) public function __invoke($e, Request $request, Response $response)
{ {
$hasRoute = $request->getAttribute(RouteResult::class) !== null; $status = $response->getStatusCode();
$isNotFound = ! $hasRoute && ! isset($err); $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
if ($isNotFound) { $status = $status < 400 ? self::STATUS_INTERNAL_SERVER_ERROR : $status;
$responsePhrase = 'Not found';
$status = 404;
} else {
$status = $response->getStatusCode();
$responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
$status = $status < 400 ? 500 : $status;
}
return new JsonResponse([ return new JsonResponse([
'error' => $this->responsePhraseToCode($responsePhrase), 'error' => $this->responsePhraseToCode($responsePhrase),

View File

@@ -1,55 +1,45 @@
<?php <?php
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\RequestMethodInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Shlinkio\Shlink\Common\Exception\RuntimeException; use Shlinkio\Shlink\Common\Exception\RuntimeException;
use Zend\Stratigility\MiddlewareInterface;
class BodyParserMiddleware implements MiddlewareInterface class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
{ {
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
$method = $request->getMethod(); $method = $request->getMethod();
$currentParams = $request->getParsedBody(); $currentParams = $request->getParsedBody();
// In requests that do not allow body or if the body has already been parsed, continue to next middleware // In requests that do not allow body or if the body has already been parsed, continue to next middleware
if (in_array($method, ['GET', 'HEAD', 'OPTIONS']) || ! empty($currentParams)) { if (! empty($currentParams) || in_array($method, [
return $out($request, $response); self::METHOD_GET,
self::METHOD_HEAD,
self::METHOD_OPTIONS
], true)) {
return $delegate->process($request);
} }
// If the accepted content is JSON, try to parse the body from JSON // If the accepted content is JSON, try to parse the body from JSON
$contentType = $this->getRequestContentType($request); $contentType = $this->getRequestContentType($request);
if (in_array($contentType, ['application/json', 'text/json', 'application/x-json'])) { if (in_array($contentType, ['application/json', 'text/json', 'application/x-json'], true)) {
return $out($this->parseFromJson($request), $response); return $delegate->process($this->parseFromJson($request));
} }
return $out($this->parseFromUrlEncoded($request), $response); return $delegate->process($this->parseFromUrlEncoded($request));
} }
/** /**

View File

@@ -2,6 +2,9 @@
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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;
@@ -14,9 +17,8 @@ use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouteResult; use Zend\Expressive\Router\RouteResult;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
use Zend\Stdlib\ErrorHandler; use Zend\Stdlib\ErrorHandler;
use Zend\Stratigility\MiddlewareInterface;
class CheckAuthenticationMiddleware implements MiddlewareInterface class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface
{ {
const AUTHORIZATION_HEADER = 'Authorization'; const AUTHORIZATION_HEADER = 'Authorization';
@@ -52,31 +54,15 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
} }
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
// 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 */
@@ -86,7 +72,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
|| $routeResult->getMatchedRouteName() === 'rest-authenticate' || $routeResult->getMatchedRouteName() === 'rest-authenticate'
|| $request->getMethod() === 'OPTIONS' || $request->getMethod() === 'OPTIONS'
) { ) {
return $out($request, $response); return $delegate->process($request);
} }
// Check that the auth header was provided, and that it belongs to a non-expired token // Check that the auth header was provided, and that it belongs to a non-expired token
@@ -103,7 +89,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
'message' => sprintf($this->translator->translate( 'message' => sprintf($this->translator->translate(
'You need to provide the Bearer type in the %s header.' 'You need to provide the Bearer type in the %s header.'
), self::AUTHORIZATION_HEADER), ), self::AUTHORIZATION_HEADER),
], 401); ], self::STATUS_UNAUTHORIZED);
} }
// Make sure the authorization type is Bearer // Make sure the authorization type is Bearer
@@ -114,7 +100,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
'message' => sprintf($this->translator->translate( 'message' => sprintf($this->translator->translate(
'Provided authorization type %s is not supported. Use Bearer instead.' 'Provided authorization type %s is not supported. Use Bearer instead.'
), $authType), ), $authType),
], 401); ], self::STATUS_UNAUTHORIZED);
} }
try { try {
@@ -126,20 +112,13 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
// Update the token expiration and continue to next middleware // Update the token expiration and continue to next middleware
$jwt = $this->jwtService->refresh($jwt); $jwt = $this->jwtService->refresh($jwt);
/** @var Response $response */ $response = $delegate->process($request);
$response = $out($request, $response);
// Return the response with the updated token on it // Return the response with the updated token on it
return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt); return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
} catch (AuthenticationException $e) { } catch (AuthenticationException $e) {
$this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e); $this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e);
return $this->createTokenErrorResponse(); return $this->createTokenErrorResponse();
} catch (\Exception $e) {
$this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e);
return $this->createTokenErrorResponse();
} catch (\Throwable $e) {
$this->logger->warning('Unexpected error occurred.' . PHP_EOL . $e);
return $this->createTokenErrorResponse();
} finally { } finally {
ErrorHandler::clean(); ErrorHandler::clean();
} }
@@ -156,6 +135,6 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
), ),
self::AUTHORIZATION_HEADER self::AUTHORIZATION_HEADER
), ),
], 401); ], self::STATUS_UNAUTHORIZED);
} }
} }

View File

@@ -1,41 +1,26 @@
<?php <?php
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
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 Zend\Stratigility\MiddlewareInterface;
class CrossDomainMiddleware implements MiddlewareInterface class CrossDomainMiddleware implements MiddlewareInterface
{ {
/** /**
* Process an incoming request and/or response. * Process an incoming server request and return a response, optionally delegating
* * to the next middleware component to create the 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 Request $request
* @param Response $response * @param DelegateInterface $delegate
* @param null|callable $out *
* @return null|Response * @return Response
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, DelegateInterface $delegate)
{ {
/** @var Response $response */ /** @var Response $response */
$response = $out($request, $response); $response = $delegate->process($request);
if (! $request->hasHeader('Origin')) { if (! $request->hasHeader('Origin')) {
return $response; return $response;
} }
@@ -49,9 +34,9 @@ class CrossDomainMiddleware implements MiddlewareInterface
// Add OPTIONS-specific headers // Add OPTIONS-specific headers
foreach ([ foreach ([
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path
'Access-Control-Max-Age' => '1000', 'Access-Control-Max-Age' => '1000',
'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), 'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'),
] as $key => $value) { ] as $key => $value) {
$response = $response->withHeader($key, $value); $response = $response->withHeader($key, $value);
} }

View File

@@ -37,19 +37,13 @@ class PathVersionMiddleware implements MiddlewareInterface
$uri = $request->getUri(); $uri = $request->getUri();
$path = $uri->getPath(); $path = $uri->getPath();
// Exclude non-rest route
if (strpos($path, '/rest') !== 0) {
return $out($request, $response);
}
// If the path does not begin with the version number, prepend v1 by default for retrocompatibility purposes // If the path does not begin with the version number, prepend v1 by default for retrocompatibility purposes
if (strpos($path, '/rest/v') !== 0) { if (strpos($path, '/v') !== 0) {
$parts = explode('/', $path); $parts = explode('/', $path);
// Remove the first empty part and the "/rest" prefix // Remove the first empty part and the
array_shift($parts); array_shift($parts);
array_shift($parts); // Prepend the version prefix
// Prepend the prefix with version array_unshift($parts, '/v1');
array_unshift($parts, '/rest/v1');
$request = $request->withUri($uri->withPath(implode('/', $parts))); $request = $request->withUri($uri->withPath(implode('/', $parts)));
} }

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction; use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -42,7 +42,7 @@ class AuthenticateActionTest extends TestCase
*/ */
public function notProvidingAuthDataReturnsError() public function notProvidingAuthDataReturnsError()
{ {
$resp = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); $resp = $this->action->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal());
$this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals(400, $resp->getStatusCode());
} }
@@ -57,7 +57,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',
]); ]);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$response->getBody()->rewind(); $response->getBody()->rewind();
@@ -75,7 +75,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',
]); ]);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
} }

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\CreateShortcodeAction; use Shlinkio\Shlink\Rest\Action\CreateShortcodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
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;
@@ -38,7 +38,10 @@ class CreateShortcodeActionTest extends TestCase
*/ */
public function missingLongUrlParamReturnsError() public function missingLongUrlParamReturnsError()
{ {
$response = $this->action->__invoke(ServerRequestFactory::fromGlobals(), new Response()); $response = $this->action->process(
ServerRequestFactory::fromGlobals(),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
} }
@@ -54,7 +57,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar', 'longUrl' => 'http://www.domain.com/foo/bar',
]); ]);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0); $this->assertTrue(strpos($response->getBody()->getContents(), 'http://foo.com/abc123') > 0);
} }
@@ -71,7 +74,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar', 'longUrl' => 'http://www.domain.com/foo/bar',
]); ]);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0); $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_URL_ERROR) > 0);
} }
@@ -88,7 +91,7 @@ class CreateShortcodeActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'longUrl' => 'http://www.domain.com/foo/bar', 'longUrl' => 'http://www.domain.com/foo/bar',
]); ]);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0);
} }

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as 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\EditTagsAction; use Shlinkio\Shlink\Rest\Action\EditTagsAction;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -33,9 +33,9 @@ class EditTagsActionTest extends TestCase
*/ */
public function notProvidingTagsReturnsError() public function notProvidingTagsReturnsError()
{ {
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
} }
@@ -49,10 +49,10 @@ class EditTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class) $this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123') ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]), ->withParsedBody(['tags' => []]),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
} }
@@ -66,10 +66,10 @@ class EditTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl()) $this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl())
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123') ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]), ->withParsedBody(['tags' => []]),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }

View File

@@ -1,14 +1,14 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; 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\GetVisitsAction;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -38,9 +38,9 @@ class GetVisitsActionTest extends TestCase
$this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([]) $this->visitsTracker->info($shortCode, Argument::type(DateRange::class))->willReturn([])
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }
@@ -55,9 +55,9 @@ class GetVisitsActionTest extends TestCase
InvalidArgumentException::class InvalidArgumentException::class
)->shouldBeCalledTimes(1); )->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
} }
@@ -72,9 +72,9 @@ class GetVisitsActionTest extends TestCase
\Exception::class \Exception::class
)->shouldBeCalledTimes(1); )->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
} }
@@ -89,10 +89,10 @@ class GetVisitsActionTest extends TestCase
->willReturn([]) ->willReturn([])
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode) ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
->withQueryParams(['endDate' => '2016-01-01 00:00:00']), ->withQueryParams(['endDate' => '2016-01-01 00:00:00']),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }

View File

@@ -1,17 +1,17 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as 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\ListShortcodesAction;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
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;
use Zend\Paginator\Paginator; use Zend\Paginator\Paginator;
class ListShortcodesActionTest extends TestCase class ListShortCodesActionTest extends TestCase
{ {
/** /**
* @var ListShortcodesAction * @var ListShortcodesAction
@@ -37,11 +37,11 @@ class ListShortcodesActionTest extends TestCase
$this->service->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter())) $this->service->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withQueryParams([ ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page, 'page' => $page,
]), ]),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }
@@ -55,11 +55,11 @@ class ListShortcodesActionTest extends TestCase
$this->service->listShortUrls($page, null, [], null)->willThrow(\Exception::class) $this->service->listShortUrls($page, null, [], null)->willThrow(\Exception::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->__invoke( $response = $this->action->process(
ServerRequestFactory::fromGlobals()->withQueryParams([ ServerRequestFactory::fromGlobals()->withQueryParams([
'page' => $page, 'page' => $page,
]), ]),
new Response() TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
} }

View File

@@ -1,13 +1,13 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
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\ResolveUrlAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response; use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -38,7 +38,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0); $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0);
} }
@@ -53,7 +53,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0); $this->assertTrue(strpos($response->getBody()->getContents(), 'http://domain.com/foo/bar') > 0);
} }
@@ -68,7 +68,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0); $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_SHORTCODE_ERROR) > 0);
} }
@@ -83,7 +83,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->__invoke($request, new Response()); $response = $this->action->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0); $this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::UNKNOWN_ERROR) > 0);
} }

View File

@@ -2,7 +2,7 @@
namespace ShlinkioTest\Shlink\Rest\Authentication; namespace ShlinkioTest\Shlink\Rest\Authentication;
use Firebase\JWT\JWT; use Firebase\JWT\JWT;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest; namespace ShlinkioTest\Shlink\Rest;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Rest\ConfigProvider; use Shlinkio\Shlink\Rest\ConfigProvider;
class ConfigProviderTest extends TestCase class ConfigProviderTest extends TestCase
@@ -24,7 +24,6 @@ class ConfigProviderTest extends TestCase
$config = $this->configProvider->__invoke(); $config = $this->configProvider->__invoke();
$this->assertArrayHasKey('error_handler', $config); $this->assertArrayHasKey('error_handler', $config);
$this->assertArrayHasKey('middleware_pipeline', $config);
$this->assertArrayHasKey('routes', $config); $this->assertArrayHasKey('routes', $config);
$this->assertArrayHasKey('dependencies', $config); $this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('translator', $config); $this->assertArrayHasKey('translator', $config);

View File

@@ -1,79 +0,0 @@
<?php
namespace ShlinkioTest\Shlink\Rest\ErrorHandler;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\Rest\ErrorHandler\JsonErrorHandler;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Router\RouteResult;
class JsonErrorHandlerTest extends TestCase
{
/**
* @var JsonErrorHandler
*/
protected $errorHandler;
public function setUp()
{
$this->errorHandler = new JsonErrorHandler();
}
/**
* @test
*/
public function noMatchedRouteReturnsNotFoundResponse()
{
$response = $this->errorHandler->__invoke(ServerRequestFactory::fromGlobals(), new Response());
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(404, $response->getStatusCode());
}
/**
* @test
*/
public function matchedRouteWithErrorReturnsMethodNotAllowedResponse()
{
$response = $this->errorHandler->__invoke(
ServerRequestFactory::fromGlobals(),
(new Response())->withStatus(405),
405
);
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(405, $response->getStatusCode());
}
/**
* @test
*/
public function responseWithErrorKeepsStatus()
{
$response = $this->errorHandler->__invoke(
ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class,
RouteResult::fromRouteMatch('foo', 'bar', [])
),
(new Response())->withStatus(401),
401
);
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(401, $response->getStatusCode());
}
/**
* @test
*/
public function responseWithoutErrorReturnsStatus500()
{
$response = $this->errorHandler->__invoke(
ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class,
RouteResult::fromRouteMatch('foo', 'bar', [])
),
(new Response())->withStatus(200),
'Some error'
);
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(500, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace ShlinkioTest\Shlink\Rest\ErrorHandler;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Rest\ErrorHandler\JsonErrorResponseGenerator;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
class JsonErrorResponseGeneratorTest extends TestCase
{
/**
* @var JsonErrorResponseGenerator
*/
protected $errorHandler;
public function setUp()
{
$this->errorHandler = new JsonErrorResponseGenerator();
}
/**
* @test
*/
public function noErrorStatusReturnsInternalServerError()
{
$response = $this->errorHandler->__invoke(null, ServerRequestFactory::fromGlobals(), new Response());
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(500, $response->getStatusCode());
}
/**
* @test
*/
public function errorStatusReturnsThatStatus()
{
$response = $this->errorHandler->__invoke(
null,
ServerRequestFactory::fromGlobals(),
(new Response())->withStatus(405)
);
$this->assertInstanceOf(Response\JsonResponse::class, $response);
$this->assertEquals(405, $response->getStatusCode());
}
}

View File

@@ -1,8 +1,11 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ServerRequestInterface as Request; use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware; use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -26,16 +29,13 @@ class BodyParserMiddlewareTest extends TestCase
public function requestsFromOtherMethodsJustFallbackToNextMiddleware() public function requestsFromOtherMethodsJustFallbackToNextMiddleware()
{ {
$request = ServerRequestFactory::fromGlobals()->withMethod('GET'); $request = ServerRequestFactory::fromGlobals()->withMethod('GET');
$test = $this; $delegate = $this->prophesize(DelegateInterface::class);
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use ($test, $request) { /** @var MethodProphecy $process */
$test->assertSame($request, $req); $process = $delegate->process($request)->willReturn(new Response());
});
$request = $request->withMethod('POST'); $this->middleware->process($request, $delegate->reveal());
$test = $this;
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use ($test, $request) { $process->shouldHaveBeenCalledTimes(1);
$test->assertSame($request, $req);
});
} }
/** /**
@@ -43,19 +43,31 @@ class BodyParserMiddlewareTest extends TestCase
*/ */
public function jsonRequestsAreJsonDecoded() public function jsonRequestsAreJsonDecoded()
{ {
$test = $this;
$body = new Stream('php://temp', 'wr'); $body = new Stream('php://temp', 'wr');
$body->write('{"foo": "bar", "bar": ["one", 5]}'); $body->write('{"foo": "bar", "bar": ["one", 5]}');
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT') $request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
->withBody($body) ->withBody($body)
->withHeader('content-type', 'application/json'); ->withHeader('content-type', 'application/json');
$test = $this; $delegate = $this->prophesize(DelegateInterface::class);
$this->middleware->__invoke($request, new Response(), function (Request $req, $resp) use ($test, $request) { /** @var MethodProphecy $process */
$test->assertNotSame($request, $req); $process = $delegate->process(Argument::type(ServerRequestInterface::class))->will(
$test->assertEquals([ function (array $args) use ($test) {
'foo' => 'bar', /** @var ServerRequestInterface $req */
'bar' => ['one', 5], $req = array_shift($args);
], $req->getParsedBody());
}); $test->assertEquals([
'foo' => 'bar',
'bar' => ['one', 5],
], $req->getParsedBody());
return new Response();
}
);
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
} }
/** /**
@@ -63,17 +75,29 @@ class BodyParserMiddlewareTest extends TestCase
*/ */
public function regularRequestsAreUrlDecoded() public function regularRequestsAreUrlDecoded()
{ {
$test = $this;
$body = new Stream('php://temp', 'wr'); $body = new Stream('php://temp', 'wr');
$body->write('foo=bar&bar[]=one&bar[]=5'); $body->write('foo=bar&bar[]=one&bar[]=5');
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT') $request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
->withBody($body); ->withBody($body);
$test = $this; $delegate = $this->prophesize(DelegateInterface::class);
$this->middleware->__invoke($request, new Response(), function (Request $req, $resp) use ($test, $request) { /** @var MethodProphecy $process */
$test->assertNotSame($request, $req); $process = $delegate->process(Argument::type(ServerRequestInterface::class))->will(
$test->assertEquals([ function (array $args) use ($test) {
'foo' => 'bar', /** @var ServerRequestInterface $req */
'bar' => ['one', 5], $req = array_shift($args);
], $req->getParsedBody());
}); $test->assertEquals([
'foo' => 'bar',
'bar' => ['one', 5],
], $req->getParsedBody());
return new Response();
}
);
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1);
} }
} }

View File

@@ -1,12 +1,16 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Authentication\JWTService; use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Router\Route;
use Zend\Expressive\Router\RouteResult; use Zend\Expressive\Router\RouteResult;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -33,49 +37,42 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function someWhiteListedSituationsFallbackToNextMiddleware() public function someWhiteListedSituationsFallbackToNextMiddleware()
{ {
$request = ServerRequestFactory::fromGlobals(); $request = ServerRequestFactory::fromGlobals();
$response = new Response(); $delegate = $this->prophesize(DelegateInterface::class);
$isCalled = false; /** @var MethodProphecy $process */
$this->assertFalse($isCalled); $process = $delegate->process($request)->willReturn(new Response());
$this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) {
$isCalled = true; $this->middleware->process($request, $delegate->reveal());
}); $process->shouldHaveBeenCalledTimes(1);
$this->assertTrue($isCalled);
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteFailure(['GET']) RouteResult::fromRouteFailure(['GET'])
); );
$response = new Response(); $delegate = $this->prophesize(DelegateInterface::class);
$isCalled = false; /** @var MethodProphecy $process */
$this->assertFalse($isCalled); $process = $delegate->process($request)->willReturn(new Response());
$this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { $this->middleware->process($request, $delegate->reveal());
$isCalled = true; $process->shouldHaveBeenCalledTimes(1);
});
$this->assertTrue($isCalled);
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('rest-authenticate', 'foo', []) RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, 'rest-authenticate'), [])
); );
$response = new Response(); $delegate = $this->prophesize(DelegateInterface::class);
$isCalled = false; /** @var MethodProphecy $process */
$this->assertFalse($isCalled); $process = $delegate->process($request)->willReturn(new Response());
$this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { $this->middleware->process($request, $delegate->reveal());
$isCalled = true; $process->shouldHaveBeenCalledTimes(1);
});
$this->assertTrue($isCalled);
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
)->withMethod('OPTIONS'); )->withMethod('OPTIONS');
$response = new Response(); $delegate = $this->prophesize(DelegateInterface::class);
$isCalled = false; /** @var MethodProphecy $process */
$this->assertFalse($isCalled); $process = $delegate->process($request)->willReturn(new Response());
$this->middleware->__invoke($request, $response, function ($req, $resp) use (&$isCalled) { $this->middleware->process($request, $delegate->reveal());
$isCalled = true; $process->shouldHaveBeenCalledTimes(1);
});
$this->assertTrue($isCalled);
} }
/** /**
@@ -85,9 +82,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
{ {
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
); );
$response = $this->middleware->__invoke($request, new Response()); $response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
@@ -99,10 +96,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken);
$response = $this->middleware->__invoke($request, new Response()); $response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
$this->assertTrue(strpos($response->getBody()->getContents(), 'You need to provide the Bearer type') > 0); $this->assertTrue(strpos($response->getBody()->getContents(), 'You need to provide the Bearer type') > 0);
} }
@@ -115,10 +113,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken);
$response = $this->middleware->__invoke($request, new Response()); $response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
$this->assertTrue( $this->assertTrue(
strpos($response->getBody()->getContents(), 'Provided authorization type Basic is not supported') > 0 strpos($response->getBody()->getContents(), 'Provided authorization type Basic is not supported') > 0
@@ -133,33 +132,33 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken);
$this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1); $this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1);
$response = $this->middleware->__invoke($request, new Response()); $response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
/** /**
* @test * @test
*/ */
public function provideCorrectTokenUpdatesExpirationAndFallbacksToNextMiddleware() public function provideCorrectTokenUpdatesExpirationAndFallsBackToNextMiddleware()
{ {
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRouteMatch('bar', 'foo', []) RouteResult::fromRoute(new Route('bar', 'foo'), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'bearer ' . $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'bearer ' . $authToken);
$this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1); $this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1);
$this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1); $this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1);
$isCalled = false; $delegate = $this->prophesize(DelegateInterface::class);
$this->assertFalse($isCalled); /** @var MethodProphecy $process */
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) { $process = $delegate->process($request)->willReturn(new Response());
$isCalled = true; $resp = $this->middleware->process($request, $delegate->reveal());
return $resp;
}); $process->shouldHaveBeenCalledTimes(1);
$this->assertTrue($isCalled); $this->assertArrayHasKey(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $resp->getHeaders());
} }
} }

View File

@@ -1,7 +1,10 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit_Framework_TestCase as TestCase; use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware; use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -12,10 +15,15 @@ class CrossDomainMiddlewareTest extends TestCase
* @var CrossDomainMiddleware * @var CrossDomainMiddleware
*/ */
protected $middleware; protected $middleware;
/**
* @var ObjectProphecy
*/
protected $delegate;
public function setUp() public function setUp()
{ {
$this->middleware = new CrossDomainMiddleware(); $this->middleware = new CrossDomainMiddleware();
$this->delegate = $this->prophesize(DelegateInterface::class);
} }
/** /**
@@ -24,13 +32,9 @@ class CrossDomainMiddlewareTest extends TestCase
public function nonCrossDomainRequestsAreNotAffected() public function nonCrossDomainRequestsAreNotAffected()
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$response = $this->middleware->__invoke( $this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
ServerRequestFactory::fromGlobals(),
$originalResponse, $response = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->delegate->reveal());
function ($req, $resp) {
return $resp;
}
);
$this->assertSame($originalResponse, $response); $this->assertSame($originalResponse, $response);
$headers = $response->getHeaders(); $headers = $response->getHeaders();
@@ -44,12 +48,11 @@ class CrossDomainMiddlewareTest extends TestCase
public function anyRequestIncludesTheAllowAccessHeader() public function anyRequestIncludesTheAllowAccessHeader()
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$response = $this->middleware->__invoke( $this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'), ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'),
$originalResponse, $this->delegate->reveal()
function ($req, $resp) {
return $resp;
}
); );
$this->assertNotSame($originalResponse, $response); $this->assertNotSame($originalResponse, $response);
@@ -64,11 +67,10 @@ class CrossDomainMiddlewareTest extends TestCase
public function optionsRequestIncludesMoreHeaders() public function optionsRequestIncludesMoreHeaders()
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$request = ServerRequestFactory::fromGlobals(['REQUEST_METHOD' => 'OPTIONS'])->withHeader('Origin', 'local'); $request = ServerRequestFactory::fromGlobals()->withMethod('OPTIONS')->withHeader('Origin', 'local');
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->__invoke($request, $originalResponse, function ($req, $resp) { $response = $this->middleware->process($request, $this->delegate->reveal());
return $resp;
});
$this->assertNotSame($originalResponse, $response); $this->assertNotSame($originalResponse, $response);
$headers = $response->getHeaders(); $headers = $response->getHeaders();

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware; use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
@@ -25,7 +25,7 @@ class PathVersionMiddlewareTest extends TestCase
*/ */
public function whenVersionIsProvidedRequestRemainsUnchanged() public function whenVersionIsProvidedRequestRemainsUnchanged()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/v2/foo')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/foo'));
$test = $this; $test = $this;
$this->middleware->__invoke($request, new Response(), function ($req) use ($request, $test) { $this->middleware->__invoke($request, new Response(), function ($req) use ($request, $test) {
$test->assertSame($request, $req); $test->assertSame($request, $req);
@@ -37,23 +37,11 @@ class PathVersionMiddlewareTest extends TestCase
*/ */
public function versionOneIsPrependedWhenNoVersionIsDefined() public function versionOneIsPrependedWhenNoVersionIsDefined()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/bar/baz')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/bar/baz'));
$test = $this; $test = $this;
$this->middleware->__invoke($request, new Response(), function (Request $req) use ($request, $test) { $this->middleware->__invoke($request, new Response(), function (Request $req) use ($request, $test) {
$test->assertNotSame($request, $req); $test->assertNotSame($request, $req);
$this->assertEquals('/rest/v1/bar/baz', $req->getUri()->getPath()); $this->assertEquals('/v1/bar/baz', $req->getUri()->getPath());
});
}
/**
* @test
*/
public function nonRestPathsAreNotProcessed()
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/non-rest'));
$test = $this;
$this->middleware->__invoke($request, new Response(), function ($req) use ($request, $test) {
$test->assertSame($request, $req);
}); });
} }
} }

View File

@@ -3,7 +3,7 @@ namespace ShlinkioTest\Shlink\Rest\Service;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;

View File

@@ -1,7 +1,7 @@
<?php <?php
namespace ShlinkioTest\Shlink\Rest\Util; namespace ShlinkioTest\Shlink\Rest\Util;
use PHPUnit_Framework_TestCase as TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Exception\WrongIpException; use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;