Compare commits

..

70 Commits

Author SHA1 Message Date
Alejandro Celaya
975260f126 Merge pull request #164 from shlinkio/develop
Develop
2018-07-09 18:48:28 +02:00
Alejandro Celaya
bd678b41f7 Merge pull request #163 from acelaya/develop
v1.10.0
2018-07-09 18:41:28 +02:00
Alejandro Celaya
66898b6ddc Added v1.10.0 to changelog 2018-07-09 18:40:48 +02:00
Alejandro Celaya
5eee683978 Added release datesto changelog 2018-07-09 17:48:23 +02:00
Alejandro Celaya
e92446df9b Updated changelog to use the keepachangelog format 2018-07-09 17:30:25 +02:00
Alejandro Celaya
63a69b05a1 Added Zend expressive swoole config provider to global config when present 2018-07-04 20:40:38 +02:00
Alejandro Celaya
c79ca1d13c Fixed phpstan issues 2018-07-04 20:28:05 +02:00
Alejandro Celaya
87c4851d7e Simplified ListKeysCommand reducing cyclomatic complexity on nested callbacks 2018-07-04 20:24:13 +02:00
Alejandro Celaya
a125c93ca3 Updated to phpstan 0.10 and infection 0.9 2018-07-04 12:08:27 +02:00
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
97 changed files with 1951 additions and 854 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,227 +1,580 @@
## CHANGELOG # CHANGELOG
### 1.7.2 ## 1.10.0 - 2018-07-09
**Bugs:** #### Added
* [135: Fix PathVersionMiddleware being ignored when using expressive 2.2](https://github.com/shlinkio/shlink/issues/135) * [#161](https://github.com/shlinkio/shlink/issues/161) AddED support for shlink to be run with [swoole](https://www.swoole.co.uk/) via [zend-expressive-swoole](https://github.com/zendframework/zend-expressive-swoole) package
### 1.7.1 #### Changed
**Enhancements:** * [#159](https://github.com/shlinkio/shlink/issues/159) Updated CHANGELOG to follow the [keep-a-changelog](https://keepachangelog.com) format
* [#160](https://github.com/shlinkio/shlink/issues/160) Update infection to v0.9 and phpstan to v 0.10
* [128: Upgrade to expressive 2.2](https://github.com/shlinkio/shlink/issues/128) #### Deprecated
**Bugs** * *Nothing*
* [126: Expressive 2.2 causes failures by triggering E_USER_DEPRECATED errors](https://github.com/shlinkio/shlink/issues/126) #### Removed
### 1.7.0 * *Nothing*
**Features** #### Fixed
* [88: Allow to disable tracking of the short URL by including a configurable query param](https://github.com/shlinkio/shlink/issues/88) * *Nothing*
* [108: Allow to edit metadata in created shortcodes](https://github.com/shlinkio/shlink/issues/108)
**Enhancements:**
* [113: Update CLI commands to use SymfonyStyle](https://github.com/shlinkio/shlink/issues/113) ## 1.9.1 - 2018-06-18
* [112: Configure cli commands lazy loading](https://github.com/shlinkio/shlink/issues/112)
**Tasks** #### Added
* [117: Make every module which throws exceptions have its own ExceptionInterface, and make them all extend Throwable](https://github.com/shlinkio/shlink/issues/117) * [#155](https://github.com/shlinkio/shlink/issues/155) Improved the pagination object returned in lists, including more meaningful properties.
* [115: Add phpstan to build matrix on PHP >=7.1 envs](https://github.com/shlinkio/shlink/issues/115)
* [114: Replace vlucas/phpdotenv dev requirement by symfony/env](https://github.com/shlinkio/shlink/issues/114) * Old structure:
```json
{
"pagination": {
"currentPage": 1,
"pagesCount": 2
}
}
```
* New structure:
```json
{
"pagination": {
"currentPage": 2,
"pagesCount": 13,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 126
}
}
```
### 1.6.2 #### Changed
**Bugs** * *Nothing*
* [109: Fix installation error due to typo in latest migration](https://github.com/shlinkio/shlink/issues/109) #### Deprecated
### 1.6.1 * *Nothing*
**Tasks** #### Removed
* [110: Create gitattributes file to define files to be excluded from distributable package](https://github.com/shlinkio/shlink/issues/110) * *Nothing*
### 1.6.0 #### Fixed
**Features** * [#154](https://github.com/shlinkio/shlink/issues/154) Fixed sizes of every result page when filtering by searchTerm
* [#157](https://github.com/shlinkio/shlink/issues/157) Background commands executed by installation process now respect the originally used php binary
* [44: Consider allowing to set custom slugs instead of generating a short code](https://github.com/shlinkio/shlink/issues/44)
* [47: Allow to limit short codes availability by date range](https://github.com/shlinkio/shlink/issues/47)
* [48: Allow to limit the number of visits to a short code](https://github.com/shlinkio/shlink/issues/48)
* [105: Added option to enable/disable URL validation by response status code.](https://github.com/shlinkio/shlink/pull/105)
**Enhancements:** ## 1.9.0 - 2018-05-07
* [27: Add repository functional tests with dbunit](https://github.com/shlinkio/shlink/issues/27) #### Added
* [86: Drop support for PHP 5](https://github.com/shlinkio/shlink/issues/86)
* [101: Make actions just capture very specific exceptions, and let the ErrorHandler catch any other exception](https://github.com/shlinkio/shlink/issues/101)
* [104: Use different templates for requested-short-code-does-not-exist and route-could-not-be-match](https://github.com/shlinkio/shlink/issues/104)
**Tasks** * [#147](https://github.com/shlinkio/shlink/issues/147) Allowed short URLs to be created on the fly using a single API request, including the API key in a query param.
* [99: Replace AnnotatedFactory by ConfigAbstractFactory](https://github.com/shlinkio/shlink/issues/99) This eases integration with third party services.
* [100: Replace twig by plates](https://github.com/shlinkio/shlink/issues/100)
* [102: Improve coding standards strictness](https://github.com/shlinkio/shlink/issues/102) With this feature, a simple request to a URL like `https://doma.in/rest/v1/short-codes/shorten?apiKey=[YOUR_API_KEY]&longUrl=[URL_TO_BE_SHORTENED]` would return the shortened one in JSON or plain text format.
**Bugs** #### Changed
* [103: Make NotFoundDelegate return proper content types based on accepted content](https://github.com/shlinkio/shlink/issues/103) * *Nothing*
### 1.5.0 #### Deprecated
**Enhancements:** * *Nothing*
* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95) #### Removed
* [59: Add tags CRUD to REST](https://github.com/shlinkio/shlink/issues/59)
* [66: Allow to import certain information from older app directory when updating](https://github.com/shlinkio/shlink/issues/66)
**Tasks** * *Nothing*
* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96) #### Fixed
* [76: Add response examples to swagger docs](https://github.com/shlinkio/shlink/issues/76)
* [93: Improve cross domain management by using the ImplicitOptionsMiddleware](https://github.com/shlinkio/shlink/issues/93)
**Bugs** * [#139](https://github.com/shlinkio/shlink/issues/139) Ensured all core actions log exceptions
* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92)
### 1.4.0 ## 1.8.1 - 2018-04-07
**Enhancements:** #### Added
* [89: Update to expressive 2](https://github.com/shlinkio/shlink/issues/89) * *Nothing*
### 1.3.1 #### Changed
**Tasks** * [#141](https://github.com/shlinkio/shlink/issues/141) Removed workaround used in `PathVersionMiddleware`, since the bug in zend-stratigility has been fixed.
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82) #### Deprecated
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
**Bugs** * *Nothing*
* [83: Short codes list: search in tags when filtering by query string](https://github.com/shlinkio/shlink/issues/83) #### Removed
* [79: Increase the number of followed redirects](https://github.com/shlinkio/shlink/issues/79)
* [75: Apply PathVersionMiddleware only to rest routes defining it by configuration instead of code](https://github.com/shlinkio/shlink/issues/75)
* [77: Allow defining database server hostname and port](https://github.com/shlinkio/shlink/issues/77)
### 1.3.0 * *Nothing*
**Enhancements:** #### Fixed
* [67: Allow to order the short codes list](https://github.com/shlinkio/shlink/issues/67) * [#140](https://github.com/shlinkio/shlink/issues/140) Fixed warning thrown during installation while trying to include doctrine script
* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/shlinkio/shlink/issues/60)
* [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/shlinkio/shlink/issues/72)
* [58: Allow to filter short URLs by tag](https://github.com/shlinkio/shlink/issues/58)
* [69: Allow to filter short codes by text query](https://github.com/shlinkio/shlink/issues/69)
**Tasks**
* [73: Tag endpoints in swagger file](https://github.com/shlinkio/shlink/issues/73) ## 1.8.0 - 2018-03-29
* [71: Separate swagger docs into multiple files](https://github.com/shlinkio/shlink/issues/71)
* [63: Add path versioning to REST API routes](https://github.com/shlinkio/shlink/issues/63)
### 1.2.2 #### Added
**Bugs** * [#125](https://github.com/shlinkio/shlink/issues/125) Implemented a path which returns a 1px image instead of a redirection.
Useful to track emails. Just add an image pointing to a URL like `https://doma.in/abc123/track` to any email and an invisible image will be generated tracking every time the email is opened.
* [#132](https://github.com/shlinkio/shlink/issues/132) Added infection to improve tests
#### Changed
* [#130](https://github.com/shlinkio/shlink/issues/130) Updated to Expressive 3
* [#137](https://github.com/shlinkio/shlink/issues/137) Updated symfony components to v4
#### Deprecated
* *Nothing*
#### Removed
* [#131](https://github.com/shlinkio/shlink/issues/131) Dropped support for PHP 7
#### Fixed
* *Nothing*
## 1.7.2 - 2018-03-26
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#135](https://github.com/shlinkio/shlink/issues/135) Fixed `PathVersionMiddleware` being ignored when using expressive 2.2
## 1.7.1 - 2018-03-21
#### Added
* *Nothing*
#### Changed
* [#128](https://github.com/shlinkio/shlink/issues/128) Upgraded to expressive 2.2
This will ease the upcoming update to expressive 3
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#126](https://github.com/shlinkio/shlink/issues/126) Fixed `E_USER_DEPRECATED` errors triggered when using Expressive 2.2
## 1.7.0 - 2018-01-21
#### Added
* [#88](https://github.com/shlinkio/shlink/issues/88) Allowed tracking of short URLs to be disabled by including a configurable query param
* [#108](https://github.com/shlinkio/shlink/issues/108) Allowed metadata to be defined when creating short codes
#### Changed
* [#113](https://github.com/shlinkio/shlink/issues/113) Updated CLI commands to use `SymfonyStyle`
* [#112](https://github.com/shlinkio/shlink/issues/112) Enabled Lazy loading in CLI commands
* [#117](https://github.com/shlinkio/shlink/issues/117) Every module which throws exceptions has now its own `ExceptionInterface` extending `Throwable`
* [#115](https://github.com/shlinkio/shlink/issues/115) Added phpstan to build matrix on PHP >=7.1 envs
* [#114](https://github.com/shlinkio/shlink/issues/114) Replaced [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) dev requirement by [symfony/dotenv](https://github.com/symfony/dotenv)
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 1.6.2 - 2017-10-25
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#109](https://github.com/shlinkio/shlink/issues/109) Fixed installation error due to typo in latest migration
## 1.6.1 - 2017-10-24
#### Added
* *Nothing*
#### Changed
* [#110](https://github.com/shlinkio/shlink/issues/110) Created `.gitattributes` file to define files to be excluded from distributable package
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 1.6.0 - 2017-10-23
#### Added
* [#44](https://github.com/shlinkio/shlink/issues/44) Now it is possible to set custom slugs for short URLs instead of using a generated short code
* [#47](https://github.com/shlinkio/shlink/issues/47) Allowed to limit short URLs availability by date range
* [#48](https://github.com/shlinkio/shlink/issues/48) Allowed to limit the number of visits to a short URL
* [#105](https://github.com/shlinkio/shlink/pull/105) Added option to enable/disable URL validation by response status code
#### Changed
* [#27](https://github.com/shlinkio/shlink/issues/27) Added repository functional tests with dbunit
* [#101](https://github.com/shlinkio/shlink/issues/101) Now specific actions just capture very specific exceptions, and let the `ErrorHandler` catch any other unhandled exception
* [#104](https://github.com/shlinkio/shlink/issues/104) Used different templates for *requested-short-code-does-not-exist* and *route-could-not-be-match*
* [#99](https://github.com/shlinkio/shlink/issues/99) Replaced usages of `AnnotatedFactory` by `ConfigAbstractFactory`
* [#100](https://github.com/shlinkio/shlink/issues/100) Updated templates engine. Replaced twig by plates
* [#102](https://github.com/shlinkio/shlink/issues/102) Improved coding standards strictness
#### Deprecated
* *Nothing*
#### Removed
* [#86](https://github.com/shlinkio/shlink/issues/86) Dropped support for PHP 5
#### Fixed
* [#103](https://github.com/shlinkio/shlink/issues/103) `NotFoundDelegate` now returns proper content types based on accepted content
## 1.5.0 - 2017-07-16
#### Added
* [#95](https://github.com/shlinkio/shlink/issues/95) Added tags CRUD to CLI
* [#59](https://github.com/shlinkio/shlink/issues/59) Added tags CRUD to REST
* [#66](https://github.com/shlinkio/shlink/issues/66) Allowed certain information to be imported from and older shlink instance directory when updating
#### Changed
* [#96](https://github.com/shlinkio/shlink/issues/96) Added namespace to functions
* [#76](https://github.com/shlinkio/shlink/issues/76) Added response examples to swagger docs
* [#93](https://github.com/shlinkio/shlink/issues/93) Improved cross domain management by using the `ImplicitOptionsMiddleware`
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#92](https://github.com/shlinkio/shlink/issues/92) Fixed formatted dates, using an ISO compliant format
## 1.4.0 - 2017-03-25
#### Added
* *Nothing*
#### Changed
* [#89](https://github.com/shlinkio/shlink/issues/89) Updated to expressive 2
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 1.3.1 - 2017-01-22
#### Added
* *Nothing*
#### Changed
* [#82](https://github.com/shlinkio/shlink/issues/82) Enabled `FastRoute` routes cache
* [#85](https://github.com/shlinkio/shlink/issues/85) Updated year in license file
* [#81](https://github.com/shlinkio/shlink/issues/81) Added docker containers config
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#83](https://github.com/shlinkio/shlink/issues/83) Fixed short codes list: search in tags when filtering by query string
* [#79](https://github.com/shlinkio/shlink/issues/79) Increased the number of followed redirects
* [#75](https://github.com/shlinkio/shlink/issues/75) Applied `PathVersionMiddleware` only to rest routes defining it by configuration instead of code
* [#77](https://github.com/shlinkio/shlink/issues/77) Allowed defining database server hostname and port
## 1.3.0 - 2016-10-23
#### Added
* [#67](https://github.com/shlinkio/shlink/issues/67) Allowed to order the short codes list
* [#60](https://github.com/shlinkio/shlink/issues/60) Accepted JSON requests in REST and used a body parser middleware to set the request's `parsedBody`
* [#72](https://github.com/shlinkio/shlink/issues/72) When listing API keys from CLI, use yellow color for enabled keys that have expired
* [#58](https://github.com/shlinkio/shlink/issues/58) Allowed to filter short URLs by tag
* [#69](https://github.com/shlinkio/shlink/issues/69) Allowed to filter short URLs by text query
* [#73](https://github.com/shlinkio/shlink/issues/73) Added tag-related endpoints to swagger file
* [#63](https://github.com/shlinkio/shlink/issues/63) Added path versioning to REST API routes
#### Changed
* [#71](https://github.com/shlinkio/shlink/issues/71) Separated swagger docs into multiple files
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*
## 1.2.2 - 2016-08-29
#### Added
* *Nothing*
#### Changed
* *Nothing*
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* Fixed minor bugs on CORS requests * Fixed minor bugs on CORS requests
### 1.2.1
**Bugs** ## 1.2.1 - 2016-08-21
* [62: Fix cross-domain requests in REST API](https://github.com/shlinkio/shlink/issues/62) #### Added
### 1.2.0 * *Nothing*
**Features** #### Changed
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/shlinkio/shlink/issues/45) * *Nothing*
* [7: Add website previews while listing available URLs](https://github.com/shlinkio/shlink/issues/7)
**Enhancements:** #### Deprecated
* [57: Add database migrations system to improve updating between versions](https://github.com/shlinkio/shlink/issues/57) * *Nothing*
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/shlinkio/shlink/issues/31)
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/shlinkio/shlink/issues/51)
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/shlinkio/shlink/issues/38)
**Tasks** #### Removed
* [55: Create update script which does not try to create a new database](https://github.com/shlinkio/shlink/issues/55) * *Nothing*
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/shlinkio/shlink/issues/54)
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/shlinkio/shlink/issues/29)
**Bugs** #### Fixed
* [53: Fix entities database interoperability](https://github.com/shlinkio/shlink/issues/53) * [#62](https://github.com/shlinkio/shlink/issues/62) Fixed cross-domain requests in REST API
* [52: Add missing htaccess file for apache environments](https://github.com/shlinkio/shlink/issues/52)
### 1.1.0
**Features** ## 1.2.0 - 2016-08-21
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/shlinkio/shlink/issues/46) #### Added
**Enhancements:** * [#45](https://github.com/shlinkio/shlink/issues/45) Allowed to define tags on short codes, to improve filtering and classification
* [#7](https://github.com/shlinkio/shlink/issues/7) Added website previews while listing available URLs
* [#57](https://github.com/shlinkio/shlink/issues/57) Added database migrations system to improve updating between versions
* [#31](https://github.com/shlinkio/shlink/issues/31) Added support for other database management systems by improving the `EntityManager` factory
* [#51](https://github.com/shlinkio/shlink/issues/51) Generated build process to create app package and ease distribution
* [#38](https://github.com/shlinkio/shlink/issues/38) Defined installation script. It will request dynamic data on the fly so that there is no need to define env vars
* [#55](https://github.com/shlinkio/shlink/issues/55) Created update script which does not try to create a new database
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/shlinkio/shlink/issues/32) #### Changed
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/shlinkio/shlink/issues/14)
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/shlinkio/shlink/issues/41)
* [13: Improve REST authentication](https://github.com/shlinkio/shlink/issues/13)
**Tasks** * [#54](https://github.com/shlinkio/shlink/issues/54) Added cache namespace to prevent name collisions with other apps in the same environment
* [#29](https://github.com/shlinkio/shlink/issues/29) Used the [acelaya/ze-content-based-error-handler](https://github.com/acelaya/ze-content-based-error-handler) package instead of custom error handler implementation
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/shlinkio/shlink/issues/39) #### Deprecated
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/shlinkio/shlink/issues/42)
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/shlinkio/shlink/issues/35)
**Bugs** * *Nothing*
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/shlinkio/shlink/issues/40) #### Removed
### 1.0.0 * *Nothing*
**Enhancements:** #### Fixed
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/shlinkio/shlink/issues/33) * [#53](https://github.com/shlinkio/shlink/issues/53) Fixed entities database interoperability
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/shlinkio/shlink/issues/15) * [#52](https://github.com/shlinkio/shlink/issues/52) Added missing htaccess file for apache environments
* [23: Translate application literals](https://github.com/shlinkio/shlink/issues/23)
* [21: Allow to filter visits by date range](https://github.com/shlinkio/shlink/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/shlinkio/shlink/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/shlinkio/shlink/issues/20)
* [11: Separate code into multiple modules](https://github.com/shlinkio/shlink/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/shlinkio/shlink/issues/18)
**Tasks**
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/shlinkio/shlink/issues/36) ## 1.1.0 - 2016-08-09
* [4: Installation steps](https://github.com/shlinkio/shlink/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/shlinkio/shlink/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/shlinkio/shlink/issues/30)
* [12: Improve code coverage](https://github.com/shlinkio/shlink/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/shlinkio/shlink/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/shlinkio/shlink/issues/19)
**Bugs** #### Added
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/shlinkio/shlink/issues/24) * [#46](https://github.com/shlinkio/shlink/issues/46) Defined a route that returns a QR code representing the shortened URL.
### 0.2.0 In order to get the QR code URL, use a pattern like `https://doma.in/abc123/qr-code`
**Enhancements:** * [#32](https://github.com/shlinkio/shlink/issues/32) Added support for other cache adapters by improving the Cache factory
* [#14](https://github.com/shlinkio/shlink/issues/14) Added logger and enabled errors logging
* [#13](https://github.com/shlinkio/shlink/issues/13) Improved REST authentication
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/shlinkio/shlink/issues/9) #### Changed
* [8: Create a REST API](https://github.com/shlinkio/shlink/issues/8)
* [10: Add more CLI functionality](https://github.com/shlinkio/shlink/issues/10)
**Tasks** * [#41](https://github.com/shlinkio/shlink/issues/41) Cached the "short code" => "URL" map to prevent extra DB hits
* [#39](https://github.com/shlinkio/shlink/issues/39) Changed copyright from "Alejandro Celaya" to "Shlink" in error pages
* [#42](https://github.com/shlinkio/shlink/issues/42) REST endpoints that need to find *something* now return a 404 when it is not found
* [#35](https://github.com/shlinkio/shlink/issues/35) Updated CLI commands to use the same PHP namespace as the one used for the command name
* [5: Create CHANGELOG file](https://github.com/shlinkio/shlink/issues/5) #### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* [#40](https://github.com/shlinkio/shlink/issues/40) Taken into account the `X-Forwarded-For` header in order to get the visitor information, in case the server is behind a load balancer or proxy
## 1.0.0 - 2016-08-01
#### Added
* [#33](https://github.com/shlinkio/shlink/issues/33) Created a command that generates a short code charset by randomizing the default one
* [#23](https://github.com/shlinkio/shlink/issues/23) Translated application literals
* [#21](https://github.com/shlinkio/shlink/issues/21) Allowed to filter visits by date range
* [#4](https://github.com/shlinkio/shlink/issues/4) Added installation steps
* [#12](https://github.com/shlinkio/shlink/issues/12) Improved code coverage
#### Changed
* [#15](https://github.com/shlinkio/shlink/issues/15) HTTP requests now return JSON/HTML responses for errors (4xx and 5xx) based on `Accept` header
* [#22](https://github.com/shlinkio/shlink/issues/22) Now visits locations data is saved on a `visit_locations` table
* [#20](https://github.com/shlinkio/shlink/issues/20) Injected cross domain headers in response only if the `Origin` header is present in the request
* [#11](https://github.com/shlinkio/shlink/issues/11) Separated code into multiple modules
* [#18](https://github.com/shlinkio/shlink/issues/18) Grouped routable middleware in an Action namespace
* [#6](https://github.com/shlinkio/shlink/issues/6) Project no longer depends on [zendframework/zend-expressive-helpers](https://github.com/zendframework/zend-expressive-helpers) package
* [#30](https://github.com/shlinkio/shlink/issues/30) Replaced the "services" first level config entry by "dependencies", in order to fulfill default Expressive naming
* [#25](https://github.com/shlinkio/shlink/issues/25) Replaced "Middleware" suffix on routable middlewares by "Action"
* [#19](https://github.com/shlinkio/shlink/issues/19) Changed the vendor and app namespace from `Acelaya\UrlShortener` to `Shlinkio\Shlink`
#### Deprecated
* *Nothing*
#### Removed
* [#36](https://github.com/shlinkio/shlink/issues/36) Removed hhvm from the CI matrix since it doesn't support array constants and will fail
#### Fixed
* [#24](https://github.com/shlinkio/shlink/issues/24) Prevented duplicated short codes errors because of the case insensitive behavior on MySQL
## 0.2.0 - 2016-08-01
#### Added
* [#8](https://github.com/shlinkio/shlink/issues/8) Created a REST API
* [#10](https://github.com/shlinkio/shlink/issues/10) Added more CLI functionality
* [#5](https://github.com/shlinkio/shlink/issues/5) Created a CHANGELOG file
#### Changed
* [#9](https://github.com/shlinkio/shlink/issues/9) Used [symfony/console](https://github.com/symfony/console) to dispatch console requests, instead of trying to integrate the process with expressive
#### Deprecated
* *Nothing*
#### Removed
* *Nothing*
#### Fixed
* *Nothing*

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,33 +12,29 @@
} }
], ],
"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/cache": "^1.6", "doctrine/cache": "^1.6",
"doctrine/collections": "^1.4",
"doctrine/common": "^2.7",
"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 +43,16 @@
}, },
"require-dev": { "require-dev": {
"filp/whoops": "^2.0", "filp/whoops": "^2.0",
"phpunit/dbunit": "^3.0", "infection/infection": "^0.9.0",
"phpunit/phpcov": "^4.0", "phpstan/phpstan": "^0.10.0",
"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.2.3",
"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 +82,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 +97,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=55 --only-covered --log-verbosity=2",
"infect-show": "infection --threads=4 --min-msi=55 --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,30 +5,23 @@ 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\Plates;
use Zend\Expressive\Router;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware; use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
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,
Template\TemplateRendererInterface::class => Plates\PlatesRendererFactory::class,
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
ErrorHandler::class => Container\ErrorHandlerFactory::class,
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class, 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,
], ],
'aliases' => [ 'delegators' => [
Middleware\ImplicitOptionsMiddleware::class => ImplicitOptionsMiddleware::class, 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,12 +16,13 @@ return [
'pre-routing' => [ 'pre-routing' => [
'middleware' => [ 'middleware' => [
ErrorHandler::class, ErrorHandler::class,
Expressive\Helper\ContentLengthMiddleware::class,
LocaleMiddleware::class, LocaleMiddleware::class,
], ],
'priority' => 11, 'priority' => 11,
], ],
'pre-routing-rest' => [ 'pre-routing-rest' => [
// 'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
PathVersionMiddleware::class, PathVersionMiddleware::class,
], ],
@@ -48,6 +50,7 @@ return [
'post-routing' => [ 'post-routing' => [
'middleware' => [ 'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class, 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,8 +19,14 @@ use Zend\ConfigAggregator;
*/ */
return (new ConfigAggregator\ConfigAggregator([ return (new ConfigAggregator\ConfigAggregator([
Zend\Expressive\ConfigProvider::class, Expressive\ConfigProvider::class,
Zend\Expressive\Router\ConfigProvider::class, Expressive\Router\ConfigProvider::class,
Expressive\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class,
Expressive\Helper\ConfigProvider::class,
\class_exists(Expressive\Swoole\ConfigProvider::class)
? Expressive\Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]),
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

@@ -116,7 +116,10 @@
], ],
"pagination": { "pagination": {
"currentPage": 5, "currentPage": 5,
"pagesCount": 12 "pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
} }
} }
} }

View File

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

View File

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

22
infection.json Normal file
View File

@@ -0,0 +1,22 @@
{
"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": "."
},
"mutators": {
"@default": true,
"IdenticalEqual": false
}
}

View File

@@ -14,7 +14,11 @@ use Zend\I18n\Translator\TranslatorInterface;
class ListKeysCommand extends Command class ListKeysCommand extends Command
{ {
const NAME = 'api-key:list'; private const ERROR_STRING_PATTERN = '<fg=red>%s</>';
private const SUCCESS_STRING_PATTERN = '<info>%s</info>';
private const WARNING_STRING_PATTERN = '<comment>%s</comment>';
public const NAME = 'api-key:list';
/** /**
* @var ApiKeyServiceInterface * @var ApiKeyServiceInterface
@@ -55,59 +59,32 @@ class ListKeysCommand extends Command
foreach ($list as $row) { foreach ($list as $row) {
$key = $row->getKey(); $key = $row->getKey();
$expiration = $row->getExpirationDate(); $expiration = $row->getExpirationDate();
$formatMethod = $this->determineFormatMethod($row); $messagePattern = $this->determineMessagePattern($row);
// Set columns for this row // Set columns for this row
$rowData = [$formatMethod($key)]; $rowData = [\sprintf($messagePattern, $key)];
if (! $enabledOnly) { if (! $enabledOnly) {
$rowData[] = $formatMethod($this->getEnabledSymbol($row)); $rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
} }
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-'; $rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
$rows[] = $rowData; $rows[] = $rowData;
} }
$io->table(array_filter([ $io->table(\array_filter([
$this->translator->translate('Key'), $this->translator->translate('Key'),
! $enabledOnly ? $this->translator->translate('Is enabled') : null, ! $enabledOnly ? $this->translator->translate('Is enabled') : null,
$this->translator->translate('Expiration date'), $this->translator->translate('Expiration date'),
]), $rows); ]), $rows);
} }
private function determineFormatMethod(ApiKey $apiKey): callable private function determineMessagePattern(ApiKey $apiKey): string
{ {
if (! $apiKey->isEnabled()) { if (! $apiKey->isEnabled()) {
return [$this, 'getErrorString']; return self::ERROR_STRING_PATTERN;
} }
return $apiKey->isExpired() ? [$this, 'getWarningString'] : [$this, 'getSuccessString']; return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
}
/**
* @param string $value
* @return string
*/
private function getErrorString(string $value): string
{
return sprintf('<fg=red>%s</>', $value);
}
/**
* @param string $value
* @return string
*/
private function getSuccessString(string $value): string
{
return sprintf('<info>%s</info>', $value);
}
/**
* @param string $value
* @return string
*/
private function getWarningString(string $value): string
{
return sprintf('<comment>%s</comment>', $value);
} }
/** /**

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

@@ -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((string) \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

@@ -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

@@ -3,10 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware; namespace Shlinkio\Shlink\Rest\Middleware;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class PathVersionMiddleware implements MiddlewareInterface class PathVersionMiddleware implements MiddlewareInterface
{ {
@@ -15,32 +15,21 @@ class PathVersionMiddleware 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
*/ */
public function process(Request $request, DelegateInterface $delegate) public function process(Request $request, RequestHandlerInterface $handler): Response
{ {
$uri = $request->getUri(); $uri = $request->getUri();
$path = $uri->getPath(); $path = $uri->getPath();
// TODO Workaround... Do not process the request if it does not start with rest
if (\strpos($path, '/rest') !== 0) {
return $delegate->process($request);
}
// 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, '/rest/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 rest part
\array_shift($parts);
\array_shift($parts);
// Prepend the version prefix
\array_unshift($parts, '/rest/v1');
$request = $request->withUri($uri->withPath(\implode('/', $parts)));
} }
return $delegate->process($request); return $handler->handle($request);
} }
} }

View File

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

View File

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

View File

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

View File

@@ -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,7 +16,6 @@ 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; use function Zend\Stratigility\middleware;
class CheckAuthenticationMiddlewareTest extends TestCase class CheckAuthenticationMiddlewareTest extends TestCase
@@ -38,9 +37,11 @@ class CheckAuthenticationMiddlewareTest extends TestCase
public function setUp() public function setUp()
{ {
$this->jwtService = $this->prophesize(JWTService::class); $this->jwtService = $this->prophesize(JWTService::class);
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([])); $this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]), [
$this->dummyMiddleware = middleware(function ($request, $handler) { AuthenticateAction::class,
return new Response\EmptyResponse; ]);
$this->dummyMiddleware = middleware(function () {
return new Response\EmptyResponse();
}); });
} }
@@ -50,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);
@@ -61,9 +62,9 @@ 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);
@@ -76,9 +77,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
AuthenticateAction::class 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);
@@ -86,9 +87,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), []) 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);
} }
@@ -102,7 +103,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::class, RouteResult::class,
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), []) 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());
} }
@@ -117,7 +118,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), []) 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);
@@ -134,7 +135,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
RouteResult::fromRoute(new Route('bar', $this->dummyMiddleware), []) 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(
@@ -154,7 +155,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
)->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());
} }
@@ -171,9 +172,9 @@ class CheckAuthenticationMiddlewareTest extends TestCase
$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,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\Assert; use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\Argument; 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;
@@ -30,25 +30,10 @@ class PathVersionMiddlewareTest extends TestCase
*/ */
public function whenVersionIsProvidedRequestRemainsUnchanged() public function whenVersionIsProvidedRequestRemainsUnchanged()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/v2/foo')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/v2/foo'));
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->process($request)->willReturn(new Response()); $process = $delegate->handle($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal());
$process->shouldHaveBeenCalled();
}
/**
* @test
*/
public function whenPathDoesNotStartWithRestRemainsUnchanged()
{
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/foo'));
$delegate = $this->prophesize(DelegateInterface::class);
$process = $delegate->process($request)->willReturn(new Response());
$this->middleware->process($request, $delegate->reveal()); $this->middleware->process($request, $delegate->reveal());
@@ -60,14 +45,14 @@ class PathVersionMiddlewareTest extends TestCase
*/ */
public function versionOneIsPrependedWhenNoVersionIsDefined() public function versionOneIsPrependedWhenNoVersionIsDefined()
{ {
$request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/rest/bar/baz')); $request = ServerRequestFactory::fromGlobals()->withUri(new Uri('/bar/baz'));
$delegate = $this->prophesize(DelegateInterface::class); $delegate = $this->prophesize(RequestHandlerInterface::class);
$delegate->process(Argument::type(Request::class))->will(function (array $args) use ($request) { $delegate->handle(Argument::type(Request::class))->will(function (array $args) use ($request) {
$req = \array_shift($args); $req = \array_shift($args);
Assert::assertNotSame($request, $req); Assert::assertNotSame($request, $req);
Assert::assertEquals('/rest/v1/bar/baz', $req->getUri()->getPath()); Assert::assertEquals('/v1/bar/baz', $req->getUri()->getPath());
return new Response(); return new Response();
}); });

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'],
[[], ''],
];
}
}

View File

@@ -1,3 +1,7 @@
parameters: parameters:
excludes_analyse: excludes_analyse:
- module/Common/src/Template/Extension/TranslatorExtension.php - module/Common/src/Template/Extension/TranslatorExtension.php
- module/Rest/src/Util/RestUtils.php
ignoreErrors:
- '#is not subtype of Throwable#'
- '#Cannot access offset#'

View File

@@ -1,9 +1,9 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Interop\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Zend\Expressive\Application; use Zend\Expressive\Application;
/** @var ContainerInterface $container */ /** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php'; $container = include __DIR__ . '/../config/container.php';
$app = $container->get(Application::class)->run(); $container->get(Application::class)->run();