mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
499 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15d49e97c0 | ||
|
|
d5e7ce38ac | ||
|
|
162d0560db | ||
|
|
1de05047ca | ||
|
|
2af5de1199 | ||
|
|
e66a724d2b | ||
|
|
9f4c2ac8d7 | ||
|
|
44f0011445 | ||
|
|
545094cddf | ||
|
|
99f45d8853 | ||
|
|
c25b5f9938 | ||
|
|
db1304c11a | ||
|
|
57714b373c | ||
|
|
5be7f839f3 | ||
|
|
aa441eb58b | ||
|
|
e6b6a40fa6 | ||
|
|
f6dde6f4c1 | ||
|
|
36ab475578 | ||
|
|
a74fe62da6 | ||
|
|
1e4de7fec4 | ||
|
|
47117d1fb7 | ||
|
|
cb8ef408a4 | ||
|
|
e5f21a88fa | ||
|
|
0458c4f798 | ||
|
|
75f6160432 | ||
|
|
5337eb48e7 | ||
|
|
86c30ee731 | ||
|
|
d68dc38959 | ||
|
|
0525639329 | ||
|
|
0d9c7282df | ||
|
|
3b95925217 | ||
|
|
fa595f7aa3 | ||
|
|
ff80f32f72 | ||
|
|
e55dbef2fc | ||
|
|
ebf2e459e8 | ||
|
|
1b5081ae21 | ||
|
|
d5736756f7 | ||
|
|
757cf2e193 | ||
|
|
3a75ac0486 | ||
|
|
3c3ef6fa05 | ||
|
|
3282bfd03b | ||
|
|
0813df6b29 | ||
|
|
df74a04085 | ||
|
|
8323b87076 | ||
|
|
48f01921e1 | ||
|
|
ae9d99257e | ||
|
|
0183c8a4b7 | ||
|
|
9a2ca35e6e | ||
|
|
2edb48e314 | ||
|
|
a81fd497d4 | ||
|
|
49cca5cd69 | ||
|
|
f92cff6241 | ||
|
|
1b4343ffc2 | ||
|
|
d5392a5f59 | ||
|
|
a65ce649ac | ||
|
|
d5dc6cea99 | ||
|
|
5ecfe9f0f0 | ||
|
|
0f5fb066d1 | ||
|
|
8e61639598 | ||
|
|
e88468d867 | ||
|
|
bc46e2f509 | ||
|
|
2241279bb6 | ||
|
|
25ffbed756 | ||
|
|
8784843a7a | ||
|
|
a964e2b3c9 | ||
|
|
7f7efd45ab | ||
|
|
af8f5afef8 | ||
|
|
dcfaed437c | ||
|
|
47e2322e33 | ||
|
|
00e7d57245 | ||
|
|
d53a3222d0 | ||
|
|
80fe3a73e2 | ||
|
|
7ab993b764 | ||
|
|
622edd2ed1 | ||
|
|
1f5faee356 | ||
|
|
076b0cf867 | ||
|
|
d4168bebc6 | ||
|
|
13c3629cd6 | ||
|
|
1eff9801e8 | ||
|
|
92858aebd6 | ||
|
|
1910724a98 | ||
|
|
ff8441fa95 | ||
|
|
9d8fb055b1 | ||
|
|
9651b3d692 | ||
|
|
9d10c8627a | ||
|
|
929d7670cb | ||
|
|
5714a8f884 | ||
|
|
159529937d | ||
|
|
394d9ff4d2 | ||
|
|
07165f344f | ||
|
|
4f2146dd9c | ||
|
|
5b9784cd9e | ||
|
|
9d9b61cf14 | ||
|
|
9d7db96e4b | ||
|
|
3d0bca2781 | ||
|
|
ffb54c4f7a | ||
|
|
a01031303f | ||
|
|
7808f6d182 | ||
|
|
a0c3b9412f | ||
|
|
c32e2053c3 | ||
|
|
a33151248d | ||
|
|
038ba3b006 | ||
|
|
f3c92f4110 | ||
|
|
17779dbbc6 | ||
|
|
c2dd5b8c47 | ||
|
|
fcb9121e5a | ||
|
|
0af1004860 | ||
|
|
917f668cf3 | ||
|
|
436499b7c4 | ||
|
|
66af9866f0 | ||
|
|
3703dedad9 | ||
|
|
37502151ef | ||
|
|
3816a10de3 | ||
|
|
bdda6067ab | ||
|
|
0f62af241f | ||
|
|
987919e87a | ||
|
|
0c03a4b7ff | ||
|
|
5d6d13c95f | ||
|
|
563021bdc1 | ||
|
|
2d6d35a398 | ||
|
|
30297ac5ac | ||
|
|
416c56dee2 | ||
|
|
6b968a6843 | ||
|
|
080965e166 | ||
|
|
c7239aaca2 | ||
|
|
110e8cb78d | ||
|
|
ed859767a8 | ||
|
|
d54b823c88 | ||
|
|
0ae44d3331 | ||
|
|
8063e643a3 | ||
|
|
3883ed15c4 | ||
|
|
a79c1f580e | ||
|
|
f4b569c245 | ||
|
|
899771cc2e | ||
|
|
863803b614 | ||
|
|
5be5e0bc60 | ||
|
|
0b8e305533 | ||
|
|
39d79366a3 | ||
|
|
d5b78f2a7e | ||
|
|
b2a63f734a | ||
|
|
82f41de87b | ||
|
|
af4c66d40a | ||
|
|
975260f126 | ||
|
|
bd678b41f7 | ||
|
|
66898b6ddc | ||
|
|
5eee683978 | ||
|
|
e92446df9b | ||
|
|
63a69b05a1 | ||
|
|
c79ca1d13c | ||
|
|
87c4851d7e | ||
|
|
a125c93ca3 | ||
|
|
a8465094c1 | ||
|
|
16f7359ac6 | ||
|
|
f9f4817ee2 | ||
|
|
c7e49f223f | ||
|
|
6e79b4ba7b | ||
|
|
f78a7f12a9 | ||
|
|
b3664597b0 | ||
|
|
8cfb4f61ca | ||
|
|
b0dbb2dae4 | ||
|
|
7c6da4985d | ||
|
|
386b0dfb7b | ||
|
|
1437ff48ce | ||
|
|
63294f20ee | ||
|
|
d8acc3c247 | ||
|
|
52d8ffa212 | ||
|
|
98ad2816e8 | ||
|
|
9d890f4227 | ||
|
|
0932d04907 | ||
|
|
1f78b5c524 | ||
|
|
59f10619ba | ||
|
|
334710e92c | ||
|
|
75b8175824 | ||
|
|
8a74ef2a33 | ||
|
|
d05ac5ce9d | ||
|
|
3100fffa2b | ||
|
|
6bbacb1017 | ||
|
|
4403dc5df9 | ||
|
|
fdc637c23d | ||
|
|
b99d662417 | ||
|
|
eb9a964c66 | ||
|
|
e5ef8d7f8c | ||
|
|
28650aee2b | ||
|
|
a2294704e6 | ||
|
|
e5e1aa2ff4 | ||
|
|
2f5290b9d3 | ||
|
|
ef3c4aadf2 | ||
|
|
c9ce56eea5 | ||
|
|
4fee656f96 | ||
|
|
d2a04259f5 | ||
|
|
e504daa1ba | ||
|
|
8793a67ce9 | ||
|
|
b4ded374e9 | ||
|
|
91d350b12f | ||
|
|
b3e25f28fd | ||
|
|
aca89f9abe | ||
|
|
243075dd78 | ||
|
|
7130425896 | ||
|
|
fe9ab20cbb | ||
|
|
6935b2ebe2 | ||
|
|
3dcc510da1 | ||
|
|
2f26c82fa6 | ||
|
|
9ddb60a882 | ||
|
|
210b08b61f | ||
|
|
42fe4bd5ce | ||
|
|
1b2a0820e5 | ||
|
|
6cf0155417 | ||
|
|
9b8be3e5b8 | ||
|
|
a27b01b895 | ||
|
|
16dd1838aa | ||
|
|
f788d6872f | ||
|
|
d0df007812 | ||
|
|
f60c217fae | ||
|
|
d3fc7d543a | ||
|
|
4d0fc1da07 | ||
|
|
ee2233c6dd | ||
|
|
ea6e0d7c7f | ||
|
|
d9d599eab4 | ||
|
|
d1ba44e1b3 | ||
|
|
dff2ad3740 | ||
|
|
f7e63710e4 | ||
|
|
d3b5cd5c57 | ||
|
|
86ed83d25e | ||
|
|
f96d0fe30a | ||
|
|
be406bd676 | ||
|
|
044278752b | ||
|
|
343d2ab44a | ||
|
|
66992f644e | ||
|
|
cf245524dd | ||
|
|
ad520811a3 | ||
|
|
ee1e1d5688 | ||
|
|
8ef0e7c25b | ||
|
|
c3d555ef3c | ||
|
|
bf8e14708b | ||
|
|
6ea59b1e4d | ||
|
|
cf8b778711 | ||
|
|
1e79969c3b | ||
|
|
5fd34e03fc | ||
|
|
ce9d6642d4 | ||
|
|
ecebdbbfa8 | ||
|
|
6f7ce709ca | ||
|
|
84094a51a2 | ||
|
|
7ba9eb8e2c | ||
|
|
e8a0c5484c | ||
|
|
0521227127 | ||
|
|
fac9455a1e | ||
|
|
3243ade4fd | ||
|
|
da21eb4a5c | ||
|
|
5ec6d538db | ||
|
|
08228d9d98 | ||
|
|
7856d64299 | ||
|
|
057bbae729 | ||
|
|
09b161304c | ||
|
|
a60c45ca4d | ||
|
|
89ed84ce28 | ||
|
|
a6c547c4da | ||
|
|
3e2c5abaa4 | ||
|
|
c202b3e518 | ||
|
|
e15b67b5dc | ||
|
|
7ddc180487 | ||
|
|
f3fbfc3692 | ||
|
|
b289e3bac2 | ||
|
|
4d4aafa6db | ||
|
|
2705070063 | ||
|
|
5e3770c105 | ||
|
|
0f0213aa87 | ||
|
|
0e2ad0dbca | ||
|
|
d275316acd | ||
|
|
0a681f0efa | ||
|
|
b17f96043a | ||
|
|
6f9b727673 | ||
|
|
79427d08d7 | ||
|
|
2ec807ba70 | ||
|
|
ede4525332 | ||
|
|
4dffc9f0c1 | ||
|
|
5de845c258 | ||
|
|
745ff51150 | ||
|
|
88b9f9fc56 | ||
|
|
fdbe076bf2 | ||
|
|
0760550767 | ||
|
|
1b94083188 | ||
|
|
1993d01110 | ||
|
|
37fb7e76d9 | ||
|
|
cc3362837b | ||
|
|
2012cc453c | ||
|
|
ea80b6d48a | ||
|
|
db956a1f40 | ||
|
|
4f3995ea80 | ||
|
|
e024ba5d94 | ||
|
|
af0ff0f65b | ||
|
|
a9094dc0f6 | ||
|
|
aca90ef907 | ||
|
|
ddfccea901 | ||
|
|
6c6cfb4fc3 | ||
|
|
482b792adf | ||
|
|
d3a86f4fae | ||
|
|
6de1d1057b | ||
|
|
d7e68d29de | ||
|
|
b8f24c3584 | ||
|
|
26c455616b | ||
|
|
909ecc2387 | ||
|
|
a9dff56a92 | ||
|
|
ef0dd416f9 | ||
|
|
781ca39938 | ||
|
|
ccb9d5e83a | ||
|
|
91f08c9ead | ||
|
|
433a5a923d | ||
|
|
501a933d2e | ||
|
|
633f3b728f | ||
|
|
be34600494 | ||
|
|
9577a4da4b | ||
|
|
a24688b92a | ||
|
|
91442a3379 | ||
|
|
0bd9f1e19f | ||
|
|
c233e807c2 | ||
|
|
a002c60183 | ||
|
|
c522879c64 | ||
|
|
d04abd1f75 | ||
|
|
c2feffa50c | ||
|
|
e282521040 | ||
|
|
d7b7db670f | ||
|
|
5a500a00d7 | ||
|
|
9fb07f4039 | ||
|
|
cb23d38b38 | ||
|
|
af7c11665c | ||
|
|
7f4678261e | ||
|
|
a1c8c51f70 | ||
|
|
6bbe66e8f1 | ||
|
|
5f0d281255 | ||
|
|
fd468cd4e9 | ||
|
|
1f7a94794d | ||
|
|
0232f68b91 | ||
|
|
070055a8b9 | ||
|
|
a3bbd06fe3 | ||
|
|
68b4cfbae0 | ||
|
|
97a54aef06 | ||
|
|
297c88c334 | ||
|
|
fef5390a62 | ||
|
|
08d18b1dc1 | ||
|
|
16a2349d86 | ||
|
|
18d9815e88 | ||
|
|
c8346bc5f8 | ||
|
|
2d85a207d1 | ||
|
|
0df8f17e7b | ||
|
|
29645e77cf | ||
|
|
ea76092681 | ||
|
|
c12e13dfd7 | ||
|
|
566940349f | ||
|
|
391ef5c323 | ||
|
|
70264be8e7 | ||
|
|
6208f6f0d5 | ||
|
|
c422a14c5c | ||
|
|
fbeb959317 | ||
|
|
e53ffc8d43 | ||
|
|
453ca1728e | ||
|
|
d10583c7c5 | ||
|
|
a9d213990e | ||
|
|
6321cc0f2d | ||
|
|
3e5f0b2451 | ||
|
|
4ad167eb30 | ||
|
|
01a4f9f867 | ||
|
|
b93d65ddc1 | ||
|
|
9ef9da0870 | ||
|
|
ba2053bd3a | ||
|
|
1260da85a7 | ||
|
|
960c7a0835 | ||
|
|
574f407a90 | ||
|
|
abf802093c | ||
|
|
7ca22f8629 | ||
|
|
6300982b07 | ||
|
|
54cb40f6ed | ||
|
|
8256f0c757 | ||
|
|
1009f9e7c6 | ||
|
|
9c8eef12ba | ||
|
|
9260b3ac6b | ||
|
|
ff98e1fb3d | ||
|
|
f3389d3738 | ||
|
|
a138f4153d | ||
|
|
602e11d5e7 | ||
|
|
3cd14153ca | ||
|
|
095d8e73b8 | ||
|
|
b37f303e76 | ||
|
|
c8368c9098 | ||
|
|
8d0bac9478 | ||
|
|
286c24f8c0 | ||
|
|
963d26f59b | ||
|
|
e07c464de8 | ||
|
|
575509c45b | ||
|
|
563e654b99 | ||
|
|
3e268e2012 | ||
|
|
b2d9f2fc01 | ||
|
|
6717102dd2 | ||
|
|
1ba7fc81ac | ||
|
|
caf4fa7fdd | ||
|
|
5c7962966d | ||
|
|
95ec7e0afa | ||
|
|
c37660f763 | ||
|
|
486ea10c3c | ||
|
|
e0f18f8d1f | ||
|
|
a66f116d66 | ||
|
|
dd099dc39c | ||
|
|
c05aeabdee | ||
|
|
23922f6c7b | ||
|
|
69a99949e1 | ||
|
|
d56cde72a3 | ||
|
|
99ffff11c7 | ||
|
|
bb050cc1b6 | ||
|
|
3547889ad5 | ||
|
|
479e694478 | ||
|
|
2368b634e3 | ||
|
|
dcc09975a9 | ||
|
|
102f5c4e12 | ||
|
|
cc688fa3ce | ||
|
|
e7f7cbcaac | ||
|
|
f9c56d7cb1 | ||
|
|
1fe2e6f6bd | ||
|
|
c3cc88f03e | ||
|
|
584e1f5643 | ||
|
|
04c479148a | ||
|
|
c45cb7bacb | ||
|
|
17be221920 | ||
|
|
10da57572f | ||
|
|
52478ca60a | ||
|
|
62b49dcb19 | ||
|
|
a365faef9c | ||
|
|
5d2698e8a1 | ||
|
|
ec4a413a5b | ||
|
|
596d1ee797 | ||
|
|
d117f82bcb | ||
|
|
18abe9d0f9 | ||
|
|
b53e51de33 | ||
|
|
9478c71af1 | ||
|
|
a2c4eebec8 | ||
|
|
2e5a7d76df | ||
|
|
288249d0b8 | ||
|
|
cd47aae902 | ||
|
|
9bd18ee041 | ||
|
|
22c76df8e6 | ||
|
|
6c87436a96 | ||
|
|
734dac9456 | ||
|
|
85ca366893 | ||
|
|
46db736af8 | ||
|
|
7530048fbd | ||
|
|
c3c03a3a3b | ||
|
|
d1018b6da7 | ||
|
|
fe7928ae0e | ||
|
|
f6c39285c9 | ||
|
|
0e2a289f9f | ||
|
|
f7424da16b | ||
|
|
2e10ee66b7 | ||
|
|
7e442671c3 | ||
|
|
ca646ec2b7 | ||
|
|
4df1af5fd8 | ||
|
|
b4548f3401 | ||
|
|
1819481710 | ||
|
|
e59ae654c0 | ||
|
|
a8bf699f2d | ||
|
|
de9d9d8667 | ||
|
|
869865f22a | ||
|
|
29fd313337 | ||
|
|
7781f07352 | ||
|
|
072371d459 | ||
|
|
51bf948458 | ||
|
|
48acded6ed | ||
|
|
6821f5cf97 | ||
|
|
a15b17e08b | ||
|
|
275b17e1e8 | ||
|
|
27b08ff47b | ||
|
|
bf7c760ca9 | ||
|
|
8af9b0ee02 | ||
|
|
b225c03ef1 | ||
|
|
8a12ed6b8c | ||
|
|
c3fd433446 | ||
|
|
0b9753582d | ||
|
|
9ac48bfbc5 | ||
|
|
85146e5676 | ||
|
|
18ae541c93 | ||
|
|
7a665ec26f | ||
|
|
e3cbac38ce | ||
|
|
31594d47b3 | ||
|
|
42f86a4a24 | ||
|
|
850ce152cd | ||
|
|
c22bbecdc5 | ||
|
|
230f2d155b | ||
|
|
47a2c18c7e | ||
|
|
52bb14bd66 | ||
|
|
8b9caf02d2 | ||
|
|
4580d11d32 | ||
|
|
8610a158d4 | ||
|
|
0a6030b35d | ||
|
|
543c0e62d0 | ||
|
|
4c76e17178 | ||
|
|
611a314cdf | ||
|
|
e7b4d24e5d | ||
|
|
cf60440288 | ||
|
|
15896045f3 | ||
|
|
a9f480ca99 | ||
|
|
4bd67d5f98 | ||
|
|
924ba58f73 |
25
.gitattributes
vendored
Normal file
25
.gitattributes
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
/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/Core/test export-ignore
|
||||
/module/Core/test-func export-ignore
|
||||
/module/Rest/test export-ignore
|
||||
.env.dist export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.phpstorm.meta.php export-ignore
|
||||
.scrutinizer.yml export-ignore
|
||||
.travis.yml export-ignore
|
||||
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
|
||||
phpstan.neon
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
.idea
|
||||
build
|
||||
composer.lock
|
||||
composer.phar
|
||||
vendor/
|
||||
.env
|
||||
data/database.sqlite
|
||||
docs/swagger-ui
|
||||
docker-compose.override.yml
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
/**
|
||||
* PhpStorm Container Interop code completion
|
||||
@@ -16,4 +17,7 @@ $STATIC_METHOD_TYPES = [
|
||||
ContainerInterface::get('') => [
|
||||
'' == '@',
|
||||
],
|
||||
ServiceManager::build('') => [
|
||||
'' == '@',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
extension="memcached.so"
|
||||
37
.travis.yml
37
.travis.yml
@@ -1,18 +1,25 @@
|
||||
language: php
|
||||
|
||||
sudo: false # Use containerized environment
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- /.*/
|
||||
|
||||
php:
|
||||
- 5.6
|
||||
- 7
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
|
||||
before_install: phpenv config-add .travis-php.ini
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: 7.3
|
||||
|
||||
before_script:
|
||||
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
|
||||
|
||||
install:
|
||||
- composer self-update
|
||||
- composer install --no-interaction
|
||||
|
||||
@@ -20,8 +27,22 @@ script:
|
||||
- mkdir build
|
||||
- composer check
|
||||
|
||||
after_script:
|
||||
after_success:
|
||||
- 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
|
||||
|
||||
sudo: false
|
||||
# Before deploying, build dist file for current travis tag
|
||||
before_deploy:
|
||||
- rm -f ocular.phar
|
||||
- ./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
|
||||
|
||||
857
CHANGELOG.md
857
CHANGELOG.md
@@ -1,94 +1,829 @@
|
||||
## CHANGELOG
|
||||
# CHANGELOG
|
||||
|
||||
### 1.2.1
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
**Bugs**
|
||||
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).
|
||||
|
||||
* [62: Fix cross-domain requests in REST API](https://github.com/acelaya/url-shortener/issues/62)
|
||||
|
||||
### 1.2.0
|
||||
## 1.13.2 - 2018-10-18
|
||||
|
||||
**Features**
|
||||
#### Added
|
||||
|
||||
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/acelaya/url-shortener/issues/45)
|
||||
* [7: Add website previews while listing available URLs](https://github.com/acelaya/url-shortener/issues/7)
|
||||
* [#233](https://github.com/shlinkio/shlink/issues/233) Added PHP 7.3 to build matrix allowing its failure.
|
||||
|
||||
**Enhancements:**
|
||||
#### Changed
|
||||
|
||||
* [57: Add database migrations system to improve updating between versions](https://github.com/acelaya/url-shortener/issues/57)
|
||||
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/acelaya/url-shortener/issues/31)
|
||||
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/acelaya/url-shortener/issues/51)
|
||||
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/acelaya/url-shortener/issues/38)
|
||||
* [#235](https://github.com/shlinkio/shlink/issues/235) Improved update instructions (thanks to [tivyhosting](https://github.com/tivyhosting)).
|
||||
|
||||
**Tasks**
|
||||
#### Deprecated
|
||||
|
||||
* [55: Create update script which does not try to create a new database](https://github.com/acelaya/url-shortener/issues/55)
|
||||
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/acelaya/url-shortener/issues/54)
|
||||
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/acelaya/url-shortener/issues/29)
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
#### Removed
|
||||
|
||||
* [53: Fix entities database interoperability](https://github.com/acelaya/url-shortener/issues/53)
|
||||
* [52: Add missing htaccess file for apache environments](https://github.com/acelaya/url-shortener/issues/52)
|
||||
* *Nothing*
|
||||
|
||||
### 1.1.0
|
||||
#### Fixed
|
||||
|
||||
**Features**
|
||||
* [#237](https://github.com/shlinkio/shlink/issues/233) Solved errors when trying to geo-locate `null` IP addresses.
|
||||
|
||||
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/acelaya/url-shortener/issues/46)
|
||||
Also improved how visitor IP addresses are discovered, thanks to [akrabat/ip-address-middleware](https://github.com/akrabat/ip-address-middleware) package.
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/acelaya/url-shortener/issues/32)
|
||||
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/acelaya/url-shortener/issues/14)
|
||||
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/acelaya/url-shortener/issues/41)
|
||||
* [13: Improve REST authentication](https://github.com/acelaya/url-shortener/issues/13)
|
||||
## 1.13.1 - 2018-10-16
|
||||
|
||||
**Tasks**
|
||||
#### Added
|
||||
|
||||
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/acelaya/url-shortener/issues/39)
|
||||
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/acelaya/url-shortener/issues/42)
|
||||
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/acelaya/url-shortener/issues/35)
|
||||
* *Nothing*
|
||||
|
||||
**Bugs**
|
||||
#### Changed
|
||||
|
||||
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/acelaya/url-shortener/issues/40)
|
||||
* *Nothing*
|
||||
|
||||
### 1.0.0
|
||||
#### Deprecated
|
||||
|
||||
**Enhancements:**
|
||||
* *Nothing*
|
||||
|
||||
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33)
|
||||
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15)
|
||||
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23)
|
||||
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21)
|
||||
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22)
|
||||
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20)
|
||||
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11)
|
||||
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18)
|
||||
#### Removed
|
||||
|
||||
**Tasks**
|
||||
* *Nothing*
|
||||
|
||||
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36)
|
||||
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4)
|
||||
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6)
|
||||
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30)
|
||||
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12)
|
||||
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25)
|
||||
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19)
|
||||
#### Fixed
|
||||
|
||||
**Bugs**
|
||||
* [#231](https://github.com/shlinkio/shlink/issues/197) Fixed error when processing visits.
|
||||
|
||||
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24)
|
||||
|
||||
### 0.2.0
|
||||
## 1.13.0 - 2018-10-06
|
||||
|
||||
**Enhancements:**
|
||||
#### Added
|
||||
|
||||
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/acelaya/url-shortener/issues/9)
|
||||
* [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8)
|
||||
* [10: Add more CLI functionality](https://github.com/acelaya/url-shortener/issues/10)
|
||||
* [#197](https://github.com/shlinkio/shlink/issues/197) Added [cakephp/chronos](https://book.cakephp.org/3.0/en/chronos.html) library for date manipulations.
|
||||
* [#214](https://github.com/shlinkio/shlink/issues/214) Improved build script, which allows builds to be done without "jumping" outside the project directory, and generates smaller dist files.
|
||||
|
||||
**Tasks**
|
||||
It also allows automating the dist file generation in travis-ci builds.
|
||||
|
||||
* [5: Create CHANGELOG file](https://github.com/acelaya/url-shortener/issues/5)
|
||||
* [#207](https://github.com/shlinkio/shlink/issues/207) Added two new config options which are asked during installation process. The config options already existed in previous shlink version, but you had to manually set their values.
|
||||
|
||||
These are the new options:
|
||||
|
||||
* Visits threshold to allow short URLs to be deleted.
|
||||
* Check the visits threshold when trying to delete a short URL via REST API.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#211](https://github.com/shlinkio/shlink/issues/211) Extracted installer to its own module, which will simplify moving it to a separated package in the future.
|
||||
* [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) Renamed REST Action classes and CLI Command classes to use the concept of `ShortUrl` instead of the concept of `ShortCode` when referring to the entity, and left the `short code` concept to the identifier which is used as a unique code for a specific `Short URL`.
|
||||
* [#181](https://github.com/shlinkio/shlink/issues/181) When importing the configuration from a previous shlink installation, it no longer asks to import every block. Instead, it is capable of detecting only new config options introduced in the new version, and ask only for those.
|
||||
|
||||
If no new options are found and you have selected to import config, no further questions will be asked and shlink will just import the old config.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* [#205](https://github.com/shlinkio/shlink/issues/205) Deprecated `[POST /authenticate]` endpoint, and allowed any API request to be automatically authenticated using the `X-Api-Key` header with a valid API key.
|
||||
|
||||
This effectively deprecates the `Authorization: Bearer <JWT>` authentication form, but it will keep working.
|
||||
|
||||
* As of [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) REST urls have changed from `/short-codes/...` to `/short-urls/...`, and the command namespaces have changed from `short-code:...` to `short-url:...`.
|
||||
|
||||
In both cases, backwards compatibility has been retained and the old ones are aliases for the new ones, but the old ones are considered deprecated.
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#203](https://github.com/shlinkio/shlink/issues/203) Fixed some warnings thrown while unzipping distributable files.
|
||||
* [#206](https://github.com/shlinkio/shlink/issues/206) An error is now thrown during installation if any required param is left empty, making the installer display a message and ask again until a value is set.
|
||||
|
||||
|
||||
## 1.12.0 - 2018-09-15
|
||||
|
||||
#### Added
|
||||
|
||||
* [#187](https://github.com/shlinkio/shlink/issues/187) Included an API endpoint and a CLI command to delete short URLs.
|
||||
|
||||
Due to the implicit danger of this operation, the deletion includes a safety check. URLs cannot be deleted if they have more than a specific amount of visits.
|
||||
|
||||
The visits threshold is set to **15** by default and currently it has to be manually changed. In future versions the installation/update process will ask you about the value of the visits threshold.
|
||||
|
||||
In order to change it, open the `config/autoload/delete_short_urls.global.php` file, which has this structure:
|
||||
|
||||
```php
|
||||
return [
|
||||
|
||||
'delete_short_urls' => [
|
||||
'visits_threshold' => 15,
|
||||
'check_visits_threshold' => true,
|
||||
],
|
||||
|
||||
];
|
||||
```
|
||||
|
||||
Properties are self explanatory. Change `check_visits_threshold` to `false` to completely disable this safety check, and change the value of `visits_threshold` to allow short URLs with a different number of visits to be deleted.
|
||||
|
||||
Once changed, delete the `data/cache/app_config.php` file (if any) to let shlink know about the new values.
|
||||
|
||||
This check is implicit for the API endpoint, but can be "disabled" for the CLI command, which will ask you when trying to delete a URL which has reached to threshold in order to force the deletion.
|
||||
|
||||
* [#183](https://github.com/shlinkio/shlink/issues/183) and [#190](https://github.com/shlinkio/shlink/issues/190) Included important documentation improvements in the repository itself. You no longer need to go to the website in order to see how to install or use shlink.
|
||||
* [#186](https://github.com/shlinkio/shlink/issues/186) Added a small robots.txt file that prevents 404 errors to be logged due to search engines trying to index the domain where shlink is located. Thanks to [@robwent](https://github.com/robwent) for the contribution.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#145](https://github.com/shlinkio/shlink/issues/145) Shlink now obfuscates IP addresses from visitors by replacing the latest octet by `0`, which does not affect geolocation and allows it to fulfil the GDPR.
|
||||
|
||||
Other known services follow this same approach, like [Google Analytics](https://support.google.com/analytics/answer/2763052?hl=en) or [Matomo](https://matomo.org/docs/privacy/#step-1-automatically-anonymize-visitor-ips)
|
||||
|
||||
* [#182](https://github.com/shlinkio/shlink/issues/182) The short URL creation API endpoints now return the same model used for lists and details endpoints.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#188](https://github.com/shlinkio/shlink/issues/188) Shlink now allows multiple short URLs to be created that resolve to the same long URL.
|
||||
|
||||
|
||||
## 1.11.0 - 2018-08-13
|
||||
|
||||
#### Added
|
||||
|
||||
* [#170](https://github.com/shlinkio/shlink/issues/170) and [#171](https://github.com/shlinkio/shlink/issues/171) Updated `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints to return more meaningful information and make their response consistent.
|
||||
|
||||
The short URLs are now represented by this object in both cases:
|
||||
|
||||
```json
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
],
|
||||
"originalUrl": "https://shlink.io"
|
||||
}
|
||||
```
|
||||
|
||||
The `originalUrl` property is considered deprecated and has been kept for backward compatibility purposes. It holds the same value as the `longUrl` property.
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* The `originalUrl` property in `[GET /short-codes]` and `[GET /short-codes/{shortCode}]` endpoints is now deprecated and replaced by the `longUrl` property.
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.10.2 - 2018-08-04
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#177](https://github.com/shlinkio/shlink/issues/177) Fixed `[GET] /short-codes` endpoint returning a 500 status code when trying to filter by `tags` and `searchTerm` at the same time.
|
||||
* [#175](https://github.com/shlinkio/shlink/issues/175) Fixed error introduced in previous version, where you could end up banned from the service used to resolve IP address locations.
|
||||
|
||||
In order to fix that, just fill [this form](http://ip-api.com/docs/unban) including your server's IP address and your server should be unbanned.
|
||||
|
||||
In order to prevent this, after resolving 150 IP addresses, shlink now waits 1 minute before trying to resolve any more addresses.
|
||||
|
||||
|
||||
## 1.10.1 - 2018-08-02
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#167](https://github.com/shlinkio/shlink/issues/167) Shlink version is now set at build time to avoid older version numbers to be kept in newer builds.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#165](https://github.com/shlinkio/shlink/issues/165) Fixed custom slugs failing when they are longer than 10 characters.
|
||||
* [#166](https://github.com/shlinkio/shlink/issues/166) Fixed unusual edge case in which visits were not properly counted when ordering by visit and filtering by search term in `[GET] /short-codes` API endpoint.
|
||||
* [#174](https://github.com/shlinkio/shlink/issues/174) Fixed geolocation not working due to a deprecation on used service.
|
||||
* [#172](https://github.com/shlinkio/shlink/issues/172) Documented missing filtering params for `[GET] /short-codes/{shortCode}/visits` API endpoint, which allow the list to be filtered by date range.
|
||||
|
||||
For example: `https://doma.in/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
|
||||
|
||||
* [#169](https://github.com/shlinkio/shlink/issues/169) Fixed unhandled error when parsing `ShortUrlMeta` and date fields are already `DateTime` instances.
|
||||
|
||||
|
||||
## 1.10.0 - 2018-07-09
|
||||
|
||||
#### Added
|
||||
|
||||
* [#161](https://github.com/shlinkio/shlink/issues/161) AddED support for shlink to be run with [swoole](https://www.swoole.co.uk/) via [zend-expressive-swoole](https://github.com/zendframework/zend-expressive-swoole) package
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#159](https://github.com/shlinkio/shlink/issues/159) Updated CHANGELOG to follow the [keep-a-changelog](https://keepachangelog.com) format
|
||||
* [#160](https://github.com/shlinkio/shlink/issues/160) Update infection to v0.9 and phpstan to v 0.10
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.9.1 - 2018-06-18
|
||||
|
||||
#### Added
|
||||
|
||||
* [#155](https://github.com/shlinkio/shlink/issues/155) Improved the pagination object returned in lists, including more meaningful properties.
|
||||
|
||||
* Old structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"pagination": {
|
||||
"currentPage": 1,
|
||||
"pagesCount": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* New structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"pagination": {
|
||||
"currentPage": 2,
|
||||
"pagesCount": 13,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 126
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#154](https://github.com/shlinkio/shlink/issues/154) Fixed sizes of every result page when filtering by searchTerm
|
||||
* [#157](https://github.com/shlinkio/shlink/issues/157) Background commands executed by installation process now respect the originally used php binary
|
||||
|
||||
|
||||
## 1.9.0 - 2018-05-07
|
||||
|
||||
#### Added
|
||||
|
||||
* [#147](https://github.com/shlinkio/shlink/issues/147) Allowed short URLs to be created on the fly using a single API request, including the API key in a query param.
|
||||
|
||||
This eases integration with third party services.
|
||||
|
||||
With this feature, a simple request to a URL like `https://doma.in/rest/v1/short-codes/shorten?apiKey=[YOUR_API_KEY]&longUrl=[URL_TO_BE_SHORTENED]` would return the shortened one in JSON or plain text format.
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#139](https://github.com/shlinkio/shlink/issues/139) Ensured all core actions log exceptions
|
||||
|
||||
|
||||
## 1.8.1 - 2018-04-07
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#141](https://github.com/shlinkio/shlink/issues/141) Removed workaround used in `PathVersionMiddleware`, since the bug in zend-stratigility has been fixed.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#140](https://github.com/shlinkio/shlink/issues/140) Fixed warning thrown during installation while trying to include doctrine script
|
||||
|
||||
|
||||
## 1.8.0 - 2018-03-29
|
||||
|
||||
#### Added
|
||||
|
||||
* [#125](https://github.com/shlinkio/shlink/issues/125) Implemented a path which returns a 1px image instead of a redirection.
|
||||
|
||||
Useful to track emails. Just add an image pointing to a URL like `https://doma.in/abc123/track` to any email and an invisible image will be generated tracking every time the email is opened.
|
||||
|
||||
* [#132](https://github.com/shlinkio/shlink/issues/132) Added infection to improve tests
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#130](https://github.com/shlinkio/shlink/issues/130) Updated to Expressive 3
|
||||
* [#137](https://github.com/shlinkio/shlink/issues/137) Updated symfony components to v4
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#131](https://github.com/shlinkio/shlink/issues/131) Dropped support for PHP 7
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.7.2 - 2018-03-26
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#135](https://github.com/shlinkio/shlink/issues/135) Fixed `PathVersionMiddleware` being ignored when using expressive 2.2
|
||||
|
||||
|
||||
## 1.7.1 - 2018-03-21
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#128](https://github.com/shlinkio/shlink/issues/128) Upgraded to expressive 2.2
|
||||
|
||||
This will ease the upcoming update to expressive 3
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#126](https://github.com/shlinkio/shlink/issues/126) Fixed `E_USER_DEPRECATED` errors triggered when using Expressive 2.2
|
||||
|
||||
|
||||
## 1.7.0 - 2018-01-21
|
||||
|
||||
#### Added
|
||||
|
||||
* [#88](https://github.com/shlinkio/shlink/issues/88) Allowed tracking of short URLs to be disabled by including a configurable query param
|
||||
* [#108](https://github.com/shlinkio/shlink/issues/108) Allowed metadata to be defined when creating short codes
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#113](https://github.com/shlinkio/shlink/issues/113) Updated CLI commands to use `SymfonyStyle`
|
||||
* [#112](https://github.com/shlinkio/shlink/issues/112) Enabled Lazy loading in CLI commands
|
||||
* [#117](https://github.com/shlinkio/shlink/issues/117) Every module which throws exceptions has now its own `ExceptionInterface` extending `Throwable`
|
||||
* [#115](https://github.com/shlinkio/shlink/issues/115) Added phpstan to build matrix on PHP >=7.1 envs
|
||||
* [#114](https://github.com/shlinkio/shlink/issues/114) Replaced [vlucas/phpdotenv](https://github.com/vlucas/phpdotenv) dev requirement by [symfony/dotenv](https://github.com/symfony/dotenv)
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.6.2 - 2017-10-25
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#109](https://github.com/shlinkio/shlink/issues/109) Fixed installation error due to typo in latest migration
|
||||
|
||||
|
||||
## 1.6.1 - 2017-10-24
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#110](https://github.com/shlinkio/shlink/issues/110) Created `.gitattributes` file to define files to be excluded from distributable package
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.6.0 - 2017-10-23
|
||||
|
||||
#### Added
|
||||
|
||||
* [#44](https://github.com/shlinkio/shlink/issues/44) Now it is possible to set custom slugs for short URLs instead of using a generated short code
|
||||
* [#47](https://github.com/shlinkio/shlink/issues/47) Allowed to limit short URLs availability by date range
|
||||
* [#48](https://github.com/shlinkio/shlink/issues/48) Allowed to limit the number of visits to a short URL
|
||||
* [#105](https://github.com/shlinkio/shlink/pull/105) Added option to enable/disable URL validation by response status code
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#27](https://github.com/shlinkio/shlink/issues/27) Added repository functional tests with dbunit
|
||||
* [#101](https://github.com/shlinkio/shlink/issues/101) Now specific actions just capture very specific exceptions, and let the `ErrorHandler` catch any other unhandled exception
|
||||
* [#104](https://github.com/shlinkio/shlink/issues/104) Used different templates for *requested-short-code-does-not-exist* and *route-could-not-be-match*
|
||||
* [#99](https://github.com/shlinkio/shlink/issues/99) Replaced usages of `AnnotatedFactory` by `ConfigAbstractFactory`
|
||||
* [#100](https://github.com/shlinkio/shlink/issues/100) Updated templates engine. Replaced twig by plates
|
||||
* [#102](https://github.com/shlinkio/shlink/issues/102) Improved coding standards strictness
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#86](https://github.com/shlinkio/shlink/issues/86) Dropped support for PHP 5
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#103](https://github.com/shlinkio/shlink/issues/103) `NotFoundDelegate` now returns proper content types based on accepted content
|
||||
|
||||
|
||||
## 1.5.0 - 2017-07-16
|
||||
|
||||
#### Added
|
||||
|
||||
* [#95](https://github.com/shlinkio/shlink/issues/95) Added tags CRUD to CLI
|
||||
* [#59](https://github.com/shlinkio/shlink/issues/59) Added tags CRUD to REST
|
||||
* [#66](https://github.com/shlinkio/shlink/issues/66) Allowed certain information to be imported from and older shlink instance directory when updating
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#96](https://github.com/shlinkio/shlink/issues/96) Added namespace to functions
|
||||
* [#76](https://github.com/shlinkio/shlink/issues/76) Added response examples to swagger docs
|
||||
* [#93](https://github.com/shlinkio/shlink/issues/93) Improved cross domain management by using the `ImplicitOptionsMiddleware`
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#92](https://github.com/shlinkio/shlink/issues/92) Fixed formatted dates, using an ISO compliant format
|
||||
|
||||
|
||||
## 1.4.0 - 2017-03-25
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#89](https://github.com/shlinkio/shlink/issues/89) Updated to expressive 2
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.3.1 - 2017-01-22
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#82](https://github.com/shlinkio/shlink/issues/82) Enabled `FastRoute` routes cache
|
||||
* [#85](https://github.com/shlinkio/shlink/issues/85) Updated year in license file
|
||||
* [#81](https://github.com/shlinkio/shlink/issues/81) Added docker containers config
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#83](https://github.com/shlinkio/shlink/issues/83) Fixed short codes list: search in tags when filtering by query string
|
||||
* [#79](https://github.com/shlinkio/shlink/issues/79) Increased the number of followed redirects
|
||||
* [#75](https://github.com/shlinkio/shlink/issues/75) Applied `PathVersionMiddleware` only to rest routes defining it by configuration instead of code
|
||||
* [#77](https://github.com/shlinkio/shlink/issues/77) Allowed defining database server hostname and port
|
||||
|
||||
|
||||
## 1.3.0 - 2016-10-23
|
||||
|
||||
#### Added
|
||||
|
||||
* [#67](https://github.com/shlinkio/shlink/issues/67) Allowed to order the short codes list
|
||||
* [#60](https://github.com/shlinkio/shlink/issues/60) Accepted JSON requests in REST and used a body parser middleware to set the request's `parsedBody`
|
||||
* [#72](https://github.com/shlinkio/shlink/issues/72) When listing API keys from CLI, use yellow color for enabled keys that have expired
|
||||
* [#58](https://github.com/shlinkio/shlink/issues/58) Allowed to filter short URLs by tag
|
||||
* [#69](https://github.com/shlinkio/shlink/issues/69) Allowed to filter short URLs by text query
|
||||
* [#73](https://github.com/shlinkio/shlink/issues/73) Added tag-related endpoints to swagger file
|
||||
* [#63](https://github.com/shlinkio/shlink/issues/63) Added path versioning to REST API routes
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#71](https://github.com/shlinkio/shlink/issues/71) Separated swagger docs into multiple files
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## 1.2.2 - 2016-08-29
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Fixed minor bugs on CORS requests
|
||||
|
||||
|
||||
## 1.2.1 - 2016-08-21
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#62](https://github.com/shlinkio/shlink/issues/62) Fixed cross-domain requests in REST API
|
||||
|
||||
|
||||
## 1.2.0 - 2016-08-21
|
||||
|
||||
#### Added
|
||||
|
||||
* [#45](https://github.com/shlinkio/shlink/issues/45) Allowed to define tags on short codes, to improve filtering and classification
|
||||
* [#7](https://github.com/shlinkio/shlink/issues/7) Added website previews while listing available URLs
|
||||
* [#57](https://github.com/shlinkio/shlink/issues/57) Added database migrations system to improve updating between versions
|
||||
* [#31](https://github.com/shlinkio/shlink/issues/31) Added support for other database management systems by improving the `EntityManager` factory
|
||||
* [#51](https://github.com/shlinkio/shlink/issues/51) Generated build process to create app package and ease distribution
|
||||
* [#38](https://github.com/shlinkio/shlink/issues/38) Defined installation script. It will request dynamic data on the fly so that there is no need to define env vars
|
||||
* [#55](https://github.com/shlinkio/shlink/issues/55) Created update script which does not try to create a new database
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#54](https://github.com/shlinkio/shlink/issues/54) Added cache namespace to prevent name collisions with other apps in the same environment
|
||||
* [#29](https://github.com/shlinkio/shlink/issues/29) Used the [acelaya/ze-content-based-error-handler](https://github.com/acelaya/ze-content-based-error-handler) package instead of custom error handler implementation
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#53](https://github.com/shlinkio/shlink/issues/53) Fixed entities database interoperability
|
||||
* [#52](https://github.com/shlinkio/shlink/issues/52) Added missing htaccess file for apache environments
|
||||
|
||||
|
||||
## 1.1.0 - 2016-08-09
|
||||
|
||||
#### Added
|
||||
|
||||
* [#46](https://github.com/shlinkio/shlink/issues/46) Defined a route that returns a QR code representing the shortened URL.
|
||||
|
||||
In order to get the QR code URL, use a pattern like `https://doma.in/abc123/qr-code`
|
||||
|
||||
* [#32](https://github.com/shlinkio/shlink/issues/32) Added support for other cache adapters by improving the Cache factory
|
||||
* [#14](https://github.com/shlinkio/shlink/issues/14) Added logger and enabled errors logging
|
||||
* [#13](https://github.com/shlinkio/shlink/issues/13) Improved REST authentication
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#41](https://github.com/shlinkio/shlink/issues/41) Cached the "short code" => "URL" map to prevent extra DB hits
|
||||
* [#39](https://github.com/shlinkio/shlink/issues/39) Changed copyright from "Alejandro Celaya" to "Shlink" in error pages
|
||||
* [#42](https://github.com/shlinkio/shlink/issues/42) REST endpoints that need to find *something* now return a 404 when it is not found
|
||||
* [#35](https://github.com/shlinkio/shlink/issues/35) Updated CLI commands to use the same PHP namespace as the one used for the command name
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#40](https://github.com/shlinkio/shlink/issues/40) Taken into account the `X-Forwarded-For` header in order to get the visitor information, in case the server is behind a load balancer or proxy
|
||||
|
||||
|
||||
## 1.0.0 - 2016-08-01
|
||||
|
||||
#### Added
|
||||
|
||||
* [#33](https://github.com/shlinkio/shlink/issues/33) Created a command that generates a short code charset by randomizing the default one
|
||||
* [#23](https://github.com/shlinkio/shlink/issues/23) Translated application literals
|
||||
* [#21](https://github.com/shlinkio/shlink/issues/21) Allowed to filter visits by date range
|
||||
* [#4](https://github.com/shlinkio/shlink/issues/4) Added installation steps
|
||||
* [#12](https://github.com/shlinkio/shlink/issues/12) Improved code coverage
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#15](https://github.com/shlinkio/shlink/issues/15) HTTP requests now return JSON/HTML responses for errors (4xx and 5xx) based on `Accept` header
|
||||
* [#22](https://github.com/shlinkio/shlink/issues/22) Now visits locations data is saved on a `visit_locations` table
|
||||
* [#20](https://github.com/shlinkio/shlink/issues/20) Injected cross domain headers in response only if the `Origin` header is present in the request
|
||||
* [#11](https://github.com/shlinkio/shlink/issues/11) Separated code into multiple modules
|
||||
* [#18](https://github.com/shlinkio/shlink/issues/18) Grouped routable middleware in an Action namespace
|
||||
* [#6](https://github.com/shlinkio/shlink/issues/6) Project no longer depends on [zendframework/zend-expressive-helpers](https://github.com/zendframework/zend-expressive-helpers) package
|
||||
* [#30](https://github.com/shlinkio/shlink/issues/30) Replaced the "services" first level config entry by "dependencies", in order to fulfill default Expressive naming
|
||||
* [#25](https://github.com/shlinkio/shlink/issues/25) Replaced "Middleware" suffix on routable middlewares by "Action"
|
||||
* [#19](https://github.com/shlinkio/shlink/issues/19) Changed the vendor and app namespace from `Acelaya\UrlShortener` to `Shlinkio\Shlink`
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* [#36](https://github.com/shlinkio/shlink/issues/36) Removed hhvm from the CI matrix since it doesn't support array constants and will fail
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#24](https://github.com/shlinkio/shlink/issues/24) Prevented duplicated short codes errors because of the case insensitive behavior on MySQL
|
||||
|
||||
|
||||
## 0.2.0 - 2016-08-01
|
||||
|
||||
#### Added
|
||||
|
||||
* [#8](https://github.com/shlinkio/shlink/issues/8) Created a REST API
|
||||
* [#10](https://github.com/shlinkio/shlink/issues/10) Added more CLI functionality
|
||||
* [#5](https://github.com/shlinkio/shlink/issues/5) Created a CHANGELOG file
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#9](https://github.com/shlinkio/shlink/issues/9) Used [symfony/console](https://github.com/symfony/console) to dispatch console requests, instead of trying to integrate the process with expressive
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Alejandro Celaya
|
||||
Copyright (c) 2018 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
|
||||
|
||||
191
README.md
191
README.md
@@ -1,9 +1,188 @@
|
||||
# Shlink
|
||||
|
||||
[](https://travis-ci.org/shlinkio/shlink)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://travis-ci.org/shlinkio/shlink)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://github.com/shlinkio/shlink/blob/master/LICENSE)
|
||||
[](https://acel.me/donate)
|
||||
|
||||
A PHP-based URL shortener application with analytics and management
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
|
||||
|
||||
## Installation
|
||||
|
||||
First make sure the host where you are going to run shlink fulfills these requirements:
|
||||
|
||||
* PHP 7.1 or greater with JSON, APCu, intl, curl, PDO and gd extensions enabled.
|
||||
* MySQL, PostgreSQL or SQLite.
|
||||
* The web server of your choice with PHP integration (Apache or Nginx recommended).
|
||||
|
||||
Then, you will need a built version of the project. There are a few ways to get it.
|
||||
|
||||
* **Using a dist file**
|
||||
|
||||
The easiest way to install shlink is by using one of the pre-bundled distributable packages.
|
||||
|
||||
Just go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink_X.X.X_dist.zip` file you will find there.
|
||||
|
||||
Finally, decompress the file in the location of your choice.
|
||||
|
||||
* **Building from sources**
|
||||
|
||||
If for any reason you want to build the project yourself, follow these steps:
|
||||
|
||||
* Clone the project with git (`git clone https://github.com/shlinkio/shlink.git`), or download it by clicking the **Clone or download** green button.
|
||||
* Download the [Composer](https://getcomposer.org/download/) PHP package manager inside the project folder.
|
||||
* Run `./build.sh 1.0.0`, replacing the version with the version number you are going to build (the version number is only used for the generated dist file).
|
||||
|
||||
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory.
|
||||
|
||||
This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.org/shlinkio/shlink), attaching generated dist file to it.
|
||||
|
||||
Despite how you built the project, you are going to need to install it now, by following these steps:
|
||||
|
||||
* 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.
|
||||
|
||||
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.
|
||||
|
||||
*Nginx:*
|
||||
|
||||
```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>
|
||||
```
|
||||
|
||||
* 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.
|
||||
|
||||
**Bonus**
|
||||
|
||||
There are a couple of time-consuming tasks that shlink expects you to do manually, or at least it is recommended, since it will improve runtime performance.
|
||||
|
||||
Those tasks can be performed using shlink's CLI, so it should be easy to schedule them to be run in the background (for example, using cron jobs):
|
||||
|
||||
* Resolve IP address locations: `/path/to/shlink/bin/cli visit:process`
|
||||
|
||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||
|
||||
* 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.
|
||||
|
||||
## 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`)
|
||||
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
|
||||
|
||||
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.
|
||||
|
||||
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
|
||||
|
||||
**Important!** It is recommended that you don't skip any version when using this process. The update gets better on every version, but older versions might make assumptions.
|
||||
|
||||
## 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/).
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
|
||||
It is probably a good idea to symlink the CLI entry point (`bin/cli`) to somewhere in your path, so that you can run shlink from any directory.
|
||||
|
||||
* **The REST API**. The complete docs on how to use the API can be found [here](https://shlink.io/api-docs), and a sandbox which also documents every endpoint can be found [here](https://shlink.io/swagger-ui/index.html).
|
||||
|
||||
However, you probably don't want to consume the raw API yourself. That's why a nice [web client](https://github.com/shlinkio/shlink-web-client) is provided that can be directly used from [https://app.shlink.io](https://app.shlink.io), or you can host it yourself too.
|
||||
|
||||
Both the API and CLI allow you to do the same operations, except for API key management, which can be done from the command line interface only.
|
||||
|
||||
### Shlink CLI Help
|
||||
|
||||
```
|
||||
Usage:
|
||||
command [options] [arguments]
|
||||
|
||||
Options:
|
||||
-h, --help Display this help message
|
||||
-q, --quiet Do not output any message
|
||||
-V, --version Display this application version
|
||||
--ansi Force ANSI output
|
||||
--no-ansi Disable ANSI output
|
||||
-n, --no-interaction Do not ask any interactive question
|
||||
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
|
||||
|
||||
Available commands:
|
||||
help Displays help for a command
|
||||
list Lists commands
|
||||
api-key
|
||||
api-key:disable Disables an API key.
|
||||
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
|
||||
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
|
||||
short-url:list [shortcode:list|short-code:list] List all short URLs
|
||||
short-url:parse [shortcode:parse|short-code:parse] Returns the long URL behind a short code
|
||||
short-url:process-previews [shortcode:process-previews|short-code:process-previews] Processes and generates the previews for every URL, improving performance for later web requests.
|
||||
short-url:visits [shortcode:visits|short-code:visits] Returns the detailed visits information for provided short code
|
||||
tag
|
||||
tag:create Creates one or more tags.
|
||||
tag:delete Deletes one or more tags.
|
||||
tag:list Lists existing tags.
|
||||
tag:rename Renames one existing tag.
|
||||
visit
|
||||
visit:process Processes visits where location is not set yet
|
||||
```
|
||||
|
||||
7
bin/cli
7
bin/cli
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
|
||||
/** @var CliApp $app */
|
||||
$app = $container->get(CliApp::class);
|
||||
$app->run();
|
||||
$container->get(CliApp::class)->run();
|
||||
|
||||
18
bin/install
18
bin/install
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new InstallCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class)->run();
|
||||
|
||||
18
bin/update
18
bin/update
@@ -1,14 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new UpdateCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class, ['isUpdate' => true])->run();
|
||||
|
||||
50
build.sh
50
build.sh
@@ -8,37 +8,51 @@ if [ "$#" -ne 1 ]; then
|
||||
fi
|
||||
|
||||
version=$1
|
||||
builtcontent=$(readlink -f '../shlink_build_tmp')
|
||||
builtcontent="./build/shlink_${version}_dist"
|
||||
projectdir=$(pwd)
|
||||
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
||||
|
||||
# Copy project content to temp dir
|
||||
echo 'Copying project files...'
|
||||
rm -rf "${builtcontent}"
|
||||
mkdir "${builtcontent}"
|
||||
cp -R "${projectdir}"/* "${builtcontent}"
|
||||
mkdir -p "${builtcontent}"
|
||||
rsync -av * "${builtcontent}" \
|
||||
--exclude=data/infra \
|
||||
--exclude=**/.gitignore \
|
||||
--exclude=CHANGELOG.md \
|
||||
--exclude=composer.lock \
|
||||
--exclude=vendor \
|
||||
--exclude=docs \
|
||||
--exclude=indocker \
|
||||
--exclude=docker* \
|
||||
--exclude=func_tests_bootstrap.php \
|
||||
--exclude=php* \
|
||||
--exclude=infection.json \
|
||||
--exclude=phpstan.neon \
|
||||
--exclude=config/autoload/*local* \
|
||||
--exclude=**/test* \
|
||||
--exclude=build*
|
||||
cd "${builtcontent}"
|
||||
|
||||
# Install dependencies
|
||||
rm -r vendor
|
||||
rm composer.lock
|
||||
composer self-update
|
||||
composer install --no-dev --optimize-autoloader
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
rm build.sh
|
||||
rm CHANGELOG.md
|
||||
rm composer.*
|
||||
rm LICENSE
|
||||
rm php*
|
||||
rm README.md
|
||||
rm -r build
|
||||
rm -f data/database.sqlite
|
||||
rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||
rm -rf config/params/{*,.gitignore}
|
||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||
|
||||
# Update shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Compressing file
|
||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip .
|
||||
echo 'Compressing files...'
|
||||
cd "${projectdir}"/build
|
||||
rm -f ./shlink_${version}_dist.zip
|
||||
zip -ry ./shlink_${version}_dist.zip ./shlink_${version}_dist
|
||||
cd "${projectdir}"
|
||||
rm -rf "${builtcontent}"
|
||||
|
||||
echo 'Done!'
|
||||
|
||||
126
composer.json
126
composer.json
@@ -1,55 +1,70 @@
|
||||
{
|
||||
"name": "shlinkio/shlink",
|
||||
"type": "project",
|
||||
"homepage": "http://shlink.io",
|
||||
"homepage": "https://shlink.io",
|
||||
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alejandro Celaya Alastrué",
|
||||
"homepage": "http://www.alejandrocelaya.com",
|
||||
"homepage": "https://www.alejandrocelaya.com",
|
||||
"email": "alejandro@alejandrocelaya.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^5.6 || ^7.0",
|
||||
"zendframework/zend-expressive": "^1.0",
|
||||
"zendframework/zend-expressive-fastroute": "^1.1",
|
||||
"zendframework/zend-expressive-twigrenderer": "^1.0",
|
||||
"zendframework/zend-stdlib": "^2.7",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-config": "^2.6",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"mtymek/expressive-config-manager": "^0.4",
|
||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||
"acelaya/ze-content-based-error-handler": "^1.0",
|
||||
"php": "^7.1",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||
"akrabat/ip-address-middleware": "^1.0",
|
||||
"cakephp/chronos": "^1.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
"doctrine/cache": "^1.6",
|
||||
"doctrine/migrations": "^1.4",
|
||||
"doctrine/orm": "^2.5",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"symfony/console": "^3.0",
|
||||
"symfony/process": "^3.0",
|
||||
"symfony/filesystem": "^3.0",
|
||||
"endroid/qr-code": "^1.7",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"monolog/monolog": "^1.21",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"endroid/qrcode": "^1.7",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"doctrine/migrations": "^1.4"
|
||||
"monolog/monolog": "^1.21",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^4.0 <4.1.5",
|
||||
"symfony/filesystem": "^4.0",
|
||||
"symfony/process": "^4.0",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-diactoros": "^2.0",
|
||||
"zendframework/zend-expressive": "^3.0",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-servicemanager": "^3.2",
|
||||
"zendframework/zend-stdlib": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.0",
|
||||
"squizlabs/php_codesniffer": "^2.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
"symfony/var-dumper": "^3.0",
|
||||
"vlucas/phpdotenv": "^2.2"
|
||||
"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",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
"zendframework/zend-expressive-tooling": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"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\\Common\\": "module/Common/src",
|
||||
"Shlinkio\\Shlink\\Installer\\": "module/Installer/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Common/functions/functions.php"
|
||||
@@ -59,19 +74,60 @@
|
||||
"psr-4": {
|
||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
||||
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test"
|
||||
"ShlinkioTest\\Shlink\\Core\\": [
|
||||
"module/Core/test",
|
||||
"module/Core/test-func"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\Common\\": [
|
||||
"module/Common/test",
|
||||
"module/Common/test-func"
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": [
|
||||
"@cs",
|
||||
"@test"
|
||||
"@stan",
|
||||
"@test",
|
||||
"@infect"
|
||||
],
|
||||
|
||||
"cs": "phpcs",
|
||||
"cs-fix": "phpcbf",
|
||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
||||
"test": "phpunit --coverage-clover build/clover.xml",
|
||||
"pretty-test": "phpunit --coverage-html build/coverage"
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
||||
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:func"
|
||||
],
|
||||
"test:unit": "phpunit --coverage-php build/coverage-unit.cov",
|
||||
"test:func": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
||||
|
||||
"test:pretty": [
|
||||
"@test:unit",
|
||||
"@test:func",
|
||||
"phpcov merge build --html build/html"
|
||||
],
|
||||
"test:unit:pretty": "phpunit --coverage-html build/coverage",
|
||||
|
||||
"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"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
|
||||
"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:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:func": "<fg=blue;options=bold>Runs functional test suites (covering entity repositories)</>",
|
||||
"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</>"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '1.2.0',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
13
config/autoload/delete_short_urls.global.php
Normal file
13
config/autoload/delete_short_urls.global.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
return [
|
||||
|
||||
'delete_short_urls' => [
|
||||
'visits_threshold' => 15,
|
||||
'check_visits_threshold' => true,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,21 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Expressive\Application::class => Container\ApplicationFactory::class,
|
||||
Router\FastRouteRouter::class => InvokableFactory::class,
|
||||
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
|
||||
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
|
||||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||
|
||||
'delegators' => [
|
||||
Expressive\Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
@@ -6,14 +10,10 @@ return [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
],
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'user' => env('DB_USER'),
|
||||
'password' => env('DB_PASSWORD'),
|
||||
'dbname' => env('DB_NAME', 'shlink'),
|
||||
'user' => Common\env('DB_USER'),
|
||||
'password' => Common\env('DB_PASSWORD'),
|
||||
'dbname' => Common\env('DB_NAME', 'shlink'),
|
||||
'charset' => 'utf8',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
14
config/autoload/entity-manager.local.php.dist
Normal file
14
config/autoload/entity-manager.local.php.dist
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'host' => 'shlink_db',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php
|
||||
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ContentBasedErrorHandler;
|
||||
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
|
||||
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
|
||||
|
||||
return [
|
||||
'dependencies' => [
|
||||
@@ -21,7 +20,7 @@ return [
|
||||
'error_handler' => [
|
||||
'plugins' => [
|
||||
'factories' => [
|
||||
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
|
||||
'text/html' => WhoopsErrorResponseGeneratorFactory::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
|
||||
@@ -1,19 +1,53 @@
|
||||
<?php
|
||||
use Zend\Expressive\Container\ApplicationFactory;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Zend\Expressive;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'pre-routing' => [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
],
|
||||
'priority' => 12,
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\PathVersionMiddleware::class,
|
||||
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
|
||||
],
|
||||
'priority' => 11,
|
||||
],
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::ROUTING_MIDDLEWARE,
|
||||
Expressive\Router\Middleware\RouteMiddleware::class,
|
||||
],
|
||||
'priority' => 10,
|
||||
],
|
||||
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\CrossDomainMiddleware::class,
|
||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::DISPATCH_MIDDLEWARE,
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
Core\Response\NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'phpwkhtmltopdf' => [
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'preview_generation' => [
|
||||
|
||||
15
config/autoload/router.global.php
Normal file
15
config/autoload/router.global.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\Expressive\Router\FastRouteRouter;
|
||||
|
||||
return [
|
||||
|
||||
'router' => [
|
||||
'fastroute' => [
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
|
||||
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
12
config/autoload/router.local.php.dist
Normal file
12
config/autoload/router.local.php.dist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
use Zend\Expressive\Router\FastRouteRouter;
|
||||
|
||||
return [
|
||||
|
||||
'router' => [
|
||||
'fastroute' => [
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,9 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'twig' => [
|
||||
'cache_dir' => 'data/cache/twig',
|
||||
'templates' => [
|
||||
'extension' => 'phtml',
|
||||
],
|
||||
|
||||
'plates' => [
|
||||
'extensions' => [
|
||||
// extension service names or instances
|
||||
],
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'locale' => env('DEFAULT_LOCALE', 'en'),
|
||||
'locale' => Common\env('DEFAULT_LOCALE', 'en'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
@@ -9,6 +12,7 @@ return [
|
||||
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
||||
],
|
||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||
'validate_url' => true,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$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);
|
||||
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Shlinkio\Shlink\CLI;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core;
|
||||
use Shlinkio\Shlink\Rest;
|
||||
use Zend\Expressive\ConfigManager;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
|
||||
/**
|
||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
||||
* then ``local.php`` and finally ``*.local.php``. This way local settings overwrite global settings.
|
||||
*
|
||||
* The configuration can be cached. This can be done by setting ``config_cache_enabled`` to ``true``.
|
||||
*
|
||||
* Obviously, if you use closures in your config you can't cache it.
|
||||
*/
|
||||
|
||||
return (new ConfigManager\ConfigManager([
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
Expressive\ConfigProvider::class,
|
||||
Expressive\Router\ConfigProvider::class,
|
||||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Expressive\Plates\ConfigProvider::class,
|
||||
Expressive\Helper\ConfigProvider::class,
|
||||
\class_exists(Expressive\Swoole\ConfigProvider::class)
|
||||
? Expressive\Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Installer\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigManager\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
use Dotenv\Dotenv;
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
@@ -9,9 +11,9 @@ require 'vendor/autoload.php';
|
||||
// If the Dotenv class exists, load env vars and enable errors
|
||||
if (class_exists(Dotenv::class)) {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
$dotenv = new Dotenv(__DIR__ . '/..');
|
||||
$dotenv->load();
|
||||
ini_set('display_errors', '1');
|
||||
$dotenv = new Dotenv();
|
||||
$dotenv->load(__DIR__ . '/../.env');
|
||||
}
|
||||
|
||||
// Build container
|
||||
|
||||
29
config/install-container.php
Normal file
29
config/install-container.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
2
data/infra/database/.gitignore
vendored
Executable file
2
data/infra/database/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
6
data/infra/db.Dockerfile
Normal file
6
data/infra/db.Dockerfile
Normal file
@@ -0,0 +1,6 @@
|
||||
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
|
||||
5
data/infra/nginx.Dockerfile
Normal file
5
data/infra/nginx.Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM nginx:1.11.6-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
# Delete default nginx vhost
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
2
data/infra/nginx/.gitignore
vendored
Executable file
2
data/infra/nginx/.gitignore
vendored
Executable file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
90
data/infra/php.Dockerfile
Normal file
90
data/infra/php.Dockerfile
Normal file
@@ -0,0 +1,90 @@
|
||||
FROM php:7.1.22-fpm-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
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 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
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/3.1.4.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/php7.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-5.1.3.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-1.0.3.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 xdebug
|
||||
ADD https://pecl.php.net/get/xdebug-2.5.0 /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
|
||||
RUN docker-php-ext-configure xdebug\
|
||||
&& docker-php-ext-install xdebug
|
||||
# cleanup
|
||||
RUN rm /tmp/xdebug.tar.gz
|
||||
|
||||
# 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
|
||||
1
data/infra/php.ini
Normal file
1
data/infra/php.ini
Normal file
@@ -0,0 +1 @@
|
||||
date.timezone = Europe/Madrid
|
||||
21
data/infra/vhost.conf
Normal file
21
data/infra/vhost.conf
Normal file
@@ -0,0 +1,21 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name shlink.local;
|
||||
root /home/shlink/www/public;
|
||||
index index.php;
|
||||
|
||||
charset utf-8;
|
||||
error_log /home/shlink/www/data/infra/nginx/shlink.error.log;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
root /home/shlink/www/public;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass shlink_php:9000;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
|
||||
49
data/migrations/Version20171021093246.php
Normal file
49
data/migrations/Version20171021093246.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20171021093246 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('valid_since')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('valid_since', Type::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
$shortUrls->addColumn('valid_until', Type::DATETIME, [
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('valid_since')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->dropColumn('valid_since');
|
||||
$shortUrls->dropColumn('valid_until');
|
||||
}
|
||||
}
|
||||
46
data/migrations/Version20171022064541.php
Normal file
46
data/migrations/Version20171022064541.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20171022064541 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if ($shortUrls->hasColumn('max_visits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->addColumn('max_visits', Type::INTEGER, [
|
||||
'unsigned' => true,
|
||||
'notnull' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
if (! $shortUrls->hasColumn('max_visits')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$shortUrls->dropColumn('max_visits');
|
||||
}
|
||||
}
|
||||
45
data/migrations/Version20180801183328.php
Normal file
45
data/migrations/Version20180801183328.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\SchemaException;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180801183328 extends AbstractMigration
|
||||
{
|
||||
private const NEW_SIZE = 255;
|
||||
private const OLD_SIZE = 10;
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::NEW_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->setSize($schema, self::OLD_SIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param int $size
|
||||
* @throws SchemaException
|
||||
*/
|
||||
private function setSize(Schema $schema, int $size): void
|
||||
{
|
||||
$schema->getTable('short_urls')->getColumn('short_code')->setLength($size);
|
||||
}
|
||||
}
|
||||
74
data/migrations/Version20180913205455.php
Normal file
74
data/migrations/Version20180913205455.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20180913205455 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Nothing to create
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->select('id', 'remote_addr')
|
||||
->from('visits');
|
||||
$st = $this->connection->executeQuery($qb->getSQL());
|
||||
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$qb->update('visits', 'v')
|
||||
->set('v.remote_addr', ':obfuscatedAddr')
|
||||
->where('v.id=:id');
|
||||
|
||||
while ($row = $st->fetch(\PDO::FETCH_ASSOC)) {
|
||||
$addr = $row['remote_addr'] ?? null;
|
||||
if ($addr === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$qb->setParameters([
|
||||
'id' => $row['id'],
|
||||
'obfuscatedAddr' => $this->determineAddress((string) $addr),
|
||||
])->execute();
|
||||
}
|
||||
}
|
||||
|
||||
private function determineAddress(string $addr): ?string
|
||||
{
|
||||
if ($addr === IpAddress::LOCALHOST) {
|
||||
return $addr;
|
||||
}
|
||||
|
||||
try {
|
||||
return (string) IpAddress::fromString($addr)->getObfuscatedCopy();
|
||||
} catch (WrongIpException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to rollback
|
||||
}
|
||||
}
|
||||
50
data/migrations/Version20180915110857.php
Normal file
50
data/migrations/Version20180915110857.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?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 Version20180915110857 extends AbstractMigration
|
||||
{
|
||||
private const ON_DELETE_MAP = [
|
||||
'visit_locations' => 'SET NULL',
|
||||
'short_urls' => 'CASCADE',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @throws SchemaException
|
||||
*/
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$foreignKeys = $visits->getForeignKeys();
|
||||
|
||||
// Remove all existing foreign keys and add them again with CASCADE delete
|
||||
foreach ($foreignKeys as $foreignKey) {
|
||||
$visits->removeForeignKey($foreignKey->getName());
|
||||
$foreignTable = $foreignKey->getForeignTableName();
|
||||
|
||||
$visits->addForeignKeyConstraint(
|
||||
$foreignTable,
|
||||
$foreignKey->getLocalColumns(),
|
||||
$foreignKey->getForeignColumns(),
|
||||
[
|
||||
'onDelete' => self::ON_DELETE_MAP[$foreignTable],
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Nothing to run
|
||||
}
|
||||
}
|
||||
0
data/proxies/.gitignore
vendored
Normal file → Executable file
0
data/proxies/.gitignore
vendored
Normal file → Executable file
8
docker-compose.override.yml.dist
Normal file
8
docker-compose.override.yml.dist
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
shlink_php:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
41
docker-compose.yml
Normal file
41
docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/nginx.Dockerfile
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./docs:/home/shlink/www/public/docs
|
||||
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
|
||||
links:
|
||||
- shlink_php
|
||||
|
||||
shlink_php:
|
||||
container_name: shlink_php
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/php.Dockerfile
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/db.Dockerfile
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- ./:/home/shlink/www
|
||||
- ./data/infra/database:/var/lib/mysql
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: shlink
|
||||
13
docs/swagger/definitions/Error.json
Normal file
13
docs/swagger/definitions/Error.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "A machine unique code"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "A human-friendly error message"
|
||||
}
|
||||
}
|
||||
}
|
||||
25
docs/swagger/definitions/Pagination.json
Normal file
25
docs/swagger/definitions/Pagination.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currentPage": {
|
||||
"type": "integer",
|
||||
"description": "The number of current page."
|
||||
},
|
||||
"pagesCount": {
|
||||
"type": "integer",
|
||||
"description": "The total number of pages that can be obtained."
|
||||
},
|
||||
"itemsPerPage": {
|
||||
"type": "integer",
|
||||
"description": "The number of items for every page."
|
||||
},
|
||||
"itemsInCurrentPage": {
|
||||
"type": "integer",
|
||||
"description": "The number of items in current page (could be smaller than itemsPerPage)."
|
||||
},
|
||||
"totalItems": {
|
||||
"type": "integer",
|
||||
"description": "The total number of items among all pages."
|
||||
}
|
||||
}
|
||||
}
|
||||
38
docs/swagger/definitions/ShortUrl.json
Normal file
38
docs/swagger/definitions/ShortUrl.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shortCode": {
|
||||
"type": "string",
|
||||
"description": "The short code for this short URL."
|
||||
},
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The short URL."
|
||||
},
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date in which the short URL was created in ISO format."
|
||||
},
|
||||
"visitsCount": {
|
||||
"type": "integer",
|
||||
"description": "The number of visits that this short URL has recieved."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "A list of tags applied to this short URL"
|
||||
},
|
||||
"originalUrl": {
|
||||
"deprecated": true,
|
||||
"type": "string",
|
||||
"description": "The original long URL. [DEPRECATED. Use longUrl instead]"
|
||||
}
|
||||
}
|
||||
}
|
||||
26
docs/swagger/definitions/Visit.json
Normal file
26
docs/swagger/definitions/Visit.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"referer": {
|
||||
"type": "string",
|
||||
"description": "The origin from which the visit was performed"
|
||||
},
|
||||
"date": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date in which the visit was performed"
|
||||
},
|
||||
"userAgent": {
|
||||
"type": "string",
|
||||
"description": "The user agent from which the visit was performed"
|
||||
},
|
||||
"visitLocation": {
|
||||
"$ref": "./VisitLocation.json"
|
||||
},
|
||||
"remoteAddr": {
|
||||
"type": "string",
|
||||
"description": "This value is deprecated and will always be null",
|
||||
"deprecated": true
|
||||
}
|
||||
}
|
||||
}
|
||||
26
docs/swagger/definitions/VisitLocation.json
Normal file
26
docs/swagger/definitions/VisitLocation.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cityName": {
|
||||
"type": "string"
|
||||
},
|
||||
"countryCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"countryName": {
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "string"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "string"
|
||||
},
|
||||
"regionName": {
|
||||
"type": "string"
|
||||
},
|
||||
"timezone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
84
docs/swagger/paths/v1_authenticate.json
Normal file
84
docs/swagger/paths/v1_authenticate.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "authenticate",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "[Deprecated] Perform authentication",
|
||||
"description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"apiKey"
|
||||
],
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"description": "The API key to authenticate with",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The authentication worked.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"description": "The authentication token that needs to be sent in the Authorization header"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "An API key was not provided.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "The API key is incorrect, is disabled or has expired.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
docs/swagger/paths/v1_short-urls.json
Normal file
253
docs/swagger/paths/v1_short-urls.json
Normal file
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "listShortUrls",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "List short URLs",
|
||||
"description": "Returns the list of short URLs.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "The page to be displayed. Defaults to 1",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "searchTerm",
|
||||
"in": "query",
|
||||
"description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "orderBy",
|
||||
"in": "query",
|
||||
"description": "The field from which you want to order the result. (Since v1.3.0)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"longUrl",
|
||||
"shortCode",
|
||||
"dateCreated",
|
||||
"visits"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of short URLs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shortUrls": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../definitions/Pagination.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 328,
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
"shortUrl": "https://doma.in/123bA",
|
||||
"longUrl": "https://www.google.com",
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 5,
|
||||
"pagesCount": 12,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 115
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"post": {
|
||||
"operationId": "createShortUrl",
|
||||
"tags": [
|
||||
"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.",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"longUrl"
|
||||
],
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"description": "The URL to parse",
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"description": "The URL to parse",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"customSlug": {
|
||||
"description": "A unique custom slug to be used instead of the generated short code",
|
||||
"type": "string"
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The result of parsing the long URL",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL that has been parsed"
|
||||
},
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The generated short URL"
|
||||
},
|
||||
"shortCode": {
|
||||
"type": "string",
|
||||
"description": "the short code that is being used in the short URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The long URL was not provided or is invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
docs/swagger/paths/v1_short-urls_shorten.json
Normal file
126
docs/swagger/paths/v1_short-urls_shorten.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortenUrl",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create a short URL",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
"in": "query",
|
||||
"description": "The API key used to authenticate the request",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "longUrl",
|
||||
"in": "query",
|
||||
"description": "The URL to be shortened",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"in": "query",
|
||||
"description": "The format in which you want the response to be returned. You can also use the \"Accept\" header instead of this",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"txt",
|
||||
"json"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of short URLs",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"longUrl": {
|
||||
"type": "string",
|
||||
"description": "The original long URL that has been shortened"
|
||||
},
|
||||
"shortUrl": {
|
||||
"type": "string",
|
||||
"description": "The generated short URL"
|
||||
},
|
||||
"shortCode": {
|
||||
"type": "string",
|
||||
"description": "the short code that is being used in the short URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"longUrl": "https://github.com/shlinkio/shlink",
|
||||
"shortUrl": "https://dom.ain/abc123",
|
||||
"shortCode": "abc123"
|
||||
},
|
||||
"text/plain": "https://dom.ain/abc123"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The long URL was not provided or is invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "INVALID_URL",
|
||||
"message": "Provided URL foo is invalid. Try with a different one."
|
||||
},
|
||||
"text/plain": "INVALID_URL"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "UNKNOWN_ERROR",
|
||||
"message": "Unexpected error occurred"
|
||||
},
|
||||
"text/plain": "UNKNOWN_ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
240
docs/swagger/paths/v1_short-urls_{shortCode}.json
Normal file
240
docs/swagger/paths/v1_short-urls_{shortCode}.json
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "getShortUrl",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Parse short code",
|
||||
"description": "Get the long URL behind a short URL's short code.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The URL info behind a short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/ShortUrl.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"operationId": "editShortUrl",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit short URL",
|
||||
"description": "Update certain meta arguments from an existing 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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string"
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short code has been properly updated."
|
||||
},
|
||||
"400": {
|
||||
"description": "Provided meta arguments are invalid.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteShortUrl",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Delete short URL",
|
||||
"description": "Deletes the short URL for provided short code.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short URL has been properly deleted."
|
||||
},
|
||||
"400": {
|
||||
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"error": "INVALID_SHORTCODE_DELETION",
|
||||
"message": "It is not possible to delete URL with short code \"abc123\" because it has reached more than \"15\" visits."
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
110
docs/swagger/paths/v1_short-urls_{shortCode}_tags.json
Normal file
110
docs/swagger/paths/v1_short-urls_{shortCode}_tags.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"put": {
|
||||
"operationId": "editShortUrlTags",
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit tags on short URL",
|
||||
"description": "Edit the tags on URL identified by provided short code.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL in which we want to edit tags.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The list of tags to set to the short URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of tags.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The request body does not contain a \"tags\" param with array type.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No short URL was found for provided short code.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
docs/swagger/paths/v1_short-urls_{shortCode}_visits.json
Normal file
126
docs/swagger/paths/v1_short-urls_{shortCode}_visits.json
Normal file
@@ -0,0 +1,126 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "getShortUrlVisits",
|
||||
"tags": [
|
||||
"Visits"
|
||||
],
|
||||
"summary": "List visits for short URL",
|
||||
"description": "Get the list of visits on the short URL behind provided short code.<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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL from which we want to get the visits.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "startDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) from which we want to get visits.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "endDate",
|
||||
"in": "query",
|
||||
"description": "The date (in ISO-8601 format) until which we want to get visits.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of visits.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visits": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "../definitions/Visit.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"visits": {
|
||||
"data": [
|
||||
{
|
||||
"referer": "https://twitter.com",
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
|
||||
"visitLocation": null
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
|
||||
"visitLocation": {
|
||||
"cityName": "Cupertino",
|
||||
"countryCode": "US",
|
||||
"countryName": "United States",
|
||||
"latitude": "37.3042",
|
||||
"longitude": "-122.0946",
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The short code does not belong to any short URL.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
275
docs/swagger/paths/v1_tags.json
Normal file
275
docs/swagger/paths/v1_tags.json
Normal file
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "listTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "List existing tags",
|
||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": {
|
||||
"data": [
|
||||
"games",
|
||||
"php",
|
||||
"shlink",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"post": {
|
||||
"operationId": "createTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"tags"
|
||||
],
|
||||
"properties": {
|
||||
"tags": {
|
||||
"description": "The list of tag names to create",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": {
|
||||
"data": [
|
||||
"games",
|
||||
"php",
|
||||
"shlink",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"operationId": "renameTag",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Rename tag",
|
||||
"description": "Renames one existing tag",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"oldName",
|
||||
"newName"
|
||||
],
|
||||
"properties": {
|
||||
"oldName": {
|
||||
"description": "Current name of the tag",
|
||||
"type": "string"
|
||||
},
|
||||
"newName": {
|
||||
"description": "New name of the tag",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The tag has been properly renamed"
|
||||
},
|
||||
"400": {
|
||||
"description": "You have not provided either the oldName or the newName params.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "There's no tag found with the name provided in oldName param.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Delete tags",
|
||||
"description": "Deletes provided list of tags",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "The names of the tags to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Tags properly deleted"
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
docs/swagger/swagger.json
Normal file
86
docs/swagger/swagger.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"version": "1.0"
|
||||
},
|
||||
|
||||
"servers": [
|
||||
{
|
||||
"url": "{schema}://{server}/rest",
|
||||
"variables": {
|
||||
"schema": {
|
||||
"default": "https",
|
||||
"enum": ["https", "http"]
|
||||
},
|
||||
"server": {
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"ApiKey": {
|
||||
"description": "A valid shlink API key",
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Api-Key"
|
||||
},
|
||||
"Bearer": {
|
||||
"description": "**[Deprecated]** The JWT identifying a previously authenticated API key",
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"tags": [
|
||||
{
|
||||
"name": "Short URLs",
|
||||
"description": "Operations that can be performed on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Tags",
|
||||
"description": "Let you handle the list of available tags"
|
||||
},
|
||||
{
|
||||
"name": "Visits",
|
||||
"description": "Operations to manage visits on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Authentication-related endpoints"
|
||||
}
|
||||
],
|
||||
|
||||
"paths": {
|
||||
"/v1/short-urls": {
|
||||
"$ref": "paths/v1_short-urls.json"
|
||||
},
|
||||
"/v1/short-urls/shorten": {
|
||||
"$ref": "paths/v1_short-urls_shorten.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/v1/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/v1/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/v1/short-urls/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||
},
|
||||
|
||||
"/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
func_tests_bootstrap.php
Normal file
33
func_tests_bootstrap.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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');
|
||||
2
indocker
Executable file
2
indocker
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bash
|
||||
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"
|
||||
22
infection.json
Normal file
22
infection.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"source": {
|
||||
"directories": [
|
||||
"module/*/src"
|
||||
],
|
||||
"excludes": []
|
||||
},
|
||||
"timeout": 10,
|
||||
"logs": {
|
||||
"text": "build/infection/infection-log.txt",
|
||||
"summary": "build/infection/summary-log.txt",
|
||||
"debug": "build/infection/debug-log.txt"
|
||||
},
|
||||
"tmpDir": "build/infection/temp",
|
||||
"phpUnit": {
|
||||
"configDir": "."
|
||||
},
|
||||
"mutators": {
|
||||
"@default": true,
|
||||
"IdenticalEqual": false
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,36 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class,
|
||||
Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::class,
|
||||
Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::class,
|
||||
]
|
||||
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,27 +1,85 @@
|
||||
<?php
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
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 Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Application::class => ApplicationFactory::class,
|
||||
Application::class => Factory\ApplicationFactory::class,
|
||||
|
||||
Command\Shortcode\GenerateShortcodeCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
|
||||
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\ListKeysCommand::class => AnnotatedFactory::class,
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
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\Visit\ProcessVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpApiLocationResolver::class,
|
||||
'translator',
|
||||
],
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||
Command\Config\GenerateSecretCommand::class => ['translator'],
|
||||
|
||||
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],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
|
||||
Binary file not shown.
@@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-08-21 18:16+0200\n"
|
||||
"PO-Revision-Date: 2016-08-21 18:16+0200\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 1.8.7.1\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
@@ -24,8 +24,8 @@ msgid "The API key to disable"
|
||||
msgstr "La clave de API a deshabilitar"
|
||||
|
||||
#, php-format
|
||||
msgid "API key %s properly disabled"
|
||||
msgstr "Clave de API %s deshabilitada correctamente"
|
||||
msgid "API key \"%s\" properly disabled"
|
||||
msgstr "Clave de API \"%s\" deshabilitada correctamente"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" does not exist."
|
||||
@@ -39,8 +39,9 @@ msgstr ""
|
||||
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||
"válido en PHP."
|
||||
|
||||
msgid "Generated API key"
|
||||
msgstr "Generada clave de API"
|
||||
#, php-format
|
||||
msgid "Generated API key: \"%s\""
|
||||
msgstr "Generada clave de API. \"%s\""
|
||||
|
||||
msgid "Lists all the available API keys."
|
||||
msgstr "Lista todas las claves de API disponibles."
|
||||
@@ -51,12 +52,12 @@ msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||
msgid "Key"
|
||||
msgstr "Clave"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
msgid "Is enabled"
|
||||
msgstr "Está habilitada"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
||||
@@ -65,8 +66,9 @@ msgstr ""
|
||||
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
|
||||
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
|
||||
|
||||
msgid "Character set:"
|
||||
msgstr "Grupo de caracteres:"
|
||||
#, php-format
|
||||
msgid "Character set: \"%s\""
|
||||
msgstr "Grupo de caracteres: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Generates a random secret string that can be used for JWT token encryption"
|
||||
@@ -74,8 +76,44 @@ msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
msgid "Secret key:"
|
||||
msgstr "Clave secreta:"
|
||||
#, php-format
|
||||
msgid "Secret key: \"%s\""
|
||||
msgstr "Clave secreta: \"%s\""
|
||||
|
||||
msgid "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 "
|
||||
@@ -92,14 +130,13 @@ msgid "Processing URL %s..."
|
||||
msgstr "Procesando URL %s..."
|
||||
|
||||
msgid " <info>Success!</info>"
|
||||
msgstr "<info>¡Correcto!</info>"
|
||||
msgstr " <info>¡Correcto!</info>"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short code for provided URL and returns the short URL"
|
||||
msgstr ""
|
||||
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
||||
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"
|
||||
@@ -107,22 +144,53 @@ 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 "A long URL was not provided. Which URL do you want to shorten?:"
|
||||
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 URL:"
|
||||
msgstr "URL procesada:"
|
||||
msgid "Processed long URL:"
|
||||
msgstr "URL larga procesada:"
|
||||
|
||||
msgid "Generated URL:"
|
||||
msgstr "URL generada:"
|
||||
msgid "Generated short URL:"
|
||||
msgstr "URL corta generada:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
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 "
|
||||
@@ -140,8 +208,8 @@ msgid "Allows to filter visits, returning only those newer than end date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to use?:"
|
||||
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
msgid "A short code was not provided. Which short code do you want to use?"
|
||||
msgstr "No se proporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
|
||||
msgid "Referer"
|
||||
msgstr "Origen"
|
||||
@@ -149,12 +217,12 @@ msgstr "Origen"
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "Remote Address"
|
||||
msgstr "Dirección remota"
|
||||
|
||||
msgid "User agent"
|
||||
msgstr "Agente de usuario"
|
||||
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
msgid "List all short URLs"
|
||||
msgstr "Listar todas las URLs cortas"
|
||||
|
||||
@@ -162,14 +230,33 @@ msgstr "Listar todas las URLs cortas"
|
||||
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 "Original URL"
|
||||
msgstr "URL original"
|
||||
msgid "Short URL"
|
||||
msgstr "URL corta"
|
||||
|
||||
msgid "Long URL"
|
||||
msgstr "URL larga"
|
||||
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
@@ -180,8 +267,8 @@ msgstr "Número de visitas"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "You have reached last page"
|
||||
msgstr "Has alcanzado la última página"
|
||||
msgid "Short URLs properly listed"
|
||||
msgstr "URLs cortas listadas correctamente"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
@@ -192,14 +279,10 @@ msgstr "Devuelve la URL larga detrás de un código corto"
|
||||
msgid "The short code to parse"
|
||||
msgstr "El código corto a convertir"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to parse?:"
|
||||
msgid "A short code was not provided. Which short code do you want to parse?"
|
||||
msgstr ""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
|
||||
|
||||
#, php-format
|
||||
msgid "No URL found for short code \"%s\""
|
||||
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
msgid "Long URL:"
|
||||
msgstr "URL larga:"
|
||||
|
||||
@@ -207,6 +290,53 @@ msgstr "URL larga:"
|
||||
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"
|
||||
|
||||
@@ -220,5 +350,30 @@ msgstr "Ignorada IP de localhost"
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "An error occurred while locating IP"
|
||||
msgstr "Se produjo un error al localizar la IP"
|
||||
|
||||
#, php-format
|
||||
msgid "IP location resolver limit reached. Waiting %s seconds..."
|
||||
msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
||||
#~ msgid "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"
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
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
|
||||
*/
|
||||
@@ -21,42 +25,30 @@ class DisableKeyCommand extends Command
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* DisableKeyCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('api-key:disable')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Disables an API key.'))
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$output->writeln(sprintf(
|
||||
$this->translator->translate('API key %s properly disabled'),
|
||||
'<info>' . $apiKey . '</info>'
|
||||
));
|
||||
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('API key "%s" does not exist.') . '</error>',
|
||||
$apiKey
|
||||
));
|
||||
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Cake\Chronos\Chronos;
|
||||
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
|
||||
*/
|
||||
@@ -21,23 +26,16 @@ class GenerateKeyCommand extends Command
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateKeyCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('api-key:generate')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
@@ -47,10 +45,13 @@ class GenerateKeyCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
||||
$output->writeln($this->translator->translate('Generated API key') . sprintf(': <info>%s</info>', $apiKey));
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function array_filter;
|
||||
use function sprintf;
|
||||
|
||||
class ListKeysCommand extends Command
|
||||
{
|
||||
private const ERROR_STRING_PATTERN = '<fg=red>%s</>';
|
||||
private const SUCCESS_STRING_PATTERN = '<info>%s</info>';
|
||||
private const WARNING_STRING_PATTERN = '<comment>%s</comment>';
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
@@ -23,23 +31,16 @@ class ListKeysCommand extends Command
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListKeysCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('api-key:list')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
@@ -49,60 +50,51 @@ class ListKeysCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
$list = $this->apiKeyService->listKeys($enabledOnly);
|
||||
|
||||
$table = new Table($output);
|
||||
if ($enabledOnly) {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
} else {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Is enabled'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
}
|
||||
$rows = [];
|
||||
|
||||
/** @var ApiKey $row */
|
||||
foreach ($list as $row) {
|
||||
$key = $row->getKey();
|
||||
$expiration = $row->getExpirationDate();
|
||||
$rowData = [];
|
||||
$messagePattern = $this->determineMessagePattern($row);
|
||||
|
||||
if ($enabledOnly) {
|
||||
$rowData[] = $key;
|
||||
} else {
|
||||
$rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key);
|
||||
$rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---');
|
||||
// Set columns for this row
|
||||
$rowData = [sprintf($messagePattern, $key)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
}
|
||||
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
||||
|
||||
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
|
||||
$table->addRow($rowData);
|
||||
$rows[] = $rowData;
|
||||
}
|
||||
|
||||
$table->render();
|
||||
$io->table(array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
]), $rows);
|
||||
}
|
||||
|
||||
private function determineMessagePattern(ApiKey $apiKey): string
|
||||
{
|
||||
if (! $apiKey->isEnabled()) {
|
||||
return self::ERROR_STRING_PATTERN;
|
||||
}
|
||||
|
||||
return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @param ApiKey $apiKey
|
||||
* @return string
|
||||
*/
|
||||
protected function getErrorString($string)
|
||||
private function getEnabledSymbol(ApiKey $apiKey): string
|
||||
{
|
||||
return sprintf('<fg=red>%s</>', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getSuccessString($string)
|
||||
{
|
||||
return sprintf('<info>%s</info>', $string);
|
||||
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
public const NAME = 'config:generate-charset';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateCharsetCommand constructor.
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({"translator"})
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('config:generate-charset')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(sprintf($this->translator->translate(
|
||||
'Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
|
||||
), UrlShortener::DEFAULT_CHARS));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
$output->writeln($this->translator->translate('Character set:') . sprintf(' <info>%s</info>', $charSet));
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
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;
|
||||
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
public const NAME = 'config:generate-secret';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateCharsetCommand constructor.
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({"translator"})
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('config:generate-secret')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate(
|
||||
'Generates a random secret string that can be used for JWT token encryption'
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
$output->writeln($this->translator->translate('Secret key:') . sprintf(' <info>%s</info>', $secret));
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @var InputInterface
|
||||
*/
|
||||
private $input;
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
private $questionHelper;
|
||||
/**
|
||||
* @var ProcessHelper
|
||||
*/
|
||||
private $processHelper;
|
||||
/**
|
||||
* @var WriterInterface
|
||||
*/
|
||||
private $configWriter;
|
||||
|
||||
/**
|
||||
* InstallCommand constructor.
|
||||
* @param WriterInterface $configWriter
|
||||
* @param callable|null $databaseCreationLogic
|
||||
*/
|
||||
public function __construct(WriterInterface $configWriter)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->configWriter = $configWriter;
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shlink:install')
|
||||
->setDescription('Installs Shlink');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->questionHelper = $this->getHelper('question');
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
$params = [];
|
||||
|
||||
$output->writeln([
|
||||
'<info>Welcome to Shlink!!</info>',
|
||||
'This process will guide you through the installation.',
|
||||
]);
|
||||
|
||||
// Check if a cached config file exists and drop it if so
|
||||
if (file_exists('data/cache/app_config.php')) {
|
||||
$output->write('Deleting old cached config...');
|
||||
if (unlink('data/cache/app_config.php')) {
|
||||
$output->writeln(' <info>Success</info>');
|
||||
} else {
|
||||
$output->writeln(
|
||||
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
|
||||
. ' new config applied.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for custom config params
|
||||
$params['DATABASE'] = $this->askDatabase();
|
||||
$params['URL_SHORTENER'] = $this->askUrlShortener();
|
||||
$params['LANGUAGE'] = $this->askLanguage();
|
||||
$params['APP'] = $this->askApplication();
|
||||
|
||||
// Generate config params files
|
||||
$config = $this->buildAppConfig($params);
|
||||
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
|
||||
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// Generate database
|
||||
if (! $this->createDatabase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run database migrations
|
||||
$output->writeln('Updating database...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate proxies
|
||||
$output->writeln('Generating proxies...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function askDatabase()
|
||||
{
|
||||
$params = [];
|
||||
$this->printTitle('DATABASE');
|
||||
|
||||
// Select database type
|
||||
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select database type (defaults to ' . $databases[0] . '):</question>',
|
||||
$databases,
|
||||
0
|
||||
));
|
||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||
|
||||
// Ask for connection params if database is not SQLite
|
||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
$params['NAME'] = $this->ask('Database name', 'shlink');
|
||||
$params['USER'] = $this->ask('Database username');
|
||||
$params['PASSWORD'] = $this->ask('Database password');
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
protected function askUrlShortener()
|
||||
{
|
||||
$this->printTitle('URL SHORTENER');
|
||||
|
||||
// Ask for URL shortener params
|
||||
return [
|
||||
'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select schema for generated short URLs (defaults to http):</question>',
|
||||
['http', 'https'],
|
||||
0
|
||||
)),
|
||||
'HOSTNAME' => $this->ask('Hostname for generated URLs'),
|
||||
'CHARS' => $this->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
|
||||
];
|
||||
}
|
||||
|
||||
protected function askLanguage()
|
||||
{
|
||||
$this->printTitle('LANGUAGE');
|
||||
|
||||
return [
|
||||
'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select default language for the application in general (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select default language for CLI executions (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
protected function askApplication()
|
||||
{
|
||||
$this->printTitle('APPLICATION');
|
||||
|
||||
return [
|
||||
'SECRET' => $this->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: $this->generateRandomString(32),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
protected function printTitle($text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$length = strlen($text) + 4;
|
||||
$header = str_repeat('*', $length);
|
||||
|
||||
$this->output->writeln([
|
||||
'',
|
||||
'<info>' . $header . '</info>',
|
||||
'<info>* ' . strtoupper($text) . ' *</info>',
|
||||
'<info>' . $header . '</info>',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
*/
|
||||
protected function ask($text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if (isset($default)) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
$value = $this->questionHelper->ask($this->input, $this->output, new Question(
|
||||
'<question>' . $text . ':</question> ',
|
||||
$default
|
||||
));
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$this->output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && empty($default) && ! $allowEmpty);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
protected function buildAppConfig(array $params)
|
||||
{
|
||||
// Build simple config
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $params['APP']['SECRET'],
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => $params['DATABASE']['DRIVER'],
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $params['LANGUAGE']['DEFAULT'],
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $params['LANGUAGE']['CLI'],
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $params['URL_SHORTENER']['SCHEMA'],
|
||||
'hostname' => $params['URL_SHORTENER']['HOSTNAME'],
|
||||
],
|
||||
'shortcode_chars' => $params['URL_SHORTENER']['CHARS'],
|
||||
],
|
||||
];
|
||||
|
||||
// Build dynamic database config
|
||||
if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = 'data/database.sqlite';
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
|
||||
$config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
|
||||
$config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
protected function createDatabase()
|
||||
{
|
||||
$this->output->writeln('Initializing database...');
|
||||
return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param string $errorMessage
|
||||
* @return bool
|
||||
*/
|
||||
protected function runCommand($command, $errorMessage)
|
||||
{
|
||||
$process = $this->processHelper->run($this->output, $command);
|
||||
if ($process->isSuccessful()) {
|
||||
$this->output->writeln(' <info>Success!</info>');
|
||||
return true;
|
||||
} else {
|
||||
if ($this->output->isVerbose()) {
|
||||
return false;
|
||||
}
|
||||
$this->output->writeln(
|
||||
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class UpdateCommand extends InstallCommand
|
||||
{
|
||||
public function createDatabase()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
101
module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php
Normal file
101
module/CLI/src/Command/ShortUrl/DeleteShortUrlCommand.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete';
|
||||
private const ALIASES = ['short-code:delete'];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
*/
|
||||
private $deleteShortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$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')
|
||||
)
|
||||
->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'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$ignoreThreshold = $input->getOption('ignore-threshold');
|
||||
|
||||
try {
|
||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
$this->retry($io, $shortCode, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
|
||||
{
|
||||
$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());
|
||||
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
||||
$forceDelete = $io->confirm($this->translator->translate('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.'));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
*/
|
||||
@@ -27,14 +30,6 @@ class GeneratePreviewCommand extends Command
|
||||
*/
|
||||
private $shortUrlService;
|
||||
|
||||
/**
|
||||
* GeneratePreviewCommand constructor.
|
||||
* @param ShortUrlServiceInterface $shortUrlService
|
||||
* @param PreviewGeneratorInterface $previewGenerator
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ShortUrlService::class, PreviewGenerator::class, "translator"})
|
||||
*/
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
PreviewGeneratorInterface $previewGenerator,
|
||||
@@ -46,17 +41,19 @@ class GeneratePreviewCommand extends Command
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('shortcode:process-previews')
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$page = 1;
|
||||
do {
|
||||
@@ -68,22 +65,20 @@ class GeneratePreviewCommand extends Command
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
|
||||
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
|
||||
}
|
||||
|
||||
protected function processUrl($url, OutputInterface $output)
|
||||
private function processUrl($url, OutputInterface $output): void
|
||||
{
|
||||
try {
|
||||
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$messages = [' <error>' . $this->translator->translate('Error') . '</error>'];
|
||||
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
|
||||
if ($output->isVerbose()) {
|
||||
$messages[] = '<error>' . $e->__toString() . '</error>';
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
|
||||
$output->writeln($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php
Normal file
152
module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Shlinkio\Shlink\Core\Util\ShortUrlBuilderTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateShortUrlCommand extends Command
|
||||
{
|
||||
use ShortUrlBuilderTrait;
|
||||
|
||||
public const NAME = 'short-url:generate';
|
||||
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(
|
||||
UrlShortenerInterface $urlShortener,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
$this->domainConfig = $domainConfig;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$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'))
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
)
|
||||
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'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(
|
||||
'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(
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
))
|
||||
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
'This will limit the number of visits for this short URL.'
|
||||
));
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (! empty($longUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$longUrl = $io->ask(
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
|
||||
);
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error($this->translator->translate('A URL was not provided!'));
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = $input->getOption('tags');
|
||||
$processedTags = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$explodedTags = \explode(',', $tag);
|
||||
$processedTags = \array_merge($processedTags, $explodedTags);
|
||||
}
|
||||
$tags = $processedTags;
|
||||
$customSlug = $input->getOption('customSlug');
|
||||
$maxVisits = $input->getOption('maxVisits');
|
||||
|
||||
try {
|
||||
$shortCode = $this->urlShortener->urlToShortCode(
|
||||
new Uri($longUrl),
|
||||
$tags,
|
||||
$this->getOptionalDate($input, 'validSince'),
|
||||
$this->getOptionalDate($input, 'validUntil'),
|
||||
$customSlug,
|
||||
$maxVisits !== null ? (int) $maxVisits : null
|
||||
)->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),
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$io->error(\sprintf(
|
||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
||||
$longUrl
|
||||
));
|
||||
} 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
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function getOptionalDate(InputInterface $input, string $fieldName): ?Chronos
|
||||
{
|
||||
$since = $input->getOption($fieldName);
|
||||
return $since !== null ? Chronos::parse($since) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
declare(strict_types=1);
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
@@ -26,23 +28,18 @@ class GetVisitsCommand extends Command
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GetVisitsCommand constructor.
|
||||
* @param VisitsTrackerInterface $visitsTracker
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({VisitsTracker::class, "translator"})
|
||||
*/
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('shortcode:visits')
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
@@ -65,58 +62,52 @@ class GetVisitsCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?')
|
||||
);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
|
||||
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('Remote Address'),
|
||||
$this->translator->translate('User agent'),
|
||||
]);
|
||||
|
||||
$rows = [];
|
||||
foreach ($visits as $row) {
|
||||
$rowData = $row->jsonSerialize();
|
||||
// Unset location info
|
||||
unset($rowData['visitLocation']);
|
||||
|
||||
$table->addRow(array_values($rowData));
|
||||
// Unset location info and remote addr
|
||||
unset($rowData['visitLocation'], $rowData['remoteAddr']);
|
||||
|
||||
$rowData['country'] = $row->getVisitLocation()->getCountryName();
|
||||
|
||||
$rows[] = \array_values($rowData);
|
||||
}
|
||||
$table->render();
|
||||
$io->table([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('User agent'),
|
||||
$this->translator->translate('Country'),
|
||||
], $rows);
|
||||
}
|
||||
|
||||
protected function getDateOption(InputInterface $input, $key)
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (isset($value)) {
|
||||
$value = new \DateTime($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
return ! empty($value) ? Chronos::parse($value) : $value;
|
||||
}
|
||||
}
|
||||
155
module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
Normal file
155
module/CLI/src/Command/ShortUrl/ListShortUrlsCommand.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\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 ListShortUrlsCommand extends Command
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
private $shortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('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
|
||||
)
|
||||
->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'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('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'
|
||||
)
|
||||
)
|
||||
->addOption(
|
||||
'showTags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$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');
|
||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$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
|
||||
);
|
||||
}
|
||||
} while ($continue);
|
||||
}
|
||||
|
||||
private function processOrderBy(InputInterface $input)
|
||||
{
|
||||
$orderBy = $input->getOption('orderBy');
|
||||
if (empty($orderBy)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$orderBy = \explode(',', $orderBy);
|
||||
return \count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
||||
}
|
||||
}
|
||||
86
module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php
Normal file
86
module/CLI/src/Command/ShortUrl/ResolveUrlCommand.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:parse';
|
||||
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$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')
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?')
|
||||
);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$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())
|
||||
);
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
||||
);
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(
|
||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateShortcodeCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateShortcodeCommand constructor.
|
||||
* @param UrlShortenerInterface $urlShortener
|
||||
* @param TranslatorInterface $translator
|
||||
* @param array $domainConfig
|
||||
*
|
||||
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
|
||||
*/
|
||||
public function __construct(
|
||||
UrlShortenerInterface $urlShortener,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
$this->domainConfig = $domainConfig;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:generate')
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (! empty($longUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
|
||||
));
|
||||
|
||||
$longUrl = $helper->ask($input, $output, $question);
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
$tags = $input->getOption('tags');
|
||||
$processedTags = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$explodedTags = explode(',', $tag);
|
||||
$processedTags = array_merge($processedTags, $explodedTags);
|
||||
}
|
||||
$tags = $processedTags;
|
||||
|
||||
try {
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
|
||||
return;
|
||||
}
|
||||
|
||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
|
||||
$shortUrl = (new Uri())->withPath($shortCode)
|
||||
->withScheme($this->domainConfig['schema'])
|
||||
->withHost($this->domainConfig['hostname']);
|
||||
|
||||
$output->writeln([
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate(
|
||||
'Provided URL "%s" is invalid. Try with a different one.'
|
||||
) . '</error>',
|
||||
$longUrl
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListShortcodesCommand extends Command
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
private $shortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListShortcodesCommand constructor.
|
||||
* @param ShortUrlServiceInterface $shortUrlService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ShortUrlService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:list')
|
||||
->setDescription($this->translator->translate('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
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$page = intval($input->getOption('page'));
|
||||
$showTags = $input->getOption('tags');
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page);
|
||||
$page++;
|
||||
$table = new Table($output);
|
||||
|
||||
$headers = [
|
||||
$this->translator->translate('Short code'),
|
||||
$this->translator->translate('Original URL'),
|
||||
$this->translator->translate('Date created'),
|
||||
$this->translator->translate('Visits count'),
|
||||
];
|
||||
if ($showTags) {
|
||||
$headers[] = $this->translator->translate('Tags');
|
||||
}
|
||||
$table->setHeaders($headers);
|
||||
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $row->jsonSerialize();
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = [];
|
||||
foreach ($row->getTags() as $tag) {
|
||||
$shortUrl['tags'][] = $tag->getName();
|
||||
}
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
$table->addRow(array_values($shortUrl));
|
||||
}
|
||||
$table->render();
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$output->writeln(
|
||||
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
|
||||
);
|
||||
} else {
|
||||
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
|
||||
sprintf('<question>' . $this->translator->translate(
|
||||
'Continue with page'
|
||||
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
|
||||
false
|
||||
));
|
||||
}
|
||||
} while ($continue);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ResolveUrlCommand constructor.
|
||||
* @param UrlShortenerInterface $urlShortener
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({UrlShortener::class, "translator"})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:parse')
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
|
||||
$shortCode
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$output->writeln(sprintf('<error>' . $this->translator->translate(
|
||||
'Provided short code "%s" has an invalid format.'
|
||||
) . '</error>', $shortCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
60
module/CLI/src/Command/Tag/CreateTagCommand.php
Normal file
60
module/CLI/src/Command/Tag/CreateTagCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
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
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('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')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly created'));
|
||||
}
|
||||
}
|
||||
60
module/CLI/src/Command/Tag/DeleteTagsCommand.php
Normal file
60
module/CLI/src/Command/Tag/DeleteTagsCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
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
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('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')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly deleted'));
|
||||
}
|
||||
}
|
||||
59
module/CLI/src/Command/Tag/ListTagsCommand.php
Normal file
59
module/CLI/src/Command/Tag/ListTagsCommand.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function array_map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||
}
|
||||
|
||||
private function getTagsRows()
|
||||
{
|
||||
$tags = $this->tagService->listTags();
|
||||
if (empty($tags)) {
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
}
|
||||
|
||||
return array_map(function (Tag $tag) {
|
||||
return [$tag->getName()];
|
||||
}, $tags);
|
||||
}
|
||||
}
|
||||
58
module/CLI/src/Command/Tag/RenameTagCommand.php
Normal file
58
module/CLI/src/Command/Tag/RenameTagCommand.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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 RenameTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:rename';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
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.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
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 function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
public const NAME = 'visit:process';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
@@ -30,14 +33,6 @@ class ProcessVisitsCommand extends Command
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ProcessVisitsCommand constructor.
|
||||
* @param VisitServiceInterface $visitService
|
||||
* @param IpLocationResolverInterface $ipLocationResolver
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
|
||||
*/
|
||||
public function __construct(
|
||||
VisitServiceInterface $visitService,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
@@ -49,43 +44,71 @@ class ProcessVisitsCommand extends Command
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('visit:process')
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Processes visits where location is not set yet')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$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();
|
||||
$output->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === self::LOCALHOST) {
|
||||
$output->writeln(
|
||||
$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);
|
||||
$output->writeln(sprintf(
|
||||
|
||||
$io->writeln(sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
continue;
|
||||
$io->writeln(
|
||||
sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||
);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
}
|
||||
|
||||
if ($count === $this->ipLocationResolver->getApiLimit()) {
|
||||
$count = 0;
|
||||
$seconds = $this->ipLocationResolver->getApiInterval();
|
||||
$io->note(sprintf(
|
||||
$this->translator->translate('IP location resolver limit reached. Waiting %s seconds...'),
|
||||
$seconds
|
||||
));
|
||||
sleep($seconds);
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln($this->translator->translate('Finished processing all IPs'));
|
||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Zend\Config\Factory;
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
@@ -18,28 +23,23 @@ class ApplicationFactory implements FactoryInterface
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @return CliApp
|
||||
* @throws NotFoundExceptionInterface
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): CliApp
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$translator = $container->get(Translator::class);
|
||||
$translator->setLocale($config['locale']);
|
||||
|
||||
$commands = isset($config['commands']) ? $config['commands'] : [];
|
||||
$commands = $config['commands'] ?? [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
foreach ($commands as $command) {
|
||||
if (! $container->has($command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$app->add($container->get($command));
|
||||
}
|
||||
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
@@ -57,6 +59,6 @@ class DisableKeyCommandTest extends TestCase
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('API key "abcd1234" does not exist.' . PHP_EOL, $output);
|
||||
$this->assertContains('API key "abcd1234" does not exist.', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
@@ -35,7 +39,8 @@ class GenerateKeyCommandTest extends TestCase
|
||||
*/
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
]);
|
||||
@@ -44,9 +49,10 @@ class GenerateKeyCommandTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function expirationDateIsDefinedIfWhenProvided()
|
||||
public function expirationDateIsDefinedIfProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
'--expirationDate' => '2016-01-01',
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Config;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
@@ -30,7 +31,6 @@ class GenerateCharsetCommandTest extends TestCase
|
||||
public function charactersAreGeneratedFromDefault()
|
||||
{
|
||||
$prefix = 'Character set: ';
|
||||
$prefixLength = strlen($prefix);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'config:generate-charset',
|
||||
@@ -38,13 +38,7 @@ class GenerateCharsetCommandTest extends TestCase
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Both default character set and the new one should have the same length
|
||||
$this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output));
|
||||
|
||||
// Both default character set and the new one should have the same characters
|
||||
$charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS));
|
||||
$orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS);
|
||||
$orderedCharset = $this->orderStringLetters($charset);
|
||||
$this->assertEquals($orderedDefault, $orderedCharset);
|
||||
$this->assertContains($prefix, $output);
|
||||
}
|
||||
|
||||
protected function orderStringLetters($string)
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Install;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $configWriter;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$processMock = $this->prophesize(Process::class);
|
||||
$processMock->isSuccessful()->willReturn(true);
|
||||
$processHelper = $this->prophesize(ProcessHelper::class);
|
||||
$processHelper->getName()->willReturn('process');
|
||||
$processHelper->setHelperSet(Argument::any())->willReturn(null);
|
||||
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
|
||||
|
||||
$app = new Application();
|
||||
$helperSet = $app->getHelperSet();
|
||||
$helperSet->set($processHelper->reveal());
|
||||
$app->setHelperSet($helperSet);
|
||||
|
||||
$this->configWriter = $this->prophesize(WriterInterface::class);
|
||||
$command = new InstallCommand($this->configWriter->reveal());
|
||||
$app->add($command);
|
||||
|
||||
$questionHelper = $command->getHelper('question');
|
||||
$questionHelper->setInputStream($this->createInputStream());
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
protected function createInputStream()
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+', false);
|
||||
fputs($stream, <<<CLI_INPUT
|
||||
|
||||
shlink_db
|
||||
alejandro
|
||||
1234
|
||||
0
|
||||
doma.in
|
||||
abc123BCA
|
||||
|
||||
1
|
||||
my_secret
|
||||
CLI_INPUT
|
||||
);
|
||||
rewind($stream);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function testInputIsProperlyParsed()
|
||||
{
|
||||
$this->configWriter->toFile(Argument::any(), [
|
||||
'app_options' => [
|
||||
'secret_key' => 'my_secret',
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'dbname' => 'shlink_db',
|
||||
'user' => 'alejandro',
|
||||
'password' => '1234',
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => 'en',
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => 'es',
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
'shortcode_chars' => 'abc123BCA',
|
||||
],
|
||||
], false)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shlink:install',
|
||||
]);
|
||||
}
|
||||
}
|
||||
120
module/CLI/test/Command/ShortUrl/DeleteShortCodeCommandTest.php
Normal file
120
module/CLI/test/Command/ShortUrl/DeleteShortCodeCommandTest.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $service;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function successMessageIsPrintedIfUrlIsProperlyDeleted()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->will(function () {
|
||||
});
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function invalidShortCodePrintsMessage()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
Exception\InvalidShortCodeException::class
|
||||
);
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
|
||||
function (array $args) {
|
||||
$ignoreThreshold = \array_pop($args);
|
||||
|
||||
if (!$ignoreThreshold) {
|
||||
throw new Exception\DeleteShortUrlException(10);
|
||||
}
|
||||
}
|
||||
);
|
||||
$this->commandTester->setInputs(['yes']);
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, false)->willThrow(
|
||||
new Exception\DeleteShortUrlException(10)
|
||||
);
|
||||
$this->commandTester->setInputs(['no']);
|
||||
|
||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(\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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user