mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-01 12:43:11 +08:00
Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
987919e87a | ||
|
|
0c03a4b7ff | ||
|
|
5d6d13c95f | ||
|
|
563021bdc1 | ||
|
|
2d6d35a398 | ||
|
|
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 | ||
|
|
a8465094c1 | ||
|
|
16f7359ac6 | ||
|
|
f9f4817ee2 | ||
|
|
c7e49f223f | ||
|
|
6e79b4ba7b | ||
|
|
f78a7f12a9 | ||
|
|
b3664597b0 | ||
|
|
8cfb4f61ca | ||
|
|
b0dbb2dae4 | ||
|
|
7c6da4985d | ||
|
|
386b0dfb7b | ||
|
|
1437ff48ce | ||
|
|
63294f20ee | ||
|
|
d8acc3c247 | ||
|
|
52d8ffa212 | ||
|
|
98ad2816e8 | ||
|
|
9d890f4227 | ||
|
|
0932d04907 | ||
|
|
1f78b5c524 | ||
|
|
59f10619ba | ||
|
|
334710e92c | ||
|
|
75b8175824 | ||
|
|
8a74ef2a33 | ||
|
|
d05ac5ce9d | ||
|
|
3100fffa2b | ||
|
|
6bbacb1017 | ||
|
|
4403dc5df9 | ||
|
|
fdc637c23d | ||
|
|
b99d662417 | ||
|
|
eb9a964c66 | ||
|
|
e5ef8d7f8c | ||
|
|
28650aee2b | ||
|
|
a2294704e6 | ||
|
|
e5e1aa2ff4 | ||
|
|
2f5290b9d3 | ||
|
|
ef3c4aadf2 | ||
|
|
c9ce56eea5 | ||
|
|
4fee656f96 | ||
|
|
d2a04259f5 | ||
|
|
e504daa1ba | ||
|
|
8793a67ce9 | ||
|
|
b4ded374e9 | ||
|
|
91d350b12f | ||
|
|
b3e25f28fd | ||
|
|
aca89f9abe | ||
|
|
243075dd78 | ||
|
|
7130425896 | ||
|
|
fe9ab20cbb | ||
|
|
6935b2ebe2 | ||
|
|
3dcc510da1 | ||
|
|
2f26c82fa6 | ||
|
|
9ddb60a882 | ||
|
|
210b08b61f | ||
|
|
42fe4bd5ce | ||
|
|
1b2a0820e5 | ||
|
|
6cf0155417 | ||
|
|
9b8be3e5b8 | ||
|
|
a27b01b895 | ||
|
|
16dd1838aa | ||
|
|
f788d6872f | ||
|
|
d0df007812 | ||
|
|
f60c217fae | ||
|
|
d3fc7d543a | ||
|
|
4d0fc1da07 | ||
|
|
ee2233c6dd | ||
|
|
ea6e0d7c7f | ||
|
|
d9d599eab4 | ||
|
|
d1ba44e1b3 | ||
|
|
dff2ad3740 | ||
|
|
f7e63710e4 | ||
|
|
d3b5cd5c57 | ||
|
|
86ed83d25e | ||
|
|
f96d0fe30a | ||
|
|
be406bd676 | ||
|
|
044278752b | ||
|
|
343d2ab44a | ||
|
|
66992f644e | ||
|
|
cf245524dd | ||
|
|
ad520811a3 | ||
|
|
ee1e1d5688 | ||
|
|
8ef0e7c25b | ||
|
|
c3d555ef3c | ||
|
|
bf8e14708b | ||
|
|
6ea59b1e4d | ||
|
|
cf8b778711 | ||
|
|
1e79969c3b | ||
|
|
5fd34e03fc | ||
|
|
ce9d6642d4 | ||
|
|
ecebdbbfa8 | ||
|
|
6f7ce709ca | ||
|
|
84094a51a2 | ||
|
|
7ba9eb8e2c | ||
|
|
e8a0c5484c | ||
|
|
0521227127 | ||
|
|
fac9455a1e | ||
|
|
3243ade4fd | ||
|
|
da21eb4a5c | ||
|
|
5ec6d538db | ||
|
|
08228d9d98 | ||
|
|
7856d64299 | ||
|
|
057bbae729 | ||
|
|
09b161304c | ||
|
|
a60c45ca4d | ||
|
|
89ed84ce28 | ||
|
|
a6c547c4da | ||
|
|
3e2c5abaa4 | ||
|
|
c202b3e518 | ||
|
|
e15b67b5dc | ||
|
|
7ddc180487 | ||
|
|
f3fbfc3692 | ||
|
|
b289e3bac2 | ||
|
|
4d4aafa6db | ||
|
|
2705070063 | ||
|
|
5e3770c105 | ||
|
|
0f0213aa87 | ||
|
|
0e2ad0dbca | ||
|
|
d275316acd | ||
|
|
0a681f0efa | ||
|
|
b17f96043a | ||
|
|
6f9b727673 | ||
|
|
79427d08d7 | ||
|
|
2ec807ba70 | ||
|
|
ede4525332 | ||
|
|
4dffc9f0c1 | ||
|
|
5de845c258 | ||
|
|
745ff51150 | ||
|
|
88b9f9fc56 | ||
|
|
fdbe076bf2 | ||
|
|
0760550767 | ||
|
|
1b94083188 | ||
|
|
1993d01110 | ||
|
|
37fb7e76d9 | ||
|
|
cc3362837b | ||
|
|
2012cc453c | ||
|
|
ea80b6d48a | ||
|
|
db956a1f40 | ||
|
|
4f3995ea80 | ||
|
|
e024ba5d94 | ||
|
|
af0ff0f65b | ||
|
|
a9094dc0f6 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -22,3 +22,4 @@ indocker export-ignore
|
||||
phpcs.xml export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
phpunit-func.xml export-ignore
|
||||
phpstan.neon
|
||||
|
||||
@@ -2,11 +2,9 @@ language: php
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- /.*/
|
||||
|
||||
php:
|
||||
- 7
|
||||
- 7.1
|
||||
- 7.2
|
||||
|
||||
|
||||
728
CHANGELOG.md
728
CHANGELOG.md
@@ -1,193 +1,679 @@
|
||||
## CHANGELOG
|
||||
# CHANGELOG
|
||||
|
||||
### 1.6.2
|
||||
## 1.11.0 - 2018-08-13
|
||||
|
||||
**Bugs**
|
||||
#### Added
|
||||
|
||||
* [109: Fix installation error due to typo in latest migration](https://github.com/shlinkio/shlink/issues/109)
|
||||
* [#170](https://github.com/shlinkio/shlink/issues/170) and [#171](https://github.com/shlinkio/shlink/issues/171) Updated `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints to return more meaningful information and make their response consistent.
|
||||
|
||||
### 1.6.1
|
||||
The short URLs are now represented by this object in both cases:
|
||||
|
||||
**Tasks**
|
||||
```json
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
],
|
||||
"originalUrl": "https://shlink.io"
|
||||
}
|
||||
```
|
||||
|
||||
* [110: Create gitattributes file to define files to be excluded from distributable package](https://github.com/shlinkio/shlink/issues/110)
|
||||
The `originalUrl` property is considered deprecated and has been kept for backward compatibility purposes. It holds the same value as the `longUrl` property.
|
||||
|
||||
### 1.6.0
|
||||
#### Changed
|
||||
|
||||
**Features**
|
||||
* *Nothing*
|
||||
|
||||
* [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)
|
||||
#### Deprecated
|
||||
|
||||
**Enhancements:**
|
||||
* The `originalUrl` property in `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints is now deprecated and replaced by the `longUrl` property.
|
||||
|
||||
* [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)
|
||||
#### Removed
|
||||
|
||||
**Tasks**
|
||||
* *Nothing*
|
||||
|
||||
* [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)
|
||||
#### Fixed
|
||||
|
||||
**Bugs**
|
||||
* *Nothing*
|
||||
|
||||
* [103: Make NotFoundDelegate return proper content types based on accepted content](https://github.com/shlinkio/shlink/issues/103)
|
||||
|
||||
### 1.5.0
|
||||
## 1.10.2 - 2018-08-04
|
||||
|
||||
**Enhancements:**
|
||||
#### Added
|
||||
|
||||
* [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)
|
||||
* *Nothing*
|
||||
|
||||
**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
|
||||
* [#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.
|
||||
|
||||
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82)
|
||||
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
|
||||
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
|
||||
|
||||
**Bugs**
|
||||
## 1.10.1 - 2018-08-02
|
||||
|
||||
* [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)
|
||||
#### Added
|
||||
|
||||
### 1.3.0
|
||||
* *Nothing*
|
||||
|
||||
**Enhancements:**
|
||||
#### Changed
|
||||
|
||||
* [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)
|
||||
* [#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.
|
||||
|
||||
**Tasks**
|
||||
#### Deprecated
|
||||
|
||||
* [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)
|
||||
* *Nothing*
|
||||
|
||||
### 1.2.2
|
||||
#### Removed
|
||||
|
||||
**Bugs**
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#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.
|
||||
|
||||
For example: `https://doma.in/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
|
||||
|
||||
* [#169](https://github.com/shlinkio/shlink/issues/169) Fixed unhandled error when parsing `ShortUrlMeta` and date fields are already `DateTime` instances.
|
||||
|
||||
|
||||
## 1.10.0 - 2018-07-09
|
||||
|
||||
#### Added
|
||||
|
||||
* [#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
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#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
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.9.1 - 2018-06-18
|
||||
|
||||
#### Added
|
||||
|
||||
* [#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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#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
|
||||
|
||||
|
||||
## 1.9.0 - 2018-05-07
|
||||
|
||||
#### Added
|
||||
|
||||
* [#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.
|
||||
|
||||
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.
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#139](https://github.com/shlinkio/shlink/issues/139) Ensured all core actions log exceptions
|
||||
|
||||
|
||||
## 1.8.1 - 2018-04-07
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#141](https://github.com/shlinkio/shlink/issues/141) Removed workaround used in `PathVersionMiddleware`, since the bug in zend-stratigility has been fixed.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### 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*
|
||||
|
||||
2
bin/cli
2
bin/cli
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
@@ -17,12 +17,11 @@ $container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
QuestionHelper::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizerPlugin::class => [QuestionHelper::class, Filesystem::class]
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
@@ -17,12 +17,11 @@ $container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
QuestionHelper::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizerPlugin::class => [QuestionHelper::class, Filesystem::class]
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
7
build.sh
7
build.sh
@@ -33,8 +33,12 @@ rm composer.*
|
||||
rm LICENSE
|
||||
rm indocker
|
||||
rm docker-compose.yml
|
||||
rm docker-compose.override.yml
|
||||
rm docker-compose.override.yml.dist
|
||||
rm func_tests_bootstrap.php
|
||||
rm php*
|
||||
rm README.md
|
||||
rm infection.json
|
||||
rm -rf build
|
||||
rm -ff data/database.sqlite
|
||||
rm -rf data/infra
|
||||
@@ -42,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"
|
||||
|
||||
@@ -12,48 +12,48 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.0",
|
||||
"acelaya/ze-content-based-error-handler": "^2.0",
|
||||
"php": "^7.1",
|
||||
"ext-json": "*",
|
||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
"doctrine/annotations": "^1.4 <1.5",
|
||||
"doctrine/cache": "^1.6 <1.7",
|
||||
"doctrine/collections": "^1.4 <1.5",
|
||||
"doctrine/common": "^2.7 <2.8",
|
||||
"doctrine/dbal": "^2.5 <2.6",
|
||||
"doctrine/cache": "^1.6",
|
||||
"doctrine/migrations": "^1.4",
|
||||
"doctrine/orm": "^2.5 <2.6",
|
||||
"endroid/qrcode": "^1.7",
|
||||
"doctrine/orm": "^2.5",
|
||||
"endroid/qr-code": "^1.7",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"http-interop/http-middleware": "^0.4.1",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"monolog/monolog": "^1.21",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^3.0",
|
||||
"symfony/filesystem": "^3.0",
|
||||
"symfony/process": "^3.0",
|
||||
"symfony/console": "^4.0",
|
||||
"symfony/filesystem": "^4.0",
|
||||
"symfony/process": "^4.0",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-expressive": "^2.0",
|
||||
"zendframework/zend-expressive-fastroute": "^2.0",
|
||||
"zendframework/zend-expressive-helpers": "^4.2",
|
||||
"zendframework/zend-expressive-platesrenderer": "^1.3",
|
||||
"zendframework/zend-diactoros": "^1.7",
|
||||
"zendframework/zend-expressive": "^3.0",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"zendframework/zend-servicemanager": "^3.2",
|
||||
"zendframework/zend-stdlib": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"filp/whoops": "^2.0",
|
||||
"phpunit/dbunit": "^3.0",
|
||||
"phpunit/phpcov": "^4.0",
|
||||
"phpunit/phpunit": "^6.0",
|
||||
"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",
|
||||
"symfony/var-dumper": "^3.0",
|
||||
"vlucas/phpdotenv": "^2.2",
|
||||
"zendframework/zend-expressive-tooling": "^0.4"
|
||||
"squizlabs/php_codesniffer": "^3.2.3",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/var-dumper": "^4.0",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -83,8 +83,10 @@
|
||||
"scripts": {
|
||||
"check": [
|
||||
"@cs",
|
||||
"@stan",
|
||||
"@test",
|
||||
"@func-test"
|
||||
"@func-test",
|
||||
"@infect"
|
||||
],
|
||||
"cs": "phpcs",
|
||||
"cs-fix": "phpcbf",
|
||||
@@ -96,13 +98,17 @@
|
||||
"@test",
|
||||
"@func-test",
|
||||
"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": {
|
||||
"process-timeout": 0,
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "7.0"
|
||||
"php": "7.1.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '1.2.0',
|
||||
'secret_key' => Common\env('SECRET_KEY'),
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -5,26 +5,24 @@ use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Middleware;
|
||||
use Zend\Expressive\Plates;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Expressive\Application::class => Container\ApplicationFactory::class,
|
||||
Template\TemplateRendererInterface::class => Plates\PlatesRendererFactory::class,
|
||||
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
|
||||
ErrorHandler::class => Container\ErrorHandlerFactory::class,
|
||||
Middleware\ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
|
||||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
Expressive\Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
|
||||
@@ -15,6 +16,7 @@ return [
|
||||
'pre-routing' => [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
LocaleMiddleware::class,
|
||||
],
|
||||
'priority' => 11,
|
||||
@@ -29,7 +31,7 @@ return [
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Application::ROUTING_MIDDLEWARE,
|
||||
Expressive\Router\Middleware\RouteMiddleware::class,
|
||||
],
|
||||
'priority' => 10,
|
||||
],
|
||||
@@ -38,7 +40,7 @@ return [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
CrossDomainMiddleware::class,
|
||||
Expressive\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
BodyParserMiddleware::class,
|
||||
CheckAuthenticationMiddleware::class,
|
||||
],
|
||||
@@ -47,7 +49,8 @@ return [
|
||||
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Application::DISPATCH_MIDDLEWARE,
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
||||
@@ -7,6 +7,7 @@ use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core;
|
||||
use Shlinkio\Shlink\Rest;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
|
||||
/**
|
||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
||||
@@ -18,6 +19,14 @@ use Zend\ConfigAggregator;
|
||||
*/
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
Expressive\ConfigProvider::class,
|
||||
Expressive\Router\ConfigProvider::class,
|
||||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Expressive\Plates\ConfigProvider::class,
|
||||
Expressive\Helper\ConfigProvider::class,
|
||||
\class_exists(Expressive\Swoole\ConfigProvider::class)
|
||||
? Expressive\Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
@@ -12,8 +12,8 @@ require 'vendor/autoload.php';
|
||||
if (class_exists(Dotenv::class)) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', '1');
|
||||
$dotenv = new Dotenv(__DIR__ . '/..');
|
||||
$dotenv->load();
|
||||
$dotenv = new Dotenv();
|
||||
$dotenv->load(__DIR__ . '/../.env');
|
||||
}
|
||||
|
||||
// Build container
|
||||
|
||||
0
data/infra/database/.gitignore
vendored
Normal file → Executable file
0
data/infra/database/.gitignore
vendored
Normal file → Executable file
0
data/infra/nginx/.gitignore
vendored
Normal file → Executable file
0
data/infra/nginx/.gitignore
vendored
Normal file → Executable file
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);
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,23 @@
|
||||
"properties": {
|
||||
"currentPage": {
|
||||
"type": "integer",
|
||||
"description": "The number of current page being displayed."
|
||||
"description": "The number of current page."
|
||||
},
|
||||
"pagesCount": {
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
"type": "string",
|
||||
"description": "The short code for this short URL."
|
||||
},
|
||||
"originalUrl": {
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The short URL."
|
||||
},
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
@@ -24,6 +28,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of tags applied to this short URL"
|
||||
},
|
||||
"originalUrl": {
|
||||
"deprecated": true,
|
||||
"type": "string",
|
||||
"description": "The original long URL. [DEPRECATED. Use longUrl instead]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "The authorization token with Bearer type",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
@@ -5,24 +5,39 @@
|
||||
],
|
||||
"summary": "Perform authentication",
|
||||
"description": "Performs an authentication",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"in": "formData",
|
||||
"description": "The API key to authenticate with",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiKey"
|
||||
],
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"description": "The API key to authenticate with",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The authentication worked.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The authentication token that needs to be sent in the Authorization header"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The authentication token that needs to be sent in the Authorization header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -34,20 +49,32 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "An API key was not provided.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The API key is incorrect, is disabled or has expired.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,59 +11,73 @@
|
||||
"in": "query",
|
||||
"description": "The page to be displayed. Defaults to 1",
|
||||
"required": false,
|
||||
"type": "integer"
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "searchTerm",
|
||||
"in": "query",
|
||||
"description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)",
|
||||
"required": false,
|
||||
"type": "string"
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orderBy",
|
||||
"in": "query",
|
||||
"description": "The field from which you want to order the result. (Since v1.3.0)",
|
||||
"enum": [
|
||||
"originalUrl",
|
||||
"shortCode",
|
||||
"dateCreated",
|
||||
"visits"
|
||||
],
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"longUrl",
|
||||
"shortCode",
|
||||
"dateCreated",
|
||||
"visits"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of short URLs",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shortUrls": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
"shortUrls": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../definitions/Pagination.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../definitions/Pagination.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,7 +89,8 @@
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "12C18",
|
||||
"originalUrl": "https://store.steampowered.com",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 328,
|
||||
"tags": [
|
||||
@@ -85,7 +100,8 @@
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"originalUrl": "https://shlink.io",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
@@ -94,7 +110,8 @@
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
"originalUrl": "https://www.google.com",
|
||||
"shortUrl": "https://doma.in/123bA",
|
||||
"longUrl": "https://www.google.com",
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
"tags": []
|
||||
@@ -102,7 +119,10 @@
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 5,
|
||||
"pagesCount": 12
|
||||
"pagesCount": 12,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 115
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,71 +130,114 @@
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"post": {
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
],
|
||||
"summary": "Create short URL",
|
||||
"description": "Creates a new short code",
|
||||
"parameters": [
|
||||
"security": [
|
||||
{
|
||||
"name": "longUrl",
|
||||
"in": "formData",
|
||||
"description": "The URL to parse",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "formData",
|
||||
"description": "The URL to parse",
|
||||
"required": false,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"longUrl"
|
||||
],
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"description": "The URL to parse",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"description": "The URL to parse",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"customSlug": {
|
||||
"description": "A unique custom slug to be used instead of the generated short code",
|
||||
"type": "string"
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The result of parsing the long URL",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL that has been parsed"
|
||||
},
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The generated short URL"
|
||||
},
|
||||
"shortCode": {
|
||||
"type": "string",
|
||||
"description": "the short code that is being used in the short URL"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL that has been parsed"
|
||||
},
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The generated short URL"
|
||||
},
|
||||
"shortCode": {
|
||||
"type": "string",
|
||||
"description": "the short code that is being used in the short URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The long URL was not provided or is invalid.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
125
docs/swagger/paths/v1_short-codes_shorten.json
Normal file
125
docs/swagger/paths/v1_short-codes_shorten.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,48 +9,153 @@
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"type": "string",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true
|
||||
},
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The long URL behind a short code.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL behind the short code."
|
||||
"description": "The URL info behind a short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"longUrl": "https://shlink.io"
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No URL was found for provided short code.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
],
|
||||
"summary": "Edit short code",
|
||||
"description": "Update certain meta arguments from an existing short URL.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short code has been properly updated."
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided meta arguments are invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,34 +10,55 @@
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"type": "string",
|
||||
"description": "The shortCode in which we want to edit tags.",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"in": "formData",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of tags to set to the short URL.",
|
||||
"required": true
|
||||
},
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of tags to set to the short URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of tags.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,20 +74,32 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "The request body does not contain a \"tags\" param with array type.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,27 +10,53 @@
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"type": "string",
|
||||
"description": "The shortCode from which we want to get the visits.",
|
||||
"required": true
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"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": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of visits.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visits": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/Visit.json"
|
||||
"visits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/Visit.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +92,22 @@
|
||||
},
|
||||
"404": {
|
||||
"description": "The short code does not belong to any short URL.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,28 @@
|
||||
],
|
||||
"summary": "List existing tags",
|
||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
||||
"parameters": [
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,8 +48,12 @@
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,31 +65,51 @@
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||
"parameters": [
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "formData",
|
||||
"description": "The list of tag names to create",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"description": "The list of tag names to create",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,8 +131,12 @@
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,45 +148,68 @@
|
||||
],
|
||||
"summary": "Rename tag",
|
||||
"description": "Renames one existing tag",
|
||||
"parameters": [
|
||||
"security": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "oldName",
|
||||
"in": "formData",
|
||||
"description": "Current name of the tag",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "newName",
|
||||
"in": "formData",
|
||||
"description": "New name of the tag",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"oldName",
|
||||
"newName"
|
||||
],
|
||||
"properties": {
|
||||
"oldName": {
|
||||
"description": "Current name of the tag",
|
||||
"type": "string"
|
||||
},
|
||||
"newName": {
|
||||
"description": "New name of the tag",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The tag has been properly renamed"
|
||||
},
|
||||
"400": {
|
||||
"description": "You have not provided either the oldName or the newName params.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "There's no tag found with the name provided in oldName param.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,15 +222,22 @@
|
||||
"summary": "Delete tags",
|
||||
"description": "Deletes provided list of tags",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "The names of the tags to delete",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -184,8 +246,12 @@
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"version": "1.0"
|
||||
},
|
||||
"schemes": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"basePath": "/rest",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/x-www-form-urlencoded",
|
||||
"application/json"
|
||||
|
||||
"servers": [
|
||||
{
|
||||
"url": "{schema}://{server}/rest",
|
||||
"variables": {
|
||||
"schema": {
|
||||
"default": "https",
|
||||
"enum": ["https", "http"]
|
||||
},
|
||||
"server": {
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"Bearer": {
|
||||
"description": "The JWT identifying a previously logged API key",
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"paths": {
|
||||
"/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
@@ -26,6 +40,9 @@
|
||||
"/v1/short-codes": {
|
||||
"$ref": "paths/v1_short-codes.json"
|
||||
},
|
||||
"/v1/short-codes/shorten": {
|
||||
"$ref": "paths/v1_short-codes_shorten.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}.json"
|
||||
},
|
||||
|
||||
22
infection.json
Normal file
22
infection.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -9,21 +9,25 @@ return [
|
||||
'cli' => [
|
||||
'locale' => Common\env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class,
|
||||
Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::class,
|
||||
Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::class,
|
||||
Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::class,
|
||||
Command\Shortcode\GenerateShortcodeCommand::NAME => Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::NAME => Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::NAME => Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::NAME => Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::NAME => Command\Shortcode\GeneratePreviewCommand::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -42,7 +42,11 @@ return [
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||
Command\Shortcode\ListShortcodesCommand::class => [Service\ShortUrlService::class, 'translator'],
|
||||
Command\Shortcode\ListShortcodesCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||
Command\Shortcode\GeneratePreviewCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
@@ -51,7 +55,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: 2017-10-21 20:17+0200\n"
|
||||
"PO-Revision-Date: 2017-10-21 20:19+0200\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.1\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"
|
||||
@@ -24,8 +24,8 @@ msgid "The API key to disable"
|
||||
msgstr "La clave de API a deshabilitar"
|
||||
|
||||
#, php-format
|
||||
msgid "API key %s properly disabled"
|
||||
msgstr "Clave de API %s deshabilitada correctamente"
|
||||
msgid "API key \"%s\" properly disabled"
|
||||
msgstr "Clave de API \"%s\" deshabilitada correctamente"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" does not exist."
|
||||
@@ -39,8 +39,9 @@ msgstr ""
|
||||
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||
"válido en PHP."
|
||||
|
||||
msgid "Generated API key"
|
||||
msgstr "Generada clave de API"
|
||||
#, php-format
|
||||
msgid "Generated API key: \"%s\""
|
||||
msgstr "Generada clave de API. \"%s\""
|
||||
|
||||
msgid "Lists all the available API keys."
|
||||
msgstr "Lista todas las claves de API disponibles."
|
||||
@@ -51,12 +52,12 @@ msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||
msgid "Key"
|
||||
msgstr "Clave"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
msgid "Is enabled"
|
||||
msgstr "Está habilitada"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
||||
@@ -65,8 +66,9 @@ msgstr ""
|
||||
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
|
||||
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
|
||||
|
||||
msgid "Character set:"
|
||||
msgstr "Grupo de caracteres:"
|
||||
#, php-format
|
||||
msgid "Character set: \"%s\""
|
||||
msgstr "Grupo de caracteres: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Generates a random secret string that can be used for JWT token encryption"
|
||||
@@ -74,8 +76,9 @@ msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
msgid "Secret key:"
|
||||
msgstr "Clave secreta:"
|
||||
#, php-format
|
||||
msgid "Secret key: \"%s\""
|
||||
msgstr "Clave secreta: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
@@ -125,17 +128,22 @@ msgid "If provided, this slug will be used instead of generating a short code"
|
||||
msgstr ""
|
||||
"Si se proporciona, este slug será usado en vez de generar un código corto"
|
||||
|
||||
msgid "A long URL was not provided. Which URL do you want to shorten?:"
|
||||
msgid "This will limit the number of visits for this short URL."
|
||||
msgstr "Esto limitará el número de visitas a esta URL acortada."
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "A long URL was not provided. Which URL do you want to shorten?:"
|
||||
msgid "A long URL was not provided. Which URL do you want to be shortened?"
|
||||
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
|
||||
|
||||
msgid "A URL was not provided!"
|
||||
msgstr "¡No se ha proporcionado una URL!"
|
||||
|
||||
msgid "Processed URL:"
|
||||
msgstr "URL procesada:"
|
||||
msgid "Processed long URL:"
|
||||
msgstr "URL larga procesada:"
|
||||
|
||||
msgid "Generated URL:"
|
||||
msgstr "URL generada:"
|
||||
msgid "Generated short URL:"
|
||||
msgstr "URL corta generada:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
@@ -166,8 +174,8 @@ msgid "Allows to filter visits, returning only those newer than end date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to use?:"
|
||||
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?:"
|
||||
msgid "A short code was not provided. Which short code do you want to use?"
|
||||
msgstr "No se proporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
|
||||
msgid "Referer"
|
||||
msgstr "Origen"
|
||||
@@ -222,8 +230,8 @@ msgstr "Número de visitas"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "You have reached last page"
|
||||
msgstr "Has alcanzado la última página"
|
||||
msgid "Short codes properly listed"
|
||||
msgstr "Códigos cortos correctamente listados"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
@@ -234,13 +242,9 @@ msgstr "Devuelve la URL larga detrás de un código corto"
|
||||
msgid "The short code to parse"
|
||||
msgstr "El código corto a convertir"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to parse?:"
|
||||
msgid "A short code was not provided. Which short code do you want to parse?"
|
||||
msgstr ""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?:"
|
||||
|
||||
#, php-format
|
||||
msgid "No URL found for short code \"%s\""
|
||||
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
|
||||
|
||||
msgid "Long URL:"
|
||||
msgstr "URL larga:"
|
||||
@@ -262,8 +266,8 @@ msgstr "El nombre de las etiquetas a crear"
|
||||
msgid "You have to provide at least one tag name"
|
||||
msgstr "Debes proporcionar al menos un nombre de etiqueta"
|
||||
|
||||
msgid "Created tags"
|
||||
msgstr "Etiquetas creadas"
|
||||
msgid "Tags properly created"
|
||||
msgstr "Etiquetas correctamente creadas"
|
||||
|
||||
msgid "Deletes one or more tags."
|
||||
msgstr "Elimina una o más etiquetas."
|
||||
@@ -271,8 +275,8 @@ msgstr "Elimina una o más etiquetas."
|
||||
msgid "The name of the tags to delete"
|
||||
msgstr "El nombre de las etiquetas a eliminar"
|
||||
|
||||
msgid "Deleted tags"
|
||||
msgstr "Etiquetas eliminadas"
|
||||
msgid "Tags properly deleted"
|
||||
msgstr "Etiquetas correctamente eliminadas"
|
||||
|
||||
msgid "Lists existing tags."
|
||||
msgstr "Lista las etiquetas existentes."
|
||||
@@ -313,5 +317,24 @@ 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"
|
||||
|
||||
#~ msgid "You have reached last page"
|
||||
#~ msgstr "Has alcanzado la última página"
|
||||
|
||||
#~ msgid "No URL found for short code \"%s\""
|
||||
#~ msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
#~ msgid "Created tags"
|
||||
#~ msgstr "Etiquetas creadas"
|
||||
|
||||
#~ msgid "Deleted tags"
|
||||
#~ msgstr "Etiquetas eliminadas"
|
||||
|
||||
@@ -8,10 +8,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
{
|
||||
const NAME = 'api-key:disable';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
@@ -30,7 +33,7 @@ class DisableKeyCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:disable')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Disables an API key.'))
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||
}
|
||||
@@ -38,18 +41,13 @@ class DisableKeyCommand extends Command
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$output->writeln(sprintf(
|
||||
$this->translator->translate('API key %s properly disabled'),
|
||||
'<info>' . $apiKey . '</info>'
|
||||
));
|
||||
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('API key "%s" does not exist.') . '</error>',
|
||||
$apiKey
|
||||
));
|
||||
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
const NAME = 'api-key:generate';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
@@ -30,7 +33,7 @@ class GenerateKeyCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:generate')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
@@ -44,6 +47,9 @@ class GenerateKeyCommand extends Command
|
||||
{
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
||||
$output->writeln($this->translator->translate('Generated API key') . sprintf(': <info>%s</info>', $apiKey));
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListKeysCommand extends Command
|
||||
{
|
||||
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
|
||||
*/
|
||||
@@ -32,7 +38,7 @@ class ListKeysCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:list')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
@@ -44,78 +50,48 @@ class ListKeysCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
$list = $this->apiKeyService->listKeys($enabledOnly);
|
||||
|
||||
$table = new Table($output);
|
||||
if ($enabledOnly) {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
} else {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Is enabled'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
}
|
||||
$rows = [];
|
||||
|
||||
/** @var ApiKey $row */
|
||||
foreach ($list as $row) {
|
||||
$key = $row->getKey();
|
||||
$expiration = $row->getExpirationDate();
|
||||
$rowData = [];
|
||||
$formatMethod = ! $row->isEnabled()
|
||||
? 'getErrorString'
|
||||
: ($row->isExpired() ? 'getWarningString' : 'getSuccessString');
|
||||
$messagePattern = $this->determineMessagePattern($row);
|
||||
|
||||
if ($enabledOnly) {
|
||||
$rowData[] = $this->{$formatMethod}($key);
|
||||
} else {
|
||||
$rowData[] = $this->{$formatMethod}($key);
|
||||
$rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row));
|
||||
// Set columns for this row
|
||||
$rowData = [\sprintf($messagePattern, $key)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
}
|
||||
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
|
||||
|
||||
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ATOM) : '-';
|
||||
$table->addRow($rowData);
|
||||
$rows[] = $rowData;
|
||||
}
|
||||
|
||||
$table->render();
|
||||
$io->table(\array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
]), $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getErrorString($string)
|
||||
private function determineMessagePattern(ApiKey $apiKey): string
|
||||
{
|
||||
return sprintf('<fg=red>%s</>', $string);
|
||||
}
|
||||
if (! $apiKey->isEnabled()) {
|
||||
return self::ERROR_STRING_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getSuccessString($string)
|
||||
{
|
||||
return sprintf('<info>%s</info>', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getWarningString($string)
|
||||
{
|
||||
return sprintf('<comment>%s</comment>', $string);
|
||||
return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ApiKey $apiKey
|
||||
* @return string
|
||||
*/
|
||||
protected function getEnabledSymbol(ApiKey $apiKey)
|
||||
private function getEnabledSymbol(ApiKey $apiKey): string
|
||||
{
|
||||
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
const NAME = 'config:generate-charset';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
@@ -24,7 +27,7 @@ class GenerateCharsetCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('config:generate-charset')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(sprintf($this->translator->translate(
|
||||
'Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
|
||||
@@ -34,6 +37,8 @@ class GenerateCharsetCommand extends Command
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
$output->writeln($this->translator->translate('Character set:') . sprintf(' <info>%s</info>', $charSet));
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
\sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,15 @@ use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
const NAME = 'config:generate-secret';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
@@ -26,7 +29,7 @@ class GenerateSecretCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('config:generate-secret')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate(
|
||||
'Generates a random secret string that can be used for JWT token encryption'
|
||||
));
|
||||
@@ -35,6 +38,8 @@ class GenerateSecretCommand extends Command
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
$output->writeln($this->translator->translate('Secret key:') . sprintf(' <info>%s</info>', $secret));
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommand extends Command
|
||||
@@ -24,17 +26,9 @@ class InstallCommand extends Command
|
||||
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
|
||||
|
||||
/**
|
||||
* @var InputInterface
|
||||
* @var SymfonyStyle
|
||||
*/
|
||||
private $input;
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
private $questionHelper;
|
||||
private $io;
|
||||
/**
|
||||
* @var ProcessHelper
|
||||
*/
|
||||
@@ -48,65 +42,81 @@ class InstallCommand extends Command
|
||||
*/
|
||||
private $filesystem;
|
||||
/**
|
||||
* @var ConfigCustomizerPluginManagerInterface
|
||||
* @var ConfigCustomizerManagerInterface
|
||||
*/
|
||||
private $configCustomizers;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isUpdate;
|
||||
/**
|
||||
* @var PhpExecutableFinder
|
||||
*/
|
||||
private $phpFinder;
|
||||
/**
|
||||
* @var string|bool
|
||||
*/
|
||||
private $phpBinary;
|
||||
|
||||
/**
|
||||
* InstallCommand constructor.
|
||||
* @param WriterInterface $configWriter
|
||||
* @param Filesystem $filesystem
|
||||
* @param ConfigCustomizerManagerInterface $configCustomizers
|
||||
* @param bool $isUpdate
|
||||
* @param PhpExecutableFinder|null $phpFinder
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __construct(
|
||||
WriterInterface $configWriter,
|
||||
Filesystem $filesystem,
|
||||
ConfigCustomizerPluginManagerInterface $configCustomizers,
|
||||
$isUpdate = false
|
||||
ConfigCustomizerManagerInterface $configCustomizers,
|
||||
bool $isUpdate = false,
|
||||
PhpExecutableFinder $phpFinder = null
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->configWriter = $configWriter;
|
||||
$this->isUpdate = $isUpdate;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->configCustomizers = $configCustomizers;
|
||||
$this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('shlink:install')
|
||||
->setDescription('Installs or updates Shlink');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return void
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->questionHelper = $this->getHelper('question');
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
$output->writeln([
|
||||
$this->io->writeln([
|
||||
'<info>Welcome to Shlink!!</info>',
|
||||
'This will guide you through the installation process.',
|
||||
]);
|
||||
|
||||
// Check if a cached config file exists and drop it if so
|
||||
if ($this->filesystem->exists('data/cache/app_config.php')) {
|
||||
$output->write('Deleting old cached config...');
|
||||
$this->io->write('Deleting old cached config...');
|
||||
try {
|
||||
$this->filesystem->remove('data/cache/app_config.php');
|
||||
$output->writeln(' <info>Success</info>');
|
||||
$this->io->writeln(' <info>Success</info>');
|
||||
} catch (IOException $e) {
|
||||
$output->writeln(
|
||||
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
|
||||
. ' new config applied.'
|
||||
$this->io->error(
|
||||
'Failed! You will have to manually delete the data/cache/app_config.php file to'
|
||||
. ' get new config applied.'
|
||||
);
|
||||
if ($output->isVerbose()) {
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
return;
|
||||
@@ -118,56 +128,65 @@ class InstallCommand extends Command
|
||||
|
||||
// Ask for custom config params
|
||||
foreach ([
|
||||
Plugin\DatabaseConfigCustomizerPlugin::class,
|
||||
Plugin\UrlShortenerConfigCustomizerPlugin::class,
|
||||
Plugin\LanguageConfigCustomizerPlugin::class,
|
||||
Plugin\ApplicationConfigCustomizerPlugin::class,
|
||||
Plugin\DatabaseConfigCustomizer::class,
|
||||
Plugin\UrlShortenerConfigCustomizer::class,
|
||||
Plugin\LanguageConfigCustomizer::class,
|
||||
Plugin\ApplicationConfigCustomizer::class,
|
||||
] as $pluginName) {
|
||||
/** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
|
||||
/** @var Plugin\ConfigCustomizerInterface $configCustomizer */
|
||||
$configCustomizer = $this->configCustomizers->get($pluginName);
|
||||
$configCustomizer->process($input, $output, $config);
|
||||
$configCustomizer->process($this->io, $config);
|
||||
}
|
||||
|
||||
// Generate config params files
|
||||
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
|
||||
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// If current command is not update, generate database
|
||||
if (! $this->isUpdate) {
|
||||
$this->output->writeln('Initializing database...');
|
||||
if (! $this->runCommand(
|
||||
'php vendor/bin/doctrine.php orm:schema-tool:create',
|
||||
'Error generating database.'
|
||||
$this->io->write('Initializing database...');
|
||||
if (! $this->runPhpCommand(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
|
||||
'Error generating database.',
|
||||
$output
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Run database migrations
|
||||
$output->writeln('Updating database...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
|
||||
$this->io->write('Updating database...');
|
||||
if (! $this->runPhpCommand(
|
||||
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
|
||||
'Error updating database.',
|
||||
$output
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate proxies
|
||||
$output->writeln('Generating proxies...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
|
||||
$this->io->write('Generating proxies...');
|
||||
if (! $this->runPhpCommand(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
|
||||
'Error generating proxies.',
|
||||
$output
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->io->success('Installation complete!');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CustomizableAppConfig
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function importConfig()
|
||||
private function importConfig(): CustomizableAppConfig
|
||||
{
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
// Ask the user if he/she wants to import an older configuration
|
||||
$importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
|
||||
'<question>Do you want to import previous configuration? (Y/n):</question> '
|
||||
));
|
||||
$importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
|
||||
if (! $importConfig) {
|
||||
return $config;
|
||||
}
|
||||
@@ -175,17 +194,16 @@ class InstallCommand extends Command
|
||||
// Ask the user for the older shlink path
|
||||
$keepAsking = true;
|
||||
do {
|
||||
$config->setImportedInstallationPath($this->ask(
|
||||
$config->setImportedInstallationPath($this->io->ask(
|
||||
'Previous shlink installation path from which to import config'
|
||||
));
|
||||
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
|
||||
$configExists = $this->filesystem->exists($configFile);
|
||||
|
||||
if (! $configExists) {
|
||||
$keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
|
||||
'Provided path does not seem to be a valid shlink root path. '
|
||||
. '<question>Do you want to try another path? (Y/n):</question> '
|
||||
));
|
||||
$keepAsking = $this->io->confirm(
|
||||
'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
|
||||
);
|
||||
}
|
||||
} while (! $configExists && $keepAsking);
|
||||
|
||||
@@ -199,51 +217,35 @@ class InstallCommand extends Command
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function ask($text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if ($default !== null) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
$value = $this->questionHelper->ask($this->input, $this->output, new Question(
|
||||
'<question>' . $text . ':</question> ',
|
||||
$default
|
||||
));
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$this->output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && $default === null && ! $allowEmpty);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param string $errorMessage
|
||||
* @param OutputInterface $output
|
||||
* @return bool
|
||||
* @throws LogicException
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function runCommand($command, $errorMessage)
|
||||
private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
|
||||
{
|
||||
$process = $this->processHelper->run($this->output, $command);
|
||||
if ($this->processHelper === null) {
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
}
|
||||
|
||||
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()) {
|
||||
$this->output->writeln(' <info>Success!</info>');
|
||||
$this->io->writeln(' <info>Success!</info>');
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
return false;
|
||||
if (! $this->io->isVerbose()) {
|
||||
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
|
||||
}
|
||||
|
||||
$this->output->writeln(
|
||||
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:process-previews';
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
*/
|
||||
@@ -39,7 +42,7 @@ class GeneratePreviewCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:process-previews')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
@@ -59,22 +62,20 @@ class GeneratePreviewCommand extends Command
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
|
||||
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
|
||||
}
|
||||
|
||||
protected function processUrl($url, OutputInterface $output)
|
||||
{
|
||||
try {
|
||||
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$messages = [' <error>' . $this->translator->translate('Error') . '</error>'];
|
||||
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
|
||||
if ($output->isVerbose()) {
|
||||
$messages[] = '<error>' . $e->__toString() . '</error>';
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
|
||||
$output->writeln($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateShortcodeCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:generate';
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
@@ -44,7 +45,7 @@ class GenerateShortcodeCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:generate')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
)
|
||||
@@ -73,19 +74,15 @@ class GenerateShortcodeCommand extends Command
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (! empty($longUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
|
||||
));
|
||||
|
||||
$longUrl = $helper->ask($input, $output, $question);
|
||||
$longUrl = $io->ask(
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
|
||||
);
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
@@ -93,23 +90,24 @@ class GenerateShortcodeCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error($this->translator->translate('A URL was not provided!'));
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = $input->getOption('tags');
|
||||
$processedTags = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$explodedTags = explode(',', $tag);
|
||||
$processedTags = array_merge($processedTags, $explodedTags);
|
||||
$explodedTags = \explode(',', $tag);
|
||||
$processedTags = \array_merge($processedTags, $explodedTags);
|
||||
}
|
||||
$tags = $processedTags;
|
||||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
|
||||
try {
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
|
||||
return;
|
||||
}
|
||||
|
||||
$shortCode = $this->urlShortener->urlToShortCode(
|
||||
new Uri($longUrl),
|
||||
$tags,
|
||||
@@ -122,22 +120,20 @@ class GenerateShortcodeCommand extends Command
|
||||
->withScheme($this->domainConfig['schema'])
|
||||
->withHost($this->domainConfig['hostname']);
|
||||
|
||||
$output->writeln([
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
|
||||
$io->writeln([
|
||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Processed long URL:'), $longUrl),
|
||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Generated short URL:'), $shortUrl),
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate(
|
||||
'Provided URL "%s" is invalid. Try with a different one.'
|
||||
) . '</error>',
|
||||
$io->error(\sprintf(
|
||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
||||
$longUrl
|
||||
));
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate(
|
||||
$io->error(\sprintf(
|
||||
$this->translator->translate(
|
||||
'Provided slug "%s" is already in use by another URL. Try with a different one.'
|
||||
) . '</error>',
|
||||
),
|
||||
$customSlug
|
||||
));
|
||||
}
|
||||
|
||||
@@ -6,17 +6,17 @@ namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:visits';
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
@@ -30,12 +30,12 @@ class GetVisitsCommand extends Command
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:visits')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
@@ -65,14 +65,10 @@ class GetVisitsCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?')
|
||||
);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
@@ -80,33 +76,32 @@ class GetVisitsCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
|
||||
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('Remote Address'),
|
||||
$this->translator->translate('User agent'),
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($visits as $row) {
|
||||
$rowData = $row->jsonSerialize();
|
||||
// Unset location info
|
||||
unset($rowData['visitLocation']);
|
||||
|
||||
$table->addRow(array_values($rowData));
|
||||
$rows[] = \array_values($rowData);
|
||||
}
|
||||
$table->render();
|
||||
$io->table([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('Remote Address'),
|
||||
$this->translator->translate('User agent'),
|
||||
], $rows);
|
||||
}
|
||||
|
||||
protected function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (isset($value)) {
|
||||
if (! empty($value)) {
|
||||
$value = new \DateTime($value);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,19 +6,20 @@ namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListShortcodesCommand extends Command
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
const NAME = 'shortcode:list';
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
@@ -27,17 +28,25 @@ class ListShortcodesCommand extends Command
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
|
||||
{
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('shortcode:list')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->addOption(
|
||||
'page',
|
||||
@@ -79,66 +88,58 @@ class ListShortcodesCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$page = (int) $input->getOption('page');
|
||||
$searchTerm = $input->getOption('searchTerm');
|
||||
$tags = $input->getOption('tags');
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
$tags = ! empty($tags) ? \explode(',', $tags) : [];
|
||||
$showTags = $input->getOption('showTags');
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$page++;
|
||||
$table = new Table($output);
|
||||
|
||||
$headers = [
|
||||
$this->translator->translate('Short code'),
|
||||
$this->translator->translate('Original URL'),
|
||||
$this->translator->translate('Short URL'),
|
||||
$this->translator->translate('Long URL'),
|
||||
$this->translator->translate('Date created'),
|
||||
$this->translator->translate('Visits count'),
|
||||
];
|
||||
if ($showTags) {
|
||||
$headers[] = $this->translator->translate('Tags');
|
||||
}
|
||||
$table->setHeaders($headers);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $row->jsonSerialize();
|
||||
$shortUrl = $transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = [];
|
||||
foreach ($row->getTags() as $tag) {
|
||||
$shortUrl['tags'][] = $tag->getName();
|
||||
}
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
$table->addRow(array_values($shortUrl));
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = \array_values($shortUrl);
|
||||
}
|
||||
$table->render();
|
||||
$io->table($headers, $rows);
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$output->writeln(
|
||||
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
|
||||
);
|
||||
$io->success($this->translator->translate('Short codes properly listed'));
|
||||
} else {
|
||||
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
|
||||
sprintf('<question>' . $this->translator->translate(
|
||||
'Continue with page'
|
||||
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
|
||||
$continue = $io->confirm(
|
||||
\sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
||||
false
|
||||
));
|
||||
);
|
||||
}
|
||||
} while ($continue);
|
||||
}
|
||||
|
||||
protected function processOrderBy(InputInterface $input)
|
||||
private function processOrderBy(InputInterface $input)
|
||||
{
|
||||
$orderBy = $input->getOption('orderBy');
|
||||
if (empty($orderBy)) {
|
||||
|
||||
@@ -7,15 +7,16 @@ use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
const NAME = 'shortcode:parse';
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
@@ -34,7 +35,7 @@ class ResolveUrlCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:parse')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
@@ -50,14 +51,10 @@ class ResolveUrlCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?')
|
||||
);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
@@ -65,27 +62,22 @@ class ResolveUrlCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
|
||||
$shortCode
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
|
||||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
$output->writeln(
|
||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
|
||||
);
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$output->writeln(sprintf('<error>' . $this->translator->translate(
|
||||
'Provided short code "%s" has an invalid format.'
|
||||
) . '</error>', $shortCode));
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
||||
);
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$output->writeln(sprintf('<error>' . $this->translator->translate(
|
||||
'Provided short code "%s" could not be found.'
|
||||
) . '</error>', $shortCode));
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:create';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
@@ -31,7 +34,7 @@ class CreateTagCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:create')
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Creates one or more tags.'))
|
||||
->addOption(
|
||||
'name',
|
||||
@@ -43,19 +46,15 @@ class CreateTagCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>%s</comment>',
|
||||
$this->translator->translate('You have to provide at least one tag name')
|
||||
));
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$output->writeln($this->translator->translate('Created tags') . sprintf(': ["<info>%s</info>"]', implode(
|
||||
'</info>", "<info>',
|
||||
$tagNames
|
||||
)));
|
||||
$io->success($this->translator->translate('Tags properly created'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteTagsCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:delete';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
@@ -31,7 +34,7 @@ class DeleteTagsCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:delete')
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Deletes one or more tags.'))
|
||||
->addOption(
|
||||
'name',
|
||||
@@ -43,19 +46,15 @@ class DeleteTagsCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>%s</comment>',
|
||||
$this->translator->translate('You have to provide at least one tag name')
|
||||
));
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$output->writeln($this->translator->translate('Deleted tags') . sprintf(': ["<info>%s</info>"]', implode(
|
||||
'</info>", "<info>',
|
||||
$tagNames
|
||||
)));
|
||||
$io->success($this->translator->translate('Tags properly deleted'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:list';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
@@ -32,17 +34,14 @@ class ListTagsCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:list')
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([$this->translator->translate('Name')])
|
||||
->setRows($this->getTagsRows());
|
||||
|
||||
$table->render();
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||
}
|
||||
|
||||
private function getTagsRows()
|
||||
@@ -52,7 +51,7 @@ class ListTagsCommand extends Command
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
}
|
||||
|
||||
return array_map(function (Tag $tag) {
|
||||
return \array_map(function (Tag $tag) {
|
||||
return [$tag->getName()];
|
||||
}, $tags);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:rename';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
@@ -32,7 +35,7 @@ class RenameTagCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:rename')
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Renames one existing tag.'))
|
||||
->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.'))
|
||||
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
||||
@@ -40,16 +43,15 @@ class RenameTagCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$output->writeln(sprintf('<info>%s</info>', $this->translator->translate('Tag properly renamed.')));
|
||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$output->writeln('<error>' . sprintf($this->translator->translate(
|
||||
'A tag with name "%s" was not found'
|
||||
), $oldName) . '</error>');
|
||||
$io->error(\sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
private const LOCALHOST = '127.0.0.1';
|
||||
public const NAME = 'visit:process';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
@@ -42,7 +44,7 @@ class ProcessVisitsCommand extends Command
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('visit:process')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Processes visits where location is not set yet')
|
||||
);
|
||||
@@ -50,33 +52,53 @@ class ProcessVisitsCommand extends Command
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
$count = 0;
|
||||
foreach ($visits as $visit) {
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$output->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) {
|
||||
$output->writeln(
|
||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
$io->writeln(
|
||||
\sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$count++;
|
||||
try {
|
||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
|
||||
$location = new VisitLocation();
|
||||
$location->exchangeArray($result);
|
||||
$visit->setVisitLocation($location);
|
||||
$this->visitService->saveVisit($visit);
|
||||
$output->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);
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln($this->translator->translate('Finished processing all IPs'));
|
||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@ namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
@@ -20,28 +23,23 @@ class ApplicationFactory implements FactoryInterface
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @return CliApp
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws ContainerExceptionInterface
|
||||
* @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)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): CliApp
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$translator = $container->get(Translator::class);
|
||||
$translator->setLocale($config['locale']);
|
||||
|
||||
$commands = isset($config['commands']) ? $config['commands'] : [];
|
||||
$commands = $config['commands'] ?? [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
foreach ($commands as $command) {
|
||||
if (! $container->has($command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$app->add($container->get($command));
|
||||
}
|
||||
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ namespace Shlinkio\Shlink\CLI\Factory;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManager;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManager;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
@@ -17,6 +16,7 @@ use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
class InstallApplicationFactory implements FactoryInterface
|
||||
{
|
||||
@@ -41,11 +41,11 @@ class InstallApplicationFactory implements FactoryInterface
|
||||
$command = new InstallCommand(
|
||||
new PhpArray(),
|
||||
$container->get(Filesystem::class),
|
||||
new ConfigCustomizerPluginManager($container, ['factories' => [
|
||||
Plugin\DatabaseConfigCustomizerPlugin::class => ConfigAbstractFactory::class,
|
||||
Plugin\UrlShortenerConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
Plugin\LanguageConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
Plugin\ApplicationConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
new ConfigCustomizerManager($container, ['factories' => [
|
||||
Plugin\DatabaseConfigCustomizer::class => ConfigAbstractFactory::class,
|
||||
Plugin\UrlShortenerConfigCustomizer::class => InvokableFactory::class,
|
||||
Plugin\LanguageConfigCustomizer::class => InvokableFactory::class,
|
||||
Plugin\ApplicationConfigCustomizer::class => InvokableFactory::class,
|
||||
]]),
|
||||
$isUpdate
|
||||
);
|
||||
|
||||
12
module/CLI/src/Install/ConfigCustomizerManager.php
Normal file
12
module/CLI/src/Install/ConfigCustomizerManager.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerInterface;
|
||||
use Zend\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class ConfigCustomizerManager extends AbstractPluginManager implements ConfigCustomizerManagerInterface
|
||||
{
|
||||
protected $instanceOf = ConfigCustomizerInterface::class;
|
||||
}
|
||||
@@ -5,6 +5,6 @@ namespace Shlinkio\Shlink\CLI\Install;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
interface ConfigCustomizerPluginManagerInterface extends ContainerInterface
|
||||
interface ConfigCustomizerManagerInterface extends ContainerInterface
|
||||
{
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
|
||||
use Zend\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class ConfigCustomizerPluginManager extends AbstractPluginManager implements ConfigCustomizerPluginManagerInterface
|
||||
{
|
||||
protected $instanceOf = ConfigCustomizerPluginInterface::class;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
abstract class AbstractConfigCustomizerPlugin implements ConfigCustomizerPluginInterface
|
||||
{
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
protected $questionHelper;
|
||||
|
||||
public function __construct(QuestionHelper $questionHelper)
|
||||
{
|
||||
$this->questionHelper = $questionHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param string $text
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function ask(InputInterface $input, OutputInterface $output, $text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if ($default !== null) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
$value = $this->questionHelper->ask($input, $output, new Question(
|
||||
'<question>' . $text . ':</question> ',
|
||||
$default
|
||||
));
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && $default === null && ! $allowEmpty);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputInterface $output
|
||||
* @param string $text
|
||||
*/
|
||||
protected function printTitle(OutputInterface $output, $text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$length = strlen($text) + 4;
|
||||
$header = str_repeat('*', $length);
|
||||
|
||||
$output->writeln([
|
||||
'',
|
||||
'<info>' . $header . '</info>',
|
||||
'<info>* ' . strtoupper($text) . ' *</info>',
|
||||
'<info>' . $header . '</info>',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('APPLICATION');
|
||||
|
||||
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validator = function ($value) {
|
||||
return $value;
|
||||
};
|
||||
$appConfig->setApp([
|
||||
'SECRET' => $io->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
$validator
|
||||
) ?: $this->generateRandomString(32),
|
||||
'DISABLE_TRACK_PARAM' => $io->ask(
|
||||
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
|
||||
. 'short URLs (leave empty and this feature won\'t be enabled)',
|
||||
null,
|
||||
$validator
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class ApplicationConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws \Symfony\Component\Console\Exception\RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'APPLICATION');
|
||||
|
||||
if ($appConfig->hasApp() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported application config? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setApp([
|
||||
'SECRET' => $this->ask(
|
||||
$input,
|
||||
$output,
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: $this->generateRandomString(32),
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
module/CLI/src/Install/Plugin/ConfigCustomizerInterface.php
Normal file
17
module/CLI/src/Install/Plugin/ConfigCustomizerInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
interface ConfigCustomizerInterface
|
||||
{
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
interface ConfigCustomizerPluginInterface
|
||||
{
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig);
|
||||
}
|
||||
78
module/CLI/src/Install/Plugin/DatabaseConfigCustomizer.php
Normal file
78
module/CLI/src/Install/Plugin/DatabaseConfigCustomizer.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(Filesystem $filesystem)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws IOException
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('DATABASE');
|
||||
|
||||
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
|
||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
try {
|
||||
$this->filesystem->copy(
|
||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
||||
CustomizableAppConfig::SQLITE_DB_PATH
|
||||
);
|
||||
} catch (IOException $e) {
|
||||
$io->error('It wasn\'t possible to import the SQLite database');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select database type
|
||||
$params = [];
|
||||
$databases = \array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $io->choice('Select database type', $databases, $databases[0]);
|
||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||
|
||||
// Ask for connection params if database is not SQLite
|
||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
$params['NAME'] = $io->ask('Database name', 'shlink');
|
||||
$params['USER'] = $io->ask('Database username');
|
||||
$params['PASSWORD'] = $io->ask('Database password');
|
||||
$params['HOST'] = $io->ask('Database host', 'localhost');
|
||||
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
}
|
||||
|
||||
$appConfig->setDatabase($params);
|
||||
}
|
||||
|
||||
private function getDefaultDbPort(string $driver): string
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* DatabaseConfigCustomizerPlugin constructor.
|
||||
* @param QuestionHelper $questionHelper
|
||||
* @param Filesystem $filesystem
|
||||
*
|
||||
* @DI\Inject({QuestionHelper::class, Filesystem::class})
|
||||
*/
|
||||
public function __construct(QuestionHelper $questionHelper, Filesystem $filesystem)
|
||||
{
|
||||
parent::__construct($questionHelper);
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws IOException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'DATABASE');
|
||||
|
||||
if ($appConfig->hasDatabase() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported database config? (Y/n):</question> '
|
||||
))) {
|
||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
try {
|
||||
$this->filesystem->copy(
|
||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
||||
CustomizableAppConfig::SQLITE_DB_PATH
|
||||
);
|
||||
} catch (IOException $e) {
|
||||
$output->writeln('<error>It wasn\'t possible to import the SQLite database</error>');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select database type
|
||||
$params = [];
|
||||
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $this->questionHelper->ask($input, $output, new ChoiceQuestion(
|
||||
'<question>Select database type (defaults to ' . $databases[0] . '):</question>',
|
||||
$databases,
|
||||
0
|
||||
));
|
||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||
|
||||
// Ask for connection params if database is not SQLite
|
||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
$params['NAME'] = $this->ask($input, $output, 'Database name', 'shlink');
|
||||
$params['USER'] = $this->ask($input, $output, 'Database username');
|
||||
$params['PASSWORD'] = $this->ask($input, $output, 'Database password');
|
||||
$params['HOST'] = $this->ask($input, $output, 'Database host', 'localhost');
|
||||
$params['PORT'] = $this->ask($input, $output, 'Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
}
|
||||
|
||||
$appConfig->setDatabase($params);
|
||||
}
|
||||
|
||||
private function getDefaultDbPort($driver)
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class DefaultConfigCustomizerPluginFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return new $requestedName($container->get(QuestionHelper::class));
|
||||
}
|
||||
}
|
||||
36
module/CLI/src/Install/Plugin/LanguageConfigCustomizer.php
Normal file
36
module/CLI/src/Install/Plugin/LanguageConfigCustomizer.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class LanguageConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('LANGUAGE');
|
||||
|
||||
if ($appConfig->hasLanguage() && $io->confirm('Do you want to keep imported language?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setLanguage([
|
||||
'DEFAULT' => $this->chooseLanguage('Select default language for the application in general', $io),
|
||||
'CLI' => $this->chooseLanguage('Select default language for CLI executions', $io),
|
||||
]);
|
||||
}
|
||||
|
||||
private function chooseLanguage(string $message, SymfonyStyle $io): string
|
||||
{
|
||||
return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class LanguageConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'LANGUAGE');
|
||||
|
||||
if ($appConfig->hasLanguage() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported language? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setLanguage([
|
||||
'DEFAULT' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
|
||||
'<question>Select default language for the application in general (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
'CLI' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
|
||||
'<question>Select default language for CLI executions (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('URL SHORTENER');
|
||||
|
||||
if ($appConfig->hasUrlShortener() && $io->confirm('Do you want to keep imported URL shortener config?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for URL shortener params
|
||||
$appConfig->setUrlShortener([
|
||||
'SCHEMA' => $io->choice(
|
||||
'Select schema for generated short URLs',
|
||||
['http', 'https'],
|
||||
'http'
|
||||
),
|
||||
'HOSTNAME' => $io->ask('Hostname for generated URLs'),
|
||||
'CHARS' => $io->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
function ($value) {
|
||||
return $value;
|
||||
}
|
||||
) ?: \str_shuffle(UrlShortener::DEFAULT_CHARS),
|
||||
'VALIDATE_URL' => $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class UrlShortenerConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'URL SHORTENER');
|
||||
|
||||
if ($appConfig->hasUrlShortener() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported URL shortener config? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for URL shortener params
|
||||
$appConfig->setUrlShortener([
|
||||
'SCHEMA' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
|
||||
'<question>Select schema for generated short URLs (defaults to http):</question>',
|
||||
['http', 'https'],
|
||||
0
|
||||
)),
|
||||
'HOSTNAME' => $this->ask($input, $output, 'Hostname for generated URLs'),
|
||||
'CHARS' => $this->ask(
|
||||
$input,
|
||||
$output,
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS),
|
||||
'VALIDATE_URL' => $this->questionHelper->ask(
|
||||
$input,
|
||||
$output,
|
||||
new ConfirmationQuestion(
|
||||
'<question>Do you want to validate long urls by 200 HTTP status code on response (Y/n):</question>'
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,7 @@ final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $this->app['SECRET'],
|
||||
'disable_track_param' => $this->app['DISABLE_TRACK_PARAM'] ?? null,
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
|
||||
@@ -59,6 +59,6 @@ class DisableKeyCommandTest extends TestCase
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('API key "abcd1234" does not exist.' . PHP_EOL, $output);
|
||||
$this->assertContains('API key "abcd1234" does not exist.', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace ShlinkioTest\Shlink\CLI\Command\Config;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
@@ -32,7 +31,6 @@ class GenerateCharsetCommandTest extends TestCase
|
||||
public function charactersAreGeneratedFromDefault()
|
||||
{
|
||||
$prefix = 'Character set: ';
|
||||
$prefixLength = strlen($prefix);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'config:generate-charset',
|
||||
@@ -40,13 +38,7 @@ class GenerateCharsetCommandTest extends TestCase
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Both default character set and the new one should have the same length
|
||||
$this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output));
|
||||
|
||||
// Both default character set and the new one should have the same characters
|
||||
$charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS));
|
||||
$orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS);
|
||||
$orderedCharset = $this->orderStringLetters($charset);
|
||||
$this->assertEquals($orderedDefault, $orderedCharset);
|
||||
$this->assertContains($prefix, $output);
|
||||
}
|
||||
|
||||
protected function orderStringLetters($string)
|
||||
|
||||
@@ -8,13 +8,14 @@ use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Process\PhpExecutableFinder;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
@@ -51,10 +52,13 @@ class InstallCommandTest extends TestCase
|
||||
|
||||
$this->configWriter = $this->prophesize(WriterInterface::class);
|
||||
|
||||
$configCustomizer = $this->prophesize(ConfigCustomizerPluginInterface::class);
|
||||
$configCustomizers = $this->prophesize(ConfigCustomizerPluginManagerInterface::class);
|
||||
$configCustomizer = $this->prophesize(ConfigCustomizerInterface::class);
|
||||
$configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class);
|
||||
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal());
|
||||
|
||||
$finder = $this->prophesize(PhpExecutableFinder::class);
|
||||
$finder->find(false)->willReturn('php');
|
||||
|
||||
$app = new Application();
|
||||
$helperSet = $app->getHelperSet();
|
||||
$helperSet->set($processHelper->reveal());
|
||||
@@ -62,7 +66,9 @@ class InstallCommandTest extends TestCase
|
||||
$this->command = new InstallCommand(
|
||||
$this->configWriter->reveal(),
|
||||
$this->filesystem->reveal(),
|
||||
$configCustomizers->reveal()
|
||||
$configCustomizers->reveal(),
|
||||
false,
|
||||
$finder->reveal()
|
||||
);
|
||||
$app->add($this->command);
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||
'longUrl' => 'http://domain.com/invalid',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(
|
||||
strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0
|
||||
$this->assertContains(
|
||||
'Provided URL "http://domain.com/invalid" is invalid.',
|
||||
$output
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
@@ -22,10 +21,6 @@ class ListShortcodesCommandTest extends TestCase
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
protected $questionHelper;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
@@ -35,10 +30,8 @@ class ListShortcodesCommandTest extends TestCase
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]));
|
||||
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||
$app->add($command);
|
||||
|
||||
$this->questionHelper = $command->getHelper('question');
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
@@ -47,10 +40,10 @@ class ListShortcodesCommandTest extends TestCase
|
||||
*/
|
||||
public function noInputCallsListJustOnce()
|
||||
{
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
@@ -61,22 +54,15 @@ class ListShortcodesCommandTest extends TestCase
|
||||
{
|
||||
// The paginator will return more than one page for the first 3 times
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = new ShortUrl();
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
||||
}
|
||||
$data = array_chunk($data, 11);
|
||||
|
||||
$questionHelper = $this->questionHelper;
|
||||
$that = $this;
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (
|
||||
&$data,
|
||||
$questionHelper,
|
||||
$that
|
||||
) {
|
||||
$questionHelper->setInputStream($that->getInputStream('y'));
|
||||
return new Paginator(new ArrayAdapter(array_shift($data)));
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
|
||||
return new Paginator(new ArrayAdapter($data));
|
||||
})->shouldBeCalledTimes(3);
|
||||
|
||||
$this->commandTester->setInputs(['y', 'y', 'n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
@@ -88,13 +74,13 @@ class ListShortcodesCommandTest extends TestCase
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = new ShortUrl();
|
||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
||||
}
|
||||
|
||||
$this->questionHelper->setInputStream($this->getInputStream('n'));
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
@@ -104,10 +90,10 @@ class ListShortcodesCommandTest extends TestCase
|
||||
public function passingPageWillMakeListStartOnThatPage()
|
||||
{
|
||||
$page = 5;
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:list',
|
||||
'--page' => $page,
|
||||
@@ -119,24 +105,15 @@ class ListShortcodesCommandTest extends TestCase
|
||||
*/
|
||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||
{
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:list',
|
||||
'--showTags' => true,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'Tags') > 0);
|
||||
}
|
||||
|
||||
protected function getInputStream($inputData)
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+', false);
|
||||
fputs($stream, $inputData);
|
||||
rewind($stream);
|
||||
|
||||
return $stream;
|
||||
$this->assertContains('Tags', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
@@ -41,7 +42,8 @@ class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$expectedUrl = 'http://domain.com/foo/bar';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl)
|
||||
$shortUrl = (new ShortUrl())->setLongUrl($expectedUrl);
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
@@ -66,7 +68,7 @@ class ResolveUrlCommandTest extends TestCase
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('Provided short code "' . $shortCode . '" could not be found.' . PHP_EOL, $output);
|
||||
$this->assertContains('Provided short code "' . $shortCode . '" could not be found.', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +85,6 @@ class ResolveUrlCommandTest extends TestCase
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output);
|
||||
$this->assertContains('Provided short code "' . $shortCode . '" has an invalid format.', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -14,10 +15,6 @@ use Zend\I18n\Translator\Translator;
|
||||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CreateTagCommand
|
||||
*/
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
@@ -56,14 +53,14 @@ 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,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Created tags: ["%s"]', implode('", "', $tagNames)), $output);
|
||||
$this->assertContains('Tags properly created', $output);
|
||||
$createTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ class DeleteTagsCommandTest extends TestCase
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Deleted tags: ["%s"]', implode('", "', $tagNames)), $output);
|
||||
$this->assertContains('Tags properly deleted', $output);
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class ApplicationConfigCustomizerPluginTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationConfigCustomizerPlugin
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new ApplicationConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('the_secret');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_secret',
|
||||
], $config->getApp());
|
||||
$askSecret->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'the_new_secret';
|
||||
});
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_new_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class ApplicationConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationConfigCustomizer
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $io;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
|
||||
$this->plugin = new ApplicationConfigCustomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_secret');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_secret',
|
||||
'DISABLE_TRACK_PARAM' => 'the_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_new_secret',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
], $config->getApp());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -5,26 +5,22 @@ namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
class DatabaseConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DatabaseConfigCustomizerPlugin
|
||||
* @var DatabaseConfigCustomizer
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
private $io;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
@@ -32,13 +28,11 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
|
||||
$this->plugin = new DatabaseConfigCustomizerPlugin(
|
||||
$this->questionHelper->reveal(),
|
||||
$this->filesystem->reveal()
|
||||
);
|
||||
$this->plugin = new DatabaseConfigCustomizer($this->filesystem->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,22 +40,23 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('MySQL');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('param');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasDatabase());
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'param',
|
||||
'USER' => 'param',
|
||||
'PASSWORD' => 'param',
|
||||
'HOST' => 'param',
|
||||
'PORT' => 'param',
|
||||
], $config->getDatabase());
|
||||
$askSecret->shouldHaveBeenCalledTimes(6);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,11 +64,9 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'MySQL';
|
||||
});
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('MySQL');
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
@@ -84,7 +77,7 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
'PORT' => 'MySQL',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
@@ -94,7 +87,9 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(7);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,8 +97,7 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
@@ -115,7 +109,7 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
'PORT' => 'MySQL',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
@@ -125,7 +119,7 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,9 +127,7 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function sqliteDatabaseIsImportedWhenRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
/** @var MethodProphecy $copy */
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
@@ -143,12 +135,12 @@ class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$copy->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class DefaultConfigCustomizerPluginFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DefaultConfigCustomizerPluginFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new DefaultConfigCustomizerPluginFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function createsProperService()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
|
||||
]]), ApplicationConfigCustomizerPlugin::class);
|
||||
$this->assertInstanceOf(ApplicationConfigCustomizerPlugin::class, $instance);
|
||||
|
||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
|
||||
]]), LanguageConfigCustomizerPlugin::class);
|
||||
$this->assertInstanceOf(LanguageConfigCustomizerPlugin::class, $instance);
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,27 @@ namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
class LanguageConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var LanguageConfigCustomizerPlugin
|
||||
* @var LanguageConfigCustomizer
|
||||
*/
|
||||
protected $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $questionHelper;
|
||||
protected $io;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new LanguageConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
$this->plugin = new LanguageConfigCustomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,18 +33,17 @@ class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('en');
|
||||
$ask = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasLanguage());
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
], $config->getLanguage());
|
||||
$askSecret->shouldHaveBeenCalledTimes(2);
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,24 +51,22 @@ class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'es';
|
||||
});
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('es');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
$choice->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,8 +74,7 @@ class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
$ask = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
@@ -89,7 +82,7 @@ class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
'CLI' => 'es',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
@@ -5,30 +5,27 @@ namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerConfigCustomizerPlugin
|
||||
* @var UrlShortenerConfigCustomizer
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
private $io;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new UrlShortenerConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
$this->plugin = new UrlShortenerConfigCustomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,20 +33,23 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('something');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('something');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('something');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasUrlShortener());
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'something',
|
||||
'HOSTNAME' => 'something',
|
||||
'CHARS' => 'something',
|
||||
'VALIDATE_URL' => 'something',
|
||||
'VALIDATE_URL' => true,
|
||||
], $config->getUrlShortener());
|
||||
$askSecret->shouldHaveBeenCalledTimes(4);
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,20 +57,18 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'foo';
|
||||
});
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('foo');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('foo');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'bar',
|
||||
'HOSTNAME' => 'bar',
|
||||
'CHARS' => 'bar',
|
||||
'VALIDATE_URL' => 'bar',
|
||||
'VALIDATE_URL' => true,
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
@@ -78,7 +76,9 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => false,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,8 +86,7 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
@@ -97,7 +96,7 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
'VALIDATE_URL' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
@@ -105,6 +104,6 @@ class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
interface ExceptionInterface
|
||||
interface ExceptionInterface extends \Throwable
|
||||
{
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, \Exception $prev = null)
|
||||
public static function fromIpAddress($ipAddress, \Throwable $prev = null)
|
||||
{
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
@@ -27,6 +27,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactory implements FactoryInterface
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return new ImplicitOptionsMiddleware(new EmptyResponse());
|
||||
return new ImplicitOptionsMiddleware(function () {
|
||||
return new EmptyResponse();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
));
|
||||
|
||||
@@ -3,10 +3,10 @@ declare(strict_types=1);
|
||||
|
||||
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\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as DelegateInterface;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class LocaleMiddleware implements MiddlewareInterface
|
||||
@@ -32,15 +32,15 @@ class LocaleMiddleware implements MiddlewareInterface
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate): Response
|
||||
{
|
||||
if (! $request->hasHeader('Accept-Language')) {
|
||||
return $delegate->process($request);
|
||||
return $delegate->handle($request);
|
||||
}
|
||||
|
||||
$locale = $request->getHeaderLine('Accept-Language');
|
||||
$this->translator->setLocale($this->normalizeLocale($locale));
|
||||
return $delegate->process($request);
|
||||
return $delegate->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ use Zend\Paginator\Adapter\AdapterInterface;
|
||||
|
||||
class PaginableRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
const ITEMS_PER_PAGE = 10;
|
||||
public const ITEMS_PER_PAGE = 10;
|
||||
|
||||
/**
|
||||
* @var PaginableRepositoryInterface
|
||||
@@ -34,7 +34,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
|
||||
$orderBy = null
|
||||
) {
|
||||
$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->tags = $tags;
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
|
||||
* @param int $itemCountPerPage Number of items per page
|
||||
* @return array
|
||||
*/
|
||||
public function getItems($offset, $itemCountPerPage)
|
||||
public function getItems($offset, $itemCountPerPage): array
|
||||
{
|
||||
return $this->paginableRepository->findList(
|
||||
$itemCountPerPage,
|
||||
@@ -66,7 +66,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
|
||||
* The return value is cast to an integer.
|
||||
* @since 5.1.0
|
||||
*/
|
||||
public function count()
|
||||
public function count(): int
|
||||
{
|
||||
return $this->paginableRepository->countList($this->searchTerm, $this->tags);
|
||||
}
|
||||
|
||||
@@ -3,29 +3,38 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Paginator\Util;
|
||||
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Zend\Paginator\Paginator;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
trait PaginatorUtilsTrait
|
||||
{
|
||||
protected function serializePaginator(Paginator $paginator)
|
||||
private function serializePaginator(Paginator $paginator, ?DataTransformerInterface $transformer = null): array
|
||||
{
|
||||
return [
|
||||
'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()),
|
||||
'data' => $this->serializeItems(ArrayUtils::iteratorToArray($paginator->getCurrentItems()), $transformer),
|
||||
'pagination' => [
|
||||
'currentPage' => $paginator->getCurrentPageNumber(),
|
||||
'pagesCount' => $paginator->count(),
|
||||
'itemsPerPage' => $paginator->getItemCountPerPage(),
|
||||
'itemsInCurrentPage' => $paginator->getCurrentItemCount(),
|
||||
'totalItems' => $paginator->getTotalItemCount(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function serializeItems(array $items, ?DataTransformerInterface $transformer = null): array
|
||||
{
|
||||
return $transformer === null ? $items : \array_map([$transformer, 'transform'], $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if provided paginator is in last page
|
||||
*
|
||||
* @param Paginator $paginator
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLastPage(Paginator $paginator)
|
||||
private function isLastPage(Paginator $paginator): bool
|
||||
{
|
||||
return $paginator->getCurrentPageNumber() >= $paginator->count();
|
||||
}
|
||||
|
||||
33
module/Common/src/Response/PixelResponse.php
Normal file
33
module/Common/src/Response/PixelResponse.php
Normal 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;
|
||||
}
|
||||
}
|
||||
9
module/Common/src/Rest/DataTransformerInterface.php
Normal file
9
module/Common/src/Rest/DataTransformerInterface.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Rest;
|
||||
|
||||
interface DataTransformerInterface
|
||||
{
|
||||
public function transform($value): array;
|
||||
}
|
||||
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,37 +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 $ipAddress
|
||||
* @return array
|
||||
*/
|
||||
public function resolveIpLocation($ipAddress)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,28 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
interface IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param $ipAddress
|
||||
* @param string $ipAddress
|
||||
* @return array
|
||||
* @throws WrongIpException
|
||||
*/
|
||||
public function resolveIpLocation($ipAddress);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace Shlinkio\Shlink\Common\Util;
|
||||
class DateRange
|
||||
{
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $startDate;
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $endDate;
|
||||
|
||||
@@ -21,7 +21,7 @@ class DateRange
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getStartDate()
|
||||
{
|
||||
@@ -29,7 +29,7 @@ class DateRange
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getEndDate()
|
||||
{
|
||||
@@ -39,8 +39,8 @@ class DateRange
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty()
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return is_null($this->startDate) && is_null($this->endDate);
|
||||
return $this->startDate === null && $this->endDate === null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,40 +3,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\DbUnit;
|
||||
|
||||
use Doctrine\DBAL\Driver\PDOConnection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\DbUnit\Database\Connection as DbConn;
|
||||
use PHPUnit\DbUnit\DataSet\IDataSet as DataSet;
|
||||
use PHPUnit\DbUnit\TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
abstract class DatabaseTestCase extends TestCase
|
||||
{
|
||||
const ENTITIES_TO_EMPTY = [];
|
||||
protected const ENTITIES_TO_EMPTY = [];
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
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
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace ShlinkioTest\Shlink\Common\Factory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
|
||||
@@ -38,8 +38,8 @@ class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
|
||||
$ref = new \ReflectionObject($instance);
|
||||
$prop = $ref->getProperty('response');
|
||||
$prop = $ref->getProperty('responseFactory');
|
||||
$prop->setAccessible(true);
|
||||
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance));
|
||||
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance)());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class LocaleMiddlewareTest extends TestCase
|
||||
public function whenNoHeaderIsPresentLocaleIsNotChanged()
|
||||
{
|
||||
$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());
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class LocaleMiddlewareTest extends TestCase
|
||||
{
|
||||
$this->assertEquals('ru', $this->translator->getLocale());
|
||||
$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());
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class LocaleMiddlewareTest extends TestCase
|
||||
*/
|
||||
public function localeGetsNormalized()
|
||||
{
|
||||
$delegate = TestUtils::createDelegateMock();
|
||||
$delegate = TestUtils::createReqHandlerMock();
|
||||
|
||||
$this->assertEquals('ru', $this->translator->getLocale());
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user