Compare commits

...

80 Commits

Author SHA1 Message Date
Alejandro Celaya
a8465094c1 Merge branch 'develop' 2018-06-18 20:49:32 +02:00
Alejandro Celaya
16f7359ac6 Merge pull request #156 from acelaya/feature/1.9.1
Improved paginator properties
2018-06-18 20:48:41 +02:00
Alejandro Celaya
f9f4817ee2 Aded v1.9.1 to changelog 2018-06-18 20:40:50 +02:00
Alejandro Celaya
c7e49f223f Fixed filtered lists not being properly paginated 2018-06-18 20:38:25 +02:00
Alejandro Celaya
6e79b4ba7b Fixed php binary used in child commands while installkation not properly inherited 2018-06-18 20:14:51 +02:00
Alejandro Celaya
f78a7f12a9 Improved paginator properties 2018-06-17 18:29:40 +02:00
Alejandro Celaya
b3664597b0 Merge branch 'develop' 2018-05-07 11:27:13 +02:00
Alejandro Celaya
8cfb4f61ca Merge pull request #148 from acelaya/feature/1.9.0
Version 1.9.0
2018-05-07 11:26:27 +02:00
Alejandro Celaya
b0dbb2dae4 Updated CreateShortCodeContentNegotiationMiddleware so that query parameter takes precedence over Accept header 2018-05-07 11:17:10 +02:00
Alejandro Celaya
7c6da4985d Updated build script to delete more development-specific files 2018-05-07 11:09:32 +02:00
Alejandro Celaya
386b0dfb7b Updated changelog 2018-05-07 11:03:28 +02:00
Alejandro Celaya
1437ff48ce Ensured all core actions log errors 2018-05-07 10:58:49 +02:00
Alejandro Celaya
63294f20ee Updated language files 2018-05-06 12:36:07 +02:00
Alejandro Celaya
d8acc3c247 Removed unused use statement 2018-05-06 12:34:21 +02:00
Alejandro Celaya
52d8ffa212 Improved CreateShortCodeContentNegotiationMiddleware sho that it takes into account the case in which an error is returned from next middleware 2018-05-06 12:28:22 +02:00
Alejandro Celaya
98ad2816e8 Documented new endpoint to create short URLs in a single step 2018-05-06 12:19:08 +02:00
Alejandro Celaya
9d890f4227 Created CreateShortCodeContentNegotiationMiddleware 2018-05-03 19:04:40 +02:00
Alejandro Celaya
0932d04907 Fixed tests namespaces to match their subject under test 2018-05-03 18:34:45 +02:00
Alejandro Celaya
1f78b5c524 Improved CreateShortCodeContentNegotiationMiddleware so that it can determine the format based on a query partameter 2018-05-03 18:32:32 +02:00
Alejandro Celaya
59f10619ba Created middleware used with short codes creation actions to handle content negotiation 2018-05-03 18:26:31 +02:00
Alejandro Celaya
334710e92c Added middleware which injects the content-length header in the response if not present 2018-05-03 18:25:57 +02:00
Alejandro Celaya
75b8175824 Fixed coding styles in config file 2018-05-03 18:05:16 +02:00
Alejandro Celaya
8a74ef2a33 Moved action to subnamespace 2018-05-03 18:04:00 +02:00
Alejandro Celaya
d05ac5ce9d Moved action to subnamespace 2018-05-03 18:03:10 +02:00
Alejandro Celaya
3100fffa2b Moved action to subnamespace 2018-05-03 18:02:45 +02:00
Alejandro Celaya
6bbacb1017 Moved action to subnamespace 2018-05-03 18:01:57 +02:00
Alejandro Celaya
4403dc5df9 Moved action to subnamespace 2018-05-03 18:00:32 +02:00
Alejandro Celaya
fdc637c23d Moved action to subnamespace 2018-05-03 17:59:28 +02:00
Alejandro Celaya
b99d662417 Created SingleStepCreateShortCodeActionTest 2018-05-03 17:57:43 +02:00
Alejandro Celaya
eb9a964c66 Removed unused use statement 2018-05-03 13:34:13 +02:00
Alejandro Celaya
e5ef8d7f8c Created action which allows short URLs to be created on a single API request 2018-05-03 13:21:43 +02:00
Alejandro Celaya
28650aee2b Fixed case sensitivity errors 2018-05-03 12:19:51 +02:00
Alejandro Celaya
a2294704e6 Split try catch to prevent undefined variables 2018-05-01 19:38:44 +02:00
Alejandro Celaya
e5e1aa2ff4 Defined abstract action which handles short codes generations 2018-05-01 19:35:12 +02:00
Alejandro Celaya
2f5290b9d3 Moved whitelisted routes in CheckAuthenticationMiddleware to external configuration 2018-05-01 18:36:42 +02:00
Alejandro Celaya
ef3c4aadf2 Moved most of rest routes config to their actions 2018-05-01 18:28:37 +02:00
Alejandro Celaya
c9ce56eea5 Added public method in AbstractRestAction which builds route definition 2018-05-01 18:16:44 +02:00
Alejandro Celaya
4fee656f96 Prepared version 1.9.0 2018-05-01 10:10:19 +02:00
Alejandro Celaya
d2a04259f5 Merge branch 'develop' 2018-04-07 09:06:45 +02:00
Alejandro Celaya
e504daa1ba Merge pull request #142 from acelaya/develop
Develop
2018-04-07 09:05:56 +02:00
Alejandro Celaya
8793a67ce9 Reduced the number of includes by pointing to dcotrine scripts with extension 2018-04-07 08:37:41 +02:00
Alejandro Celaya
b4ded374e9 Updated changelog 2018-04-07 08:32:06 +02:00
Alejandro Celaya
91d350b12f Removed path workaround in PathVersionMiddleware and simplified code 2018-04-07 08:31:03 +02:00
Alejandro Celaya
b3e25f28fd Added v1.8.1 to changelog 2018-04-07 08:25:01 +02:00
Alejandro Celaya
aca89f9abe Updated links to doctrine CLI scripts to avoid depending on symlinks 2018-04-07 08:21:34 +02:00
Alejandro Celaya
243075dd78 Merge branch 'develop' 2018-03-29 09:52:00 +02:00
Alejandro Celaya
7130425896 Merge pull request #133 from acelaya/feature/1.8.0
1.8.0
2018-03-29 09:50:58 +02:00
Alejandro Celaya
fe9ab20cbb Applied some improvements 2018-03-27 23:57:29 +02:00
Alejandro Celaya
6935b2ebe2 Updated system so that NotFoundDelegate is used 2018-03-26 20:37:04 +02:00
Alejandro Celaya
3dcc510da1 Updated to symfony 4 2018-03-26 20:32:12 +02:00
Alejandro Celaya
2f26c82fa6 Removed expressive migration tool from dev dependencies 2018-03-26 20:25:30 +02:00
Alejandro Celaya
9ddb60a882 Updated changelog including v1.8.0 2018-03-26 20:22:57 +02:00
Alejandro Celaya
210b08b61f Created PixelActionTest 2018-03-26 20:17:38 +02:00
Alejandro Celaya
42fe4bd5ce Created new action to track visits, which returns an empty pixel 2018-03-26 20:13:03 +02:00
Alejandro Celaya
1b2a0820e5 Updated to phpunit 7 and dropped dbunit dependency 2018-03-26 19:09:10 +02:00
Alejandro Celaya
6cf0155417 Updated minimum required MSI 2018-03-26 19:06:49 +02:00
Alejandro Celaya
9b8be3e5b8 Fixed phpstan errors 2018-03-26 19:05:26 +02:00
Alejandro Celaya
a27b01b895 Fixed tests 2018-03-26 19:02:41 +02:00
Alejandro Celaya
16dd1838aa Updated to expressive 3 2018-03-26 18:49:28 +02:00
Alejandro Celaya
f788d6872f Added infection to the build matrix 2018-03-26 18:16:59 +02:00
Alejandro Celaya
d0df007812 Dropped support for PHP 7.0 2018-03-26 18:16:59 +02:00
Alejandro Celaya
f60c217fae Merge pull request #136 from acelaya/feature/1.7.2
Feature/1.7.2
2018-03-26 18:13:48 +02:00
Alejandro Celaya
d3fc7d543a Updated changelog 2018-03-26 18:13:08 +02:00
Alejandro Celaya
4d0fc1da07 Fixed PathVersionMiddleware not being properly propagated 2018-03-26 17:53:22 +02:00
Alejandro Celaya
ee2233c6dd Updated PathVersionMiddleware to single-pass middleware 2018-03-26 17:36:58 +02:00
Alejandro Celaya
ea6e0d7c7f Merge branch 'develop' 2018-03-21 16:31:27 +01:00
Alejandro Celaya
d9d599eab4 Updated changelog 2018-03-21 16:31:00 +01:00
Alejandro Celaya
d1ba44e1b3 Merge pull request #128 from weirdan/upgrade-to-expressive-2.2
Upgrade to expressive 2.2
2018-03-21 16:27:09 +01:00
Bruce Weirdan
dff2ad3740 define property to please scrutinizer 2018-03-21 12:13:03 +02:00
Bruce Weirdan
f7e63710e4 updated tests to fix deprecations
also fixed cs errors in middleware-pipeline
2018-03-21 02:05:55 +02:00
Bruce Weirdan
d3b5cd5c57 fixed middleware deprecations 2018-03-21 01:46:26 +02:00
Alejandro Celaya
86ed83d25e Merge branch 'develop' 2018-02-03 10:23:18 +01:00
Alejandro Celaya
f96d0fe30a Merge pull request #124 from acelaya/feature/o-a-s-3
Feature/o a s 3
2018-02-03 10:14:32 +01:00
Alejandro Celaya
be406bd676 Removed no-longer used Authorization parameter 2018-02-03 10:13:10 +01:00
Alejandro Celaya
044278752b Fixed server 2018-02-03 10:09:42 +01:00
Alejandro Celaya
343d2ab44a Added domain 2018-02-03 10:07:37 +01:00
Alejandro Celaya
66992f644e Added default value for server 2018-02-03 10:06:04 +01:00
Alejandro Celaya
cf245524dd Added missing base path in server 2018-02-03 10:01:16 +01:00
Alejandro Celaya
ad520811a3 Fixed dynamic host 2018-02-03 09:55:53 +01:00
Alejandro Celaya
ee1e1d5688 Updated swagger docs to OAS3 2018-02-03 09:53:40 +01:00
102 changed files with 2038 additions and 956 deletions

View File

