mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-01 20:53:14 +08:00
Compare commits
277 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f563e777cc | ||
|
|
a63447b12b | ||
|
|
0f81c3ab92 | ||
|
|
425f254453 | ||
|
|
a9d9ec5bf9 | ||
|
|
0c5c752ffe | ||
|
|
4b556cd79f | ||
|
|
3d32a90f8e | ||
|
|
0b4c334163 | ||
|
|
312fc0984b | ||
|
|
30bf1c2641 | ||
|
|
2d1d7357a3 | ||
|
|
c70077c525 | ||
|
|
d2fad0128f | ||
|
|
62133c994f | ||
|
|
091ea974eb | ||
|
|
955ae00036 | ||
|
|
7d4de590e5 | ||
|
|
292937b962 | ||
|
|
08bd4f131c | ||
|
|
38cc83a4ee | ||
|
|
687a1cc9c7 | ||
|
|
1bcd03b150 | ||
|
|
e2abe23895 | ||
|
|
5c5dde48de | ||
|
|
d9f11e190f | ||
|
|
1ab2d7a240 | ||
|
|
580050cb7d | ||
|
|
eab5659163 | ||
|
|
397b350cfc | ||
|
|
c0130c997a | ||
|
|
fd7f1b32dd | ||
|
|
0e286d8261 | ||
|
|
ce7d2d1fb0 | ||
|
|
2175b8a7bb | ||
|
|
6c0893cdf8 | ||
|
|
25927a296d | ||
|
|
ee4db44fe8 | ||
|
|
b8cb38ae5c | ||
|
|
899bfdce2b | ||
|
|
456960e1f0 | ||
|
|
04e03e9b6e | ||
|
|
a7283da016 | ||
|
|
672321abab | ||
|
|
2059b4050b | ||
|
|
171b43c517 | ||
|
|
ccb7c8f8d9 | ||
|
|
abbc66ac07 | ||
|
|
2d18ef5cee | ||
|
|
79c132219b | ||
|
|
04d4d4a8d7 | ||
|
|
a918113ba0 | ||
|
|
810b25ff14 | ||
|
|
c4fd8d5120 | ||
|
|
772494f46f | ||
|
|
594e7da256 | ||
|
|
49668547d7 | ||
|
|
4c46aaead8 | ||
|
|
d61f5faf59 | ||
|
|
5756609531 | ||
|
|
ea1b285d52 | ||
|
|
bc61b55b94 | ||
|
|
48f6a96da8 | ||
|
|
967f1657d2 | ||
|
|
f90a323374 | ||
|
|
d289c62532 | ||
|
|
05695e8cd6 | ||
|
|
d6a7a6ce66 | ||
|
|
05c7672de3 | ||
|
|
ce515767ce | ||
|
|
76d8fd1023 | ||
|
|
558e259b84 | ||
|
|
f467bed24c | ||
|
|
fa753ad6fb | ||
|
|
22d61fead7 | ||
|
|
c4af1471f0 | ||
|
|
87ba7a7179 | ||
|
|
e7c5cf0846 | ||
|
|
1aaedb8d90 | ||
|
|
284de28f76 | ||
|
|
687d8d91a9 | ||
|
|
771087c6c6 | ||
|
|
1fd3e6365e | ||
|
|
28989296eb | ||
|
|
fd8d73af38 | ||
|
|
144a5415da | ||
|
|
d58e24bce5 | ||
|
|
0f86123ccb | ||
|
|
3f65ef998c | ||
|
|
29d49dfbf4 | ||
|
|
701d17f6f2 | ||
|
|
642431c43e | ||
|
|
3c5b47784d | ||
|
|
64d7fe8bbf | ||
|
|
32070b1fa7 | ||
|
|
8b3324e143 | ||
|
|
f40a5a029c | ||
|
|
eac82a602c | ||
|
|
d1312e0934 | ||
|
|
58dbee10c5 | ||
|
|
f8207994dc | ||
|
|
2030401859 | ||
|
|
8966cf9910 | ||
|
|
4eb4df9ca2 | ||
|
|
32861b1c72 | ||
|
|
7248ca2e9b | ||
|
|
a6ec93f883 | ||
|
|
a28c1d17c5 | ||
|
|
fb705b44a4 | ||
|
|
a32bab9fd0 | ||
|
|
6396e7f964 | ||
|
|
c898cef277 | ||
|
|
baeba54b06 | ||
|
|
f5ee5bf7fb | ||
|
|
73605414f9 | ||
|
|
6045c371e1 | ||
|
|
97a9289d5f | ||
|
|
1983fc9b67 | ||
|
|
bb40d84212 | ||
|
|
46a35c553e | ||
|
|
080943e810 | ||
|
|
62fb3863c6 | ||
|
|
2db03a163d | ||
|
|
9e3dd82efe | ||
|
|
9f1989bfef | ||
|
|
c0bdd8fc77 | ||
|
|
8a23c90e46 | ||
|
|
9095e5b057 | ||
|
|
52c18115af | ||
|
|
737137b19f | ||
|
|
7b78bee135 | ||
|
|
accda36a7b | ||
|
|
69dd9eb067 | ||
|
|
a562bc661d | ||
|
|
258f12f684 | ||
|
|
4dc8d77a5a | ||
|
|
7c5825d1bc | ||
|
|
6ba4d8e947 | ||
|
|
3faf6e967f | ||
|
|
a7a5667301 | ||
|
|
d4924897b2 | ||
|
|
f2d39ca55a | ||
|
|
743d052f55 | ||
|
|
17dbab5ee8 | ||
|
|
8cb5e07c7b | ||
|
|
e9972783d2 | ||
|
|
84f6080a38 | ||
|
|
1b5c1e4e52 | ||
|
|
d7e89ebdae | ||
|
|
aa413dab6d | ||
|
|
b876870bd8 | ||
|
|
05e56cc845 | ||
|
|
d6c158ce98 | ||
|
|
1d4ef4e9a4 | ||
|
|
4d2684be52 | ||
|
|
6947805b5c | ||
|
|
d0e0aea0f1 | ||
|
|
b0f250ed8a | ||
|
|
45254606d4 | ||
|
|
03ee46d903 | ||
|
|
c4afc7a923 | ||
|
|
afa2a5b0f0 | ||
|
|
b40057d423 | ||
|
|
282ffef200 | ||
|
|
22b02de405 | ||
|
|
f0330e9ae3 | ||
|
|
0c26490e3f | ||
|
|
cfaecd93e4 | ||
|
|
ccbc6c7a75 | ||
|
|
2fc2ad98aa | ||
|
|
16590b2dbb | ||
|
|
f40349479e | ||
|
|
9f60c8dffe | ||
|
|
5abd9d1a40 | ||
|
|
0ae5a53d86 | ||
|
|
15a70d0157 | ||
|
|
ededb68ef1 | ||
|
|
09add5fbff | ||
|
|
e30f49a791 | ||
|
|
64737b741b | ||
|
|
d4d65bdf37 | ||
|
|
90732a4fad | ||
|
|
c5015f5828 | ||
|
|
aa77c944d8 | ||
|
|
b8faa6714a | ||
|
|
f48f98f4d7 | ||
|
|
79b2a0839f | ||
|
|
6094d17718 | ||
|
|
d2ed7d6417 | ||
|
|
a705ef21a9 | ||
|
|
67e465c479 | ||
|
|
ed3883b52c | ||
|
|
71ea0bcb5e | ||
|
|
dd2cffeee9 | ||
|
|
1ceabf3bc3 | ||
|
|
17fcd637f2 | ||
|
|
d44bc4b182 | ||
|
|
4760406221 | ||
|
|
0aae0d888c | ||
|
|
1bc01057f3 | ||
|
|
c1906606c6 | ||
|
|
1363194909 | ||
|
|
d945e0c31b | ||
|
|
0af7b75af5 | ||
|
|
36a42cb064 | ||
|
|
4db0acc0e7 | ||
|
|
8f4800aa47 | ||
|
|
4745a37549 | ||
|
|
8fc949898b | ||
|
|
d4758b0e91 | ||
|
|
a07e4b17be | ||
|
|
b9dd975bc6 | ||
|
|
9964d3e24b | ||
|
|
58e8c8e182 | ||
|
|
c7339f6cfa | ||
|
|
1aa78f766a | ||
|
|
bf56e6adaf | ||
|
|
e915b7e499 | ||
|
|
de0470d200 | ||
|
|
3d7cf6992e | ||
|
|
06db082e3f | ||
|
|
4a383cecaf | ||
|
|
9a0f9207be | ||
|
|
0e3a0a1eec | ||
|
|
fd6d180eba | ||
|
|
d152e2ef9a | ||
|
|
b530cf4461 | ||
|
|
bbe85cde31 | ||
|
|
2c3cbe7146 | ||
|
|
2358308f4d | ||
|
|
58bff4fa73 | ||
|
|
098f7afc70 | ||
|
|
4070b1e23d | ||
|
|
d9d4c8a70c | ||
|
|
05abe49d8b | ||
|
|
a71245b883 | ||
|
|
057f88a36a | ||
|
|
32fcdd9d94 | ||
|
|
313927827d | ||
|
|
358b2b661e | ||
|
|
3eddacdff8 | ||
|
|
95d4cde649 | ||
|
|
d1d947bf12 | ||
|
|
40815e5b38 | ||
|
|
8fc1d23e03 | ||
|
|
5ec8c229a1 | ||
|
|
2412ec2195 | ||
|
|
bfb96b0ae8 | ||
|
|
f64920e510 | ||
|
|
664dc333ac | ||
|
|
521f6f2b18 | ||
|
|
6986d03c53 | ||
|
|
e6e38e3ca2 | ||
|
|
951d08f914 | ||
|
|
8e1e8ba7de | ||
|
|
877b098b09 | ||
|
|
e046eddda9 | ||
|
|
084b1169d7 | ||
|
|
f7ceeff05a | ||
|
|
e0d41a2b8a | ||
|
|
6b9f9f0f44 | ||
|
|
025135b8c6 | ||
|
|
77d810b735 | ||
|
|
e1222de05b | ||
|
|
459f807e67 | ||
|
|
32df1370a6 | ||
|
|
f18f8c89ec | ||
|
|
787b791651 | ||
|
|
2eca0da852 | ||
|
|
9e49604ce2 | ||
|
|
5f85c61d6a | ||
|
|
cd58855e1f | ||
|
|
13c64b0db0 | ||
|
|
55e021ba20 | ||
|
|
26fd61a3ed | ||
|
|
46482522bb | ||
|
|
98e3e22896 |
10
.gitattributes
vendored
10
.gitattributes
vendored
@@ -1,12 +1,14 @@
|
||||
/config/test export-ignore
|
||||
/data/infra export-ignore
|
||||
/docs export-ignore
|
||||
/module/CLI/test export-ignore
|
||||
/module/CLI/test-resources export-ignore
|
||||
/module/Common/test export-ignore
|
||||
/module/Common/test-func export-ignore
|
||||
/module/Common/test-db export-ignore
|
||||
/module/Core/test export-ignore
|
||||
/module/Core/test-func export-ignore
|
||||
/module/Core/test-db export-ignore
|
||||
/module/Rest/test export-ignore
|
||||
/module/Rest/test-api export-ignore
|
||||
.env.dist export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
@@ -17,9 +19,9 @@ build.sh export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
docker-compose.override.yml.dist export-ignore
|
||||
docker-compose.yml export-ignore
|
||||
func_tests_bootstrap.php export-ignore
|
||||
indocker export-ignore
|
||||
phpcs.xml export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
phpunit-func.xml export-ignore
|
||||
phpunit-api.xml export-ignore
|
||||
phpunit-db.xml export-ignore
|
||||
phpstan.neon
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,5 +5,8 @@ composer.phar
|
||||
vendor/
|
||||
.env
|
||||
data/database.sqlite
|
||||
docs/swagger-ui
|
||||
data/shlink-tests.db
|
||||
data/GeoLite2-City.mmdb
|
||||
docs/swagger-ui*
|
||||
docker-compose.override.yml
|
||||
.phpunit.result.cache
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
/**
|
||||
* PhpStorm Container Interop code completion
|
||||
@@ -17,7 +17,7 @@ $STATIC_METHOD_TYPES = [
|
||||
ContainerInterface::get('') => [
|
||||
'' == '@',
|
||||
],
|
||||
ServiceManager::build('') => [
|
||||
ServiceLocatorInterface::build('') => [
|
||||
'' == '@',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
tools:
|
||||
external_code_coverage: true
|
||||
external_code_coverage:
|
||||
timeout: 600
|
||||
checks:
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
php:
|
||||
code_rating: true
|
||||
duplication: true
|
||||
build:
|
||||
nodes:
|
||||
analysis:
|
||||
tests:
|
||||
override:
|
||||
- php-scrutinizer-run
|
||||
|
||||
35
.travis.yml
35
.travis.yml
@@ -1,7 +1,5 @@
|
||||
language: php
|
||||
|
||||
sudo: false # Use containerized environment
|
||||
|
||||
branches:
|
||||
only:
|
||||
- /.*/
|
||||
@@ -11,13 +9,11 @@ php:
|
||||
- 7.2
|
||||
- 7.3
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: 7.3
|
||||
|
||||
before_install:
|
||||
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- yes | pecl install swoole
|
||||
- phpenv config-rm xdebug.ini || return 0
|
||||
|
||||
install:
|
||||
- composer self-update
|
||||
@@ -25,10 +21,11 @@ install:
|
||||
|
||||
script:
|
||||
- mkdir build
|
||||
- composer check
|
||||
- composer ci
|
||||
|
||||
after_success:
|
||||
- vendor/bin/phpcov merge build --clover build/clover.xml
|
||||
- rm -f build/clover.xml
|
||||
- phpdbg -qrr vendor/bin/phpcov merge build --clover build/clover.xml
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||
|
||||
@@ -38,11 +35,17 @@ before_deploy:
|
||||
- ./build.sh ${TRAVIS_TAG#?}
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
- provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
- provider: script
|
||||
script: bash data/travis/trigger_docker_build.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
|
||||
232
CHANGELOG.md
232
CHANGELOG.md
@@ -2,7 +2,237 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## 1.16.2 - 2019-03-05
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#368](https://github.com/shlinkio/shlink/issues/368) Fixed error produced when running a `SELECT COUNT(...)` with `ORDER BY` in PostgreSQL databases.
|
||||
|
||||
|
||||
## 1.16.1 - 2019-02-26
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#363](https://github.com/shlinkio/shlink/issues/363) Updated to `shlinkio/php-coding-standard` version 1.1.0
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#362](https://github.com/shlinkio/shlink/issues/362) Fixed all visits without an IP address being processed every time the `visit:process` command is executed.
|
||||
|
||||
|
||||
## 1.16.0 - 2019-02-23
|
||||
|
||||
#### Added
|
||||
|
||||
* [#304](https://github.com/shlinkio/shlink/issues/304) Added health endpoint to check healthiness of the service. Useful in container-based infrastructures.
|
||||
|
||||
Call [GET /rest/health] in order to get a response like this:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/health+json
|
||||
Content-Length: 681
|
||||
|
||||
{
|
||||
"status": "pass",
|
||||
"version": "1.16.0",
|
||||
"links": {
|
||||
"about": "https://shlink.io",
|
||||
"project": "https://github.com/shlinkio/shlink"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The status code can be `200 OK` in case of success or `503 Service Unavailable` in case of error, while the `status` property will be one of `pass` or `fail`, as defined in the [Health check RFC](https://inadarei.github.io/rfc-healthcheck/).
|
||||
|
||||
* [#279](https://github.com/shlinkio/shlink/issues/279) Added new `findIfExists` flag to the `[POST /short-url]` REST endpoint and the `short-urls:generate` CLI command. It can be used to return existing short URLs when found, instead of creating new ones.
|
||||
|
||||
Thanks to this flag you won't need to remember if you created a short URL for a long one. It will just create it if needed or return the existing one if possible.
|
||||
|
||||
The behavior might be a little bit counterintuitive when combined with other params. This is how the endpoint behaves when providing this new flag:
|
||||
|
||||
* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.
|
||||
* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.
|
||||
* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.
|
||||
|
||||
* [#336](https://github.com/shlinkio/shlink/issues/336) Added an API test suite which performs API calls to an actual instance of the web service.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#342](https://github.com/shlinkio/shlink/issues/342) The installer no longer asks for a charset to be provided, and instead, it shuffles the base62 charset.
|
||||
* [#320](https://github.com/shlinkio/shlink/issues/320) Replaced query builder by plain DQL for all queries which do not need to be dynamically generated.
|
||||
* [#330](https://github.com/shlinkio/shlink/issues/330) No longer allow failures on PHP 7.3 envs during project CI build.
|
||||
* [#335](https://github.com/shlinkio/shlink/issues/335) Renamed functional test suite to database test suite, since that better describes what it actually does.
|
||||
* [#346](https://github.com/shlinkio/shlink/issues/346) Extracted installer as an independent tool.
|
||||
* [#261](https://github.com/shlinkio/shlink/issues/261) Increased mutation score to 70%.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* [#351](https://github.com/shlinkio/shlink/issues/351) Deprecated `config:generate-charset` and `config:generate-secret` CLI commands.
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#317](https://github.com/shlinkio/shlink/issues/317) Fixed error while trying to generate previews because of global config file being deleted by mistake by build script.
|
||||
* [#307](https://github.com/shlinkio/shlink/issues/307) Fixed memory leak while trying to process huge amounts of visits due to the query not being properly paginated.
|
||||
|
||||
|
||||
## 1.15.1 - 2018-12-16
|
||||
|
||||
#### Added
|
||||
|
||||
* [#162](https://github.com/shlinkio/shlink/issues/162) Added non-rest endpoints to swagger definition.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#312](https://github.com/shlinkio/shlink/issues/312) Now all config files both in `php` and `json` format are loaded from `config/params` folder, easing users to provided customizations to docker image.
|
||||
* [#226](https://github.com/shlinkio/shlink/issues/226) Updated how table are rendered in CLI commands, making use of new features in Symfony 4.2.
|
||||
* [#321](https://github.com/shlinkio/shlink/issues/321) Extracted entities mappings from entities to external config files.
|
||||
* [#308](https://github.com/shlinkio/shlink/issues/308) Automated docker image building.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#301](https://github.com/shlinkio/shlink/issues/301) Removed custom `AccessLogFactory` in favor of the implementation included in [zendframework/zend-expressive-swoole](https://github.com/zendframework/zend-expressive-swoole) v2.2.0
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#309](https://github.com/shlinkio/shlink/issues/309) Added missing favicon to prevent 404 errors logged when an error page is loaded in a browser.
|
||||
* [#310](https://github.com/shlinkio/shlink/issues/310) Fixed execution context not being properly detected, making `CloseDbConnectionMiddlware` to be always piped. Now the check is not even made, which simplifies everything.
|
||||
|
||||
|
||||
## 1.15.0 - 2018-12-02
|
||||
|
||||
#### Added
|
||||
|
||||
* [#208](https://github.com/shlinkio/shlink/issues/208) Added initial support to run shlink using [swoole](https://www.swoole.co.uk/), a non-blocking IO server which improves the performance of shlink from 4 to 10 times.
|
||||
|
||||
Run shlink with `./vendor/bin/zend-expressive-swoole start` to start-up the service, which will be exposed in port `8080`.
|
||||
|
||||
Adding the `-d` flag, it will be started as a background service. Then you can use the `./vendor/bin/zend-expressive-swoole stop` command in order to stop it.
|
||||
|
||||
* [#266](https://github.com/shlinkio/shlink/issues/266) Added pagination to `GET /short-urls/{shortCode}/visits` endpoint.
|
||||
|
||||
In order to make it backwards compatible, it keeps returning all visits by default, but it now allows to provide the `page` and `itemsPerPage` query parameters in order to configure the number of items to get.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#267](https://github.com/shlinkio/shlink/issues/267) API responses and the CLI interface is no longer translated and uses english always. Only not found error templates are still translated.
|
||||
* [#289](https://github.com/shlinkio/shlink/issues/289) Extracted coding standard rules to a separated package.
|
||||
* [#273](https://github.com/shlinkio/shlink/issues/273) Improved code coverage in repository classes.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#278](https://github.com/shlinkio/shlink/pull/278) Added missing `X-Api-Key` header to the list of valid cross domain headers.
|
||||
* [#295](https://github.com/shlinkio/shlink/pull/295) Fixed custom slugs so that they are case sensitive and do not try to lowercase provided values.
|
||||
|
||||
|
||||
## 1.14.1 - 2018-11-17
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#260](https://github.com/shlinkio/shlink/issues/260) Increased mutation score to 65%.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#271](https://github.com/shlinkio/shlink/issues/271) Fixed memory leak produced when processing high amounts of visits at the same time.
|
||||
* [#272](https://github.com/shlinkio/shlink/issues/272) Fixed errors produced when trying to process visits multiple times in parallel, by using a lock which ensures only one instance is run at a time.
|
||||
|
||||
|
||||
## 1.14.0 - 2018-11-16
|
||||
|
||||
#### Added
|
||||
|
||||
* [#236](https://github.com/shlinkio/shlink/issues/236) Added option to define a redirection to a custom URL when a user hits an invalid short URL.
|
||||
|
||||
It only affects URLs matched as "short URL" where the short code is invalid, not any 404 that happens in the app. For example, a request to the path `/foo/bar` will keep returning a 404.
|
||||
|
||||
This new option will be asked by the installer both for new shlink installations and for any previous shlink version which is updated.
|
||||
|
||||
* [#189](https://github.com/shlinkio/shlink/issues/189) and [#240](https://github.com/shlinkio/shlink/issues/240) Added new [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/)-based geolocation service which is faster and more reliable than previous one.
|
||||
|
||||
It does not have API limit problems, since it uses a local database file.
|
||||
|
||||
Previous service is still used as a fallback in case GeoLite DB does not contain any IP address.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#241](https://github.com/shlinkio/shlink/issues/241) Fixed columns in `visit_locations` table, to be snake_case instead of camelCase.
|
||||
* [#228](https://github.com/shlinkio/shlink/issues/228) Updated how exceptions are serialized into logs, by using monlog's `PsrLogMessageProcessor`.
|
||||
* [#225](https://github.com/shlinkio/shlink/issues/225) Performance and maintainability slightly improved by enforcing via code sniffer that all global namespace classes, functions and constants are explicitly imported.
|
||||
* [#196](https://github.com/shlinkio/shlink/issues/196) Reduced anemic model in entities, defining more expressive public APIs instead.
|
||||
* [#249](https://github.com/shlinkio/shlink/issues/249) Added [functional-php](https://github.com/lstrojny/functional-php) to ease collections handling.
|
||||
* [#253](https://github.com/shlinkio/shlink/issues/253) Increased `user_agent` column length in `visits` table to 512.
|
||||
* [#256](https://github.com/shlinkio/shlink/issues/256) Updated to Infection v0.11.
|
||||
* [#202](https://github.com/shlinkio/shlink/issues/202) Added missing response examples to OpenAPI docs.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#223](https://github.com/shlinkio/shlink/issues/223) Fixed PHPStan errors produced with symfony/console 4.1.5
|
||||
|
||||
|
||||
## 1.13.2 - 2018-10-18
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Alejandro Celaya
|
||||
Copyright (c) 2016-2019 Alejandro Celaya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
190
README.md
190
README.md
@@ -9,6 +9,14 @@
|
||||
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Update to new version](#update-to-new-version)
|
||||
- [Using a docker image](#using-a-docker-image)
|
||||
- [Using shlink](#using-shlink)
|
||||
- [Shlink CLI Help](#shlink-cli-help)
|
||||
|
||||
## Installation
|
||||
|
||||
First make sure the host where you are going to run shlink fulfills these requirements:
|
||||
@@ -44,52 +52,132 @@ Despite how you built the project, you are going to need to install it now, by f
|
||||
* If you are going to use MySQL or PostgreSQL, create an empty database with the name of your choice.
|
||||
* Recursively grant write permissions to the `data` directory. Shlink uses it to cache some information.
|
||||
* Setup the application by running the `bin/install` script. It is a command line tool that will guide you through the installation process. **Take into account that this tool has to be run directly on the server where you plan to host Shlink. Do not run it before uploading/moving it there.**
|
||||
* Configure the web server of your choice to serve shlink using your short domain.
|
||||
* Expose shlink to the web, either by using a traditional web server + fast CGI approach, or by using a [swoole](https://www.swoole.co.uk/) non-blocking server.
|
||||
|
||||
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, this would be the basic configuration for Nginx and Apache.
|
||||
* **Using a web server:**
|
||||
|
||||
*Nginx:*
|
||||
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, these would be the basic configurations for Nginx and Apache.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
*Nginx:*
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
```nginx
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Apache:*
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
* **Using swoole:**
|
||||
|
||||
**Important!** Swoole support is still experimental. Use it with care, and report any found issue.
|
||||
|
||||
First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`.
|
||||
|
||||
Once installed, it's actually pretty easy to get shlink up and running with swoole. Just run `./vendor/bin/zend-expressive-swoole start -d` and you will get shlink running on port 8080.
|
||||
|
||||
However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted.
|
||||
|
||||
For that reason, you should create a daemon script, in `/etc/init.d/shlink_swoole`, like this one, replacing `/path/to/shlink` by the path to your shlink installation:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: shlink_swoole
|
||||
# Required-Start: $local_fs $network $named $time $syslog
|
||||
# Required-Stop: $local_fs $network $named $time $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
LOGDIR=/var/log/shlink
|
||||
LOGFILE=${LOGDIR}/shlink_swoole.log
|
||||
|
||||
start() {
|
||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole already running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Starting shlink with swoole' >&2
|
||||
mkdir -p "$LOGDIR"
|
||||
touch "$LOGFILE"
|
||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
||||
echo 'Shlink started' >&2
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
stop() {
|
||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole not running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Stopping shlink with swoole' >&2
|
||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
||||
echo 'Shlink stopped' >&2
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
esac
|
||||
```
|
||||
|
||||
*Apache:*
|
||||
Then run these commands to enable the service and start it:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
* `sudo chmod +x /etc/init.d/shlink_swoole`
|
||||
* `sudo update-rc.d shlink_swoole defaults`
|
||||
* `sudo update-rc.d shlink_swoole enable`
|
||||
* `/etc/init.d/shlink_swoole start`
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
Now again, you can access shlink on port 8080, but this time the service will be automatically run at system start-up, and all access logs will be written in `/var/log/shlink/shlink_swoole.log` (you will probably want to [rotate those logs](https://www.digitalocean.com/community/tutorials/how-to-manage-logfiles-with-logrotate-on-ubuntu-16-04). You can find an example logrotate config file [here](data/infra/examples/shlink-daemon-logrotate.conf)).
|
||||
|
||||
* Generate your first API key by running `bin/cli api-key:generate`. You will need the key in order to interact with shlink's API.
|
||||
* Finally access to [https://app.shlink.io](https://app.shlink.io) and configure your server to start creating short URLs.
|
||||
@@ -104,19 +192,30 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
|
||||
|
||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||
|
||||
* Update IP geolocation database: `/path/to/shlink/bin/cli visit:update-db`
|
||||
|
||||
When shlink is installed it downloads a fresh [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) db file. Running this command will update this file.
|
||||
|
||||
The file is updated the first Tuesday of every month, so it should be enough running this command the first Wednesday.
|
||||
|
||||
* Generate website previews: `/path/to/shlink/bin/cli short-url:process-previews`
|
||||
|
||||
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
|
||||
|
||||
*Any of those commands accept the `-q` flag, which makes it not display any output. This is recommended when configuring the commands as cron jobs.*
|
||||
|
||||
In future versions, it is planed that, when using **swoole** to serve shlink, some of these tasks are automatically run without blocking the request and also, without having to configure cron jobs. Probably resolving IP locations and generating previews.
|
||||
|
||||
## Update to new version
|
||||
|
||||
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
|
||||
|
||||
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`)
|
||||
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`)
|
||||
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`).
|
||||
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`).
|
||||
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
|
||||
4. If you are using shlink with swoole, restart the service by running `/etc/init.d/shlink_swoole restart`.
|
||||
|
||||
The script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some the assets neccessary for Shlink to function.
|
||||
The `bin/update` script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some assets shlink needs to work.
|
||||
|
||||
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
|
||||
|
||||
@@ -124,17 +223,15 @@ Right now, it does not import cached info (like website previews), but it will.
|
||||
|
||||
## Using a docker image
|
||||
|
||||
Currently there's no official docker image, but there's a work in progress alpha version you can find [here](https://hub.docker.com/r/shlinkio/shlink/).
|
||||
Starting with version 1.15.0, an official docker image is provided. You can find the docs on how to use it [here](https://hub.docker.com/r/shlinkio/shlink/).
|
||||
|
||||
The idea will be that you can just generate a container using the image and provide predefined config files via volumes or CLI arguments, so that you get shlink up and running.
|
||||
|
||||
Currently the image does not expose an entry point which let's you interact with shlink's CLI interface, nor allows configuration to be passed.
|
||||
The idea is that you can just generate a container using the image and provide custom config via env vars.
|
||||
|
||||
## Using shlink
|
||||
|
||||
Once shlink is installed, there are two main ways to interact with it:
|
||||
|
||||
* **The command line**. Try running `bin/cli` and see all the available commands.
|
||||
* **The command line**. Try running `bin/cli` and see all the [available commands](#shlink-cli-help).
|
||||
|
||||
All of those commands can be run with the `--help`/`-h` flag in order to see how to use them and all the available options.
|
||||
|
||||
@@ -169,8 +266,8 @@ Available commands:
|
||||
api-key:generate Generates a new valid API key.
|
||||
api-key:list Lists all the available API keys.
|
||||
config
|
||||
config:generate-charset Generates a character set sample just by shuffling the default one, "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||
config:generate-secret Generates a random secret string that can be used for JWT token encryption
|
||||
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
|
||||
short-url
|
||||
short-url:delete [short-code:delete] Deletes a short URL
|
||||
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
|
||||
@@ -185,4 +282,7 @@ Available commands:
|
||||
tag:rename Renames one existing tag.
|
||||
visit
|
||||
visit:process Processes visits where location is not set yet
|
||||
visit:update-db Updates the GeoLite2 database file used to geolocate IP addresses
|
||||
```
|
||||
|
||||
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)
|
||||
|
||||
12
bin/install
12
bin/install
@@ -2,11 +2,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
use function chdir;
|
||||
use function dirname;
|
||||
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class)->run();
|
||||
chdir(dirname(__DIR__));
|
||||
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$run(false);
|
||||
|
||||
14
bin/test/run-api-tests.sh
Executable file
14
bin/test/run-api-tests.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
export APP_ENV=test
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
vendor/bin/zend-expressive-swoole stop
|
||||
|
||||
echo 'Starting server...'
|
||||
vendor/bin/zend-expressive-swoole start -d
|
||||
sleep 2
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox
|
||||
vendor/bin/zend-expressive-swoole stop
|
||||
12
bin/update
12
bin/update
@@ -2,11 +2,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
use function chdir;
|
||||
use function dirname;
|
||||
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class, ['isUpdate' => true])->run();
|
||||
chdir(dirname(__DIR__));
|
||||
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
|
||||
$run(true);
|
||||
|
||||
12
build.sh
12
build.sh
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
if [[ "$#" -ne 1 ]]; then
|
||||
echo "Usage:" >&2
|
||||
echo " $0 {version}" >&2
|
||||
exit 1
|
||||
@@ -10,14 +10,18 @@ fi
|
||||
version=$1
|
||||
builtcontent="./build/shlink_${version}_dist"
|
||||
projectdir=$(pwd)
|
||||
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
||||
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
||||
|
||||
# Copy project content to temp dir
|
||||
echo 'Copying project files...'
|
||||
rm -rf "${builtcontent}"
|
||||
mkdir -p "${builtcontent}"
|
||||
rsync -av * "${builtcontent}" \
|
||||
--exclude=bin/test \
|
||||
--exclude=data/infra \
|
||||
--exclude=data/travis \
|
||||
--exclude=data/migrations_template.txt \
|
||||
--exclude=data/GeoLite2-City.mmdb \
|
||||
--exclude=**/.gitignore \
|
||||
--exclude=CHANGELOG.md \
|
||||
--exclude=composer.lock \
|
||||
@@ -25,11 +29,11 @@ rsync -av * "${builtcontent}" \
|
||||
--exclude=docs \
|
||||
--exclude=indocker \
|
||||
--exclude=docker* \
|
||||
--exclude=func_tests_bootstrap.php \
|
||||
--exclude=php* \
|
||||
--exclude=infection.json \
|
||||
--exclude=phpstan.neon \
|
||||
--exclude=config/autoload/*local* \
|
||||
--exclude=config/test \
|
||||
--exclude=**/test* \
|
||||
--exclude=build*
|
||||
cd "${builtcontent}"
|
||||
@@ -37,7 +41,7 @@ cd "${builtcontent}"
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||
${composerBin} install --no-dev --optimize-autoloader --apcu-autoloader --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
|
||||
@@ -24,21 +24,25 @@
|
||||
"doctrine/orm": "^2.5",
|
||||
"endroid/qr-code": "^1.7",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"geoip2/geoip2": "^2.9",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"lstrojny/functional-php": "^1.8",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"monolog/monolog": "^1.21",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^4.0 <4.1.5",
|
||||
"symfony/filesystem": "^4.0",
|
||||
"symfony/process": "^4.0",
|
||||
"shlinkio/shlink-installer": "^1.1",
|
||||
"symfony/console": "^4.2",
|
||||
"symfony/filesystem": "^4.2",
|
||||
"symfony/lock": "^4.2",
|
||||
"symfony/process": "^4.2",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-diactoros": "^2.0",
|
||||
"zendframework/zend-diactoros": "^2.1.1",
|
||||
"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-expressive-swoole": "^2.2",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
@@ -46,15 +50,17 @@
|
||||
"zendframework/zend-stdlib": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"devster/ubench": "^2.0",
|
||||
"doctrine/data-fixtures": "^1.3",
|
||||
"filp/whoops": "^2.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.2.3",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/var-dumper": "^4.0",
|
||||
"infection/infection": "^0.12.2",
|
||||
"phpstan/phpstan": "^0.11.2",
|
||||
"phpunit/phpcov": "^6.0@dev || ^5.0",
|
||||
"phpunit/phpunit": "^8.0 || ^7.5",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~1.1.0",
|
||||
"symfony/dotenv": "^4.2",
|
||||
"symfony/var-dumper": "^4.2",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
},
|
||||
@@ -63,8 +69,7 @@
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
|
||||
"Shlinkio\\Shlink\\Installer\\": "module/Installer/src"
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Common/functions/functions.php"
|
||||
@@ -74,58 +79,76 @@
|
||||
"psr-4": {
|
||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
|
||||
"ShlinkioTest\\Shlink\\Core\\": [
|
||||
"module/Core/test",
|
||||
"module/Core/test-func"
|
||||
"module/Core/test-db"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\Common\\": [
|
||||
"module/Common/test",
|
||||
"module/Common/test-func"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test"
|
||||
"module/Common/test-db"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": [
|
||||
"ci": [
|
||||
"@cs",
|
||||
"@stan",
|
||||
"@test",
|
||||
"@infect"
|
||||
"@test:ci",
|
||||
"@infect:ci"
|
||||
],
|
||||
|
||||
"cs": "phpcs",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
||||
"stan": "phpstan analyse module/*/src/ --level=5 -c phpstan.neon",
|
||||
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:func"
|
||||
"@test:db",
|
||||
"@test:api"
|
||||
],
|
||||
"test:unit": "phpunit --coverage-php build/coverage-unit.cov",
|
||||
"test:func": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db",
|
||||
"@test:api"
|
||||
],
|
||||
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml --testdox",
|
||||
"test:db": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-db.xml --coverage-php build/coverage-db.cov --testdox",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
|
||||
"test:pretty": [
|
||||
"@test:unit",
|
||||
"@test:func",
|
||||
"phpcov merge build --html build/html"
|
||||
"@test",
|
||||
"phpdbg -qrr vendor/bin/phpcov merge build --html build/html"
|
||||
],
|
||||
"test:unit:pretty": "phpunit --coverage-html build/coverage",
|
||||
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
|
||||
|
||||
"infect": "infection --threads=4 --min-msi=60 --only-covered --log-verbosity=2",
|
||||
"infect:show": "infection --threads=4 --min-msi=60 --only-covered --log-verbosity=2 --show-mutations"
|
||||
"infect": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered",
|
||||
"infect:ci": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --coverage=build",
|
||||
"infect:show": "infection --threads=4 --min-msi=70 --log-verbosity=default --only-covered --show-mutations",
|
||||
"infect:test": [
|
||||
"@test:unit:ci",
|
||||
"@infect:ci"
|
||||
]
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
|
||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test:ci\" and \"infect:ci\"</>",
|
||||
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
||||
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:func": "<fg=blue;options=bold>Runs functional test suites (covering entity repositories)</>",
|
||||
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:db": "<fg=blue;options=bold>Runs database test suites (covering entity repositories)</>",
|
||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",
|
||||
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||
"infect": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
|
||||
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>"
|
||||
"infect:ci": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>",
|
||||
"infect:test": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
|
||||
@@ -8,7 +8,7 @@ return [
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
'secret_key' => env('SECRET_KEY', ''),
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
|
||||
|
||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
|
||||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
@@ -23,6 +18,12 @@ return [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
|
||||
'lazy_services' => [
|
||||
'proxies_target_dir' => 'data/proxies',
|
||||
'proxies_namespace' => 'ShlinkProxy',
|
||||
'write_proxy_files' => true,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
23
config/autoload/dependencies.local.php.dist
Normal file
23
config/autoload/dependencies.local.php.dist
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'lazy_services' => [
|
||||
'write_proxy_files' => false,
|
||||
],
|
||||
|
||||
'initializers' => [
|
||||
function (ContainerInterface $container, $instance) {
|
||||
if ($instance instanceof Log\LoggerAwareInterface) {
|
||||
$instance->setLogger($container->get(Log\LoggerInterface::class));
|
||||
}
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
@@ -10,9 +10,9 @@ return [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
],
|
||||
'connection' => [
|
||||
'user' => Common\env('DB_USER'),
|
||||
'password' => Common\env('DB_PASSWORD'),
|
||||
'dbname' => Common\env('DB_NAME', 'shlink'),
|
||||
'user' => env('DB_USER'),
|
||||
'password' => env('DB_PASSWORD'),
|
||||
'dbname' => env('DB_NAME', 'shlink'),
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
|
||||
|
||||
return [
|
||||
|
||||
12
config/autoload/geolite2.global.php
Normal file
12
config/autoload/geolite2.global.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
|
||||
],
|
||||
|
||||
];
|
||||
39
config/autoload/installer.global.php
Normal file
39
config/autoload/installer.global.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
return [
|
||||
|
||||
'installer_plugins_expected_config' => [
|
||||
Plugin\LanguageConfigCustomizer::class => [
|
||||
Plugin\LanguageConfigCustomizer::DEFAULT_LANG,
|
||||
],
|
||||
|
||||
Plugin\UrlShortenerConfigCustomizer::class => [
|
||||
Plugin\UrlShortenerConfigCustomizer::SCHEMA,
|
||||
Plugin\UrlShortenerConfigCustomizer::HOSTNAME,
|
||||
Plugin\UrlShortenerConfigCustomizer::CHARS,
|
||||
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL,
|
||||
Plugin\UrlShortenerConfigCustomizer::ENABLE_NOT_FOUND_REDIRECTION,
|
||||
Plugin\UrlShortenerConfigCustomizer::NOT_FOUND_REDIRECT_TO,
|
||||
],
|
||||
|
||||
Plugin\ApplicationConfigCustomizer::class => [
|
||||
Plugin\ApplicationConfigCustomizer::SECRET,
|
||||
Plugin\ApplicationConfigCustomizer::DISABLE_TRACK_PARAM,
|
||||
Plugin\ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD,
|
||||
Plugin\ApplicationConfigCustomizer::VISITS_THRESHOLD,
|
||||
],
|
||||
|
||||
Plugin\DatabaseConfigCustomizer::class => [
|
||||
Plugin\DatabaseConfigCustomizer::DRIVER,
|
||||
Plugin\DatabaseConfigCustomizer::NAME,
|
||||
Plugin\DatabaseConfigCustomizer::USER,
|
||||
Plugin\DatabaseConfigCustomizer::PASSWORD,
|
||||
Plugin\DatabaseConfigCustomizer::HOST,
|
||||
Plugin\DatabaseConfigCustomizer::PORT,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => true,
|
||||
'config_cache_enabled' => false,
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
|
||||
];
|
||||
|
||||
25
config/autoload/locks.global.php
Normal file
25
config/autoload/locks.global.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'locks' => [
|
||||
'locks_dir' => __DIR__ . '/../../data/locks',
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
||||
Lock\Factory::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||
Lock\Factory::class => [Lock\Store\FlockStore::class],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,32 +1,72 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'formatters' => [
|
||||
'dashed' => [
|
||||
'format' => '[%datetime%] %channel%.%level_name% - %message% %context%' . PHP_EOL,
|
||||
'format' => '[%datetime%] %channel%.%level_name% - %message%' . PHP_EOL,
|
||||
'include_stacktraces' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'class' => RotatingFileHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'filename' => 'data/log/shlink_log.log',
|
||||
'max_files' => 30,
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
'swoole_access_handler' => [
|
||||
'class' => StreamHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'stream' => 'php://stdout',
|
||||
],
|
||||
],
|
||||
|
||||
'processors' => [
|
||||
'exception_with_new_line' => [
|
||||
'class' => Common\Logger\Processor\ExceptionWithNewLineProcessor::class,
|
||||
],
|
||||
'psr3' => [
|
||||
'class' => Processor\PsrLogMessageProcessor::class,
|
||||
],
|
||||
],
|
||||
|
||||
'loggers' => [
|
||||
'Shlink' => [
|
||||
'handlers' => ['rotating_file_handler'],
|
||||
'handlers' => ['shlink_rotating_handler'],
|
||||
'processors' => ['exception_with_new_line', 'psr3'],
|
||||
],
|
||||
'Swoole' => [
|
||||
'handlers' => ['swoole_access_handler'],
|
||||
'processors' => ['psr3'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
|
||||
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger-name' => 'Logger_Swoole',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Logger;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'level' => Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
Common\Middleware\CloseDbConnectionMiddleware::class,
|
||||
],
|
||||
'priority' => 12,
|
||||
],
|
||||
@@ -47,6 +47,9 @@ return [
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
|
||||
// Only if a not found error is triggered, set-up the locale to be used
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
Core\Response\NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
|
||||
15
config/autoload/swoole.global.php
Normal file
15
config/autoload/swoole.global.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'enable_coroutine' => true,
|
||||
|
||||
'swoole-http-server' => [
|
||||
'host' => '0.0.0.0',
|
||||
'process-name' => 'shlink',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
@@ -11,8 +12,12 @@ return [
|
||||
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
|
||||
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
||||
],
|
||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortenerOptions::DEFAULT_CHARS),
|
||||
'validate_url' => true,
|
||||
'not_found_short_url' => [
|
||||
'enable_redirection' => false,
|
||||
'redirect_to' => null,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -3,9 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'phpwkhtmltopdf' => [
|
||||
'wkhtmltopdf' => [
|
||||
'images' => [
|
||||
'binary' => 'bin/wkhtmltoimage',
|
||||
'binary' => __DIR__ . '/../../bin/wkhtmltoimage',
|
||||
'type' => 'jpg',
|
||||
],
|
||||
],
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => false,
|
||||
'config_cache_enabled' => true,
|
||||
ConfigAggregator::ENABLE_CACHE => true,
|
||||
|
||||
];
|
||||
|
||||
@@ -6,30 +6,8 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
$isTest = false;
|
||||
foreach ($_SERVER['argv'] as $i => $arg) {
|
||||
if ($arg === '--test') {
|
||||
unset($_SERVER['argv'][$i]);
|
||||
$isTest = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ContainerInterface|ServiceManager $container */
|
||||
$container = include __DIR__ . '/container.php';
|
||||
|
||||
// If in testing env, override DB connection to use an in-memory sqlite database
|
||||
if ($isTest) {
|
||||
$container->setAllowOverride(true);
|
||||
$config = $container->get('config');
|
||||
$config['entity_manager']['connection'] = [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => realpath(sys_get_temp_dir()) . '/shlink-tests.db',
|
||||
];
|
||||
$container->setService('config', $config);
|
||||
}
|
||||
|
||||
/** @var EntityManager $em */
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
return ConsoleRunner::createHelperSet($em);
|
||||
|
||||
@@ -7,20 +7,22 @@ use Acelaya\ExpressiveErrorHandler;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
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([]),
|
||||
Expressive\Swoole\ConfigProvider::class,
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Installer\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
||||
new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
|
||||
env('APP_ENV') === 'test'
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Factory\InstallApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
return $container;
|
||||
27
config/test/bootstrap_api_tests.php
Normal file
27
config/test/bootstrap_api_tests.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function touch;
|
||||
|
||||
// Create an empty .env file
|
||||
if (! file_exists('.env')) {
|
||||
touch('.env');
|
||||
}
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$testHelper = $container->get(TestHelper::class);
|
||||
$config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
$testHelper->createTestDb($config['entity_manager']['connection']['path']);
|
||||
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) {
|
||||
$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []);
|
||||
});
|
||||
21
config/test/bootstrap_db_tests.php
Normal file
21
config/test/bootstrap_db_tests.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function touch;
|
||||
|
||||
// Create an empty .env file
|
||||
if (! file_exists('.env')) {
|
||||
touch('.env');
|
||||
}
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$config = $container->get('config');
|
||||
|
||||
$container->get(TestHelper::class)->createTestDb($config['entity_manager']['connection']['path']);
|
||||
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||
65
config/test/test_config.global.php
Normal file
65
config/test/test_config.global.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
$swooleTestingHost = '127.0.0.1';
|
||||
$swooleTestingPort = 9999;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => true,
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'host' => $swooleTestingHost,
|
||||
'port' => $swooleTestingPort,
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'services' => [
|
||||
'shlink_test_api_client' => new Client([
|
||||
'base_uri' => sprintf('http://%s:%s/', $swooleTestingHost, $swooleTestingPort),
|
||||
'http_errors' => false,
|
||||
]),
|
||||
],
|
||||
'factories' => [
|
||||
Common\TestHelper::class => InvokableFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => sys_get_temp_dir() . '/shlink-tests.db',
|
||||
// 'path' => __DIR__ . '/../../data/shlink-tests.db',
|
||||
],
|
||||
],
|
||||
|
||||
'data_fixtures' => [
|
||||
'paths' => [
|
||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
2
data/infra/database_pg/.gitignore
vendored
Executable file
2
data/infra/database_pg/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,6 +0,0 @@
|
||||
FROM mysql:5.7
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
# Enable remote access (default is localhost only, we change this
|
||||
# otherwise our database would not be reachable from outside the container)
|
||||
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf
|
||||
11
data/infra/examples/apache-vhost.conf
Normal file
11
data/infra/examples/apache-vhost.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
22
data/infra/examples/nginx-vhost.conf
Normal file
22
data/infra/examples/nginx-vhost.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
13
data/infra/examples/shlink-daemon-logrotate.conf
Normal file
13
data/infra/examples/shlink-daemon-logrotate.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
/var/log/shlink/shlink_swoole.log {
|
||||
su root root
|
||||
daily
|
||||
missingok
|
||||
rotate 120
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 root root
|
||||
postrotate
|
||||
/etc/init.d/shlink_swoole restart
|
||||
endscript
|
||||
}
|
||||
54
data/infra/examples/shlink-daemon.sh
Normal file
54
data/infra/examples/shlink-daemon.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: shlink_swoole
|
||||
# Required-Start: $local_fs $network $named $time $syslog
|
||||
# Required-Stop: $local_fs $network $named $time $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
LOGDIR=/var/log/shlink
|
||||
LOGFILE=${LOGDIR}/shlink_swoole.log
|
||||
|
||||
start() {
|
||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole already running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Starting shlink with swoole' >&2
|
||||
mkdir -p "$LOGDIR"
|
||||
touch "$LOGFILE"
|
||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
||||
echo 'Shlink started' >&2
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole not running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Stopping shlink with swoole' >&2
|
||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
||||
echo 'Shlink stopped' >&2
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
esac
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM nginx:1.11.6-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
# Delete default nginx vhost
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
@@ -1,6 +1,12 @@
|
||||
FROM php:7.1.22-fpm-alpine
|
||||
FROM php:7.3.1-fpm-alpine3.8
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV PREDIS_VERSION 4.2.0
|
||||
ENV MEMCACHED_VERSION 3.1.3
|
||||
ENV APCU_VERSION 5.1.16
|
||||
ENV APCU_BC_VERSION 1.0.4
|
||||
ENV XDEBUG_VERSION "2.7.0RC1"
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
@@ -16,17 +22,17 @@ RUN docker-php-ext-install pdo_sqlite
|
||||
RUN apk add --no-cache --virtual icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache --virtual zlib-dev
|
||||
RUN apk add --no-cache --virtual libzip-dev zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache --virtual libmcrypt-dev
|
||||
RUN docker-php-ext-install mcrypt
|
||||
|
||||
RUN apk add --no-cache --virtual libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
|
||||
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/redis\
|
||||
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
|
||||
# configure and install
|
||||
@@ -38,7 +44,7 @@ RUN rm /tmp/phpredis.tar.gz
|
||||
# Install memcached extension
|
||||
RUN apk add --no-cache --virtual cyrus-sasl-dev
|
||||
RUN apk add --no-cache --virtual libmemcached-dev
|
||||
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
|
||||
ADD https://github.com/php-memcached-dev/php-memcached/archive/v$MEMCACHED_VERSION.tar.gz /tmp/memcached.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/memcached\
|
||||
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
|
||||
# configure and install
|
||||
@@ -48,7 +54,7 @@ RUN docker-php-ext-configure memcached\
|
||||
RUN rm /tmp/memcached.tar.gz
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
|
||||
# configure and install
|
||||
@@ -58,7 +64,7 @@ RUN docker-php-ext-configure apcu\
|
||||
RUN rm /tmp/apcu.tar.gz
|
||||
|
||||
# Install APCu-BC extension
|
||||
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
|
||||
ADD https://pecl.php.net/get/apcu_bc-$APCU_BC_VERSION.tgz /tmp/apcu_bc.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu-bc\
|
||||
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
|
||||
# configure and install
|
||||
@@ -72,7 +78,7 @@ RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install xdebug
|
||||
ADD https://pecl.php.net/get/xdebug-2.5.0 /tmp/xdebug.tar.gz
|
||||
ADD https://pecl.php.net/get/xdebug-$XDEBUG_VERSION /tmp/xdebug.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/xdebug\
|
||||
&& tar xf /tmp/xdebug.tar.gz -C /usr/src/php/ext/xdebug --strip-components=1
|
||||
# configure and install
|
||||
|
||||
103
data/infra/swoole.Dockerfile
Normal file
103
data/infra/swoole.Dockerfile
Normal file
@@ -0,0 +1,103 @@
|
||||
FROM php:7.3.1-cli-alpine3.8
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV PREDIS_VERSION 4.2.0
|
||||
ENV MEMCACHED_VERSION 3.1.3
|
||||
ENV APCU_VERSION 5.1.16
|
||||
ENV APCU_BC_VERSION 1.0.4
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install iconv
|
||||
RUN docker-php-ext-install mbstring
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache --virtual sqlite-libs
|
||||
RUN apk add --no-cache --virtual sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
|
||||
RUN apk add --no-cache --virtual icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache --virtual libzip-dev zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache --virtual libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/$PREDIS_VERSION.tar.gz /tmp/phpredis.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/redis\
|
||||
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure redis\
|
||||
&& docker-php-ext-install redis
|
||||
# cleanup
|
||||
RUN rm /tmp/phpredis.tar.gz
|
||||
|
||||
# Install memcached extension
|
||||
RUN apk add --no-cache --virtual cyrus-sasl-dev
|
||||
RUN apk add --no-cache --virtual libmemcached-dev
|
||||
ADD https://github.com/php-memcached-dev/php-memcached/archive/v$MEMCACHED_VERSION.tar.gz /tmp/memcached.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/memcached\
|
||||
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure memcached\
|
||||
&& docker-php-ext-install memcached
|
||||
# cleanup
|
||||
RUN rm /tmp/memcached.tar.gz
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure apcu\
|
||||
&& docker-php-ext-install apcu
|
||||
# cleanup
|
||||
RUN rm /tmp/apcu.tar.gz
|
||||
|
||||
# Install APCu-BC extension
|
||||
ADD https://pecl.php.net/get/apcu_bc-$APCU_BC_VERSION.tgz /tmp/apcu_bc.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu-bc\
|
||||
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure apcu-bc\
|
||||
&& docker-php-ext-install apcu-bc
|
||||
# cleanup
|
||||
RUN rm /tmp/apcu_bc.tar.gz
|
||||
|
||||
# Load APCU.ini before APC.ini
|
||||
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install swoole
|
||||
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
||||
pecl install swoole && \
|
||||
docker-php-ext-enable swoole && \
|
||||
apk del .phpize-deps
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
RUN chmod +x composer.phar
|
||||
RUN mv composer.phar /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
||||
VOLUME /home/shlink
|
||||
WORKDIR /home/shlink
|
||||
|
||||
# Expose swoole port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD /usr/local/bin/composer update && \
|
||||
# When restarting the container, swoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done
|
||||
2
data/locks/.gitignore
vendored
Normal file
2
data/locks/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -6,6 +6,7 @@ namespace ShlinkMigrations;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use PDO;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
|
||||
@@ -38,7 +39,7 @@ final class Version20180913205455 extends AbstractMigration
|
||||
->set('v.remote_addr', ':obfuscatedAddr')
|
||||
->where('v.id=:id');
|
||||
|
||||
while ($row = $st->fetch(\PDO::FETCH_ASSOC)) {
|
||||
while ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||
$addr = $row['remote_addr'] ?? null;
|
||||
if ($addr === null) {
|
||||
continue;
|
||||
|
||||
68
data/migrations/Version20181020060559.php
Normal file
68
data/migrations/Version20181020060559.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20181020060559 extends AbstractMigration
|
||||
{
|
||||
private const COLUMNS = [
|
||||
'countryCode' => 'country_code',
|
||||
'countryName' => 'country_name',
|
||||
'regionName' => 'region_name',
|
||||
'cityName' => 'city_name',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->createColumns($schema->getTable('visit_locations'), self::COLUMNS);
|
||||
}
|
||||
|
||||
private function createColumns(Table $visitLocations, array $columnNames): void
|
||||
{
|
||||
foreach ($columnNames as $name) {
|
||||
if (! $visitLocations->hasColumn($name)) {
|
||||
$visitLocations->addColumn($name, Type::STRING, ['notnull' => false]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
// If the camel case columns do not exist, do nothing
|
||||
if (! $visitLocations->hasColumn('countryCode')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visit_locations');
|
||||
foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) {
|
||||
$qb->set($snakeCaseName, $camelCaseName);
|
||||
}
|
||||
$qb->execute();
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No down
|
||||
}
|
||||
}
|
||||
40
data/migrations/Version20181020065148.php
Normal file
40
data/migrations/Version20181020065148.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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 Version20181020065148 extends AbstractMigration
|
||||
{
|
||||
private const CAMEL_CASE_COLUMNS = [
|
||||
'countryCode',
|
||||
'countryName',
|
||||
'regionName',
|
||||
'cityName',
|
||||
];
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitLocations = $schema->getTable('visit_locations');
|
||||
|
||||
foreach (self::CAMEL_CASE_COLUMNS as $name) {
|
||||
if ($visitLocations->hasColumn($name)) {
|
||||
$visitLocations->dropColumn($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No down
|
||||
}
|
||||
}
|
||||
36
data/migrations/Version20181110175521.php
Normal file
36
data/migrations/Version20181110175521.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20181110175521 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->getUserAgentColumn($schema)->setLength(512);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->getUserAgentColumn($schema)->setLength(256);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function getUserAgentColumn(Schema $schema): Column
|
||||
{
|
||||
return $schema->getTable('visits')->getColumn('user_agent');
|
||||
}
|
||||
}
|
||||
20
data/migrations_template.txt
Normal file
20
data/migrations_template.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace <namespace>;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version<version> extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
<up>
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
<down>
|
||||
}
|
||||
}
|
||||
20
data/travis/trigger_docker_build.sh
Normal file
20
data/travis/trigger_docker_build.sh
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Get latest commit in master, in plain text
|
||||
LATEST_MASTER_COMMIT=$(curl -H "Accept: application/vnd.github.sha" -X GET https://api.github.com/repos/shlinkio/shlink-docker-image/commits/master)
|
||||
|
||||
# Create new tag and a ref to the tag, which will trigger image build on it
|
||||
curl -u acelaya:${GITHUB_OAUTH_KEY} \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{ \"tag\": \"${TRAVIS_TAG}\", \"message\": \"${TRAVIS_TAG}\", \"object\": \"${LATEST_MASTER_COMMIT}\", \"type\": \"commit\" }" \
|
||||
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/tags
|
||||
curl -u acelaya:${GITHUB_OAUTH_KEY} \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{ \"ref\": \"refs/tags/${TRAVIS_TAG}\", \"sha\": \"${LATEST_MASTER_COMMIT}\" }" \
|
||||
-X POST https://api.github.com/repos/shlinkio/shlink-docker-image/git/refs
|
||||
|
||||
# Trigger image build for "latest
|
||||
curl -H "Content-Type: application/json" \
|
||||
--data '{ "docker_tag": "latest" }' \
|
||||
-X POST https://registry.hub.docker.com/u/shlinkio/shlink/trigger/${DOCKER_TRIGGER_TOKEN}/
|
||||
@@ -1,4 +1,4 @@
|
||||
version: '2'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_php:
|
||||
@@ -6,3 +6,21 @@ services:
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_swoole:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_postgres:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
version: '2'
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/nginx.Dockerfile
|
||||
image: nginx:1.15.9-alpine
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./docs:/home/shlink/www/public/docs
|
||||
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
|
||||
- ./data/infra/vhost.conf:/etc/nginx/conf.d/default.conf
|
||||
links:
|
||||
- shlink_php
|
||||
|
||||
@@ -25,12 +23,24 @@ services:
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/swoole.Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./:/home/shlink
|
||||
links:
|
||||
- shlink_db
|
||||
- shlink_db_postgres
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/db.Dockerfile
|
||||
image: mysql:5.7
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
@@ -39,3 +49,16 @@ services:
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: shlink
|
||||
|
||||
shlink_db_postgres:
|
||||
container_name: shlink_db_postgres
|
||||
image: postgres:10.7-alpine
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/database_pg:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: root
|
||||
POSTGRES_DB: shlink
|
||||
PGDATA: /var/lib/postgresql/data/pgdata
|
||||
|
||||
31
docs/swagger/definitions/Health.json
Normal file
31
docs/swagger/definitions/Health.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"pass",
|
||||
"fail"
|
||||
],
|
||||
"description": "The status of the service"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Shlink version"
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"about": {
|
||||
"type": "string",
|
||||
"description": "About shlink"
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Shlink project repository"
|
||||
}
|
||||
},
|
||||
"description": "A list of links"
|
||||
}
|
||||
}
|
||||
}
|
||||
62
docs/swagger/paths/health.json
Normal file
62
docs/swagger/paths/health.json
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "health",
|
||||
"tags": [
|
||||
"Monitoring"
|
||||
],
|
||||
"summary": "Check healthiness",
|
||||
"description": "Checks the healthiness of the service, making sure it can access required resources.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The passing health status",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Health.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"status": "pass",
|
||||
"version": "1.16.0",
|
||||
"links": {
|
||||
"about": "https://shlink.io",
|
||||
"project": "https://github.com/shlinkio/shlink"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "The failing health status",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Health.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"status": "fail",
|
||||
"version": "1.16.0",
|
||||
"links": {
|
||||
"about": "https://shlink.io",
|
||||
"project": "https://github.com/shlinkio/shlink"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create short URL",
|
||||
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
@@ -197,6 +197,10 @@
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
},
|
||||
"findIfExists": {
|
||||
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,23 +213,22 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 0,
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
||||
@@ -45,21 +45,7 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
@@ -71,10 +57,16 @@
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"longUrl": "https://github.com/shlinkio/shlink",
|
||||
"shortUrl": "https://dom.ain/abc123",
|
||||
"shortCode": "abc123"
|
||||
"shortUrl": "https://doma.in/abc123",
|
||||
"shortCode": "abc123",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 0,
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
},
|
||||
"text/plain": "https://dom.ain/abc123"
|
||||
"text/plain": "https://doma.in/abc123"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
||||
@@ -33,6 +33,24 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "The page to display. Defaults to 1",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itemsPerPage",
|
||||
"in": "query",
|
||||
"description": "The amount of items to return on every page. Defaults to all the items",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -59,6 +77,9 @@
|
||||
"items": {
|
||||
"$ref": "../definitions/Visit.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../definitions/Pagination.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +117,14 @@
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 5,
|
||||
"pagesCount": 12,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 115
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
docs/swagger/paths/{shortCode}.json
Normal file
36
docs/swagger/paths/{shortCode}.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrl",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL",
|
||||
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"302": {
|
||||
"description": "Visit properly tracked and redirected"
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
docs/swagger/paths/{shortCode}_preview.json
Normal file
44
docs/swagger/paths/{shortCode}_preview.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrlPreview",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL preview image",
|
||||
"description": "Returns the preview of the page behind a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
56
docs/swagger/paths/{shortCode}_qr-code.json
Normal file
56
docs/swagger/paths/{shortCode}_qr-code.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortUrlQrCode",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL QR code",
|
||||
"description": "Generates a QR code image pointing to a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"in": "path",
|
||||
"description": "The size of the image to be returned.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 50,
|
||||
"maximum": 1000,
|
||||
"default": 300
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "QR code in PNG format",
|
||||
"content": {
|
||||
"image/png": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
docs/swagger/paths/{shortCode}_track.json
Normal file
44
docs/swagger/paths/{shortCode}_track.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "trackShortUrl",
|
||||
"tags": [
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL tracking pixel",
|
||||
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image in GIF format",
|
||||
"content": {
|
||||
"image/gif": {
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,20 @@
|
||||
"version": "1.0"
|
||||
},
|
||||
|
||||
"externalDocs": {
|
||||
"url": "https://shlink.io/api-docs",
|
||||
"description": "Find more info on how to start using this API here"
|
||||
},
|
||||
|
||||
"servers": [
|
||||
{
|
||||
"url": "{schema}://{server}/rest",
|
||||
"url": "{scheme}://{host}",
|
||||
"variables": {
|
||||
"schema": {
|
||||
"scheme": {
|
||||
"default": "https",
|
||||
"enum": ["https", "http"]
|
||||
},
|
||||
"server": {
|
||||
"host": {
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
@@ -51,35 +56,60 @@
|
||||
"name": "Visits",
|
||||
"description": "Operations to manage visits on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Monitoring",
|
||||
"description": "Public endpoints designed to monitor the service"
|
||||
},
|
||||
{
|
||||
"name": "URL Shortener",
|
||||
"description": "Non-rest endpoints, used to be publicly exposed"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Authentication-related endpoints"
|
||||
"description": "**[Deprecated]** Authentication-related endpoints"
|
||||
}
|
||||
],
|
||||
|
||||
"paths": {
|
||||
"/v1/short-urls": {
|
||||
"/rest/v1/short-urls": {
|
||||
"$ref": "paths/v1_short-urls.json"
|
||||
},
|
||||
"/v1/short-urls/shorten": {
|
||||
"/rest/v1/short-urls/shorten": {
|
||||
"$ref": "paths/v1_short-urls_shorten.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}": {
|
||||
"/rest/v1/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}/tags": {
|
||||
"/rest/v1/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/v1/tags": {
|
||||
"/rest/v1/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/v1/short-urls/{shortCode}/visits": {
|
||||
"/rest/v1/short-urls/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||
},
|
||||
|
||||
"/v1/authenticate": {
|
||||
"/rest/health": {
|
||||
"$ref": "paths/health.json"
|
||||
},
|
||||
|
||||
"/{shortCode}": {
|
||||
"$ref": "paths/{shortCode}.json"
|
||||
},
|
||||
"/{shortCode}/track": {
|
||||
"$ref": "paths/{shortCode}_track.json"
|
||||
},
|
||||
"/{shortCode}/qr-code": {
|
||||
"$ref": "paths/{shortCode}_qr-code.json"
|
||||
},
|
||||
"/{shortCode}/preview": {
|
||||
"$ref": "paths/{shortCode}_preview.json"
|
||||
},
|
||||
|
||||
"/rest/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
// Create an empty .env file
|
||||
if (! file_exists('.env')) {
|
||||
touch('.env');
|
||||
}
|
||||
|
||||
$shlinkDbPath = realpath(sys_get_temp_dir()) . '/shlink-tests.db';
|
||||
if (file_exists($shlinkDbPath)) {
|
||||
unlink($shlinkDbPath);
|
||||
}
|
||||
|
||||
/** @var ServiceManager $sm */
|
||||
$sm = require __DIR__ . '/config/container.php';
|
||||
$sm->setAllowOverride(true);
|
||||
$config = $sm->get('config');
|
||||
$config['entity_manager']['connection'] = [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => $shlinkDbPath,
|
||||
];
|
||||
$sm->setService('config', $config);
|
||||
|
||||
// Create database
|
||||
$process = new Process('vendor/bin/doctrine orm:schema-tool:create --no-interaction -q --test', __DIR__);
|
||||
$process->inheritEnvironmentVariables()
|
||||
->mustRun();
|
||||
|
||||
DatabaseTestCase::$em = $sm->get('em');
|
||||
8
indocker
8
indocker
@@ -1,2 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"
|
||||
|
||||
# Run docker containers if they are not up yet
|
||||
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
docker exec -it shlink_swoole /bin/sh -c "$*"
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
"source": {
|
||||
"directories": [
|
||||
"module/*/src"
|
||||
],
|
||||
"excludes": []
|
||||
]
|
||||
},
|
||||
"timeout": 10,
|
||||
"timeout": 5,
|
||||
"logs": {
|
||||
"text": "build/infection/infection-log.txt",
|
||||
"summary": "build/infection/summary-log.txt",
|
||||
@@ -17,6 +16,7 @@
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"IdenticalEqual": false
|
||||
"IdenticalEqual": false,
|
||||
"NotIdenticalNotEqual": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@ name: ShlinkMigrations
|
||||
migrations_namespace: ShlinkMigrations
|
||||
table_name: migrations
|
||||
migrations_directory: data/migrations
|
||||
custom_template: data/migrations_template.txt
|
||||
|
||||
@@ -3,12 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
@@ -18,6 +15,7 @@ return [
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||
|
||||
@@ -3,13 +3,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
@@ -25,9 +27,10 @@ return [
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -41,45 +44,28 @@ return [
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||
Service\UrlShortener::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
PreviewGenerator::class,
|
||||
'translator',
|
||||
],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [
|
||||
Service\ShortUrl\DeleteShortUrlService::class,
|
||||
'translator',
|
||||
],
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpApiLocationResolver::class,
|
||||
'translator',
|
||||
IpLocationResolverInterface::class,
|
||||
Lock\Factory::class,
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||
Command\Config\GenerateSecretCommand::class => ['translator'],
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, 'translator'],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class, 'translator'],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class, 'translator'],
|
||||
|
||||
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'translation_file_patterns' => [
|
||||
[
|
||||
'type' => 'gettext',
|
||||
'base_dir' => __DIR__ . '/../lang',
|
||||
'pattern' => '%s.mo',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
Binary file not shown.
@@ -1,379 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-09-16 18:36+0200\n"
|
||||
"PO-Revision-Date: 2018-09-16 18: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.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: translate;translatePlural\n"
|
||||
"X-Poedit-SearchPath-0: src\n"
|
||||
"X-Poedit-SearchPath-1: config\n"
|
||||
|
||||
msgid "Disables an API key."
|
||||
msgstr "Desahbilita una clave de API."
|
||||
|
||||
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"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" does not exist."
|
||||
msgstr "La clave de API \"%s\" no existe."
|
||||
|
||||
msgid "Generates a new valid API key."
|
||||
msgstr "Genera una nueva clave de API válida."
|
||||
|
||||
msgid "The date in which the API key should expire. Use any valid PHP format."
|
||||
msgstr ""
|
||||
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||
"válido en PHP."
|
||||
|
||||
#, 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."
|
||||
|
||||
msgid "Tells if only enabled API keys should be returned."
|
||||
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||
|
||||
msgid "Key"
|
||||
msgstr "Clave"
|
||||
|
||||
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\". "
|
||||
"Then it can be set in the SHORTCODE_CHARS environment variable"
|
||||
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"
|
||||
|
||||
#, 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"
|
||||
msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
#, php-format
|
||||
msgid "Secret key: \"%s\""
|
||||
msgstr "Clave secreta: \"%s\""
|
||||
|
||||
msgid "Deletes a short URL"
|
||||
msgstr "Elimina una URL"
|
||||
|
||||
msgid "The short code for the short URL to be deleted"
|
||||
msgstr "El código corto de la URL corta a eliminar"
|
||||
|
||||
msgid ""
|
||||
"Ignores the safety visits threshold check, which could make short URLs with "
|
||||
"many visits to be accidentally deleted"
|
||||
msgstr ""
|
||||
"Ignora el límite de seguridad de visitas, pudiendo resultar en el borrado "
|
||||
"accidental de URLs con muchas visitas"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" could not be found."
|
||||
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"It was not possible to delete the short URL with short code \"%s\" because "
|
||||
"it has more than %s visits."
|
||||
msgstr ""
|
||||
"No se pudo eliminar la URL acortada con código corto \"%s\" porque tiene más "
|
||||
"de %s visitas."
|
||||
|
||||
msgid "Do you want to delete it anyway?"
|
||||
msgstr "¿Aún así quieres eliminarla?"
|
||||
|
||||
msgid "Short URL was not deleted."
|
||||
msgstr "La URL corta no ha sido eliminada."
|
||||
|
||||
#, php-format
|
||||
msgid "Short URL with short code \"%s\" successfully deleted."
|
||||
msgstr "La URL acortada con el código corto \"%s\" eliminada correctamente."
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
"for later web requests."
|
||||
msgstr ""
|
||||
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
|
||||
"para peticiones web posteriores."
|
||||
|
||||
msgid "Finished processing all URLs"
|
||||
msgstr "Finalizado el procesado de todas las URLs"
|
||||
|
||||
#, php-format
|
||||
msgid "Processing URL %s..."
|
||||
msgstr "Procesando URL %s..."
|
||||
|
||||
msgid " <info>Success!</info>"
|
||||
msgstr " <info>¡Correcto!</info>"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short URL for provided long URL and returns it"
|
||||
msgstr "Genera una URL corta para la URL larga proporcionada y la devuelve"
|
||||
|
||||
msgid "The long URL to parse"
|
||||
msgstr "La URL larga a procesar"
|
||||
|
||||
msgid "Tags to apply to the new short URL"
|
||||
msgstr "Etiquetas a aplicar a la nueva URL acortada"
|
||||
|
||||
msgid ""
|
||||
"The date from which this short URL will be valid. If someone tries to access "
|
||||
"it before this date, it will not be found."
|
||||
msgstr ""
|
||||
"La fecha desde la cual será válida esta URL acortada. Si alguien intenta "
|
||||
"acceder a ella antes de esta fecha, no será encontrada."
|
||||
|
||||
msgid ""
|
||||
"The date until which this short URL will be valid. If someone tries to "
|
||||
"access it after this date, it will not be found."
|
||||
msgstr ""
|
||||
"La fecha hasta la cual será válida está URL acortada. Si alguien intenta "
|
||||
"acceder a ella después de esta fecha, no será encontrada."
|
||||
|
||||
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 "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 long URL:"
|
||||
msgstr "URL larga procesada:"
|
||||
|
||||
msgid "Generated short URL:"
|
||||
msgstr "URL corta generada:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Provided slug \"%s\" is already in use by another URL. Try with a different "
|
||||
"one."
|
||||
msgstr ""
|
||||
"El slug proporcionado \"%s\" ya está siendo usado para otra URL. Prueba con "
|
||||
"uno diferente."
|
||||
|
||||
msgid "Returns the detailed visits information for provided short code"
|
||||
msgstr ""
|
||||
"Devuelve la información detallada de visitas para el código corto "
|
||||
"proporcionado"
|
||||
|
||||
msgid "The short code which visits we want to get"
|
||||
msgstr "El código corto del cual queremos obtener las visitas"
|
||||
|
||||
msgid "Allows to filter visits, returning only those older than start date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
|
||||
"startDate"
|
||||
|
||||
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 proporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
|
||||
msgid "Referer"
|
||||
msgstr "Origen"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "User agent"
|
||||
msgstr "Agente de usuario"
|
||||
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
msgid "List all short URLs"
|
||||
msgstr "Listar todas las URLs cortas"
|
||||
|
||||
#, php-format
|
||||
msgid "The first page to list (%s items per page)"
|
||||
msgstr "La primera página a listar (%s elementos por página)"
|
||||
|
||||
msgid ""
|
||||
"A query used to filter results by searching for it on the longUrl and "
|
||||
"shortCode fields"
|
||||
msgstr ""
|
||||
"Una consulta usada para filtrar el resultado buscándola en los campos "
|
||||
"longUrl y shortCode"
|
||||
|
||||
msgid "A comma-separated list of tags to filter results"
|
||||
msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado"
|
||||
|
||||
msgid ""
|
||||
"The field from which we want to order by. Pass ASC or DESC separated by a "
|
||||
"comma"
|
||||
msgstr ""
|
||||
"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma"
|
||||
|
||||
msgid "Whether to display the tags or not"
|
||||
msgstr "Si se desea mostrar las etiquetas o no"
|
||||
|
||||
msgid "Short code"
|
||||
msgstr "Código corto"
|
||||
|
||||
msgid "Short URL"
|
||||
msgstr "URL corta"
|
||||
|
||||
msgid "Long URL"
|
||||
msgstr "URL larga"
|
||||
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
|
||||
msgid "Visits count"
|
||||
msgstr "Número de visitas"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Short URLs properly listed"
|
||||
msgstr "URLs cortas listadas correctamente"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
|
||||
msgid "Returns the long URL behind a short code"
|
||||
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?"
|
||||
msgstr ""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
|
||||
|
||||
msgid "Long URL:"
|
||||
msgstr "URL larga:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
msgid "Creates one or more tags."
|
||||
msgstr "Crea una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to create"
|
||||
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 "Tags properly created"
|
||||
msgstr "Etiquetas correctamente creadas"
|
||||
|
||||
msgid "Deletes one or more tags."
|
||||
msgstr "Elimina una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to delete"
|
||||
msgstr "El nombre de las etiquetas a eliminar"
|
||||
|
||||
msgid "Tags properly deleted"
|
||||
msgstr "Etiquetas correctamente eliminadas"
|
||||
|
||||
msgid "Lists existing tags."
|
||||
msgstr "Lista las etiquetas existentes."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
msgid "No tags yet"
|
||||
msgstr "Aún no hay etiquetas"
|
||||
|
||||
msgid "Renames one existing tag."
|
||||
msgstr "Renombra una etiqueta existente."
|
||||
|
||||
msgid "Current name of the tag."
|
||||
msgstr "Nombre actual de la etiqueta."
|
||||
|
||||
msgid "New name of the tag."
|
||||
msgstr "Nuevo nombre de la etiqueta."
|
||||
|
||||
msgid "Tag properly renamed."
|
||||
msgstr "Etiqueta correctamente renombrada."
|
||||
|
||||
#, php-format
|
||||
msgid "A tag with name \"%s\" was not found"
|
||||
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
msgid "Processing IP"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
msgid "Ignored localhost address"
|
||||
msgstr "Ignorada IP de localhost"
|
||||
|
||||
#, php-format
|
||||
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 "Remote Address"
|
||||
#~ msgstr "Dirección remota"
|
||||
|
||||
#~ msgid "Original URL"
|
||||
#~ msgstr "URL original"
|
||||
|
||||
#~ 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"
|
||||
@@ -3,52 +3,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Disables an API key.'))
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||
->setDescription('Disables an API key.')
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
||||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$io->error(sprintf('API key "%s" does not exist.', $apiKey));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,54 +4,48 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Generates a new valid API key.')
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The date in which the API key should expire. Use any valid PHP format.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||
);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Generated API key: "%s"', $apiKey));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
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;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
|
||||
class ListKeysCommand extends Command
|
||||
@@ -22,62 +24,51 @@ class ListKeysCommand extends Command
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Tells if only enabled API keys should be returned.')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Lists all the available API keys.')
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
'e',
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if only enabled API keys should be returned.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
$list = $this->apiKeyService->listKeys($enabledOnly);
|
||||
$rows = [];
|
||||
|
||||
/** @var ApiKey $row */
|
||||
foreach ($list as $row) {
|
||||
$key = $row->getKey();
|
||||
$expiration = $row->getExpirationDate();
|
||||
$messagePattern = $this->determineMessagePattern($row);
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [sprintf($messagePattern, $key)];
|
||||
$rowData = [sprintf($messagePattern, $apiKey)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||
}
|
||||
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
||||
return $rowData;
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
$rows[] = $rowData;
|
||||
}
|
||||
|
||||
$io->table(array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
ShlinkTable::fromOutput($output)->render(array_filter([
|
||||
'Key',
|
||||
! $enabledOnly ? 'Is enabled' : null,
|
||||
'Expiration date',
|
||||
]), $rows);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function determineMessagePattern(ApiKey $apiKey): string
|
||||
|
||||
@@ -3,44 +3,37 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
/** @deprecated */
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
public const NAME = 'config:generate-charset';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$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'
|
||||
), UrlShortener::DEFAULT_CHARS));
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(sprintf(
|
||||
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
|
||||
UrlShortenerOptions::DEFAULT_CHARS
|
||||
))
|
||||
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||
);
|
||||
$charSet = str_shuffle(UrlShortenerOptions::DEFAULT_CHARS);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,44 +3,36 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/** @deprecated */
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
public const NAME = 'config:generate-secret';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate(
|
||||
'Generates a random secret string that can be used for JWT token encryption'
|
||||
));
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption')
|
||||
->setHelp(
|
||||
'<fg=red;options=bold>This command is deprecated. Better leave shlink generate the secret key.</>'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
|
||||
);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -11,27 +12,21 @@ 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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete';
|
||||
private const ALIASES = ['short-code:delete'];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
*/
|
||||
/** @var DeleteShortUrlServiceInterface */
|
||||
private $deleteShortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService, TranslatorInterface $translator)
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
{
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -39,26 +34,18 @@ class DeleteShortUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Deletes a short URL')
|
||||
)
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code for the short URL to be deleted')
|
||||
)
|
||||
->setDescription('Deletes a short URL')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
|
||||
->addOption(
|
||||
'ignore-threshold',
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate(
|
||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||
. 'accidentally deleted'
|
||||
)
|
||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||
. 'accidentally deleted'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
@@ -66,36 +53,37 @@ class DeleteShortUrlCommand extends Command
|
||||
|
||||
try {
|
||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
$this->retry($io, $shortCode, $e);
|
||||
return $this->retry($io, $shortCode, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
|
||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): int
|
||||
{
|
||||
$warningMsg = \sprintf($this->translator->translate(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.'
|
||||
), $shortCode, $e->getVisitsThreshold());
|
||||
$warningMsg = sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
|
||||
$shortCode,
|
||||
$e->getVisitsThreshold()
|
||||
);
|
||||
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
||||
$forceDelete = $io->confirm($this->translator->translate('Do you want to delete it anyway?'), false);
|
||||
$forceDelete = $io->confirm('Do you want to delete it anyway?', false);
|
||||
|
||||
if ($forceDelete) {
|
||||
$this->runDelete($io, $shortCode, true);
|
||||
} else {
|
||||
$io->warning($this->translator->translate('Short URL was not deleted.'));
|
||||
$io->warning('Short URL was not deleted.');
|
||||
}
|
||||
|
||||
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
|
||||
{
|
||||
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
|
||||
$io->success(\sprintf(
|
||||
$this->translator->translate('Short URL with short code "%s" successfully deleted.'),
|
||||
$shortCode
|
||||
));
|
||||
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
@@ -10,35 +11,24 @@ 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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
*/
|
||||
/** @var PreviewGeneratorInterface */
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
PreviewGeneratorInterface $previewGenerator,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, PreviewGeneratorInterface $previewGenerator)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->previewGenerator = $previewGenerator;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -47,13 +37,11 @@ class GeneratePreviewCommand extends Command
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$page = 1;
|
||||
do {
|
||||
@@ -61,21 +49,22 @@ class GeneratePreviewCommand extends Command
|
||||
$page += 1;
|
||||
|
||||
foreach ($shortUrls as $shortUrl) {
|
||||
$this->processUrl($shortUrl->getOriginalUrl(), $output);
|
||||
$this->processUrl($shortUrl->getLongUrl(), $output);
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
|
||||
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function processUrl($url, OutputInterface $output): void
|
||||
{
|
||||
try {
|
||||
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$output->write(sprintf('Processing URL %s...', $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||
$output->writeln(' <info>Success!</info>');
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
|
||||
$output->writeln(' <error>Error</error>');
|
||||
if ($output->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -15,7 +17,12 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\curry;
|
||||
use function Functional\flatten;
|
||||
use function Functional\unique;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateShortUrlCommand extends Command
|
||||
{
|
||||
@@ -24,28 +31,16 @@ class GenerateShortUrlCommand extends Command
|
||||
public const NAME = 'short-url:generate';
|
||||
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(
|
||||
UrlShortenerInterface $urlShortener,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
$this->domainConfig = $domainConfig;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -53,30 +48,46 @@ class GenerateShortUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short URL for provided long URL and returns it')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->setDescription('Generates a short URL for provided long URL and returns it')
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
'Tags to apply to the new short URL'
|
||||
)
|
||||
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
->addOption(
|
||||
'validSince',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.'
|
||||
))
|
||||
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'validUntil',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.'
|
||||
))
|
||||
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'customSlug',
|
||||
'c',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
))
|
||||
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'maxVisits',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.'
|
||||
));
|
||||
)
|
||||
->addOption(
|
||||
'findIfExists',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'This will force existing matching URL to be returned if found, instead of creating a new one.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
@@ -87,30 +98,23 @@ class GenerateShortUrlCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$longUrl = $io->ask(
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
|
||||
);
|
||||
$longUrl = $io->ask('A long URL was not provided. Which URL do you want to be shortened?');
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error($this->translator->translate('A URL was not provided!'));
|
||||
return;
|
||||
$io->error('A URL was not provided!');
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
$tags = $input->getOption('tags');
|
||||
$processedTags = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$explodedTags = \explode(',', $tag);
|
||||
$processedTags = \array_merge($processedTags, $explodedTags);
|
||||
}
|
||||
$tags = $processedTags;
|
||||
$explodeWithComma = curry('explode')(',');
|
||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
|
||||
@@ -118,29 +122,29 @@ class GenerateShortUrlCommand extends Command
|
||||
$shortCode = $this->urlShortener->urlToShortCode(
|
||||
new Uri($longUrl),
|
||||
$tags,
|
||||
$this->getOptionalDate($input, 'validSince'),
|
||||
$this->getOptionalDate($input, 'validUntil'),
|
||||
$customSlug,
|
||||
$maxVisits !== null ? (int) $maxVisits : null
|
||||
ShortUrlMeta::createFromParams(
|
||||
$this->getOptionalDate($input, 'validSince'),
|
||||
$this->getOptionalDate($input, 'validUntil'),
|
||||
$customSlug,
|
||||
$maxVisits !== null ? (int) $maxVisits : null,
|
||||
$input->getOption('findIfExists')
|
||||
)
|
||||
)->getShortCode();
|
||||
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
|
||||
|
||||
$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),
|
||||
sprintf('Processed long URL: <info>%s</info>', $longUrl),
|
||||
sprintf('Generated short URL: <info>%s</info>', $shortUrl),
|
||||
]);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidUrlException $e) {
|
||||
$io->error(\sprintf(
|
||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
||||
$longUrl
|
||||
));
|
||||
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$io->error(\sprintf(
|
||||
$this->translator->translate(
|
||||
'Provided slug "%s" is already in use by another URL. Try with a different one.'
|
||||
),
|
||||
$customSlug
|
||||
));
|
||||
$io->error(
|
||||
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
|
||||
);
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -12,26 +16,22 @@ 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;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
|
||||
use function array_map;
|
||||
use function Functional\select_keys;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
/** @var VisitsTrackerInterface */
|
||||
private $visitsTracker;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -40,25 +40,19 @@ class GetVisitsCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code which visits we want to get')
|
||||
)
|
||||
->setDescription('Returns the detailed visits information for provided short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
|
||||
->addOption(
|
||||
'startDate',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those older than start date')
|
||||
'Allows to filter visits, returning only those older than start date'
|
||||
)
|
||||
->addOption(
|
||||
'endDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
|
||||
'Allows to filter visits, returning only those newer than end date'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,39 +64,28 @@ class GetVisitsCommand extends Command
|
||||
}
|
||||
|
||||
$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?')
|
||||
);
|
||||
$shortCode = $io->ask('A short code was not provided. Which short code do you want to use?');
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$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));
|
||||
$rows = [];
|
||||
foreach ($visits as $row) {
|
||||
$rowData = $row->jsonSerialize();
|
||||
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
|
||||
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
|
||||
|
||||
// Unset location info and remote addr
|
||||
unset($rowData['visitLocation'], $rowData['remoteAddr']);
|
||||
|
||||
$rowData['country'] = $row->getVisitLocation()->getCountryName();
|
||||
|
||||
$rows[] = \array_values($rowData);
|
||||
}
|
||||
$io->table([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('User agent'),
|
||||
$this->translator->translate('Country'),
|
||||
], $rows);
|
||||
$rows = array_map(function (Visit $visit) {
|
||||
$rowData = $visit->jsonSerialize();
|
||||
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||
}, $visits);
|
||||
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
|
||||
@@ -3,8 +3,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -12,7 +15,13 @@ 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;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function sprintf;
|
||||
|
||||
class ListShortUrlsCommand extends Command
|
||||
{
|
||||
@@ -21,27 +30,15 @@ class ListShortUrlsCommand extends Command
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
@@ -50,96 +47,93 @@ class ListShortUrlsCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->setDescription('List all short URLs')
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
$this->translator->translate('The first page to list (%s items per page)'),
|
||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||
),
|
||||
1
|
||||
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
|
||||
'1'
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('A comma-separated list of tags to filter results')
|
||||
'A comma-separated list of tags to filter results'
|
||||
)
|
||||
->addOption(
|
||||
'orderBy',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
->addOption(
|
||||
'showTags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$page = (int) $input->getOption('page');
|
||||
$searchTerm = $input->getOption('searchTerm');
|
||||
$tags = $input->getOption('tags');
|
||||
$tags = ! empty($tags) ? \explode(',', $tags) : [];
|
||||
$showTags = $input->getOption('showTags');
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
$showTags = (bool) $input->getOption('showTags');
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
|
||||
$page++;
|
||||
|
||||
$headers = [
|
||||
$this->translator->translate('Short code'),
|
||||
$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');
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = \implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = \array_values($shortUrl);
|
||||
}
|
||||
$io->table($headers, $rows);
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$io->success($this->translator->translate('Short URLs properly listed'));
|
||||
} else {
|
||||
$continue = $io->confirm(
|
||||
\sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
||||
false
|
||||
);
|
||||
}
|
||||
$continue = $this->isLastPage($result)
|
||||
? false
|
||||
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
|
||||
} while ($continue);
|
||||
|
||||
$io->newLine();
|
||||
$io->success('Short URLs properly listed');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function renderPage(
|
||||
InputInterface $input,
|
||||
OutputInterface $output,
|
||||
int $page,
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
bool $showTags,
|
||||
DataTransformerInterface $transformer
|
||||
): Paginator {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
$headers[] = 'Tags';
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $transformer->transform($row);
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
unset($shortUrl['originalUrl']);
|
||||
$rows[] = array_values($shortUrl);
|
||||
}
|
||||
|
||||
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
|
||||
$result,
|
||||
'Page %s of %s'
|
||||
));
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function processOrderBy(InputInterface $input)
|
||||
@@ -149,7 +143,7 @@ class ListShortUrlsCommand extends Command
|
||||
return null;
|
||||
}
|
||||
|
||||
$orderBy = \explode(',', $orderBy);
|
||||
return \count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
||||
$orderBy = explode(',', $orderBy);
|
||||
return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
@@ -11,27 +12,21 @@ 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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:parse';
|
||||
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
||||
public function __construct(UrlShortenerInterface $urlShortener)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -39,12 +34,8 @@ class ResolveUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
->setDescription('Returns the long URL behind a short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
@@ -55,32 +46,27 @@ class ResolveUrlCommand extends Command
|
||||
}
|
||||
|
||||
$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?')
|
||||
);
|
||||
$shortCode = $io->ask('A short code was not provided. Which short code do you want to parse?');
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
|
||||
try {
|
||||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
$output->writeln(
|
||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
|
||||
);
|
||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,58 +3,52 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Creates one or more tags.'))
|
||||
->setDescription('Creates one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to create')
|
||||
'The name of the tags to create'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
return;
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly created'));
|
||||
$io->success('Tags properly created');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,58 +3,52 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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
|
||||
{
|
||||
public const NAME = 'tag:delete';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Deletes one or more tags.'))
|
||||
->setDescription('Deletes one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to delete')
|
||||
'The name of the tags to delete'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
return;
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly deleted'));
|
||||
$io->success('Tags properly deleted');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,57 +3,51 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Console\ShlinkTable;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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;
|
||||
use function array_map;
|
||||
|
||||
use function Functional\map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
->setDescription('Lists existing tags.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||
ShlinkTable::fromOutput($output)->render(['Name'], $this->getTagsRows());
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
private function getTagsRows()
|
||||
private function getTagsRows(): array
|
||||
{
|
||||
$tags = $this->tagService->listTags();
|
||||
if (empty($tags)) {
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
return [['No tags yet']];
|
||||
}
|
||||
|
||||
return array_map(function (Tag $tag) {
|
||||
return [$tag->getName()];
|
||||
}, $tags);
|
||||
return map($tags, function (Tag $tag) {
|
||||
return [(string) $tag];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -10,39 +11,32 @@ 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;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:rename';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->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.'));
|
||||
->setDescription('Renames one existing tag.')
|
||||
->addArgument('oldName', InputArgument::REQUIRED, 'Current name of the tag.')
|
||||
->addArgument('newName', InputArgument::REQUIRED, 'New name of the tag.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
@@ -50,9 +44,11 @@ class RenameTagCommand extends Command
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||
$io->success('Tag properly renamed.');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
$io->error(sprintf('A tag with name "%s" was not found', $oldName));
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,112 +3,110 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
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;
|
||||
use function sleep;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:process';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
*/
|
||||
/** @var VisitServiceInterface */
|
||||
private $visitService;
|
||||
/**
|
||||
* @var IpLocationResolverInterface
|
||||
*/
|
||||
/** @var IpLocationResolverInterface */
|
||||
private $ipLocationResolver;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
/** @var OutputInterface */
|
||||
private $output;
|
||||
|
||||
public function __construct(
|
||||
VisitServiceInterface $visitService,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
TranslatorInterface $translator
|
||||
Locker $locker
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Processes visits where location is not set yet')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Processes visits where location is not set yet');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$this->output = $output;
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
$count = 0;
|
||||
foreach ($visits as $visit) {
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$io->writeln(
|
||||
sprintf('<comment>%s</comment>', $this->translator->translate('Ignored visit with no IP address')),
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$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);
|
||||
|
||||
$io->writeln(sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
$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);
|
||||
}
|
||||
$lock = $this->locker->createLock(self::NAME);
|
||||
if (! $lock->acquire()) {
|
||||
$io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
|
||||
return ExitCodes::EXIT_WARNING;
|
||||
}
|
||||
|
||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||
try {
|
||||
$this->visitService->locateUnlocatedVisits(
|
||||
[$this, 'getGeolocationDataForVisit'],
|
||||
function (VisitLocation $location) use ($output) {
|
||||
if (! $location->isEmpty()) {
|
||||
$output->writeln(
|
||||
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$io->success('Finished processing all IPs');
|
||||
} finally {
|
||||
$lock->release();
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
public function getGeolocationDataForVisit(Visit $visit): Location
|
||||
{
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$this->output->writeln(
|
||||
'<comment>Ignored visit with no IP address</comment>',
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
throw IpCannotBeLocatedException::forEmptyAddress();
|
||||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
|
||||
throw IpCannotBeLocatedException::forLocalhost();
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
} catch (WrongIpException $e) {
|
||||
$this->output->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $this->output);
|
||||
}
|
||||
|
||||
throw IpCannotBeLocatedException::forError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
67
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
67
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UpdateDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:update-db';
|
||||
|
||||
/** @var DbUpdaterInterface */
|
||||
private $geoLiteDbUpdater;
|
||||
|
||||
public function __construct(DbUpdaterInterface $geoLiteDbUpdater)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Updates the GeoLite2 database file used to geolocate IP addresses')
|
||||
->setHelp(
|
||||
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
||||
. 'every first Wednesday'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$progressBar = new ProgressBar($output);
|
||||
$progressBar->start();
|
||||
|
||||
try {
|
||||
$this->geoLiteDbUpdater->downloadFreshCopy(function (int $total, int $downloaded) use ($progressBar) {
|
||||
$progressBar->setMaxSteps($total);
|
||||
$progressBar->setProgress($downloaded);
|
||||
});
|
||||
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->success('GeoLite2 database properly updated');
|
||||
return ExitCodes::EXIT_SUCCESS;
|
||||
} catch (RuntimeException $e) {
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->error('An error occurred while updating GeoLite2 database');
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ 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;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
@@ -34,8 +33,6 @@ class ApplicationFactory implements FactoryInterface
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$translator = $container->get(Translator::class);
|
||||
$translator->setLocale($config['locale']);
|
||||
|
||||
$commands = $config['commands'] ?? [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
|
||||
11
module/CLI/src/Util/ExitCodes.php
Normal file
11
module/CLI/src/Util/ExitCodes.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Util;
|
||||
|
||||
final class ExitCodes
|
||||
{
|
||||
public const EXIT_SUCCESS = 0;
|
||||
public const EXIT_FAILURE = -1;
|
||||
public const EXIT_WARNING = 1;
|
||||
}
|
||||
@@ -10,55 +10,51 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function providedApiKeyIsDisabled()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorIsReturnedIfServiceThrowsException()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains('API key "abcd1234" does not exist.', $output);
|
||||
|
||||
$this->assertStringContainsString('API key "abcd1234" properly disabled', $output);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function errorIsReturnedIfServiceThrowsException()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$disable = $this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('API key "abcd1234" does not exist.', $output);
|
||||
$disable->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,46 +12,41 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$create = $this->apiKeyService->create(null)->willReturn(new ApiKey());
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Generated API key: ', $output);
|
||||
$create->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function expirationDateIsDefinedIfProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledTimes(1)
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
|
||||
@@ -10,55 +10,59 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsNotProvidedEverythingIsListed()
|
||||
/** @test */
|
||||
public function everythingIsListedIfEnabledOnlyIsNotProvided(): void
|
||||
{
|
||||
$this->apiKeyService->listKeys(false)->willReturn([
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
]);
|
||||
])->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Key', $output);
|
||||
$this->assertStringContainsString('Is enabled', $output);
|
||||
$this->assertStringContainsString(' +++ ', $output);
|
||||
$this->assertStringNotContainsString(' --- ', $output);
|
||||
$this->assertStringContainsString('Expiration date', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsProvidedOnlyThoseKeysAreListed()
|
||||
/** @test */
|
||||
public function onlyEnabledKeysAreListedIfEnabledOnlyIsProvided(): void
|
||||
{
|
||||
$this->apiKeyService->listKeys(true)->willReturn([
|
||||
(new ApiKey())->disable(),
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
])->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
'--enabledOnly' => true,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Key', $output);
|
||||
$this->assertStringNotContainsString('Is enabled', $output);
|
||||
$this->assertStringNotContainsString(' +++ ', $output);
|
||||
$this->assertStringNotContainsString(' --- ', $output);
|
||||
$this->assertStringContainsString('Expiration date', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,27 +7,26 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
use function implode;
|
||||
use function sort;
|
||||
use function str_split;
|
||||
|
||||
class GenerateCharsetCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$command = new GenerateCharsetCommand(Translator::factory([]));
|
||||
$command = new GenerateCharsetCommand();
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function charactersAreGeneratedFromDefault()
|
||||
{
|
||||
$prefix = 'Character set: ';
|
||||
@@ -38,7 +37,7 @@ class GenerateCharsetCommandTest extends TestCase
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Both default character set and the new one should have the same length
|
||||
$this->assertContains($prefix, $output);
|
||||
$this->assertStringContainsString($prefix, $output);
|
||||
}
|
||||
|
||||
protected function orderStringLetters($string)
|
||||
|
||||
@@ -11,34 +11,32 @@ use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DeleteShortCodeCommandTest extends TestCase
|
||||
use const PHP_EOL;
|
||||
|
||||
use function array_pop;
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $service;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function successMessageIsPrintedIfUrlIsProperlyDeleted()
|
||||
/** @test */
|
||||
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->will(function () {
|
||||
@@ -47,14 +45,15 @@ class DeleteShortCodeCommandTest extends TestCase
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$this->assertStringContainsString(
|
||||
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
|
||||
$output
|
||||
);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function invalidShortCodePrintsMessage()
|
||||
/** @test */
|
||||
public function invalidShortCodePrintsMessage(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
@@ -64,42 +63,51 @@ class DeleteShortCodeCommandTest extends TestCase
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$this->assertStringContainsString(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRetryDeleteAnswers
|
||||
*/
|
||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted()
|
||||
{
|
||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
||||
array $retryAnswer,
|
||||
int $expectedDeleteCalls,
|
||||
string $expectedMessage
|
||||
): void {
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
|
||||
function (array $args) {
|
||||
$ignoreThreshold = \array_pop($args);
|
||||
$ignoreThreshold = array_pop($args);
|
||||
|
||||
if (!$ignoreThreshold) {
|
||||
throw new Exception\DeleteShortUrlException(10);
|
||||
}
|
||||
}
|
||||
);
|
||||
$this->commandTester->setInputs(['yes']);
|
||||
$this->commandTester->setInputs($retryAnswer);
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\sprintf(
|
||||
$this->assertStringContainsString(sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||
$shortCode
|
||||
), $output);
|
||||
$this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(2);
|
||||
$this->assertStringContainsString($expectedMessage, $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined()
|
||||
public function provideRetryDeleteAnswers(): iterable
|
||||
{
|
||||
yield 'answering yes to retry' => [['yes'], 2, 'Short URL with short code "abc123" successfully deleted.'];
|
||||
yield 'answering no to retry' => [['no'], 1, 'Short URL was not deleted.'];
|
||||
yield 'answering default to retry' => [[PHP_EOL], 1, 'Short URL was not deleted.'];
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
@@ -110,11 +118,11 @@ class DeleteShortCodeCommandTest extends TestCase
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\sprintf(
|
||||
$this->assertStringContainsString(sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||
$shortCode
|
||||
), $output);
|
||||
$this->assertContains('Short URL was not deleted.', $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
||||
$this->assertStringContainsString('Short URL was not deleted.', $output);
|
||||
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
@@ -13,74 +13,71 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
use function count;
|
||||
use function substr_count;
|
||||
|
||||
class GeneratePreviewCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||
|
||||
$command = new GeneratePreviewCommand(
|
||||
$this->shortUrlService->reveal(),
|
||||
$this->previewGenerator->reveal(),
|
||||
Translator::factory([])
|
||||
);
|
||||
$command = new GeneratePreviewCommand($this->shortUrlService->reveal(), $this->previewGenerator->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function previewsForEveryUrlAreGenerated()
|
||||
{
|
||||
$paginator = $this->createPaginator([
|
||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||
new ShortUrl('http://foo.com'),
|
||||
new ShortUrl('https://bar.com'),
|
||||
new ShortUrl('http://baz.com/something'),
|
||||
]);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||
|
||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
|
||||
$generatePreview1 = $this->previewGenerator->generatePreview('http://foo.com')->willReturn('');
|
||||
$generatePreview2 = $this->previewGenerator->generatePreview('https://bar.com')->willReturn('');
|
||||
$generatePreview3 = $this->previewGenerator->generatePreview('http://baz.com/something')->willReturn('');
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:process-previews',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Processing URL http://foo.com', $output);
|
||||
$this->assertStringContainsString('Processing URL https://bar.com', $output);
|
||||
$this->assertStringContainsString('Processing URL http://baz.com/something', $output);
|
||||
$this->assertStringContainsString('Finished processing all URLs', $output);
|
||||
$generatePreview1->shouldHaveBeenCalledOnce();
|
||||
$generatePreview2->shouldHaveBeenCalledOnce();
|
||||
$generatePreview3->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function exceptionWillOutputError()
|
||||
{
|
||||
$items = [
|
||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||
new ShortUrl('http://foo.com'),
|
||||
new ShortUrl('https://bar.com'),
|
||||
new ShortUrl('http://baz.com/something'),
|
||||
];
|
||||
$paginator = $this->createPaginator($items);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
||||
->shouldBeCalledTimes(count($items));
|
||||
|
||||
|
||||
@@ -3,32 +3,29 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateShortcodeCommandTest extends TestCase
|
||||
class GenerateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $urlShortener;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $urlShortener;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'foo.com',
|
||||
]);
|
||||
@@ -37,42 +34,61 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())
|
||||
->willReturn(
|
||||
(new ShortUrl())->setShortCode('abc123')
|
||||
->setLongUrl('')
|
||||
)
|
||||
->shouldBeCalledTimes(1);
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn(
|
||||
(new ShortUrl(''))->setShortCode('abc123')
|
||||
);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/foo/bar',
|
||||
'--maxVisits' => '3',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
|
||||
|
||||
$this->assertStringContainsString('http://foo.com/abc123', $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function exceptionWhileParsingLongUrlOutputsError()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/invalid',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains(
|
||||
$this->assertStringContainsString(
|
||||
'Provided URL "http://domain.com/invalid" is invalid.',
|
||||
$output
|
||||
);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function properlyProcessesProvidedTags()
|
||||
{
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
||||
Argument::type(UriInterface::class),
|
||||
Argument::that(function (array $tags) {
|
||||
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
|
||||
return $tags;
|
||||
}),
|
||||
Argument::cetera()
|
||||
)->willReturn((new ShortUrl(''))->setShortCode('abc123'));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/foo/bar',
|
||||
'--tags' => ['foo,bar', 'baz', 'boo,zar,baz'],
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('http://foo.com/abc123', $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
@@ -8,42 +8,42 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\Model\Location;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $visitsTracker;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $visitsTracker;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
|
||||
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
|
||||
$command = new GetVisitsCommand($this->visitsTracker->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function noDateFlagsTriesToListWithoutDateRange()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
|
||||
new Paginator(new ArrayAdapter([]))
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
@@ -51,17 +51,18 @@ class GetVisitsCommandTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function providingDateFlagsTheListGetsFiltered()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$startDate = '2016-01-01';
|
||||
$endDate = '2016-02-01';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->visitsTracker->info(
|
||||
$shortCode,
|
||||
new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
)
|
||||
->willReturn(new Paginator(new ArrayAdapter([])))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
@@ -71,25 +72,25 @@ class GetVisitsCommandTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function outputIsProperlyGenerated()
|
||||
/** @test */
|
||||
public function outputIsProperlyGenerated(): void
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
|
||||
(new Visit())->setReferer('foo')
|
||||
->setVisitLocation((new VisitLocation())->setCountryName('Spain'))
|
||||
->setUserAgent('bar'),
|
||||
])->shouldBeCalledTimes(1);
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn(
|
||||
new Paginator(new ArrayAdapter([
|
||||
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
|
||||
new VisitLocation(new Location('', 'Spain', '', '', 0, 0, ''))
|
||||
),
|
||||
]))
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertGreaterThan(0, \strpos($output, 'foo'));
|
||||
$this->assertGreaterThan(0, \strpos($output, 'Spain'));
|
||||
$this->assertGreaterThan(0, \strpos($output, 'bar'));
|
||||
$this->assertStringContainsString('foo', $output);
|
||||
$this->assertStringContainsString('Spain', $output);
|
||||
$this->assertStringContainsString('bar', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,51 +11,42 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ListShortcodesCommandTest extends TestCase
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $shortUrlService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), []);
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function noInputCallsListJustOnce()
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function loadingMorePagesCallsListMoreTimes()
|
||||
{
|
||||
// The paginator will return more than one page for the first 3 times
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
||||
$data[] = new ShortUrl('url_' . $i);
|
||||
}
|
||||
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
|
||||
@@ -64,34 +55,44 @@ class ListShortcodesCommandTest extends TestCase
|
||||
|
||||
$this->commandTester->setInputs(['y', 'y', 'n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
||||
$this->assertStringContainsString('Continue with page 3?', $output);
|
||||
$this->assertStringContainsString('Continue with page 4?', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function havingMorePagesButAnsweringNoCallsListJustOnce()
|
||||
{
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
||||
$data[] = new ShortUrl('url_' . $i);
|
||||
}
|
||||
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledTimes(1);
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertStringContainsString('url_1', $output);
|
||||
$this->assertStringContainsString('url_9', $output);
|
||||
$this->assertStringNotContainsString('url_10', $output);
|
||||
$this->assertStringNotContainsString('url_20', $output);
|
||||
$this->assertStringNotContainsString('url_30', $output);
|
||||
$this->assertStringContainsString('Continue with page 2?', $output);
|
||||
$this->assertStringNotContainsString('Continue with page 3?', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function passingPageWillMakeListStartOnThatPage()
|
||||
{
|
||||
$page = 5;
|
||||
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
@@ -100,13 +101,11 @@ class ListShortcodesCommandTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
/** @test */
|
||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||
{
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute([
|
||||
@@ -114,6 +113,6 @@ class ListShortcodesCommandTest extends TestCase
|
||||
'--showTags' => true,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains('Tags', $output);
|
||||
$this->assertStringContainsString('Tags', $output);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user