mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-04 22:33:12 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30297ac5ac | ||
|
|
416c56dee2 | ||
|
|
6b968a6843 | ||
|
|
080965e166 | ||
|
|
c7239aaca2 | ||
|
|
110e8cb78d | ||
|
|
ed859767a8 | ||
|
|
d54b823c88 | ||
|
|
0ae44d3331 | ||
|
|
8063e643a3 | ||
|
|
3883ed15c4 | ||
|
|
a79c1f580e | ||
|
|
f4b569c245 | ||
|
|
899771cc2e | ||
|
|
863803b614 | ||
|
|
5be5e0bc60 | ||
|
|
0b8e305533 | ||
|
|
39d79366a3 | ||
|
|
d5b78f2a7e | ||
|
|
b2a63f734a | ||
|
|
82f41de87b | ||
|
|
af4c66d40a | ||
|
|
975260f126 | ||
|
|
bd678b41f7 | ||
|
|
66898b6ddc | ||
|
|
5eee683978 | ||
|
|
e92446df9b | ||
|
|
63a69b05a1 | ||
|
|
c79ca1d13c | ||
|
|
87c4851d7e | ||
|
|
a125c93ca3 |
694
CHANGELOG.md
694
CHANGELOG.md
@@ -1,274 +1,638 @@
|
||||
## CHANGELOG
|
||||
# CHANGELOG
|
||||
|
||||
### 1.9.1
|
||||
## 1.10.2 - 2018-08-04
|
||||
|
||||
**Bugs:**
|
||||
#### Added
|
||||
|
||||
* [154: When filtering by searchTerm, sizes of every result page has an unexpected behavior](https://github.com/shlinkio/shlink/issues/154)
|
||||
* [157: Background commands executed by installation process do not respect the used php binary](https://github.com/shlinkio/shlink/issues/157)
|
||||
* *Nothing*
|
||||
|
||||
**Enhancements:**
|
||||
#### Changed
|
||||
|
||||
* [155: Improve the pagination object returned in lists, including more meaningful properties](https://github.com/shlinkio/shlink/issues/155)
|
||||
* *Nothing*
|
||||
|
||||
### 1.9.0
|
||||
#### Deprecated
|
||||
|
||||
**Features**
|
||||
* *Nothing*
|
||||
|
||||
* [147: Allow short URLs to be created on the fly with query param authentication](https://github.com/shlinkio/shlink/issues/147)
|
||||
#### Removed
|
||||
|
||||
**Bugs:**
|
||||
* *Nothing*
|
||||
|
||||
* [139: Make sure all core actions log exceptions](https://github.com/shlinkio/shlink/issues/139)
|
||||
#### Fixed
|
||||
|
||||
### 1.8.1
|
||||
* [#177](https://github.com/shlinkio/shlink/issues/177) Fixed `[GET] /short-codes` endpoint returning a 500 status code when trying to filter by `tags` and `searchTerm` at the same time.
|
||||
* [#175](https://github.com/shlinkio/shlink/issues/175) Fixed error introduced in previous version, where you could end up banned from the service used to resolve IP address locations.
|
||||
|
||||
**Tasks**
|
||||
In order to fix that, just fill [this form](http://ip-api.com/docs/unban) including your server's IP address and your server should be unbanned.
|
||||
|
||||
In order to prevent this, after resolving 150 IP addresses, shlink now waits 1 minute before trying to resolve any more addresses.
|
||||
|
||||
* [141: Remove workaround used in PathVersionMiddleware](https://github.com/shlinkio/shlink/issues/141)
|
||||
|
||||
**Bugs:**
|
||||
## 1.10.1 - 2018-08-02
|
||||
|
||||
* [140: Installation failed. Warning thrown while trying to include doctrine script](https://github.com/shlinkio/shlink/issues/140)
|
||||
#### Added
|
||||
|
||||
### 1.8.0
|
||||
* *Nothing*
|
||||
|
||||
**Features**
|
||||
#### Changed
|
||||
|
||||
* [125: Implement a path which returns a 1px image instead of a redirection](https://github.com/shlinkio/shlink/issues/125)
|
||||
* [#167](https://github.com/shlinkio/shlink/issues/167) Shlink version is now set at build time to avoid older version numbers to be kept in newer builds.
|
||||
|
||||
**Enhancements:**
|
||||
#### Deprecated
|
||||
|
||||
* [130: Update to Expressive 3](https://github.com/shlinkio/shlink/issues/130)
|
||||
* [137: Update symfony packages to v4](https://github.com/shlinkio/shlink/issues/137)
|
||||
* *Nothing*
|
||||
|
||||
**Tasks**
|
||||
#### Removed
|
||||
|
||||
* [131: Drop support for PHP 7](https://github.com/shlinkio/shlink/issues/131)
|
||||
* [132: Add infection to improve tests](https://github.com/shlinkio/shlink/issues/132)
|
||||
* *Nothing*
|
||||
|
||||
### 1.7.2
|
||||
#### Fixed
|
||||
|
||||
**Bugs:**
|
||||
* [#165](https://github.com/shlinkio/shlink/issues/165) Fixed custom slugs failing when they are longer than 10 characters.
|
||||
* [#166](https://github.com/shlinkio/shlink/issues/166) Fixed unusual edge case in which visits were not properly counted when ordering by visit and filtering by search term in `[GET] /short-codes` API endpoint.
|
||||
* [#174](https://github.com/shlinkio/shlink/issues/174) Fixed geolocation not working due to a deprecation on used service.
|
||||
* [#172](https://github.com/shlinkio/shlink/issues/172) Documented missing filtering params for `[GET] /short-codes/{shortCode}/visits` API endpoint, which allow the list to be filtered by date range.
|
||||
|
||||
* [135: Fix PathVersionMiddleware being ignored when using expressive 2.2](https://github.com/shlinkio/shlink/issues/135)
|
||||
For example: `https://doma.in/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
|
||||
|
||||
### 1.7.1
|
||||
* [#169](https://github.com/shlinkio/shlink/issues/169) Fixed unhandled error when parsing `ShortUrlMeta` and date fields are already `DateTime` instances.
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [128: Upgrade to expressive 2.2](https://github.com/shlinkio/shlink/issues/128)
|
||||
## 1.10.0 - 2018-07-09
|
||||
|
||||
**Bugs**
|
||||
#### Added
|
||||
|
||||
* [126: Expressive 2.2 causes failures by triggering E_USER_DEPRECATED errors](https://github.com/shlinkio/shlink/issues/126)
|
||||
* [#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.0
|
||||
#### Changed
|
||||
|
||||
**Features**
|
||||
* [#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
|
||||
|
||||
* [88: Allow to disable tracking of the short URL by including a configurable query param](https://github.com/shlinkio/shlink/issues/88)
|
||||
* [108: Allow to edit metadata in created shortcodes](https://github.com/shlinkio/shlink/issues/108)
|
||||
#### Deprecated
|
||||
|
||||
**Enhancements:**
|
||||
* *Nothing*
|
||||
|
||||
* [113: Update CLI commands to use SymfonyStyle](https://github.com/shlinkio/shlink/issues/113)
|
||||
* [112: Configure cli commands lazy loading](https://github.com/shlinkio/shlink/issues/112)
|
||||
#### Removed
|
||||
|
||||
**Tasks**
|
||||
* *Nothing*
|
||||
|
||||
* [117: Make every module which throws exceptions have its own ExceptionInterface, and make them all extend Throwable](https://github.com/shlinkio/shlink/issues/117)
|
||||
* [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)
|
||||
#### Fixed
|
||||
|
||||
### 1.6.2
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [109: Fix installation error due to typo in latest migration](https://github.com/shlinkio/shlink/issues/109)
|
||||
## 1.9.1 - 2018-06-18
|
||||
|
||||
### 1.6.1
|
||||
#### Added
|
||||
|
||||
**Tasks**
|
||||
* [#155](https://github.com/shlinkio/shlink/issues/155) Improved the pagination object returned in lists, including more meaningful properties.
|
||||
|
||||
* Old structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"pagination": {
|
||||
"currentPage": 1,
|
||||
"pagesCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* New structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"pagination": {
|
||||
"currentPage": 2,
|
||||
"pagesCount": 13,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 126
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* [110: Create gitattributes file to define files to be excluded from distributable package](https://github.com/shlinkio/shlink/issues/110)
|
||||
#### Changed
|
||||
|
||||
### 1.6.0
|
||||
* *Nothing*
|
||||
|
||||
**Features**
|
||||
#### Deprecated
|
||||
|
||||
* [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)
|
||||
* *Nothing*
|
||||
|
||||
**Enhancements:**
|
||||
#### Removed
|
||||
|
||||
* [27: Add repository functional tests with dbunit](https://github.com/shlinkio/shlink/issues/27)
|
||||
* [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)
|
||||
* *Nothing*
|
||||
|
||||
**Tasks**
|
||||
#### Fixed
|
||||
|
||||
* [99: Replace AnnotatedFactory by ConfigAbstractFactory](https://github.com/shlinkio/shlink/issues/99)
|
||||
* [100: Replace twig by plates](https://github.com/shlinkio/shlink/issues/100)
|
||||
* [102: Improve coding standards strictness](https://github.com/shlinkio/shlink/issues/102)
|
||||
* [#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
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [103: Make NotFoundDelegate return proper content types based on accepted content](https://github.com/shlinkio/shlink/issues/103)
|
||||
## 1.9.0 - 2018-05-07
|
||||
|
||||
### 1.5.0
|
||||
#### Added
|
||||
|
||||
**Enhancements:**
|
||||
* [#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.
|
||||
|
||||
* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95)
|
||||
* [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)
|
||||
This eases integration with third party services.
|
||||
|
||||
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.
|
||||
|
||||
**Tasks**
|
||||
#### Changed
|
||||
|
||||
* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96)
|
||||
* [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)
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
#### Deprecated
|
||||
|
||||
* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92)
|
||||
* *Nothing*
|
||||
|
||||
### 1.4.0
|
||||
#### Removed
|
||||
|
||||
**Enhancements:**
|
||||
* *Nothing*
|
||||
|
||||
* [89: Update to expressive 2](https://github.com/shlinkio/shlink/issues/89)
|
||||
#### Fixed
|
||||
|
||||
### 1.3.1
|
||||
* [#139](https://github.com/shlinkio/shlink/issues/139) Ensured all core actions log exceptions
|
||||
|
||||
**Tasks**
|
||||
|
||||
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82)
|
||||
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
|
||||
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
|
||||
## 1.8.1 - 2018-04-07
|
||||
|
||||
**Bugs**
|
||||
#### Added
|
||||
|
||||
* [83: Short codes list: search in tags when filtering by query string](https://github.com/shlinkio/shlink/issues/83)
|
||||
* [79: Increase the number of followed redirects](https://github.com/shlinkio/shlink/issues/79)
|
||||
* [75: Apply PathVersionMiddleware only to rest routes defining it by configuration instead of code](https://github.com/shlinkio/shlink/issues/75)
|
||||
* [77: Allow defining database server hostname and port](https://github.com/shlinkio/shlink/issues/77)
|
||||
* *Nothing*
|
||||
|
||||
### 1.3.0
|
||||
#### Changed
|
||||
|
||||
**Enhancements:**
|
||||
* [#141](https://github.com/shlinkio/shlink/issues/141) Removed workaround used in `PathVersionMiddleware`, since the bug in zend-stratigility has been fixed.
|
||||
|
||||
* [67: Allow to order the short codes list](https://github.com/shlinkio/shlink/issues/67)
|
||||
* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/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)
|
||||
#### Deprecated
|
||||
|
||||
**Tasks**
|
||||
* *Nothing*
|
||||
|
||||
* [73: Tag endpoints in swagger file](https://github.com/shlinkio/shlink/issues/73)
|
||||
* [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)
|
||||
#### Removed
|
||||
|
||||
### 1.2.2
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
#### Fixed
|
||||
|
||||
* [#140](https://github.com/shlinkio/shlink/issues/140) Fixed warning thrown during installation while trying to include doctrine script
|
||||
|
||||
|
||||
## 1.8.0 - 2018-03-29
|
||||
|
||||
#### Added
|
||||
|
||||
* [#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
|
||||
|
||||
### 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)
|
||||
* [7: Add website previews while listing available URLs](https://github.com/shlinkio/shlink/issues/7)
|
||||
* *Nothing*
|
||||
|
||||
**Enhancements:**
|
||||
#### Deprecated
|
||||
|
||||
* [57: Add database migrations system to improve updating between versions](https://github.com/shlinkio/shlink/issues/57)
|
||||
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/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)
|
||||
* *Nothing*
|
||||
|
||||
**Tasks**
|
||||
#### Removed
|
||||
|
||||
* [55: Create update script which does not try to create a new database](https://github.com/shlinkio/shlink/issues/55)
|
||||
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/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)
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
#### Fixed
|
||||
|
||||
* [53: Fix entities database interoperability](https://github.com/shlinkio/shlink/issues/53)
|
||||
* [52: Add missing htaccess file for apache environments](https://github.com/shlinkio/shlink/issues/52)
|
||||
* [#62](https://github.com/shlinkio/shlink/issues/62) Fixed cross-domain requests in REST API
|
||||
|
||||
### 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)
|
||||
* [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)
|
||||
#### Changed
|
||||
|
||||
**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)
|
||||
* [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)
|
||||
#### Deprecated
|
||||
|
||||
**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)
|
||||
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/shlinkio/shlink/issues/15)
|
||||
* [23: Translate application literals](https://github.com/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)
|
||||
* [#53](https://github.com/shlinkio/shlink/issues/53) Fixed entities database interoperability
|
||||
* [#52](https://github.com/shlinkio/shlink/issues/52) Added missing htaccess file for apache environments
|
||||
|
||||
**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)
|
||||
* [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)
|
||||
## 1.1.0 - 2016-08-09
|
||||
|
||||
**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)
|
||||
* [8: Create a REST API](https://github.com/shlinkio/shlink/issues/8)
|
||||
* [10: Add more CLI functionality](https://github.com/shlinkio/shlink/issues/10)
|
||||
#### Changed
|
||||
|
||||
**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*
|
||||
|
||||
3
build.sh
3
build.sh
@@ -46,6 +46,9 @@ rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||
rm -rf config/params/{*,.gitignore}
|
||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||
|
||||
# Update shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Compressing file
|
||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
"php": "^7.1",
|
||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
"doctrine/annotations": "^1.4",
|
||||
"doctrine/cache": "^1.6",
|
||||
"doctrine/collections": "^1.4",
|
||||
"doctrine/common": "^2.7",
|
||||
"doctrine/dbal": "^2.5",
|
||||
"doctrine/migrations": "^1.4",
|
||||
"doctrine/orm": "^2.5",
|
||||
"endroid/qr-code": "^1.7",
|
||||
@@ -47,12 +43,12 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"filp/whoops": "^2.0",
|
||||
"infection/infection": "^0.8.1",
|
||||
"phpstan/phpstan": "0.9",
|
||||
"infection/infection": "^0.9.0",
|
||||
"phpstan/phpstan": "^0.10.0",
|
||||
"phpunit/phpcov": "^5.0",
|
||||
"phpunit/phpunit": "^7.0",
|
||||
"slevomat/coding-standard": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^3.1 <3.2",
|
||||
"squizlabs/php_codesniffer": "^3.2.3",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/var-dumper": "^4.0",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
@@ -103,8 +99,8 @@
|
||||
"phpcov merge build --html build/html"
|
||||
],
|
||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
||||
"infect": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2",
|
||||
"infect-show": "infection --threads=4 --min-msi=65 --only-covered --log-verbosity=2 --show-mutations",
|
||||
"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": {
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '1.7.0',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
],
|
||||
|
||||
|
||||
@@ -24,6 +24,9 @@ return (new ConfigAggregator\ConfigAggregator([
|
||||
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,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
|
||||
45
data/migrations/Version20180801183328.php
Normal file
45
data/migrations/Version20180801183328.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180801183328 extends AbstractMigration
|
||||
{
|
||||
private const NEW_SIZE = 255;
|
||||
private const OLD_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::NEW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::OLD_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param int $size
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function setSize(Schema $schema, int $size): void
|
||||
{
|
||||
$schema->getTable('short_urls')->getColumn('short_code')->setLength($size);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
|
||||
"required": false,
|
||||
|
||||
@@ -15,6 +15,24 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) from which we want to get visits.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "endDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) until which we want to get visits.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
|
||||
@@ -14,5 +14,9 @@
|
||||
"tmpDir": "build/infection/temp",
|
||||
"phpUnit": {
|
||||
"configDir": "."
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"IdenticalEqual": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
@@ -51,7 +51,7 @@ return [
|
||||
],
|
||||
Command\Visit\ProcessVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpLocationResolver::class,
|
||||
IpApiLocationResolver::class,
|
||||
'translator',
|
||||
],
|
||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||
|
||||
Binary file not shown.
@@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-01-21 09:36+0100\n"
|
||||
"PO-Revision-Date: 2018-01-21 09:39+0100\n"
|
||||
"POT-Creation-Date: 2018-08-04 16:35+0200\n"
|
||||
"PO-Revision-Date: 2018-08-04 16:37+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.4\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
@@ -317,6 +317,13 @@ msgstr "Ignorada IP de localhost"
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "An error occurred while locating IP"
|
||||
msgstr "Se produjo un error al localizar la IP"
|
||||
|
||||
#, php-format
|
||||
msgid "IP location resolver limit reached. Waiting %s seconds..."
|
||||
msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
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
|
||||
@@ -55,59 +59,32 @@ class ListKeysCommand extends Command
|
||||
foreach ($list as $row) {
|
||||
$key = $row->getKey();
|
||||
$expiration = $row->getExpirationDate();
|
||||
$formatMethod = $this->determineFormatMethod($row);
|
||||
$messagePattern = $this->determineMessagePattern($row);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [$formatMethod($key)];
|
||||
$rowData = [\sprintf($messagePattern, $key)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = $formatMethod($this->getEnabledSymbol($row));
|
||||
$rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
}
|
||||
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
|
||||
|
||||
$rows[] = $rowData;
|
||||
}
|
||||
|
||||
$io->table(array_filter([
|
||||
$io->table(\array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
]), $rows);
|
||||
}
|
||||
|
||||
private function determineFormatMethod(ApiKey $apiKey): callable
|
||||
private function determineMessagePattern(ApiKey $apiKey): string
|
||||
{
|
||||
if (! $apiKey->isEnabled()) {
|
||||
return [$this, 'getErrorString'];
|
||||
return self::ERROR_STRING_PATTERN;
|
||||
}
|
||||
|
||||
return $apiKey->isExpired() ? [$this, 'getWarningString'] : [$this, 'getSuccessString'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,8 +15,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
const NAME = 'visit:process';
|
||||
private const LOCALHOST = '127.0.0.1';
|
||||
public const NAME = 'visit:process';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
@@ -55,16 +55,18 @@ class ProcessVisitsCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
$count = 0;
|
||||
foreach ($visits as $visit) {
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
$io->write(\sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === self::LOCALHOST) {
|
||||
$io->writeln(
|
||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
\sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$count++;
|
||||
try {
|
||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
|
||||
@@ -73,12 +75,27 @@ class ProcessVisitsCommand extends Command
|
||||
$visit->setVisitLocation($location);
|
||||
$this->visitService->saveVisit($visit);
|
||||
|
||||
$io->writeln(sprintf(
|
||||
$io->writeln(\sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
continue;
|
||||
$io->writeln(
|
||||
\sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||
);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
}
|
||||
|
||||
if ($count === $this->ipLocationResolver->getApiLimit()) {
|
||||
$count = 0;
|
||||
$seconds = $this->ipLocationResolver->getApiInterval();
|
||||
$io->note(\sprintf(
|
||||
$this->translator->translate('IP location resolver limit reached. Waiting %s seconds...'),
|
||||
$seconds
|
||||
));
|
||||
\sleep($seconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
@@ -37,7 +38,8 @@ class GenerateKeyCommandTest extends TestCase
|
||||
*/
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
]);
|
||||
@@ -46,9 +48,10 @@ class GenerateKeyCommandTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function expirationDateIsDefinedIfWhenProvided()
|
||||
public function expirationDateIsDefinedIfProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
'--expirationDate' => '2016-01-01',
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
@@ -52,7 +53,7 @@ class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
$tagNames = ['foo', 'bar'];
|
||||
/** @var MethodProphecy $createTags */
|
||||
$createTags = $this->tagService->createTags($tagNames)->willReturn([]);
|
||||
$createTags = $this->tagService->createTags($tagNames)->willReturn(new ArrayCollection());
|
||||
|
||||
$this->commandTester->execute([
|
||||
'--name' => $tagNames,
|
||||
|
||||
@@ -7,7 +7,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
use Symfony\Component\Console\Application;
|
||||
@@ -32,7 +32,9 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitService::class);
|
||||
$this->ipResolver = $this->prophesize(IpLocationResolver::class);
|
||||
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||
$this->ipResolver->getApiLimit()->willReturn(10000000000);
|
||||
|
||||
$command = new ProcessVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
@@ -95,4 +97,41 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'Ignored localhost address') > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function sleepsEveryTimeTheApiLimitIsReached()
|
||||
{
|
||||
$visits = [
|
||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
];
|
||||
$apiLimit = 3;
|
||||
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits);
|
||||
$this->visitService->saveVisit(Argument::any())->will(function () {
|
||||
});
|
||||
|
||||
$getApiLimit = $this->ipResolver->getApiLimit()->willReturn($apiLimit);
|
||||
$getApiInterval = $this->ipResolver->getApiInterval()->willReturn(0);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
]);
|
||||
|
||||
$getApiLimit->shouldHaveBeenCalledTimes(\count($visits));
|
||||
$getApiInterval->shouldHaveBeenCalledTimes(\round(\count($visits) / $apiLimit));
|
||||
$resolveIpLocation->shouldHaveBeenCalledTimes(\count($visits));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ return [
|
||||
|
||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||
|
||||
Service\IpLocationResolver::class => ConfigAbstractFactory::class,
|
||||
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
@@ -51,7 +51,7 @@ return [
|
||||
ConfigAbstractFactory::class => [
|
||||
TranslatorExtension::class => ['translator'],
|
||||
LocaleMiddleware::class => ['translator'],
|
||||
Service\IpLocationResolver::class => ['httpClient'],
|
||||
Service\IpApiLocationResolver::class => ['httpClient'],
|
||||
Service\PreviewGenerator::class => [
|
||||
ImageBuilder::class,
|
||||
Filesystem::class,
|
||||
|
||||
@@ -23,8 +23,7 @@ class EntityManagerFactory implements FactoryInterface
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
@@ -32,14 +31,14 @@ class EntityManagerFactory implements FactoryInterface
|
||||
$globalConfig = $container->get('config');
|
||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||
$emConfig = isset($globalConfig['entity_manager']) ? $globalConfig['entity_manager'] : [];
|
||||
$connecitonConfig = isset($emConfig['connection']) ? $emConfig['connection'] : [];
|
||||
$ormConfig = isset($emConfig['orm']) ? $emConfig['orm'] : [];
|
||||
$emConfig = $globalConfig['entity_manager'] ?? [];
|
||||
$connectionConfig = $emConfig['connection'] ?? [];
|
||||
$ormConfig = $emConfig['orm'] ?? [];
|
||||
|
||||
return EntityManager::create($connecitonConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
isset($ormConfig['entities_paths']) ? $ormConfig['entities_paths'] : [],
|
||||
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
$ormConfig['entities_paths'] ?? [],
|
||||
$isDevMode,
|
||||
isset($ormConfig['proxies_dir']) ? $ormConfig['proxies_dir'] : null,
|
||||
$ormConfig['proxies_dir'] ?? null,
|
||||
$cache,
|
||||
false
|
||||
));
|
||||
|
||||
@@ -26,7 +26,7 @@ class PixelResponse extends Response
|
||||
private function createBody(): StreamInterface
|
||||
{
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
$body->write(\base64_decode(self::BASE_64_IMAGE));
|
||||
$body->write((string) \base64_decode(self::BASE_64_IMAGE));
|
||||
$body->rewind();
|
||||
return $body;
|
||||
}
|
||||
|
||||
71
module/Common/src/Service/IpApiLocationResolver.php
Normal file
71
module/Common/src/Service/IpApiLocationResolver.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
private const SERVICE_PATTERN = 'http://ip-api.com/json/%s';
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
public function __construct(Client $httpClient)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ipAddress
|
||||
* @return array
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(\sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(\json_decode((string) $response->getBody(), true));
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function mapFields(array $entry): array
|
||||
{
|
||||
return [
|
||||
'country_code' => $entry['countryCode'] ?? '',
|
||||
'country_name' => $entry['country'] ?? '',
|
||||
'region_name' => $entry['regionName'] ?? '',
|
||||
'city' => $entry['city'] ?? '',
|
||||
'latitude' => $entry['lat'] ?? '',
|
||||
'longitude' => $entry['lon'] ?? '',
|
||||
'time_zone' => $entry['timezone'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the interval in seconds that needs to be waited when the API limit is reached
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getApiInterval(): int
|
||||
{
|
||||
return 65; // ip-api interval is 1 minute. Return 5 extra seconds just in case
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getApiLimit(): ?int
|
||||
{
|
||||
return 145; // ip-api limit is 150 requests per minute. Leave 5 less requests just in case
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class IpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
const SERVICE_PATTERN = 'http://freegeoip.net/json/%s';
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
public function __construct(Client $httpClient)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $ipAddress
|
||||
* @return array
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return json_decode((string) $response->getBody(), true);
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,18 @@ interface IpLocationResolverInterface
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation(string $ipAddress): array;
|
||||
|
||||
/**
|
||||
* Returns the interval in seconds that needs to be waited when the API limit is reached
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getApiInterval(): int;
|
||||
|
||||
/**
|
||||
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getApiLimit(): ?int;
|
||||
}
|
||||
|
||||
84
module/Common/test/Service/IpApiLocationResolverTest.php
Normal file
84
module/Common/test/Service/IpApiLocationResolverTest.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
|
||||
class IpApiLocationResolverTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var IpApiLocationResolver
|
||||
*/
|
||||
protected $ipResolver;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->client = $this->prophesize(Client::class);
|
||||
$this->ipResolver = new IpApiLocationResolver($this->client->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function correctIpReturnsDecodedInfo()
|
||||
{
|
||||
$actual = [
|
||||
'countryCode' => 'bar',
|
||||
'lat' => 5,
|
||||
'lon' => 10,
|
||||
];
|
||||
$expected = [
|
||||
'country_code' => 'bar',
|
||||
'country_name' => '',
|
||||
'region_name' => '',
|
||||
'city' => '',
|
||||
'latitude' => 5,
|
||||
'longitude' => 10,
|
||||
'time_zone' => '',
|
||||
];
|
||||
$response = new Response();
|
||||
$response->getBody()->write(\json_encode($actual));
|
||||
$response->getBody()->rewind();
|
||||
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willReturn($response)
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException \Shlinkio\Shlink\Common\Exception\WrongIpException
|
||||
*/
|
||||
public function guzzleExceptionThrowsShlinkException()
|
||||
{
|
||||
$this->client->get('http://ip-api.com/json/1.2.3.4')->willThrow(new TransferException())
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function getApiIntervalReturnsExpectedValue()
|
||||
{
|
||||
$this->assertEquals(65, $this->ipResolver->getApiInterval());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function getApiLimitReturnsExpectedValue()
|
||||
{
|
||||
$this->assertEquals(145, $this->ipResolver->getApiLimit());
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\TransferException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
|
||||
class IpLocationResolverTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var IpLocationResolver
|
||||
*/
|
||||
protected $ipResolver;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->client = $this->prophesize(Client::class);
|
||||
$this->ipResolver = new IpLocationResolver($this->client->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function correctIpReturnsDecodedInfo()
|
||||
{
|
||||
$expected = [
|
||||
'foo' => 'bar',
|
||||
'baz' => 'foo',
|
||||
];
|
||||
$response = new Response();
|
||||
$response->getBody()->write(json_encode($expected));
|
||||
$response->getBody()->rewind();
|
||||
|
||||
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willReturn($response)
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->assertEquals($expected, $this->ipResolver->resolveIpLocation('1.2.3.4'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @expectedException \Shlinkio\Shlink\Common\Exception\WrongIpException
|
||||
*/
|
||||
public function guzzleExceptionThrowsShlinkException()
|
||||
{
|
||||
$this->client->get('http://freegeoip.net/json/1.2.3.4')->willThrow(new TransferException())
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->ipResolver->resolveIpLocation('1.2.3.4');
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||
* name="short_code",
|
||||
* type="string",
|
||||
* nullable=false,
|
||||
* length=10,
|
||||
* length=255,
|
||||
* unique=true
|
||||
* )
|
||||
*/
|
||||
|
||||
@@ -78,19 +78,34 @@ final class ShortUrlMeta
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
$this->validSince = $inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE);
|
||||
$this->validSince = $this->validSince !== null ? new \DateTime($this->validSince) : null;
|
||||
$this->validUntil = $inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL);
|
||||
$this->validUntil = $this->validUntil !== null ? new \DateTime($this->validUntil) : null;
|
||||
$this->validSince = $this->parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_SINCE));
|
||||
$this->validUntil = $this->parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL));
|
||||
$this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG);
|
||||
$this->maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS);
|
||||
$this->maxVisits = $this->maxVisits !== null ? (int) $this->maxVisits : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|\DateTime|null $date
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidSince()
|
||||
private function parseDateField($date): ?\DateTime
|
||||
{
|
||||
if ($date === null || $date instanceof \DateTime) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
if (\is_string($date)) {
|
||||
return new \DateTime($date);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidSince(): ?\DateTime
|
||||
{
|
||||
return $this->validSince;
|
||||
}
|
||||
@@ -103,7 +118,7 @@ final class ShortUrlMeta
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidUntil()
|
||||
public function getValidUntil(): ?\DateTime
|
||||
{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
@@ -51,16 +51,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
$order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
|
||||
if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
|
||||
$qb->addSelect('COUNT(v) AS totalVisits')
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
->leftJoin('s.visits', 'v')
|
||||
->groupBy('s')
|
||||
->orderBy('totalVisits', $order);
|
||||
|
||||
return \array_column($qb->getQuery()->getResult(), 0);
|
||||
} elseif (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
|
||||
$qb->orderBy('s.' . $fieldName, $order);
|
||||
}
|
||||
|
||||
if (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
|
||||
$qb->orderBy('s.' . $fieldName, $order);
|
||||
}
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
@@ -92,7 +93,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
|
||||
// Apply search term to every searchable field if not empty
|
||||
if (! empty($searchTerm)) {
|
||||
$qb->leftJoin('s.tags', 't');
|
||||
// Left join with tags only if no tags were provided. In case of tags, an inner join will be done later
|
||||
if (empty($tags)) {
|
||||
$qb->leftJoin('s.tags', 't');
|
||||
}
|
||||
|
||||
$conditions = [
|
||||
$qb->expr()->like('s.originalUrl', ':searchPattern'),
|
||||
|
||||
@@ -28,16 +28,18 @@ class TagService implements TagServiceInterface
|
||||
* @return Tag[]
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function listTags()
|
||||
public function listTags(): array
|
||||
{
|
||||
return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']);
|
||||
/** @var Tag[] $tags */
|
||||
$tags = $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']);
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tagNames
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTags(array $tagNames)
|
||||
public function deleteTags(array $tagNames): void
|
||||
{
|
||||
/** @var TagRepository $repo */
|
||||
$repo = $this->em->getRepository(Tag::class);
|
||||
@@ -50,7 +52,7 @@ class TagService implements TagServiceInterface
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames)
|
||||
public function createTags(array $tagNames): Collection
|
||||
{
|
||||
$tags = $this->tagNamesToEntities($this->em, $tagNames);
|
||||
$this->em->flush();
|
||||
@@ -65,7 +67,7 @@ class TagService implements TagServiceInterface
|
||||
* @throws EntityDoesNotExistException
|
||||
* @throws ORM\OptimisticLockException
|
||||
*/
|
||||
public function renameTag($oldName, $newName)
|
||||
public function renameTag($oldName, $newName): Tag
|
||||
{
|
||||
$criteria = ['name' => $oldName];
|
||||
/** @var Tag|null $tag */
|
||||
|
||||
@@ -12,13 +12,13 @@ interface TagServiceInterface
|
||||
/**
|
||||
* @return Tag[]
|
||||
*/
|
||||
public function listTags();
|
||||
public function listTags(): array;
|
||||
|
||||
/**
|
||||
* @param string[] $tagNames
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTags(array $tagNames);
|
||||
public function deleteTags(array $tagNames): void;
|
||||
|
||||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
@@ -26,7 +26,7 @@ interface TagServiceInterface
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames);
|
||||
public function createTags(array $tagNames): Collection;
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
@@ -34,5 +34,5 @@ interface TagServiceInterface
|
||||
* @return Tag
|
||||
* @throws EntityDoesNotExistException
|
||||
*/
|
||||
public function renameTag($oldName, $newName);
|
||||
public function renameTag($oldName, $newName): Tag;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,17 @@ namespace ShlinkioTest\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
|
||||
|
||||
class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
const ENTITIES_TO_EMPTY = [
|
||||
protected const ENTITIES_TO_EMPTY = [
|
||||
ShortUrl::class,
|
||||
Visit::class,
|
||||
Tag::class,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -38,7 +40,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
|
||||
$bar = new ShortUrl();
|
||||
$bar->setOriginalUrl('bar')
|
||||
->setShortCode('bar')
|
||||
->setShortCode('bar_very_long_text')
|
||||
->setValidSince((new \DateTime())->add(new \DateInterval('P1M')));
|
||||
$this->getEntityManager()->persist($bar);
|
||||
|
||||
@@ -79,4 +81,35 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
|
||||
$this->assertEquals($count, $this->repo->countList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function findListProperlyFiltersByTagAndSearchTerm()
|
||||
{
|
||||
$tag = new Tag('bar');
|
||||
$this->getEntityManager()->persist($tag);
|
||||
|
||||
$foo = new ShortUrl();
|
||||
$foo->setOriginalUrl('foo')
|
||||
->setShortCode('foo')
|
||||
->setTags(new ArrayCollection([$tag]));
|
||||
$this->getEntityManager()->persist($foo);
|
||||
|
||||
$bar = new ShortUrl();
|
||||
$bar->setOriginalUrl('bar')
|
||||
->setShortCode('bar_very_long_text');
|
||||
$this->getEntityManager()->persist($bar);
|
||||
|
||||
$foo2 = new ShortUrl();
|
||||
$foo2->setOriginalUrl('foo_2')
|
||||
->setShortCode('foo_2');
|
||||
$this->getEntityManager()->persist($foo2);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$result = $this->repo->findList(null, null, 'foo', ['bar']);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame($foo, $result[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
|
||||
|
||||
class TagRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
const ENTITIES_TO_EMPTY = [
|
||||
protected const ENTITIES_TO_EMPTY = [
|
||||
Tag::class,
|
||||
];
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
|
||||
|
||||
class VisitRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
const ENTITIES_TO_EMPTY = [
|
||||
protected const ENTITIES_TO_EMPTY = [
|
||||
VisitLocation::class,
|
||||
Visit::class,
|
||||
ShortUrl::class,
|
||||
|
||||
@@ -25,7 +25,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||
* @param \DateTime $expirationDate
|
||||
* @return ApiKey
|
||||
*/
|
||||
public function create(\DateTime $expirationDate = null)
|
||||
public function create(\DateTime $expirationDate = null): ApiKey
|
||||
{
|
||||
$key = new ApiKey();
|
||||
if ($expirationDate !== null) {
|
||||
@@ -44,7 +44,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function check(string $key)
|
||||
public function check(string $key): bool
|
||||
{
|
||||
/** @var ApiKey|null $apiKey */
|
||||
$apiKey = $this->getByKey($key);
|
||||
@@ -58,7 +58,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||
* @return ApiKey
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function disable(string $key)
|
||||
public function disable(string $key): ApiKey
|
||||
{
|
||||
/** @var ApiKey|null $apiKey */
|
||||
$apiKey = $this->getByKey($key);
|
||||
@@ -77,10 +77,12 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||
* @return ApiKey[]
|
||||
*/
|
||||
public function listKeys(bool $enabledOnly = false)
|
||||
public function listKeys(bool $enabledOnly = false): array
|
||||
{
|
||||
$conditions = $enabledOnly ? ['enabled' => true] : [];
|
||||
return $this->em->getRepository(ApiKey::class)->findBy($conditions);
|
||||
/** @var ApiKey[] $apiKeys */
|
||||
$apiKeys = $this->em->getRepository(ApiKey::class)->findBy($conditions);
|
||||
return $apiKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,7 +91,7 @@ class ApiKeyService implements ApiKeyServiceInterface
|
||||
* @param string $key
|
||||
* @return ApiKey|null
|
||||
*/
|
||||
public function getByKey(string $key)
|
||||
public function getByKey(string $key): ?ApiKey
|
||||
{
|
||||
/** @var ApiKey|null $apiKey */
|
||||
$apiKey = $this->em->getRepository(ApiKey::class)->findOneBy([
|
||||
|
||||
@@ -14,7 +14,7 @@ interface ApiKeyServiceInterface
|
||||
* @param \DateTime $expirationDate
|
||||
* @return ApiKey
|
||||
*/
|
||||
public function create(\DateTime $expirationDate = null);
|
||||
public function create(\DateTime $expirationDate = null): ApiKey;
|
||||
|
||||
/**
|
||||
* Checks if provided key is a valid api key
|
||||
@@ -22,7 +22,7 @@ interface ApiKeyServiceInterface
|
||||
* @param string $key
|
||||
* @return bool
|
||||
*/
|
||||
public function check(string $key);
|
||||
public function check(string $key): bool;
|
||||
|
||||
/**
|
||||
* Disables provided api key
|
||||
@@ -31,7 +31,7 @@ interface ApiKeyServiceInterface
|
||||
* @return ApiKey
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function disable(string $key);
|
||||
public function disable(string $key): ApiKey;
|
||||
|
||||
/**
|
||||
* Lists all existing api keys
|
||||
@@ -39,7 +39,7 @@ interface ApiKeyServiceInterface
|
||||
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||
* @return ApiKey[]
|
||||
*/
|
||||
public function listKeys(bool $enabledOnly = false);
|
||||
public function listKeys(bool $enabledOnly = false): array;
|
||||
|
||||
/**
|
||||
* Tries to find one API key by its key string
|
||||
@@ -47,5 +47,5 @@ interface ApiKeyServiceInterface
|
||||
* @param string $key
|
||||
* @return ApiKey|null
|
||||
*/
|
||||
public function getByKey(string $key);
|
||||
public function getByKey(string $key): ?ApiKey;
|
||||
}
|
||||
|
||||
@@ -2,3 +2,5 @@ parameters:
|
||||
excludes_analyse:
|
||||
- module/Common/src/Template/Extension/TranslatorExtension.php
|
||||
- module/Rest/src/Util/RestUtils.php
|
||||
ignoreErrors:
|
||||
- '#is not subtype of Throwable#'
|
||||
|
||||
Reference in New Issue
Block a user