@@ -5,7 +5,6 @@ branches:
- /.*/ - /.*/
php: php:
- 7
- 7.1 - 7.1
- 7.2 - 7.2
@@ -16,12 +15,10 @@ before_install:
before_script: before_script:
- composer self-update - composer self-update
- composer install --no-interaction - composer install --no-interaction
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then composer global require --dev phpstan/phpstan:0.9.*; fi
script: script:
- mkdir build - mkdir build
- composer check - composer check
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then ~/.composer/vendor/bin/phpstan analyse module/*/src/ --level=6 -c phpstan.neon; fi
after_script: after_script:
- vendor/bin/phpcov merge build --clover build/clover.xml - vendor/bin/phpcov merge build --clover build/clover.xml

View File

@@ -1,5 +1,68 @@
## CHANGELOG ## CHANGELOG
### 1.9.1
**Bugs:**
* [154: When filtering by searchTerm, sizes of every result page has an unexpected behavior](https://github.com/shlinkio/shlink/issues/154)
* [157: Background commands executed by installation process do not respect the used php binary](https://github.com/shlinkio/shlink/issues/157)
**Enhancements:**
* [155: Improve the pagination object returned in lists, including more meaningful properties](https://github.com/shlinkio/shlink/issues/155)
### 1.9.0
**Features**
* [147: Allow short URLs to be created on the fly with query param authentication](https://github.com/shlinkio/shlink/issues/147)
**Bugs:**
* [139: Make sure all core actions log exceptions](https://github.com/shlinkio/shlink/issues/139)
### 1.8.1
**Tasks**
* [141: Remove workaround used in PathVersionMiddleware](https://github.com/shlinkio/shlink/issues/141)
**Bugs:**
* [140: Installation failed. Warning thrown while trying to include doctrine script](https://github.com/shlinkio/shlink/issues/140)
### 1.8.0
**Features**
* [125: Implement a path which returns a 1px image instead of a redirection](https://github.com/shlinkio/shlink/issues/125)
**Enhancements:**
* [130: Update to Expressive 3](https://github.com/shlinkio/shlink/issues/130)
* [137: Update symfony packages to v4](https://github.com/shlinkio/shlink/issues/137)
**Tasks**
* [131: Drop support for PHP 7](https://github.com/shlinkio/shlink/issues/131)
* [132: Add infection to improve tests](https://github.com/shlinkio/shlink/issues/132)
### 1.7.2
**Bugs:**
* [135: Fix PathVersionMiddleware being ignored when using expressive 2.2](https://github.com/shlinkio/shlink/issues/135)
### 1.7.1
**Enhancements:**
* [128: Upgrade to expressive 2.2](https://github.com/shlinkio/shlink/issues/128)
**Bugs**
* [126: Expressive 2.2 causes failures by triggering E_USER_DEPRECATED errors](https://github.com/shlinkio/shlink/issues/126)
### 1.7.0 ### 1.7.0
**Features** **Features**

View File

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

View File

@@ -12,8 +12,8 @@
} }
], ],
"require": { "require": {
"php": "^7.0", "php": "^7.1",
"acelaya/ze-content-based-error-handler": "^2.0", "acelaya/ze-content-based-error-handler": "^2.2",
"cocur/slugify": "^3.0", "cocur/slugify": "^3.0",
"doctrine/annotations": "^1.4", "doctrine/annotations": "^1.4",
"doctrine/cache": "^1.6", "doctrine/cache": "^1.6",
@@ -22,23 +22,23 @@
"doctrine/dbal": "^2.5", "doctrine/dbal": "^2.5",
"doctrine/migrations": "^1.4", "doctrine/migrations": "^1.4",
"doctrine/orm": "^2.5", "doctrine/orm": "^2.5",
"endroid/qrcode": "^1.7", "endroid/qr-code": "^1.7",
"firebase/php-jwt": "^4.0", "firebase/php-jwt": "^4.0",
"guzzlehttp/guzzle": "^6.2", "guzzlehttp/guzzle": "^6.2",
"http-interop/http-middleware": "^0.4.1",
"mikehaertl/phpwkhtmltopdf": "^2.2", "mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21", "monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"symfony/console": "^3.4", "symfony/console": "^4.0",
"symfony/filesystem": "^3.0", "symfony/filesystem": "^4.0",
"symfony/process": "^3.0", "symfony/process": "^4.0",
"theorchard/monolog-cascade": "^0.4", "theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0", "zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0", "zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-expressive": "^2.0", "zendframework/zend-diactoros": "^1.7",
"zendframework/zend-expressive-fastroute": "^2.0", "zendframework/zend-expressive": "^3.0",
"zendframework/zend-expressive-helpers": "^4.2", "zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-platesrenderer": "^1.3", "zendframework/zend-expressive-helpers": "^5.0",
"zendframework/zend-expressive-platesrenderer": "^2.0",
"zendframework/zend-i18n": "^2.7", "zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8", "zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6", "zendframework/zend-paginator": "^2.6",
@@ -47,14 +47,16 @@
}, },
"require-dev": { "require-dev": {
"filp/whoops": "^2.0", "filp/whoops": "^2.0",
"phpunit/dbunit": "^3.0", "infection/infection": "^0.8.1",
"phpunit/phpcov": "^4.0", "phpstan/phpstan": "0.9",
"phpunit/phpunit": "^6.0", "phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.0",
"slevomat/coding-standard": "^4.0", "slevomat/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.1 <3.2", "squizlabs/php_codesniffer": "^3.1 <3.2",
"symfony/dotenv": "^3.4", "symfony/dotenv": "^4.0",
"symfony/var-dumper": "^3.0", "symfony/var-dumper": "^4.0",
"zendframework/zend-expressive-tooling": "^0.4" "zendframework/zend-component-installer": "^2.1",
"zendframework/zend-expressive-tooling": "^1.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -84,8 +86,10 @@
"scripts": { "scripts": {
"check": [ "check": [
"@cs", "@cs",
"@stan",
"@test", "@test",
"@func-test" "@func-test",
"@infect"
], ],
"cs": "phpcs", "cs": "phpcs",
"cs-fix": "phpcbf", "cs-fix": "phpcbf",
@@ -97,13 +101,17 @@
"@test", "@test",
"@func-test", "@func-test",
"phpcov merge build --html build/html" "phpcov merge build --html build/html"
] ],
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
"infect": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2",
"infect-show": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2 --show-mutations",
"expressive": "expressive"
}, },
"config": { "config": {
"process-timeout": 0, "process-timeout": 0,
"sort-packages": true, "sort-packages": true,
"platform": { "platform": {
"php": "7.0.8" "php": "7.1.8"
} }
} }
} }

View File

@@ -5,26 +5,24 @@ use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory
use Zend\Expressive; use Zend\Expressive;
use Zend\Expressive\Container; use Zend\Expressive\Container;
use Zend\Expressive\Helper; use Zend\Expressive\Helper;
use Zend\Expressive\Middleware; use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Plates;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\ServiceManager\Factory\InvokableFactory; use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [ return [
'dependencies' => [ 'dependencies' => [
'factories' => [ 'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class, ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
Template\TemplateRendererInterface::class => Plates\PlatesRendererFactory::class,
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
ErrorHandler::class => Container\ErrorHandlerFactory::class,
Middleware\ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class, Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class, Helper\ServerUrlHelper::class => InvokableFactory::class,
], ],
'delegators' => [
Expressive\Application::class => [
Container\ApplicationConfigInjectionDelegator::class,
],
],
], ],
]; ];

View File

@@ -2,6 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware; use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware; use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware; use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
@@ -15,6 +16,7 @@ return [
'pre-routing' => [ 'pre-routing' => [
'middleware' => [ 'middleware' => [
ErrorHandler::class, ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
LocaleMiddleware::class, LocaleMiddleware::class,
], ],
'priority' => 11, 'priority' => 11,
@@ -29,7 +31,7 @@ return [
'routing' => [ 'routing' => [
'middleware' => [ 'middleware' => [
Expressive\Application::ROUTING_MIDDLEWARE, Expressive\Router\Middleware\RouteMiddleware::class,
], ],
'priority' => 10, 'priority' => 10,
], ],
@@ -38,7 +40,7 @@ return [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
CrossDomainMiddleware::class, CrossDomainMiddleware::class,
Expressive\Middleware\ImplicitOptionsMiddleware::class, Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
BodyParserMiddleware::class, BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class, CheckAuthenticationMiddleware::class,
], ],
@@ -47,7 +49,8 @@ return [
'post-routing' => [ 'post-routing' => [
'middleware' => [ 'middleware' => [
Expressive\Application::DISPATCH_MIDDLEWARE, Expressive\Router\Middleware\DispatchMiddleware::class,
NotFoundHandler::class,
], ],
'priority' => 1, 'priority' => 1,
], ],

View File

@@ -7,6 +7,7 @@ use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core; use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest; use Shlinkio\Shlink\Rest;
use Zend\ConfigAggregator; use Zend\ConfigAggregator;
use Zend\Expressive;
/** /**
* 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``.
@@ -18,6 +19,11 @@ use Zend\ConfigAggregator;
*/ */
return (new ConfigAggregator\ConfigAggregator([ return (new ConfigAggregator\ConfigAggregator([
Expressive\ConfigProvider::class,
Expressive\Router\ConfigProvider::class,
Expressive\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class,
Expressive\Helper\ConfigProvider::class,
ExpressiveErrorHandler\ConfigProvider::class, ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class, Common\ConfigProvider::class,
Core\ConfigProvider::class, Core\ConfigProvider::class,

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

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

View File

@@ -3,11 +3,23 @@
"properties": { "properties": {
"currentPage": { "currentPage": {
"type": "integer", "type": "integer",
"description": "The number of current page being displayed." "description": "The number of current page."
}, },
"pagesCount": { "pagesCount": {
"type": "integer", "type": "integer",
"description": "The total number of pages that can be displayed." "description": "The total number of pages that can be obtained."
},
"itemsPerPage": {
"type": "integer",
"description": "The number of items for every page."
},
"itemsInCurrentPage": {
"type": "integer",
"description": "The number of items in current page (could be smaller than itemsPerPage)."
},
"totalItems": {
"type": "integer",
"description": "The total number of items among all pages."
} }
} }
} }

View File

@@ -1,7 +0,0 @@
{
"name": "Authorization",
"in": "header",
"description": "The authorization token with Bearer type",
"required": true,
"type": "string"
}

View File

@@ -5,24 +5,39 @@
], ],
"summary": "Perform authentication", "summary": "Perform authentication",
"description": "Performs an authentication", "description": "Performs an authentication",
"parameters": [ "requestBody": {
{ "description": "Request body.",
"name": "apiKey", "required": true,
"in": "formData", "content": {
"description": "The API key to authenticate with", "application/json": {
"required": true, "schema": {
"type": "string" "type": "object",
"required": [
"apiKey"
],
"properties": {
"apiKey": {
"description": "The API key to authenticate with",
"type": "string"
}
}
}
}
} }
], },
"responses": { "responses": {
"200": { "200": {
"description": "The authentication worked.", "description": "The authentication worked.",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"token": { "type": "object",
"type": "string", "properties": {
"description": "The authentication token that needs to be sent in the Authorization header" "token": {
"type": "string",
"description": "The authentication token that needs to be sent in the Authorization header"
}
}
} }
} }
}, },
@@ -34,20 +49,32 @@
}, },
"400": { "400": {
"description": "An API key was not provided.", "description": "An API key was not provided.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"401": { "401": {
"description": "The API key is incorrect, is disabled or has expired.", "description": "The API key is incorrect, is disabled or has expired.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

@@ -11,59 +11,73 @@
"in": "query", "in": "query",
"description": "The page to be displayed. Defaults to 1", "description": "The page to be displayed. Defaults to 1",
"required": false, "required": false,
"type": "integer" "schema": {
"type": "integer"
}
}, },
{ {
"name": "searchTerm", "name": "searchTerm",
"in": "query", "in": "query",
"description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)", "description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)",
"required": false, "required": false,
"type": "string" "schema": {
"type": "string"
}
}, },
{ {
"name": "tags", "name": "tags",
"in": "query", "in": "query",
"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", "schema": {
"items": { "type": "array",
"type": "string" "items": {
"type": "string"
}
} }
}, },
{ {
"name": "orderBy", "name": "orderBy",
"in": "query", "in": "query",
"description": "The field from which you want to order the result. (Since v1.3.0)", "description": "The field from which you want to order the result. (Since v1.3.0)",
"enum": [
"originalUrl",
"shortCode",
"dateCreated",
"visits"
],
"required": false, "required": false,
"type": "string" "schema": {
}, "type": "string",
"enum": [
"originalUrl",
"shortCode",
"dateCreated",
"visits"
]
}
}
],
"security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The list of short URLs", "description": "The list of short URLs",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"shortUrls": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "shortUrls": {
"type": "array", "type": "object",
"items": { "properties": {
"$ref": "../definitions/ShortUrl.json" "data": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrl.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
} }
},
"pagination": {
"$ref": "../definitions/Pagination.json"
} }
} }
} }
@@ -102,7 +116,10 @@
], ],
"pagination": { "pagination": {
"currentPage": 5, "currentPage": 5,
"pagesCount": 12 "pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
} }
} }
} }
@@ -110,8 +127,12 @@
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }
@@ -123,87 +144,97 @@
], ],
"summary": "Create short URL", "summary": "Create short URL",
"description": "Creates a new short code", "description": "Creates a new short code",
"parameters": [ "security": [
{ {
"name": "longUrl", "Bearer": []
"in": "formData",
"description": "The URL to parse",
"required": true,
"type": "string"
},
{
"name": "tags",
"in": "formData",
"description": "The URL to parse",
"required": false,
"type": "array",
"items": {
"type": "string"
}
},
{
"name": "validSince",
"in": "formData",
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"required": false,
"type": "string"
},
{
"name": "validUntil",
"in": "formData",
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"required": false,
"type": "string"
},
{
"name": "customSlug",
"in": "formData",
"description": "A unique custom slug to be used instead of the generated short code",
"required": false,
"type": "string"
},
{
"name": "maxVisits",
"in": "formData",
"description": "The maximum number of allowed visits for this short code",
"required": false,
"type": "number"
},
{
"$ref": "../parameters/Authorization.json"
} }
], ],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"longUrl"
],
"properties": {
"longUrl": {
"description": "The URL to parse",
"type": "string"
},
"tags": {
"description": "The URL to parse",
"type": "array",
"items": {
"type": "string"
}
},
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string"
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string"
},
"customSlug": {
"description": "A unique custom slug to be used instead of the generated short code",
"type": "string"
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
}
}
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "The result of parsing the long URL", "description": "The result of parsing the long URL",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"longUrl": { "type": "object",
"type": "string", "properties": {
"description": "The original long URL that has been parsed" "longUrl": {
}, "type": "string",
"shortUrl": { "description": "The original long URL that has been parsed"
"type": "string", },
"description": "The generated short URL" "shortUrl": {
}, "type": "string",
"shortCode": { "description": "The generated short URL"
"type": "string", },
"description": "the short code that is being used in the short URL" "shortCode": {
"type": "string",
"description": "the short code that is being used in the short URL"
}
}
} }
} }
} }
}, },
"400": { "400": {
"description": "The long URL was not provided or is invalid.", "description": "The long URL was not provided or is invalid.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

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

View File

@@ -9,23 +9,31 @@
{ {
"name": "shortCode", "name": "shortCode",
"in": "path", "in": "path",
"type": "string",
"description": "The short code to resolve.", "description": "The short code to resolve.",
"required": true "required": true,
}, "schema": {
"type": "string"
}
}
],
"security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The long URL behind a short code.", "description": "The long URL behind a short code.",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"longUrl": { "type": "object",
"type": "string", "properties": {
"description": "The original long URL behind the short code." "longUrl": {
"type": "string",
"description": "The original long URL behind the short code."
}
}
} }
} }
}, },
@@ -37,20 +45,32 @@
}, },
"400": { "400": {
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.", "description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"404": { "404": {
"description": "No URL was found for provided short code.", "description": "No URL was found for provided short code.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }
@@ -66,33 +86,41 @@
{ {
"name": "shortCode", "name": "shortCode",
"in": "path", "in": "path",
"type": "string",
"description": "The short code to edit.", "description": "The short code to edit.",
"required": true "required": true,
}, "schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string"
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string"
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
}
}
}
}
}
},
"security": [
{ {
"name": "validSince", "Bearer": []
"in": "formData",
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"required": false,
"type": "string"
},
{
"name": "validUntil",
"in": "formData",
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"required": false,
"type": "string"
},
{
"name": "maxVisits",
"in": "formData",
"description": "The maximum number of allowed visits for this short code",
"required": false,
"type": "number"
},
{
"$ref": "../parameters/Authorization.json"
} }
], ],
"responses": { "responses": {
@@ -101,20 +129,32 @@
}, },
"400": { "400": {
"description": "Provided meta arguments are invalid.", "description": "Provided meta arguments are invalid.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"404": { "404": {
"description": "No short URL was found for provided short code.", "description": "No short URL was found for provided short code.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

@@ -10,34 +10,55 @@
{ {
"name": "shortCode", "name": "shortCode",
"in": "path", "in": "path",
"type": "string",
"description": "The shortCode in which we want to edit tags.", "description": "The shortCode in which we want to edit tags.",
"required": true "required": true,
}, "schema": {
{
"name": "tags",
"in": "formData",
"type": "array",
"items": {
"type": "string" "type": "string"
}, }
"description": "The list of tags to set to the short URL.", }
"required": true ],
}, "requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"tags"
],
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "The list of tags to set to the short URL."
}
}
}
}
}
},
"security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "List of tags.", "description": "List of tags.",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"tags": { "type": "object",
"type": "array", "properties": {
"items": { "tags": {
"type": "string" "type": "array",
"items": {
"type": "string"
}
}
} }
} }
} }
@@ -53,20 +74,32 @@
}, },
"400": { "400": {
"description": "The request body does not contain a \"tags\" param with array type.", "description": "The request body does not contain a \"tags\" param with array type.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"404": { "404": {
"description": "No short URL was found for provided short code.", "description": "No short URL was found for provided short code.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

@@ -10,27 +10,35 @@
{ {
"name": "shortCode", "name": "shortCode",
"in": "path", "in": "path",
"type": "string",
"description": "The shortCode from which we want to get the visits.", "description": "The shortCode from which we want to get the visits.",
"required": true "required": true,
}, "schema": {
"type": "string"
}
}
],
"security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "List of visits.", "description": "List of visits.",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"visits": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "visits": {
"type": "array", "type": "object",
"items": { "properties": {
"$ref": "../definitions/Visit.json" "data": {
"type": "array",
"items": {
"$ref": "../definitions/Visit.json"
}
}
} }
} }
} }
@@ -66,14 +74,22 @@
}, },
"404": { "404": {
"description": "The short code does not belong to any short URL.", "description": "The short code does not belong to any short URL.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

@@ -5,24 +5,28 @@
], ],
"summary": "List existing tags", "summary": "List existing tags",
"description": "Returns the list of all tags used in any short URL, ordered by name", "description": "Returns the list of all tags used in any short URL, ordered by name",
"parameters": [ "security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The list of tags", "description": "The list of tags",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"tags": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "tags": {
"type": "array", "type": "object",
"items": { "properties": {
"type": "string" "data": {
"type": "array",
"items": {
"type": "string"
}
}
} }
} }
} }
@@ -44,8 +48,12 @@
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }
@@ -57,31 +65,51 @@
], ],
"summary": "Create tags", "summary": "Create tags",
"description": "Provided a list of tags, creates all that do not yet exist", "description": "Provided a list of tags, creates all that do not yet exist",
"parameters": [ "security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
},
{
"name": "tags[]",
"in": "formData",
"description": "The list of tag names to create",
"required": true,
"type": "array"
} }
], ],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"tags"
],
"properties": {
"tags": {
"description": "The list of tag names to create",
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"responses": { "responses": {
"200": { "200": {
"description": "The list of tags", "description": "The list of tags",
"schema": { "content": {
"type": "object", "application/json": {
"properties": { "schema": {
"tags": {
"type": "object", "type": "object",
"properties": { "properties": {
"data": { "tags": {
"type": "array", "type": "object",
"items": { "properties": {
"type": "string" "data": {
"type": "array",
"items": {
"type": "string"
}
}
} }
} }
} }
@@ -103,8 +131,12 @@
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }
@@ -116,45 +148,68 @@
], ],
"summary": "Rename tag", "summary": "Rename tag",
"description": "Renames one existing tag", "description": "Renames one existing tag",
"parameters": [ "security": [
{ {
"$ref": "../parameters/Authorization.json" "Bearer": []
},
{
"name": "oldName",
"in": "formData",
"description": "Current name of the tag",
"required": true,
"type": "string"
},
{
"name": "newName",
"in": "formData",
"description": "New name of the tag",
"required": true,
"type": "string"
} }
], ],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"oldName",
"newName"
],
"properties": {
"oldName": {
"description": "Current name of the tag",
"type": "string"
},
"newName": {
"description": "New name of the tag",
"type": "string"
}
}
}
}
}
},
"responses": { "responses": {
"204": { "204": {
"description": "The tag has been properly renamed" "description": "The tag has been properly renamed"
}, },
"400": { "400": {
"description": "You have not provided either the oldName or the newName params.", "description": "You have not provided either the oldName or the newName params.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"404": { "404": {
"description": "There's no tag found with the name provided in oldName param.", "description": "There's no tag found with the name provided in oldName param.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }
@@ -167,15 +222,22 @@
"summary": "Delete tags", "summary": "Delete tags",
"description": "Deletes provided list of tags", "description": "Deletes provided list of tags",
"parameters": [ "parameters": [
{
"$ref": "../parameters/Authorization.json"
},
{ {
"name": "tags[]", "name": "tags[]",
"in": "query", "in": "query",
"description": "The names of the tags to delete", "description": "The names of the tags to delete",
"required": true, "required": true,
"type": "array" "schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"security": [
{
"Bearer": []
} }
], ],
"responses": { "responses": {
@@ -184,8 +246,12 @@
}, },
"500": { "500": {
"description": "Unexpected error.", "description": "Unexpected error.",
"schema": { "content": {
"$ref": "../definitions/Error.json" "application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
} }
} }
} }

View File

@@ -1,23 +1,37 @@
{ {
"swagger": "2.0", "openapi": "3.0.0",
"info": { "info": {
"title": "Shlink", "title": "Shlink",
"description": "Shlink, the self-hosted URL shortener", "description": "Shlink, the self-hosted URL shortener",
"version": "1.0" "version": "1.0"
}, },
"schemes": [
"http", "servers": [
"https" {
], "url": "{schema}://{server}/rest",
"basePath": "/rest", "variables": {
"produces": [ "schema": {
"application/json" "default": "https",
], "enum": ["https", "http"]
"consumes": [ },
"application/x-www-form-urlencoded", "server": {
"application/json" "default": ""
}
}
}
], ],
"components": {
"securitySchemes": {
"Bearer": {
"description": "The JWT identifying a previously logged API key",
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": { "paths": {
"/v1/authenticate": { "/v1/authenticate": {
"$ref": "paths/v1_authenticate.json" "$ref": "paths/v1_authenticate.json"
@@ -26,6 +40,9 @@
"/v1/short-codes": { "/v1/short-codes": {
"$ref": "paths/v1_short-codes.json" "$ref": "paths/v1_short-codes.json"
}, },
"/v1/short-codes/shorten": {
"$ref": "paths/v1_short-codes_shorten.json"
},
"/v1/short-codes/{shortCode}": { "/v1/short-codes/{shortCode}": {
"$ref": "paths/v1_short-codes_{shortCode}.json" "$ref": "paths/v1_short-codes_{shortCode}.json"
}, },

18
infection.json Normal file
View File

@@ -0,0 +1,18 @@
{
"source": {
"directories": [
"module/*/src"
],
"excludes": []
},
"timeout": 10,
"logs": {
"text": "build/infection/infection-log.txt",
"summary": "build/infection/summary-log.txt",
"debug": "build/infection/debug-log.txt"
},
"tmpDir": "build/infection/temp",
"phpUnit": {
"configDir": "."
}
}

View File

@@ -18,6 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Zend\Config\Writer\WriterInterface; use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command class InstallCommand extends Command
@@ -48,6 +49,14 @@ class InstallCommand extends Command
* @var bool * @var bool
*/ */
private $isUpdate; private $isUpdate;
/**
* @var PhpExecutableFinder
*/
private $phpFinder;
/**
* @var string|bool
*/
private $phpBinary;
/** /**
* InstallCommand constructor. * InstallCommand constructor.
@@ -55,22 +64,25 @@ class InstallCommand extends Command
* @param Filesystem $filesystem * @param Filesystem $filesystem
* @param ConfigCustomizerManagerInterface $configCustomizers * @param ConfigCustomizerManagerInterface $configCustomizers
* @param bool $isUpdate * @param bool $isUpdate
* @param PhpExecutableFinder|null $phpFinder
* @throws LogicException * @throws LogicException
*/ */
public function __construct( public function __construct(
WriterInterface $configWriter, WriterInterface $configWriter,
Filesystem $filesystem, Filesystem $filesystem,
ConfigCustomizerManagerInterface $configCustomizers, ConfigCustomizerManagerInterface $configCustomizers,
$isUpdate = false bool $isUpdate = false,
PhpExecutableFinder $phpFinder = null
) { ) {
parent::__construct(); parent::__construct();
$this->configWriter = $configWriter; $this->configWriter = $configWriter;
$this->isUpdate = $isUpdate; $this->isUpdate = $isUpdate;
$this->filesystem = $filesystem; $this->filesystem = $filesystem;
$this->configCustomizers = $configCustomizers; $this->configCustomizers = $configCustomizers;
$this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
} }
public function configure() protected function configure(): void
{ {
$this $this
->setName('shlink:install') ->setName('shlink:install')
@@ -84,7 +96,7 @@ class InstallCommand extends Command
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface * @throws NotFoundExceptionInterface
*/ */
public function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output): void
{ {
$this->io = new SymfonyStyle($input, $output); $this->io = new SymfonyStyle($input, $output);
@@ -133,8 +145,8 @@ class InstallCommand extends Command
// If current command is not update, generate database // If current command is not update, generate database
if (! $this->isUpdate) { if (! $this->isUpdate) {
$this->io->write('Initializing database...'); $this->io->write('Initializing database...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/bin/doctrine.php orm:schema-tool:create', 'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
'Error generating database.', 'Error generating database.',
$output $output
)) { )) {
@@ -144,8 +156,8 @@ class InstallCommand extends Command
// Run database migrations // Run database migrations
$this->io->write('Updating database...'); $this->io->write('Updating database...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/bin/doctrine-migrations migrations:migrate', 'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
'Error updating database.', 'Error updating database.',
$output $output
)) { )) {
@@ -154,8 +166,8 @@ class InstallCommand extends Command
// Generate proxies // Generate proxies
$this->io->write('Generating proxies...'); $this->io->write('Generating proxies...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/bin/doctrine.php orm:generate-proxies', 'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
'Error generating proxies.', 'Error generating proxies.',
$output $output
)) { )) {
@@ -213,23 +225,27 @@ class InstallCommand extends Command
* @throws LogicException * @throws LogicException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
private function runCommand($command, $errorMessage, OutputInterface $output): bool private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
{ {
if ($this->processHelper === null) { if ($this->processHelper === null) {
$this->processHelper = $this->getHelper('process'); $this->processHelper = $this->getHelper('process');
} }
$process = $this->processHelper->run($output, $command); if ($this->phpBinary === null) {
$this->phpBinary = $this->phpFinder->find(false) ?: 'php';
}
$this->io->writeln('Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"');
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
if ($process->isSuccessful()) { if ($process->isSuccessful()) {
$this->io->writeln(' <info>Success!</info>'); $this->io->writeln(' <info>Success!</info>');
return true; return true;
} }
if ($this->io->isVerbose()) { if (! $this->io->isVerbose()) {
return false; $this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
} }
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
return false; return false;
} }
} }

View File

@@ -15,6 +15,7 @@ use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Zend\Config\Writer\WriterInterface; use Zend\Config\Writer\WriterInterface;
@@ -55,6 +56,9 @@ class InstallCommandTest extends TestCase
$configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class); $configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class);
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal()); $configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal());
$finder = $this->prophesize(PhpExecutableFinder::class);
$finder->find(false)->willReturn('php');
$app = new Application(); $app = new Application();
$helperSet = $app->getHelperSet(); $helperSet = $app->getHelperSet();
$helperSet->set($processHelper->reveal()); $helperSet->set($processHelper->reveal());
@@ -62,7 +66,9 @@ class InstallCommandTest extends TestCase
$this->command = new InstallCommand( $this->command = new InstallCommand(
$this->configWriter->reveal(), $this->configWriter->reveal(),
$this->filesystem->reveal(), $this->filesystem->reveal(),
$configCustomizers->reveal() $configCustomizers->reveal(),
false,
$finder->reveal()
); );
$app->add($this->command); $app->add($this->command);

View File

@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Common\Factory;
use Interop\Container\ContainerInterface; use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException; use Interop\Container\Exception\ContainerException;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware; use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\Exception\ServiceNotCreatedException; use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException; use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface; use Zend\ServiceManager\Factory\FactoryInterface;
@@ -27,6 +27,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
*/ */
public function __invoke(ContainerInterface $container, $requestedName, array $options = null) public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{ {
return new ImplicitOptionsMiddleware(new EmptyResponse()); return new ImplicitOptionsMiddleware(function () {
return new EmptyResponse();
});
} }
} }

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Middleware; namespace Shlinkio\Shlink\Common\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 Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
class LocaleMiddleware implements MiddlewareInterface class LocaleMiddleware implements MiddlewareInterface
@@ -32,15 +32,15 @@ class LocaleMiddleware implements MiddlewareInterface
* *
* @return Response * @return Response
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, DelegateInterface $delegate): Response
{ {
if (! $request->hasHeader('Accept-Language')) { if (! $request->hasHeader('Accept-Language')) {
return $delegate->process($request); return $delegate->handle($request);
} }
$locale = $request->getHeaderLine('Accept-Language'); $locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale)); $this->translator->setLocale($this->normalizeLocale($locale));
return $delegate->process($request); return $delegate->handle($request);
} }
/** /**

View File

@@ -8,7 +8,7 @@ use Zend\Paginator\Adapter\AdapterInterface;
class PaginableRepositoryAdapter implements AdapterInterface class PaginableRepositoryAdapter implements AdapterInterface
{ {
const ITEMS_PER_PAGE = 10; public const ITEMS_PER_PAGE = 10;
/** /**
* @var PaginableRepositoryInterface * @var PaginableRepositoryInterface
@@ -34,7 +34,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
$orderBy = null $orderBy = null
) { ) {
$this->paginableRepository = $paginableRepository; $this->paginableRepository = $paginableRepository;
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null; $this->searchTerm = $searchTerm !== null ? \trim(\strip_tags($searchTerm)) : null;
$this->orderBy = $orderBy; $this->orderBy = $orderBy;
$this->tags = $tags; $this->tags = $tags;
} }
@@ -46,7 +46,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
* @param int $itemCountPerPage Number of items per page * @param int $itemCountPerPage Number of items per page
* @return array * @return array
*/ */
public function getItems($offset, $itemCountPerPage) public function getItems($offset, $itemCountPerPage): array
{ {
return $this->paginableRepository->findList( return $this->paginableRepository->findList(
$itemCountPerPage, $itemCountPerPage,
@@ -66,7 +66,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
* The return value is cast to an integer. * The return value is cast to an integer.
* @since 5.1.0 * @since 5.1.0
*/ */
public function count() public function count(): int
{ {
return $this->paginableRepository->countList($this->searchTerm, $this->tags); return $this->paginableRepository->countList($this->searchTerm, $this->tags);
} }

View File

@@ -8,13 +8,16 @@ use Zend\Stdlib\ArrayUtils;
trait PaginatorUtilsTrait trait PaginatorUtilsTrait
{ {
protected function serializePaginator(Paginator $paginator) protected function serializePaginator(Paginator $paginator): array
{ {
return [ return [
'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()), 'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()),
'pagination' => [ 'pagination' => [
'currentPage' => $paginator->getCurrentPageNumber(), 'currentPage' => $paginator->getCurrentPageNumber(),
'pagesCount' => $paginator->count(), 'pagesCount' => $paginator->count(),
'itemsPerPage' => $paginator->getItemCountPerPage(),
'itemsInCurrentPage' => $paginator->getCurrentItemCount(),
'totalItems' => $paginator->getTotalItemCount(),
], ],
]; ];
} }
@@ -25,7 +28,7 @@ trait PaginatorUtilsTrait
* @param Paginator $paginator * @param Paginator $paginator
* @return bool * @return bool
*/ */
protected function isLastPage(Paginator $paginator) protected function isLastPage(Paginator $paginator): bool
{ {
return $paginator->getCurrentPageNumber() >= $paginator->count(); return $paginator->getCurrentPageNumber() >= $paginator->count();
} }

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Response;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
class PixelResponse extends Response
{
private const BASE_64_IMAGE = 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==';
private const CONTENT_TYPE = 'image/gif';
public function __construct(int $status = 200, array $headers = [])
{
$headers['content-type'] = self::CONTENT_TYPE;
parent::__construct($this->createBody(), $status, $headers);
}
/**
* Create the message body.
*
* @return StreamInterface
*/
private function createBody(): StreamInterface
{
$body = new Stream('php://temp', 'wb+');
$body->write(\base64_decode(self::BASE_64_IMAGE));
$body->rewind();
return $body;
}
}

View File

@@ -3,40 +3,17 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\DbUnit; namespace ShlinkioTest\Shlink\Common\DbUnit;
use Doctrine\DBAL\Driver\PDOConnection;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\DbUnit\Database\Connection as DbConn; use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\DataSet\IDataSet as DataSet;
use PHPUnit\DbUnit\TestCase;
abstract class DatabaseTestCase extends TestCase abstract class DatabaseTestCase extends TestCase
{ {
const ENTITIES_TO_EMPTY = []; protected const ENTITIES_TO_EMPTY = [];
/** /**
* @var EntityManagerInterface * @var EntityManagerInterface
*/ */
public static $em; public static $em;
/**
* @var DbConn
*/
private static $conn;
public function getConnection(): DbConn
{
if (isset(self::$conn)) {
return self::$conn;
}
/** @var PDOConnection $pdo */
$pdo = static::$em->getConnection()->getWrappedConnection();
return self::$conn = $this->createDefaultDBConnection($pdo, static::$em->getConnection()->getDatabase());
}
public function getDataSet(): DataSet
{
return $this->createArrayDataSet([]);
}
protected function getEntityManager(): EntityManagerInterface protected function getEntityManager(): EntityManagerInterface
{ {

View File

@@ -6,7 +6,7 @@ namespace ShlinkioTest\Shlink\Common\Factory;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory; use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware; use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\ServiceManager\ServiceManager; use Zend\ServiceManager\ServiceManager;
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
@@ -38,8 +38,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
$instance = $this->factory->__invoke(new ServiceManager(), ''); $instance = $this->factory->__invoke(new ServiceManager(), '');
$ref = new \ReflectionObject($instance); $ref = new \ReflectionObject($instance);
$prop = $ref->getProperty('response'); $prop = $ref->getProperty('responseFactory');
$prop->setAccessible(true); $prop->setAccessible(true);
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance)); $this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance)());
} }
} }

View File

@@ -32,7 +32,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->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal()); $this->middleware->process(ServerRequestFactory::fromGlobals(), TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());
} }
@@ -43,7 +43,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->process($request, TestUtils::createDelegateMock()->reveal()); $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals('es', $this->translator->getLocale()); $this->assertEquals('es', $this->translator->getLocale());
} }
@@ -52,7 +52,7 @@ class LocaleMiddlewareTest extends TestCase
*/ */
public function localeGetsNormalized() public function localeGetsNormalized()
{ {
$delegate = TestUtils::createDelegateMock(); $delegate = TestUtils::createReqHandlerMock();
$this->assertEquals('ru', $this->translator->getLocale()); $this->assertEquals('ru', $this->translator->getLocale());

View File

@@ -3,22 +3,22 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Util; namespace ShlinkioTest\Shlink\Common\Util;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophet; use Prophecy\Prophet;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
class TestUtils class TestUtils
{ {
private static $prophet; private static $prophet;
public static function createDelegateMock(ResponseInterface $response = null, RequestInterface $request = null) public static function createReqHandlerMock(ResponseInterface $response = null, RequestInterface $request = null)
{ {
$argument = $request ?: Argument::any(); $argument = $request ?: Argument::any();
$delegate = static::getProphet()->prophesize(DelegateInterface::class); $delegate = static::getProphet()->prophesize(RequestHandlerInterface::class);
$delegate->process($argument)->willReturn($response ?: new Response()); $delegate->handle($argument)->willReturn($response ?: new Response());
return $delegate; return $delegate;
} }

View File

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

View File

@@ -13,6 +13,12 @@ return [
'middleware' => Action\RedirectAction::class, 'middleware' => Action\RedirectAction::class,
'allowed_methods' => ['GET'], 'allowed_methods' => ['GET'],
], ],
[
'name' => 'pixel-tracking',
'path' => '/{shortCode}/track',
'middleware' => Action\PixelAction::class,
'allowed_methods' => ['GET'],
],
[ [
'name' => 'short-url-qr-code', 'name' => 'short-url-qr-code',
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]', 'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
abstract class AbstractTrackingAction implements MiddlewareInterface
{
use ErrorResponseBuilderTrait;
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var VisitsTrackerInterface
*/
private $visitTracker;
/**
* @var AppOptions
*/
private $appOptions;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(
UrlShortenerInterface $urlShortener,
VisitsTrackerInterface $visitTracker,
AppOptions $appOptions,
LoggerInterface $logger = null
) {
$this->urlShortener = $urlShortener;
$this->visitTracker = $visitTracker;
$this->appOptions = $appOptions;
$this->logger = $logger ?: new NullLogger();
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$shortCode = $request->getAttribute('shortCode', '');
$query = $request->getQueryParams();
$disableTrackParam = $this->appOptions->getDisableTrackParam();
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
// Track visit to this short code
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
$this->visitTracker->track($shortCode, $request);
}
return $this->createResp($longUrl);
} catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('An error occurred while tracking short code.' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $handler);
}
}
abstract protected function createResp(string $longUrl): ResponseInterface;
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Psr\Http\Message\ResponseInterface;
use Shlinkio\Shlink\Common\Response\PixelResponse;
class PixelAction extends AbstractTrackingAction
{
protected function createResp(string $longUrl): ResponseInterface
{
return new PixelResponse();
}
}

View File

@@ -3,10 +3,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
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\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException; use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface; use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait; use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
@@ -28,11 +30,19 @@ class PreviewAction implements MiddlewareInterface
* @var UrlShortenerInterface * @var UrlShortenerInterface
*/ */
private $urlShortener; private $urlShortener;
/**
* @var LoggerInterface
*/
private $logger;
public function __construct(PreviewGeneratorInterface $previewGenerator, UrlShortenerInterface $urlShortener) public function __construct(
{ PreviewGeneratorInterface $previewGenerator,
UrlShortenerInterface $urlShortener,
LoggerInterface $logger = null
) {
$this->previewGenerator = $previewGenerator; $this->previewGenerator = $previewGenerator;
$this->urlShortener = $urlShortener; $this->urlShortener = $urlShortener;
$this->logger = $logger ?: new NullLogger();
} }
/** /**
@@ -40,11 +50,11 @@ class PreviewAction implements MiddlewareInterface
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
@@ -52,12 +62,9 @@ class PreviewAction implements MiddlewareInterface
$url = $this->urlShortener->shortCodeToUrl($shortCode); $url = $this->urlShortener->shortCodeToUrl($shortCode);
$imagePath = $this->previewGenerator->generatePreview($url); $imagePath = $this->previewGenerator->generatePreview($url);
return $this->generateImageResponse($imagePath); return $this->generateImageResponse($imagePath);
} catch (InvalidShortCodeException $e) { } catch (InvalidShortCodeException | EntityDoesNotExistException | PreviewGenerationException $e) {
return $this->buildErrorResponse($request, $delegate); $this->logger->warning('An error occurred while generating preview image.' . PHP_EOL . $e);
} catch (EntityDoesNotExistException $e) { return $this->buildErrorResponse($request, $handler);
return $this->buildErrorResponse($request, $delegate);
} catch (PreviewGenerationException $e) {
return $this->buildErrorResponse($request, $delegate);
} }
} }
} }

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
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\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Common\Response\QrCodeResponse;
@@ -15,6 +15,7 @@ use Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Zend\Expressive\Router\Exception\RuntimeException;
use Zend\Expressive\Router\RouterInterface; use Zend\Expressive\Router\RouterInterface;
class QrCodeAction implements MiddlewareInterface class QrCodeAction implements MiddlewareInterface
@@ -49,22 +50,21 @@ class QrCodeAction implements MiddlewareInterface
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
* @throws \InvalidArgumentException
* @throws RuntimeException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
// 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 {
$this->urlShortener->shortCodeToUrl($shortCode); $this->urlShortener->shortCodeToUrl($shortCode);
} catch (InvalidShortCodeException $e) { } catch (InvalidShortCodeException | EntityDoesNotExistException $e) {
$this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e); $this->logger->warning('An error occurred while creating QR code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $delegate); return $this->buildErrorResponse($request, $handler);
} catch (EntityDoesNotExistException $e) {
$this->logger->warning('Tried to create a QR code with a not found short code' . PHP_EOL . $e);
return $this->buildErrorResponse($request, $delegate);
} }
$path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]); $path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]);
@@ -80,7 +80,7 @@ class QrCodeAction implements MiddlewareInterface
* @param Request $request * @param Request $request
* @return int * @return int
*/ */
protected function getSizeParam(Request $request) private function getSizeParam(Request $request): int
{ {
$size = (int) $request->getAttribute('size', 300); $size = (int) $request->getAttribute('size', 300);
if ($size < 50) { if ($size < 50) {

View File

@@ -3,75 +3,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action; namespace Shlinkio\Shlink\Core\Action;
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 Shlinkio\Shlink\Core\Action\Util\ErrorResponseBuilderTrait;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Zend\Diactoros\Response\RedirectResponse; use Zend\Diactoros\Response\RedirectResponse;
class RedirectAction implements MiddlewareInterface class RedirectAction extends AbstractTrackingAction
{ {
use ErrorResponseBuilderTrait; protected function createResp(string $longUrl): Response
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var VisitsTrackerInterface
*/
private $visitTracker;
/**
* @var AppOptions
*/
private $appOptions;
public function __construct(
UrlShortenerInterface $urlShortener,
VisitsTrackerInterface $visitTracker,
AppOptions $appOptions
) {
$this->urlShortener = $urlShortener;
$this->visitTracker = $visitTracker;
$this->appOptions = $appOptions;
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param Request $request
* @param DelegateInterface $delegate
*
* @return Response
*/
public function process(Request $request, DelegateInterface $delegate)
{ {
$shortCode = $request->getAttribute('shortCode', ''); // Return a redirect response to the long URL.
$query = $request->getQueryParams(); // Use a temporary redirect to make sure browsers always hit the server for analytics purposes
$disableTrackParam = $this->appOptions->getDisableTrackParam(); return new RedirectResponse($longUrl);
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
// Track visit to this short code
if ($disableTrackParam === null || ! \array_key_exists($disableTrackParam, $query)) {
$this->visitTracker->track($shortCode, $request);
}
// Return a redirect response to the long URL.
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
return new RedirectResponse($longUrl);
} catch (InvalidShortCodeException $e) {
return $this->buildErrorResponse($request, $delegate);
} catch (EntityDoesNotExistException $e) {
return $this->buildErrorResponse($request, $delegate);
}
} }
} }

View File

@@ -3,16 +3,18 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action\Util; namespace Shlinkio\Shlink\Core\Action\Util;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Response\NotFoundDelegate; use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
trait ErrorResponseBuilderTrait trait ErrorResponseBuilderTrait
{ {
private function buildErrorResponse(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface private function buildErrorResponse(
{ ServerRequestInterface $request,
$request = $request->withAttribute(NotFoundDelegate::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code'); RequestHandlerInterface $handler
return $delegate->process($request); ): ResponseInterface {
$request = $request->withAttribute(NotFoundHandler::NOT_FOUND_TEMPLATE, 'ShlinkCore::invalid-short-code');
return $handler->handle($request);
} }
} }

View File

@@ -57,7 +57,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return string * @return string
*/ */
public function getReferer() public function getReferer(): string
{ {
return $this->referer; return $this->referer;
} }
@@ -66,7 +66,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $referer * @param string $referer
* @return $this * @return $this
*/ */
public function setReferer($referer) public function setReferer($referer): self
{ {
$this->referer = $referer; $this->referer = $referer;
return $this; return $this;
@@ -75,7 +75,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return \DateTime * @return \DateTime
*/ */
public function getDate() public function getDate(): \DateTime
{ {
return $this->date; return $this->date;
} }
@@ -84,7 +84,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param \DateTime $date * @param \DateTime $date
* @return $this * @return $this
*/ */
public function setDate($date) public function setDate($date): self
{ {
$this->date = $date; $this->date = $date;
return $this; return $this;
@@ -93,7 +93,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return ShortUrl * @return ShortUrl
*/ */
public function getShortUrl() public function getShortUrl(): ShortUrl
{ {
return $this->shortUrl; return $this->shortUrl;
} }
@@ -102,7 +102,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param ShortUrl $shortUrl * @param ShortUrl $shortUrl
* @return $this * @return $this
*/ */
public function setShortUrl($shortUrl) public function setShortUrl($shortUrl): self
{ {
$this->shortUrl = $shortUrl; $this->shortUrl = $shortUrl;
return $this; return $this;
@@ -111,7 +111,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return string * @return string
*/ */
public function getRemoteAddr() public function getRemoteAddr(): string
{ {
return $this->remoteAddr; return $this->remoteAddr;
} }
@@ -120,7 +120,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $remoteAddr * @param string $remoteAddr
* @return $this * @return $this
*/ */
public function setRemoteAddr($remoteAddr) public function setRemoteAddr($remoteAddr): self
{ {
$this->remoteAddr = $remoteAddr; $this->remoteAddr = $remoteAddr;
return $this; return $this;
@@ -129,7 +129,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return string * @return string
*/ */
public function getUserAgent() public function getUserAgent(): string
{ {
return $this->userAgent; return $this->userAgent;
} }
@@ -138,7 +138,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param string $userAgent * @param string $userAgent
* @return $this * @return $this
*/ */
public function setUserAgent($userAgent) public function setUserAgent($userAgent): self
{ {
$this->userAgent = $userAgent; $this->userAgent = $userAgent;
return $this; return $this;
@@ -147,7 +147,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* @return VisitLocation * @return VisitLocation
*/ */
public function getVisitLocation() public function getVisitLocation(): VisitLocation
{ {
return $this->visitLocation; return $this->visitLocation;
} }
@@ -156,7 +156,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
* @param VisitLocation $visitLocation * @param VisitLocation $visitLocation
* @return $this * @return $this
*/ */
public function setVisitLocation($visitLocation) public function setVisitLocation($visitLocation): self
{ {
$this->visitLocation = $visitLocation; $this->visitLocation = $visitLocation;
return $this; return $this;
@@ -165,11 +165,11 @@ class Visit extends AbstractEntity implements \JsonSerializable
/** /**
* Specify data which should be serialized to JSON * Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php * @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>, * @return array data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource. * which is a value of any type other than a resource.
* @since 5.4.0 * @since 5.4.0
*/ */
public function jsonSerialize() public function jsonSerialize(): array
{ {
return [ return [
'referer' => $this->referer, 'referer' => $this->referer,

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Middleware; namespace Shlinkio\Shlink\Core\Middleware;
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 Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response as DiactResp; use Zend\Diactoros\Response as DiactResp;
class QrCodeCacheMiddleware implements MiddlewareInterface class QrCodeCacheMiddleware implements MiddlewareInterface
@@ -27,11 +27,11 @@ class QrCodeCacheMiddleware implements MiddlewareInterface
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$cacheKey = $request->getUri()->getPath(); $cacheKey = $request->getUri()->getPath();
@@ -45,7 +45,7 @@ class QrCodeCacheMiddleware implements MiddlewareInterface
// 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 = $delegate->process($request); $resp = $handler->handle($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

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

View File

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

View File

@@ -25,7 +25,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
$orderBy = null $orderBy = null
): array { ): array {
$qb = $this->createListQueryBuilder($searchTerm, $tags); $qb = $this->createListQueryBuilder($searchTerm, $tags);
$qb->select('s'); $qb->select('DISTINCT s');
// Set limit and offset // Set limit and offset
if ($limit !== null) { if ($limit !== null) {
@@ -47,17 +47,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
protected function processOrderByForList(QueryBuilder $qb, $orderBy) protected function processOrderByForList(QueryBuilder $qb, $orderBy)
{ {
$fieldName = is_array($orderBy) ? key($orderBy) : $orderBy; $fieldName = \is_array($orderBy) ? \key($orderBy) : $orderBy;
$order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC'; $order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
if (in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) { if (\in_array($fieldName, ['visits', 'visitsCount', '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')
->orderBy('totalVisits', $order); ->orderBy('totalVisits', $order);
return array_column($qb->getQuery()->getResult(), 0); return \array_column($qb->getQuery()->getResult(), 0);
} elseif (in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) { } elseif (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
$qb->orderBy('s.' . $fieldName, $order); $qb->orderBy('s.' . $fieldName, $order);
} }
@@ -74,7 +74,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
public function countList(string $searchTerm = null, array $tags = []): int public function countList(string $searchTerm = null, array $tags = []): int
{ {
$qb = $this->createListQueryBuilder($searchTerm, $tags); $qb = $this->createListQueryBuilder($searchTerm, $tags);
$qb->select('COUNT(s)'); $qb->select('COUNT(DISTINCT s)');
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
} }
@@ -92,7 +92,7 @@ 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'); $qb->leftJoin('s.tags', 't');
$conditions = [ $conditions = [
$qb->expr()->like('s.originalUrl', ':searchPattern'), $qb->expr()->like('s.originalUrl', ':searchPattern'),
@@ -102,8 +102,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
// Unpack and apply search conditions // Unpack and apply search conditions
$qb->andWhere($qb->expr()->orX(...$conditions)); $qb->andWhere($qb->expr()->orX(...$conditions));
$searchTerm = '%' . $searchTerm . '%'; $qb->setParameter('searchPattern', '%' . $searchTerm . '%');
$qb->setParameter('searchPattern', $searchTerm);
} }
// Filter by tags if provided // Filter by tags if provided
@@ -119,7 +118,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
* @param string $shortCode * @param string $shortCode
* @return ShortUrl|null * @return ShortUrl|null
*/ */
public function findOneByShortCode(string $shortCode) public function findOneByShortCode(string $shortCode): ?ShortUrl
{ {
$now = new \DateTimeImmutable(); $now = new \DateTimeImmutable();

View File

@@ -4,15 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Response; namespace Shlinkio\Shlink\Core\Response;
use Fig\Http\Message\StatusCodeInterface; use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Expressive\Template\TemplateRendererInterface; use Zend\Expressive\Template\TemplateRendererInterface;
class NotFoundDelegate implements DelegateInterface class NotFoundHandler implements RequestHandlerInterface
{ {
const NOT_FOUND_TEMPLATE = 'notFoundTemplate'; public const NOT_FOUND_TEMPLATE = 'notFoundTemplate';
/** /**
* @var TemplateRendererInterface * @var TemplateRendererInterface
@@ -37,14 +37,14 @@ class NotFoundDelegate implements DelegateInterface
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$accepts = explode(',', $request->getHeaderLine('Accept')); $accepts = \explode(',', $request->getHeaderLine('Accept'));
$accept = array_shift($accepts); $accept = \array_shift($accepts);
$status = StatusCodeInterface::STATUS_NOT_FOUND; $status = StatusCodeInterface::STATUS_NOT_FOUND;
// If the first accepted type is json, return a json response // If the first accepted type is json, return a json response
if (in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) { if (\in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) {
return new Response\JsonResponse([ return new Response\JsonResponse([
'error' => 'NOT_FOUND', 'error' => 'NOT_FOUND',
'message' => 'Not found', 'message' => 'Not found',

View File

@@ -14,7 +14,7 @@ trait TagManagerTrait
* @param string[] $tags * @param string[] $tags
* @return Collections\Collection|Tag[] * @return Collections\Collection|Tag[]
*/ */
protected function tagNamesToEntities(EntityManagerInterface $em, array $tags) private function tagNamesToEntities(EntityManagerInterface $em, array $tags)
{ {
$entities = []; $entities = [];
foreach ($tags as $tagName) { foreach ($tags as $tagName) {
@@ -33,8 +33,8 @@ trait TagManagerTrait
* @param string $tagName * @param string $tagName
* @return string * @return string
*/ */
protected function normalizeTagName($tagName) private function normalizeTagName($tagName): string
{ {
return str_replace(' ', '-', strtolower(trim($tagName))); return \str_replace(' ', '-', \strtolower(\trim($tagName)));
} }
} }

View File

@@ -9,7 +9,7 @@ use Zend\InputFilter\Input;
trait InputFactoryTrait trait InputFactoryTrait
{ {
public function createInput($name, $required = true): Input private function createInput($name, $required = true): Input
{ {
$input = new Input($name); $input = new Input($name);
$input->setRequired($required) $input->setRequired($required)

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Response\PixelResponse;
use Shlinkio\Shlink\Core\Action\PixelAction;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory;
class PixelActionTest extends TestCase
{
/**
* @var RedirectAction
*/
protected $action;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
/**
* @var ObjectProphecy
*/
protected $visitTracker;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->visitTracker = $this->prophesize(VisitsTracker::class);
$this->action = new PixelAction(
$this->urlShortener->reveal(),
$this->visitTracker->reveal(),
new AppOptions()
);
}
/**
* @test
*/
public function imageIsReturned()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn('http://domain.com/foo/bar')
->shouldBeCalledTimes(1);
$this->visitTracker->track(Argument::cetera())->willReturn(null)
->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(PixelResponse::class, $response);
$this->assertEquals(200, $response->getStatusCode());
$this->assertEquals('image/gif', $response->getHeaderLine('content-type'));
}
}

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
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\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@@ -47,8 +47,8 @@ class PreviewActionTest extends TestCase
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->process(Argument::cetera())->shouldBeCalledTimes(1) $delegate->handle(Argument::cetera())->shouldBeCalledTimes(1)
->willReturn(new Response()); ->willReturn(new Response());
$this->action->process( $this->action->process(
@@ -70,7 +70,7 @@ class PreviewActionTest extends TestCase
$resp = $this->action->process( $resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal() TestUtils::createReqHandlerMock()->reveal()
); );
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length')); $this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
@@ -85,9 +85,9 @@ class PreviewActionTest 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); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\QrCodeResponse; use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Action\QrCodeAction; use Shlinkio\Shlink\Core\Action\QrCodeAction;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
@@ -46,8 +46,8 @@ class QrCodeActionTest extends TestCase
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class) $this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->process(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -65,9 +65,9 @@ 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); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process( $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -84,7 +84,7 @@ class QrCodeActionTest extends TestCase
{ {
$shortCode = 'abc123'; $shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn('')->shouldBeCalledTimes(1); $this->urlShortener->shortCodeToUrl($shortCode)->willReturn('')->shouldBeCalledTimes(1);
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->action->process( $resp = $this->action->process(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode), ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
@@ -93,6 +93,6 @@ class QrCodeActionTest extends TestCase
$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); $delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
} }
} }

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action; namespace ShlinkioTest\Shlink\Core\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction; use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Options\AppOptions; use Shlinkio\Shlink\Core\Options\AppOptions;
@@ -57,7 +57,7 @@ class RedirectActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response); $this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());
@@ -76,9 +76,9 @@ class RedirectActionTest extends TestCase
$this->visitTracker->track(Argument::cetera())->willReturn(null) $this->visitTracker->track(Argument::cetera())->willReturn(null)
->shouldNotBeCalled(); ->shouldNotBeCalled();
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process(Argument::any())->willReturn(new Response()); $process = $delegate->handle(Argument::any())->willReturn(new Response());
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$this->action->process($request, $delegate->reveal()); $this->action->process($request, $delegate->reveal());
@@ -100,7 +100,7 @@ class RedirectActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode) $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode)
->withQueryParams(['foobar' => true]); ->withQueryParams(['foobar' => true]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertInstanceOf(Response\RedirectResponse::class, $response); $this->assertInstanceOf(Response\RedirectResponse::class, $response);
$this->assertEquals(302, $response->getStatusCode()); $this->assertEquals(302, $response->getStatusCode());

View File

@@ -5,9 +5,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 Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Psr\Http\Server\RequestHandlerInterface;
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;
@@ -35,8 +35,8 @@ class QrCodeCacheMiddlewareTest extends TestCase
*/ */
public function noCachedPathFallsBackToNextMiddleware() public function noCachedPathFallsBackToNextMiddleware()
{ {
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->process(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1); $delegate->handle(Argument::any())->willReturn(new Response())->shouldBeCalledTimes(1);
$this->middleware->process(ServerRequestFactory::fromGlobals()->withUri( $this->middleware->process(ServerRequestFactory::fromGlobals()->withUri(
new Uri('/foo/bar') new Uri('/foo/bar')
@@ -53,7 +53,7 @@ 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); $delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->middleware->process( $resp = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withUri($uri), ServerRequestFactory::fromGlobals()->withUri($uri),
@@ -64,6 +64,6 @@ class QrCodeCacheMiddlewareTest extends TestCase
$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); $delegate->handle(Argument::any())->shouldHaveBeenCalledTimes(0);
} }
} }

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

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

View File

@@ -5,12 +5,15 @@ namespace Shlinkio\Shlink\Rest\Action;
use Fig\Http\Message\RequestMethodInterface; use Fig\Http\Message\RequestMethodInterface;
use Fig\Http\Message\StatusCodeInterface; use Fig\Http\Message\StatusCodeInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface abstract class AbstractRestAction implements RequestHandlerInterface, RequestMethodInterface, StatusCodeInterface
{ {
protected const ROUTE_PATH = '';
protected const ROUTE_ALLOWED_METHODS = [];
/** /**
* @var LoggerInterface * @var LoggerInterface
*/ */
@@ -20,4 +23,14 @@ abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodI
{ {
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
public static function getRouteDef(array $prevMiddleware = [], array $postMiddleware = []): array
{
return [
'name' => static::class,
'middleware' => \array_merge($prevMiddleware, [static::class], $postMiddleware),
'path' => static::ROUTE_PATH,
'allowed_methods' => static::ROUTE_ALLOWED_METHODS,
];
}
} }

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action;
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;
@@ -16,6 +15,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class AuthenticateAction extends AbstractRestAction class AuthenticateAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/authenticate';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
@@ -43,11 +45,10 @@ class AuthenticateAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @return Response
* @return null|Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function handle(Request $request): Response
{ {
$authData = $request->getParsedBody(); $authData = $request->getParsedBody();
if (! isset($authData['apiKey'])) { if (! isset($authData['apiKey'])) {
@@ -61,7 +62,7 @@ class AuthenticateAction extends AbstractRestAction
// Authenticate using provided API key // Authenticate using provided API key
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']); $apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
if (! isset($apiKey) || ! $apiKey->isValid()) { if ($apiKey === null || ! $apiKey->isValid()) {
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.'),

View File

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

View File

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

View File

@@ -1,15 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Core\Exception; use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
@@ -17,6 +17,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class EditShortCodeAction extends AbstractRestAction class EditShortCodeAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */
@@ -41,12 +44,11 @@ class EditShortCodeAction extends AbstractRestAction
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate): ResponseInterface public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$postData = (array) $request->getParsedBody(); $postData = (array) $request->getParsedBody();
$shortCode = $request->getAttribute('shortCode', ''); $shortCode = $request->getAttribute('shortCode', '');

View File

@@ -1,20 +1,23 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
use 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;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class EditShortcodeTagsAction extends AbstractRestAction class EditShortCodeTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */
@@ -36,11 +39,10 @@ class EditShortcodeTagsAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @return Response
* @return null|Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function handle(Request $request): Response
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
$bodyParams = $request->getParsedBody(); $bodyParams = $request->getParsedBody();

View File

@@ -1,22 +1,25 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
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;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait; use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesAction extends AbstractRestAction class ListShortCodesAction extends AbstractRestAction
{ {
use PaginatorUtilsTrait; use PaginatorUtilsTrait;
protected const ROUTE_PATH = '/short-codes';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var ShortUrlServiceInterface * @var ShortUrlServiceInterface
*/ */
@@ -38,11 +41,10 @@ class ListShortcodesAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @return Response
* @return null|Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function handle(Request $request): Response
{ {
try { try {
$params = $this->queryToListParams($request->getQueryParams()); $params = $this->queryToListParams($request->getQueryParams());
@@ -61,13 +63,13 @@ class ListShortcodesAction extends AbstractRestAction
* @param array $query * @param array $query
* @return array * @return array
*/ */
public function queryToListParams(array $query) private function queryToListParams(array $query): array
{ {
return [ return [
isset($query['page']) ? $query['page'] : 1, $query['page'] ?? 1,
isset($query['searchTerm']) ? $query['searchTerm'] : null, $query['searchTerm'] ?? null,
isset($query['tags']) ? $query['tags'] : [], $query['tags'] ?? [],
isset($query['orderBy']) ? $query['orderBy'] : null, $query['orderBy'] ?? null,
]; ];
} }
} }

View File

@@ -1,21 +1,24 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\ShortCode;
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;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlAction extends AbstractRestAction class ResolveUrlAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var UrlShortenerInterface * @var UrlShortenerInterface
*/ */
@@ -37,11 +40,10 @@ class ResolveUrlAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @return Response
* @return null|Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function handle(Request $request): Response
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');

View File

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

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag; namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class CreateTagsAction extends AbstractRestAction class CreateTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */
@@ -29,15 +31,14 @@ class CreateTagsAction extends AbstractRestAction
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate) public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$body = $request->getParsedBody(); $body = $request->getParsedBody();
$tags = isset($body['tags']) ? $body['tags'] : []; $tags = $body['tags'] ?? [];
return new JsonResponse([ return new JsonResponse([
'tags' => [ 'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag; namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\EmptyResponse;
class DeleteTagsAction extends AbstractRestAction class DeleteTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_DELETE];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */
@@ -29,14 +31,13 @@ class DeleteTagsAction extends AbstractRestAction
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate) public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$query = $request->getQueryParams(); $query = $request->getQueryParams();
$tags = isset($query['tags']) ? $query['tags'] : []; $tags = $query['tags'] ?? [];
$this->tagService->deleteTags($tags); $this->tagService->deleteTags($tags);
return new EmptyResponse(); return new EmptyResponse();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag; namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -13,6 +12,9 @@ use Zend\Diactoros\Response\JsonResponse;
class ListTagsAction extends AbstractRestAction class ListTagsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */
@@ -29,12 +31,11 @@ class ListTagsAction extends AbstractRestAction
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate) public function handle(ServerRequestInterface $request): ResponseInterface
{ {
return new JsonResponse([ return new JsonResponse([
'tags' => [ 'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\Tag; namespace Shlinkio\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -17,6 +16,9 @@ use Zend\I18n\Translator\TranslatorInterface;
class UpdateTagAction extends AbstractRestAction class UpdateTagAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/tags';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_PUT];
/** /**
* @var TagServiceInterface * @var TagServiceInterface
*/ */
@@ -41,12 +43,11 @@ class UpdateTagAction extends AbstractRestAction
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param ServerRequestInterface $request * @param ServerRequestInterface $request
* @param DelegateInterface $delegate
* *
* @return ResponseInterface * @return ResponseInterface
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(ServerRequestInterface $request, DelegateInterface $delegate) public function handle(ServerRequestInterface $request): ResponseInterface
{ {
$body = $request->getParsedBody(); $body = $request->getParsedBody();
if (! isset($body['oldName'], $body['newName'])) { if (! isset($body['oldName'], $body['newName'])) {

View File

@@ -1,21 +1,24 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action; namespace Shlinkio\Shlink\Rest\Action\Visit;
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;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface; use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\I18n\Translator\TranslatorInterface; use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsAction extends AbstractRestAction class GetVisitsAction extends AbstractRestAction
{ {
protected const ROUTE_PATH = '/short-codes/{shortCode}/visits';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_GET];
/** /**
* @var VisitsTrackerInterface * @var VisitsTrackerInterface
*/ */
@@ -37,11 +40,10 @@ class GetVisitsAction extends AbstractRestAction
/** /**
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @return Response
* @return null|Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function handle(Request $request): Response
{ {
$shortCode = $request->getAttribute('shortCode'); $shortCode = $request->getAttribute('shortCode');
$startDate = $this->getDateQueryParam($request, 'startDate'); $startDate = $this->getDateQueryParam($request, 'startDate');

View File

@@ -92,7 +92,7 @@ class JWTService implements JWTServiceInterface
* @param array $data * @param array $data
* @return string * @return string
*/ */
protected function encode(array $data): string private function encode(array $data): string
{ {
return JWT::encode($data, $this->appOptions->getSecretKey(), self::DEFAULT_ENCRYPTION_ALG); return JWT::encode($data, $this->appOptions->getSecretKey(), self::DEFAULT_ENCRYPTION_ALG);
} }
@@ -101,7 +101,7 @@ class JWTService implements JWTServiceInterface
* @param string $jwt * @param string $jwt
* @return array * @return array
*/ */
protected function decode(string $jwt): array private function decode(string $jwt): array
{ {
return (array) JWT::decode($jwt, $this->appOptions->getSecretKey(), [self::DEFAULT_ENCRYPTION_ALG]); return (array) JWT::decode($jwt, $this->appOptions->getSecretKey(), [self::DEFAULT_ENCRYPTION_ALG]);
} }

View File

@@ -44,7 +44,7 @@ class ApiKey extends AbstractEntity
/** /**
* @return string * @return string
*/ */
public function getKey() public function getKey(): string
{ {
return $this->key; return $this->key;
} }
@@ -53,7 +53,7 @@ class ApiKey extends AbstractEntity
* @param string $key * @param string $key
* @return $this * @return $this
*/ */
public function setKey($key) public function setKey($key): self
{ {
$this->key = $key; $this->key = $key;
return $this; return $this;
@@ -62,7 +62,7 @@ class ApiKey extends AbstractEntity
/** /**
* @return \DateTime|null * @return \DateTime|null
*/ */
public function getExpirationDate() public function getExpirationDate(): ?\DateTime
{ {
return $this->expirationDate; return $this->expirationDate;
} }
@@ -71,7 +71,7 @@ class ApiKey extends AbstractEntity
* @param \DateTime $expirationDate * @param \DateTime $expirationDate
* @return $this * @return $this
*/ */
public function setExpirationDate($expirationDate) public function setExpirationDate($expirationDate): self
{ {
$this->expirationDate = $expirationDate; $this->expirationDate = $expirationDate;
return $this; return $this;
@@ -80,9 +80,9 @@ class ApiKey extends AbstractEntity
/** /**
* @return bool * @return bool
*/ */
public function isExpired() public function isExpired(): bool
{ {
if (! isset($this->expirationDate)) { if ($this->expirationDate === null) {
return false; return false;
} }
@@ -92,7 +92,7 @@ class ApiKey extends AbstractEntity
/** /**
* @return boolean * @return boolean
*/ */
public function isEnabled() public function isEnabled(): bool
{ {
return $this->enabled; return $this->enabled;
} }
@@ -101,7 +101,7 @@ class ApiKey extends AbstractEntity
* @param boolean $enabled * @param boolean $enabled
* @return $this * @return $this
*/ */
public function setEnabled($enabled) public function setEnabled($enabled): self
{ {
$this->enabled = $enabled; $this->enabled = $enabled;
return $this; return $this;
@@ -112,7 +112,7 @@ class ApiKey extends AbstractEntity
* *
* @return $this * @return $this
*/ */
public function disable() public function disable(): self
{ {
return $this->setEnabled(false); return $this->setEnabled(false);
} }
@@ -122,17 +122,17 @@ class ApiKey extends AbstractEntity
* *
* @return bool * @return bool
*/ */
public function isValid() public function isValid(): bool
{ {
return $this->isEnabled() && ! $this->isExpired(); return $this->isEnabled() && ! $this->isExpired();
} }
/** /**
* The string repesentation of an API key is the key itself * The string representation of an API key is the key itself
* *
* @return string * @return string
*/ */
public function __toString() public function __toString(): string
{ {
return $this->getKey(); return $this->getKey();
} }

View File

@@ -20,7 +20,7 @@ class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, Sta
* @return Response * @return Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function __invoke($e, Request $request, Response $response) public function __invoke(?\Throwable $e, Request $request, Response $response)
{ {
$status = $response->getStatusCode(); $status = $response->getStatusCode();
$responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase(); $responsePhrase = $status < 400 ? 'Internal Server Error' : $response->getReasonPhrase();
@@ -32,8 +32,8 @@ class JsonErrorResponseGenerator implements ErrorResponseGeneratorInterface, Sta
], $status); ], $status);
} }
protected function responsePhraseToCode(string $responsePhrase): string private function responsePhraseToCode(string $responsePhrase): string
{ {
return strtoupper(str_replace(' ', '_', $responsePhrase)); return \strtoupper(\str_replace(' ', '_', $responsePhrase));
} }
} }

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\RequestMethodInterface; 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 Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Exception\RuntimeException; use Shlinkio\Shlink\Rest\Exception\RuntimeException;
class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterface
@@ -17,11 +17,11 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$method = $request->getMethod(); $method = $request->getMethod();
$currentParams = $request->getParsedBody(); $currentParams = $request->getParsedBody();
@@ -32,16 +32,16 @@ class BodyParserMiddleware implements MiddlewareInterface, RequestMethodInterfac
self::METHOD_HEAD, self::METHOD_HEAD,
self::METHOD_OPTIONS, self::METHOD_OPTIONS,
], true)) { ], true)) {
return $delegate->process($request); return $handler->handle($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'], true)) { if (\in_array($contentType, ['application/json', 'text/json', 'application/x-json'], true)) {
return $delegate->process($this->parseFromJson($request)); return $handler->handle($this->parseFromJson($request));
} }
return $delegate->process($this->parseFromUrlEncoded($request)); return $handler->handle($this->parseFromUrlEncoded($request));
} }
/** /**

View File

@@ -4,13 +4,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\StatusCodeInterface; 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\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface; use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException; use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
@@ -21,7 +20,7 @@ use Zend\Stdlib\ErrorHandler;
class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterface
{ {
const AUTHORIZATION_HEADER = 'Authorization'; public const AUTHORIZATION_HEADER = 'Authorization';
/** /**
* @var TranslatorInterface * @var TranslatorInterface
@@ -35,14 +34,20 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
* @var LoggerInterface * @var LoggerInterface
*/ */
private $logger; private $logger;
/**
* @var array
*/
private $routesWhitelist;
public function __construct( public function __construct(
JWTServiceInterface $jwtService, JWTServiceInterface $jwtService,
TranslatorInterface $translator, TranslatorInterface $translator,
array $routesWhitelist,
LoggerInterface $logger = null LoggerInterface $logger = null
) { ) {
$this->translator = $translator; $this->translator = $translator;
$this->jwtService = $jwtService; $this->jwtService = $jwtService;
$this->routesWhitelist = $routesWhitelist;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
@@ -51,23 +56,23 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* @throws \ErrorException * @throws \ErrorException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
// 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|null $routeResult */ /** @var RouteResult|null $routeResult */
$routeResult = $request->getAttribute(RouteResult::class); $routeResult = $request->getAttribute(RouteResult::class);
if ($routeResult === null if ($routeResult === null
|| $routeResult->isFailure() || $routeResult->isFailure()
|| $routeResult->getMatchedRouteName() === AuthenticateAction::class
|| $request->getMethod() === 'OPTIONS' || $request->getMethod() === 'OPTIONS'
|| \in_array($routeResult->getMatchedRouteName(), $this->routesWhitelist, true)
) { ) {
return $delegate->process($request); return $handler->handle($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
@@ -77,22 +82,22 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
// Get token making sure the an authorization type is provided // Get token making sure the an authorization type is provided
$authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER); $authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER);
$authTokenParts = explode(' ', $authToken); $authTokenParts = \explode(' ', $authToken);
if (count($authTokenParts) === 1) { if (\count($authTokenParts) === 1) {
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR, 'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
'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),
], self::STATUS_UNAUTHORIZED); ], self::STATUS_UNAUTHORIZED);
} }
// Make sure the authorization type is Bearer // Make sure the authorization type is Bearer
list($authType, $jwt) = $authTokenParts; [$authType, $jwt] = $authTokenParts;
if (strtolower($authType) !== 'bearer') { if (\strtolower($authType) !== 'bearer') {
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR, 'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
'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),
], self::STATUS_UNAUTHORIZED); ], self::STATUS_UNAUTHORIZED);
@@ -107,7 +112,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
// 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);
$response = $delegate->process($request); $response = $handler->handle($request);
// 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);
@@ -119,11 +124,15 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
} }
} }
protected function createTokenErrorResponse() /**
* @return JsonResponse
* @throws \InvalidArgumentException
*/
private function createTokenErrorResponse(): JsonResponse
{ {
return new JsonResponse([ return new JsonResponse([
'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR, 'error' => RestUtils::INVALID_AUTH_TOKEN_ERROR,
'message' => sprintf( 'message' => \sprintf(
$this->translator->translate( $this->translator->translate(
'Missing or invalid auth token provided. Perform a new authentication request and send provided ' 'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the "%s" header' . 'token on every new request on the "%s" header'

View File

@@ -4,10 +4,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Fig\Http\Message\RequestMethodInterface; 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 Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterface class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterface
{ {
@@ -16,15 +16,15 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa
* to the next middleware component to create the response. * to the next middleware component to create the response.
* *
* @param Request $request * @param Request $request
* @param DelegateInterface $delegate * @param RequestHandlerInterface $handler
* *
* @return Response * @return Response
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
/** @var Response $response */ /** @var Response $response */
$response = $delegate->process($request); $response = $handler->handle($request);
if (! $request->hasHeader('Origin')) { if (! $request->hasHeader('Origin')) {
return $response; return $response;
} }

View File

@@ -5,51 +5,31 @@ namespace Shlinkio\Shlink\Rest\Middleware;
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 Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class PathVersionMiddleware implements MiddlewareInterface class PathVersionMiddleware 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 RequestHandlerInterface $handler
* @param null|callable $out *
* @return null|Response * @return Response
* @throws \InvalidArgumentException
*/ */
public function __invoke(Request $request, Response $response, callable $out = null) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$uri = $request->getUri(); $uri = $request->getUri();
$path = $uri->getPath(); $path = $uri->getPath();
// If the path does not begin with the version number, prepend v1 by default for BC compatibility purposes // If the path does not begin with the version number, prepend v1 by default for BC compatibility purposes
if (strpos($path, '/v') !== 0) { if (\strpos($path, '/v') !== 0) {
$parts = explode('/', $path); $request = $request->withUri($uri->withPath('/v1' . $uri->getPath()));
// Remove the first empty part and the
array_shift($parts);
// Prepend the version prefix
array_unshift($parts, '/v1');
$request = $request->withUri($uri->withPath(implode('/', $parts)));
} }
return $out($request, $response); return $handler->handle($request);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -9,16 +9,16 @@ use Shlinkio\Shlink\Rest\Exception as Rest;
class RestUtils class RestUtils
{ {
const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE'; public const INVALID_SHORTCODE_ERROR = 'INVALID_SHORTCODE';
const INVALID_URL_ERROR = 'INVALID_URL'; public const INVALID_URL_ERROR = 'INVALID_URL';
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT'; public const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
const INVALID_SLUG_ERROR = 'INVALID_SLUG'; public const INVALID_SLUG_ERROR = 'INVALID_SLUG';
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS'; public const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN'; public const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION'; public const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION';
const INVALID_API_KEY_ERROR = 'INVALID_API_KEY'; public const INVALID_API_KEY_ERROR = 'INVALID_API_KEY';
const NOT_FOUND_ERROR = 'NOT_FOUND'; public const NOT_FOUND_ERROR = 'NOT_FOUND';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR'; public const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
public static function getRestErrorCodeFromException(\Throwable $e) public static function getRestErrorCodeFromException(\Throwable $e)
{ {
@@ -30,6 +30,7 @@ class RestUtils
case $e instanceof Core\NonUniqueSlugException: case $e instanceof Core\NonUniqueSlugException:
return self::INVALID_SLUG_ERROR; return self::INVALID_SLUG_ERROR;
case $e instanceof Common\InvalidArgumentException: case $e instanceof Common\InvalidArgumentException:
case $e instanceof Core\InvalidArgumentException:
case $e instanceof Core\ValidationException: case $e instanceof Core\ValidationException:
return self::INVALID_ARGUMENT_ERROR; return self::INVALID_ARGUMENT_ERROR;
case $e instanceof Rest\AuthenticationException: case $e instanceof Rest\AuthenticationException:

View File

@@ -10,7 +10,6 @@ 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 ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -47,7 +46,7 @@ class AuthenticateActionTest extends TestCase
*/ */
public function notProvidingAuthDataReturnsError() public function notProvidingAuthDataReturnsError()
{ {
$resp = $this->action->process(ServerRequestFactory::fromGlobals(), TestUtils::createDelegateMock()->reveal()); $resp = $this->action->handle(ServerRequestFactory::fromGlobals());
$this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals(400, $resp->getStatusCode());
} }
@@ -62,7 +61,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',
]); ]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$response->getBody()->rewind(); $response->getBody()->rewind();
@@ -80,7 +79,7 @@ class AuthenticateActionTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withParsedBody([ $request = ServerRequestFactory::fromGlobals()->withParsedBody([
'apiKey' => 'foo', 'apiKey' => 'foo',
]); ]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@@ -9,17 +9,16 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\CreateShortcodeAction; use Shlinkio\Shlink\Rest\Action\ShortCode\CreateShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use 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;
class CreateShortcodeActionTest extends TestCase class CreateShortCodeActionTest extends TestCase
{ {
/** /**
* @var CreateShortcodeAction * @var CreateShortCodeAction
*/ */
protected $action; protected $action;
/** /**
@@ -30,7 +29,7 @@ class CreateShortcodeActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->urlShortener = $this->prophesize(UrlShortener::class); $this->urlShortener = $this->prophesize(UrlShortener::class);
$this->action = new CreateShortcodeAction($this->urlShortener->reveal(), Translator::factory([]), [ $this->action = new CreateShortCodeAction($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http', 'schema' => 'http',
'hostname' => 'foo.com', 'hostname' => 'foo.com',
]); ]);
@@ -41,10 +40,7 @@ class CreateShortcodeActionTest extends TestCase
*/ */
public function missingLongUrlParamReturnsError() public function missingLongUrlParamReturnsError()
{ {
$response = $this->action->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals());
ServerRequestFactory::fromGlobals(),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
} }
@@ -60,7 +56,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->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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);
} }
@@ -77,7 +73,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->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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);
} }
@@ -100,7 +96,7 @@ class CreateShortcodeActionTest extends TestCase
'longUrl' => 'http://www.domain.com/foo/bar', 'longUrl' => 'http://www.domain.com/foo/bar',
'customSlug' => 'foo', 'customSlug' => 'foo',
]); ]);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
$this->assertContains(RestUtils::INVALID_SLUG_ERROR, (string) $response->getBody()); $this->assertContains(RestUtils::INVALID_SLUG_ERROR, (string) $response->getBody());
} }
@@ -117,7 +113,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->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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,16 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\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;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Rest\Action\EditShortCodeAction; use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use Zend\Diactoros\Response\JsonResponse; use Zend\Diactoros\Response\JsonResponse;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -43,7 +42,7 @@ class EditShortCodeActionTest extends TestCase
]); ]);
/** @var JsonResponse $resp */ /** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$payload = $resp->getPayload(); $payload = $resp->getPayload();
$this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals(400, $resp->getStatusCode());
@@ -65,7 +64,7 @@ class EditShortCodeActionTest extends TestCase
); );
/** @var JsonResponse $resp */ /** @var JsonResponse $resp */
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$payload = $resp->getPayload(); $payload = $resp->getPayload();
$this->assertEquals(404, $resp->getStatusCode()); $this->assertEquals(404, $resp->getStatusCode());
@@ -85,7 +84,7 @@ class EditShortCodeActionTest extends TestCase
]); ]);
$updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(new ShortUrl()); $updateMeta = $this->shortUrlService->updateMetadataByShortCode(Argument::cetera())->willReturn(new ShortUrl());
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$this->assertEquals(204, $resp->getStatusCode()); $this->assertEquals(204, $resp->getStatusCode());
$updateMeta->shouldHaveBeenCalled(); $updateMeta->shouldHaveBeenCalled();

View File

@@ -1,22 +1,21 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\EditShortcodeTagsAction; use Shlinkio\Shlink\Rest\Action\ShortCode\EditShortCodeTagsAction;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
class EditShortcodeTagsActionTest extends TestCase class EditShortCodeTagsActionTest extends TestCase
{ {
/** /**
* @var EditShortcodeTagsAction * @var EditShortCodeTagsAction
*/ */
protected $action; protected $action;
/** /**
@@ -27,7 +26,7 @@ class EditShortcodeTagsActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->shortUrlService = $this->prophesize(ShortUrlService::class); $this->shortUrlService = $this->prophesize(ShortUrlService::class);
$this->action = new EditShortcodeTagsAction($this->shortUrlService->reveal(), Translator::factory([])); $this->action = new EditShortCodeTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
} }
/** /**
@@ -35,10 +34,7 @@ class EditShortcodeTagsActionTest extends TestCase
*/ */
public function notProvidingTagsReturnsError() public function notProvidingTagsReturnsError()
{ {
$response = $this->action->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'));
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(400, $response->getStatusCode()); $this->assertEquals(400, $response->getStatusCode());
} }
@@ -51,10 +47,9 @@ class EditShortcodeTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class) $this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class)
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->process( $response = $this->action->handle(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123') ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]), ->withParsedBody(['tags' => []])
TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
} }
@@ -68,10 +63,9 @@ class EditShortcodeTagsActionTest extends TestCase
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl()) $this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl())
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->process( $response = $this->action->handle(
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123') ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
->withParsedBody(['tags' => []]), ->withParsedBody(['tags' => []])
TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }

View File

@@ -1,13 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Rest\Action\ListShortcodesAction; use Shlinkio\Shlink\Rest\Action\ShortCode\ListShortCodesAction;
use 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;
@@ -16,7 +15,7 @@ use Zend\Paginator\Paginator;
class ListShortCodesActionTest extends TestCase class ListShortCodesActionTest extends TestCase
{ {
/** /**
* @var ListShortcodesAction * @var ListShortCodesAction
*/ */
protected $action; protected $action;
/** /**
@@ -27,7 +26,7 @@ class ListShortCodesActionTest extends TestCase
public function setUp() public function setUp()
{ {
$this->service = $this->prophesize(ShortUrlService::class); $this->service = $this->prophesize(ShortUrlService::class);
$this->action = new ListShortcodesAction($this->service->reveal(), Translator::factory([])); $this->action = new ListShortCodesAction($this->service->reveal(), Translator::factory([]));
} }
/** /**
@@ -39,12 +38,9 @@ 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->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
ServerRequestFactory::fromGlobals()->withQueryParams([ 'page' => $page,
'page' => $page, ]));
]),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }
@@ -57,12 +53,9 @@ 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->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withQueryParams([
ServerRequestFactory::fromGlobals()->withQueryParams([ 'page' => $page,
'page' => $page, ]));
]),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
} }
} }

View File

@@ -1,16 +1,15 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\ShortCode;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException; use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\ResolveUrlAction; use Shlinkio\Shlink\Rest\Action\ShortCode\ResolveUrlAction;
use Shlinkio\Shlink\Rest\Util\RestUtils; use Shlinkio\Shlink\Rest\Util\RestUtils;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -41,7 +40,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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);
} }
@@ -56,7 +55,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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);
} }
@@ -71,7 +70,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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);
} }
@@ -86,7 +85,7 @@ class ResolveUrlActionTest extends TestCase
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode); $request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
$response = $this->action->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->action->handle($request);
$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

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

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag; namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -40,7 +39,7 @@ class CreateTagsActionTest extends TestCase
/** @var MethodProphecy $deleteTags */ /** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection()); $deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection());
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
$deleteTags->shouldHaveBeenCalled(); $deleteTags->shouldHaveBeenCalled();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag; namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -39,7 +38,7 @@ class DeleteTagsActionTest extends TestCase
/** @var MethodProphecy $deleteTags */ /** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->deleteTags($tags ?: []); $deleteTags = $this->tagService->deleteTags($tags ?: []);
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $response = $this->action->handle($request);
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$deleteTags->shouldHaveBeenCalled(); $deleteTags->shouldHaveBeenCalled();

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag; namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -37,10 +36,7 @@ class ListTagsActionTest extends TestCase
/** @var MethodProphecy $listTags */ /** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([new Tag('foo'), new Tag('bar')]); $listTags = $this->tagService->listTags()->willReturn([new Tag('foo'), new Tag('bar')]);
$resp = $this->action->process( $resp = $this->action->handle(ServerRequestFactory::fromGlobals());
ServerRequestFactory::fromGlobals(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$this->assertEquals([ $this->assertEquals([
'tags' => [ 'tags' => [

View File

@@ -3,7 +3,6 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action\Tag; namespace ShlinkioTest\Shlink\Rest\Action\Tag;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
@@ -39,7 +38,7 @@ class UpdateTagActionTest extends TestCase
public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams) public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams)
{ {
$request = ServerRequestFactory::fromGlobals()->withParsedBody($bodyParams); $request = ServerRequestFactory::fromGlobals()->withParsedBody($bodyParams);
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$this->assertEquals(400, $resp->getStatusCode()); $this->assertEquals(400, $resp->getStatusCode());
} }
@@ -65,7 +64,7 @@ class UpdateTagActionTest extends TestCase
/** @var MethodProphecy $rename */ /** @var MethodProphecy $rename */
$rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class); $rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class);
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$this->assertEquals(404, $resp->getStatusCode()); $this->assertEquals(404, $resp->getStatusCode());
$rename->shouldHaveBeenCalled(); $rename->shouldHaveBeenCalled();
@@ -83,7 +82,7 @@ class UpdateTagActionTest extends TestCase
/** @var MethodProphecy $rename */ /** @var MethodProphecy $rename */
$rename = $this->tagService->renameTag('foo', 'bar')->willReturn(new Tag()); $rename = $this->tagService->renameTag('foo', 'bar')->willReturn(new Tag());
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal()); $resp = $this->action->handle($request);
$this->assertEquals(204, $resp->getStatusCode()); $this->assertEquals(204, $resp->getStatusCode());
$rename->shouldHaveBeenCalled(); $rename->shouldHaveBeenCalled();

View File

@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action; namespace ShlinkioTest\Shlink\Rest\Action\Visit;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
@@ -9,8 +9,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker; use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Rest\Action\GetVisitsAction; use Shlinkio\Shlink\Rest\Action\Visit\GetVisitsAction;
use ShlinkioTest\Shlink\Common\Util\TestUtils;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
use Zend\I18n\Translator\Translator; use Zend\I18n\Translator\Translator;
@@ -40,10 +39,7 @@ 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->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }
@@ -57,10 +53,7 @@ class GetVisitsActionTest extends TestCase
InvalidArgumentException::class InvalidArgumentException::class
)->shouldBeCalledTimes(1); )->shouldBeCalledTimes(1);
$response = $this->action->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(404, $response->getStatusCode()); $this->assertEquals(404, $response->getStatusCode());
} }
@@ -74,10 +67,7 @@ class GetVisitsActionTest extends TestCase
\Exception::class \Exception::class
)->shouldBeCalledTimes(1); )->shouldBeCalledTimes(1);
$response = $this->action->process( $response = $this->action->handle(ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode));
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
TestUtils::createDelegateMock()->reveal()
);
$this->assertEquals(500, $response->getStatusCode()); $this->assertEquals(500, $response->getStatusCode());
} }
@@ -91,10 +81,9 @@ class GetVisitsActionTest extends TestCase
->willReturn([]) ->willReturn([])
->shouldBeCalledTimes(1); ->shouldBeCalledTimes(1);
$response = $this->action->process( $response = $this->action->handle(
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'])
TestUtils::createDelegateMock()->reveal()
); );
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
} }

View File

@@ -3,11 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
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;
@@ -31,9 +31,9 @@ class BodyParserMiddlewareTest extends TestCase
public function requestsFromOtherMethodsJustFallbackToNextMiddleware() public function requestsFromOtherMethodsJustFallbackToNextMiddleware()
{ {
$request = ServerRequestFactory::fromGlobals()->withMethod('GET'); $request = ServerRequestFactory::fromGlobals()->withMethod('GET');
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
@@ -51,9 +51,9 @@ class BodyParserMiddlewareTest extends TestCase
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT') $request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
->withBody($body) ->withBody($body)
->withHeader('content-type', 'application/json'); ->withHeader('content-type', 'application/json');
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process(Argument::type(ServerRequestInterface::class))->will( $process = $delegate->handle(Argument::type(ServerRequestInterface::class))->will(
function (array $args) use ($test) { function (array $args) use ($test) {
/** @var ServerRequestInterface $req */ /** @var ServerRequestInterface $req */
$req = array_shift($args); $req = array_shift($args);
@@ -82,9 +82,9 @@ class BodyParserMiddlewareTest extends TestCase
$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);
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process(Argument::type(ServerRequestInterface::class))->will( $process = $delegate->handle(Argument::type(ServerRequestInterface::class))->will(
function (array $args) use ($test) { function (array $args) use ($test) {
/** @var ServerRequestInterface $req */ /** @var ServerRequestInterface $req */
$req = array_shift($args); $req = array_shift($args);

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
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\Middleware\CheckAuthenticationMiddleware; use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
@@ -16,6 +16,7 @@ use Zend\Diactoros\ServerRequestFactory;
use Zend\Expressive\Router\Route; 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;
use function Zend\Stratigility\middleware;
class CheckAuthenticationMiddlewareTest extends TestCase class CheckAuthenticationMiddlewareTest extends TestCase
{ {
@@ -28,10 +29,20 @@ class CheckAuthenticationMiddlewareTest extends TestCase
*/ */
protected $jwtService; protected $jwtService;
/**
* @var callable
*/
protected $dummyMiddleware;
public function setUp() public function setUp()
{ {
$this->jwtService = $this->prophesize(JWTService::class); $this->jwtService = $this->prophesize(JWTService::class);
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([])); $this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]), [
AuthenticateAction::class,
]);
$this->dummyMiddleware = middleware(function () {
return new Response\EmptyResponse();
});
} }
/** /**
@@ -40,9 +51,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function someWhiteListedSituationsFallbackToNextMiddleware() public function someWhiteListedSituationsFallbackToNextMiddleware()
{ {
$request = ServerRequestFactory::fromGlobals(); $request = ServerRequestFactory::fromGlobals();
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1); $process->shouldHaveBeenCalledTimes(1);
@@ -51,29 +62,34 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class, RouteResult::class,
RouteResult::fromRouteFailure(['GET']) RouteResult::fromRouteFailure(['GET'])
); );
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1); $process->shouldHaveBeenCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, AuthenticateAction::class)) RouteResult::fromRoute(new Route(
'foo',
$this->dummyMiddleware,
Route::HTTP_METHOD_ANY,
AuthenticateAction::class
))
); );
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1); $process->shouldHaveBeenCalledTimes(1);
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withMethod('OPTIONS'); )->withMethod('OPTIONS');
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1); $process->shouldHaveBeenCalledTimes(1);
} }
@@ -85,9 +101,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
{ {
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
); );
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
@@ -99,10 +115,10 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->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);
@@ -116,10 +132,10 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken); )->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken);
$response = $this->middleware->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
$this->assertTrue( $this->assertTrue(
@@ -135,11 +151,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->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->process($request, TestUtils::createDelegateMock()->reveal()); $response = $this->middleware->process($request, TestUtils::createReqHandlerMock()->reveal());
$this->assertEquals(401, $response->getStatusCode()); $this->assertEquals(401, $response->getStatusCode());
} }
@@ -151,14 +167,14 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$authToken = 'ABC-abc'; $authToken = 'ABC-abc';
$request = ServerRequestFactory::fromGlobals()->withAttribute( $request = ServerRequestFactory::fromGlobals()->withAttribute(
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', 'foo'), []) RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), [])
)->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);
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
/** @var MethodProphecy $process */ /** @var MethodProphecy $process */
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$resp = $this->middleware->process($request, $delegate->reveal()); $resp = $this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalledTimes(1); $process->shouldHaveBeenCalledTimes(1);

View File

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
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;
@@ -25,7 +25,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function setUp() public function setUp()
{ {
$this->middleware = new CrossDomainMiddleware(); $this->middleware = new CrossDomainMiddleware();
$this->delegate = $this->prophesize(DelegateInterface::class); $this->delegate = $this->prophesize(RequestHandlerInterface::class);
} }
/** /**
@@ -34,7 +34,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function nonCrossDomainRequestsAreNotAffected() public function nonCrossDomainRequestsAreNotAffected()
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1); $this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->delegate->reveal()); $response = $this->middleware->process(ServerRequestFactory::fromGlobals(), $this->delegate->reveal());
$this->assertSame($originalResponse, $response); $this->assertSame($originalResponse, $response);
@@ -50,7 +50,7 @@ class CrossDomainMiddlewareTest extends TestCase
public function anyRequestIncludesTheAllowAccessHeader() public function anyRequestIncludesTheAllowAccessHeader()
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1); $this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process( $response = $this->middleware->process(
ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'), ServerRequestFactory::fromGlobals()->withHeader('Origin', 'local'),
@@ -70,7 +70,7 @@ class CrossDomainMiddlewareTest extends TestCase
{ {
$originalResponse = new Response(); $originalResponse = new Response();
$request = ServerRequestFactory::fromGlobals()->withMethod('OPTIONS')->withHeader('Origin', 'local'); $request = ServerRequestFactory::fromGlobals()->withMethod('OPTIONS')->withHeader('Origin', 'local');
$this->delegate->process(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1); $this->delegate->handle(Argument::any())->willReturn($originalResponse)->shouldbeCalledTimes(1);
$response = $this->middleware->process($request, $this->delegate->reveal()); $response = $this->middleware->process($request, $this->delegate->reveal());
$this->assertNotSame($originalResponse, $response); $this->assertNotSame($originalResponse, $response);

View File

@@ -3,8 +3,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Middleware; namespace ShlinkioTest\Shlink\Rest\Middleware;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware; use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Diactoros\Response; use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;
@@ -28,10 +31,13 @@ class PathVersionMiddlewareTest extends TestCase
public function whenVersionIsProvidedRequestRemainsUnchanged() public function whenVersionIsProvidedRequestRemainsUnchanged()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/foo')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/foo'));
$test = $this;
$this->middleware->__invoke($request, new Response(), function ($req) use ($request, $test) { $delegate = $this->prophesize(RequestHandlerInterface::class);
$test->assertSame($request, $req); $process = $delegate->handle($request)->willReturn(new Response());
});
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalled();
} }
/** /**
@@ -40,10 +46,17 @@ class PathVersionMiddlewareTest extends TestCase
public function versionOneIsPrependedWhenNoVersionIsDefined() public function versionOneIsPrependedWhenNoVersionIsDefined()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/bar/baz')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/bar/baz'));
$test = $this;
$this->middleware->__invoke($request, new Response(), function (Request $req) use ($request, $test) { $delegate = $this->prophesize(RequestHandlerInterface::class);
$test->assertNotSame($request, $req); $delegate->handle(Argument::type(Request::class))->will(function (array $args) use ($request) {
$this->assertEquals('/v1/bar/baz', $req->getUri()->getPath()); $req = \array_shift($args);
Assert::assertNotSame($request, $req);
Assert::assertEquals('/v1/bar/baz', $req->getUri()->getPath());
return new Response();
}); });
$this->middleware->process($request, $delegate->reveal());
} }
} }

View File

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

Some files were not shown because too many files have changed in this diff Show More