mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
589 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
228bd83b75 | ||
|
|
a21dcb852a | ||
|
|
058391cf06 | ||
|
|
24e6acc6e8 | ||
|
|
56d299a7dc | ||
|
|
575e6bf707 | ||
|
|
e50c21440f | ||
|
|
7cff11080d | ||
|
|
72381f9844 | ||
|
|
b6792d3fb8 | ||
|
|
2f0d658432 | ||
|
|
8c1865c3ec | ||
|
|
096d2098d6 | ||
|
|
882d64ae11 | ||
|
|
3352bcd186 | ||
|
|
9743c1624d | ||
|
|
e85d59c5a4 | ||
|
|
ac0ff8fb94 | ||
|
|
90f93ee4ec | ||
|
|
794d926e3a | ||
|
|
bd41ebef9f | ||
|
|
725370704f | ||
|
|
f03b7689ce | ||
|
|
fb31e2a5e4 | ||
|
|
d688c6da7e | ||
|
|
618784dc3b | ||
|
|
9d64d4ed1d | ||
|
|
7f02243c6c | ||
|
|
3916c68126 | ||
|
|
a6f0c66331 | ||
|
|
bdfb220126 | ||
|
|
abcf2f86be | ||
|
|
a4d8ebdfc9 | ||
|
|
b51c149c30 | ||
|
|
39095a3098 | ||
|
|
765199727e | ||
|
|
c7043af853 | ||
|
|
02a8ef7dd9 | ||
|
|
6bb8c1b2f5 | ||
|
|
3cf253fd0f | ||
|
|
0365728337 | ||
|
|
b8143a5bb4 | ||
|
|
531a19dde9 | ||
|
|
69ff7de481 | ||
|
|
ffc0555c7c | ||
|
|
84a7981dfa | ||
|
|
2573c2bf98 | ||
|
|
3b4c1501f3 | ||
|
|
e836bedecc | ||
|
|
a797b74a70 | ||
|
|
ab497403ca | ||
|
|
d4dea9a1d2 | ||
|
|
28d93ea5e0 | ||
|
|
e6a31b16ed | ||
|
|
9553192281 | ||
|
|
74069f2d24 | ||
|
|
b4b00a57c1 | ||
|
|
a516ef691d | ||
|
|
e80b7448f5 | ||
|
|
f129544f83 | ||
|
|
9fa291a32f | ||
|
|
d06e92ffc2 | ||
|
|
1b83344995 | ||
|
|
cf49393ef2 | ||
|
|
f2ecbceae9 | ||
|
|
c582eba753 | ||
|
|
de86b62cdd | ||
|
|
73150471e9 | ||
|
|
ec751f4ac2 | ||
|
|
e652166289 | ||
|
|
a671d555cb | ||
|
|
6240554f4c | ||
|
|
4ee9c9bbe3 | ||
|
|
c830439085 | ||
|
|
f2196583c8 | ||
|
|
3dbca2115c | ||
|
|
b45d8de27d | ||
|
|
3ba46bbbfa | ||
|
|
06f3f0c86c | ||
|
|
06f07e3e40 | ||
|
|
740740b8c6 | ||
|
|
b6ed39b18b | ||
|
|
958c4704f8 | ||
|
|
ef075fb0ce | ||
|
|
556520583a | ||
|
|
399c56a097 | ||
|
|
f078d95588 | ||
|
|
33911afcd6 | ||
|
|
ae8d31e83f | ||
|
|
72c4052012 | ||
|
|
f713a1fa7e | ||
|
|
62488ac4e5 | ||
|
|
ab4c6e5fca | ||
|
|
26f4a969c9 | ||
|
|
703965915d | ||
|
|
24e38a3cf9 | ||
|
|
b12cfaedf3 | ||
|
|
71807e698c | ||
|
|
1d155298c1 | ||
|
|
4dfc5ae681 | ||
|
|
26f237069c | ||
|
|
b6e1c65c4c | ||
|
|
11f94b8306 | ||
|
|
01bcedef7a | ||
|
|
e51384fcc0 | ||
|
|
83c53c8b2e | ||
|
|
1afe08caed | ||
|
|
7289833928 | ||
|
|
f4d10df0f3 | ||
|
|
652b0df054 | ||
|
|
0e9ea5027c | ||
|
|
658303d375 | ||
|
|
ccc3a4b584 | ||
|
|
ef5ac86e0a | ||
|
|
91b90b276a | ||
|
|
85c32c3c9a | ||
|
|
40838255a7 | ||
|
|
a67ccb384f | ||
|
|
cb31e5a581 | ||
|
|
3c12a55872 | ||
|
|
6da8b11674 | ||
|
|
552489611f | ||
|
|
e48d0f4f0c | ||
|
|
49b6063501 | ||
|
|
dd049feb40 | ||
|
|
76a86c452e | ||
|
|
41aec15fab | ||
|
|
245cb0e35d | ||
|
|
7a0b1e8494 | ||
|
|
70c1c9f018 | ||
|
|
97e965157b | ||
|
|
04bbd471ff | ||
|
|
650a286982 | ||
|
|
ad44a8441a | ||
|
|
b339cf2429 | ||
|
|
9cd97c2f1e | ||
|
|
a7f6b60cba | ||
|
|
0d7dc50670 | ||
|
|
4bc5b9261f | ||
|
|
fb572d5abb | ||
|
|
8fa4219b30 | ||
|
|
a52d0cd419 | ||
|
|
0080ab5132 | ||
|
|
8afa582aa5 | ||
|
|
d847c7648e | ||
|
|
c140db16d1 | ||
|
|
adbf7c6f5e | ||
|
|
5cec697be3 | ||
|
|
587bbfdd73 | ||
|
|
b3a2ceedea | ||
|
|
621f18bf40 | ||
|
|
99c1a59dd4 | ||
|
|
3a149c9edc | ||
|
|
fdaf5fb2f3 | ||
|
|
2f83e90c8b | ||
|
|
05acd4ae88 | ||
|
|
87007677ed | ||
|
|
4ee0032c2a | ||
|
|
06583a0bc1 | ||
|
|
024c9c1a7a | ||
|
|
f3855dbc6f | ||
|
|
758dac47c3 | ||
|
|
81393a76b4 | ||
|
|
9949bb654d | ||
|
|
b0b9902f40 | ||
|
|
5aa8de11f4 | ||
|
|
b18c9e495f | ||
|
|
d3590234a3 | ||
|
|
39adef8ab8 | ||
|
|
13e443880a | ||
|
|
45961144b9 | ||
|
|
34129b8d24 | ||
|
|
48bd97fe41 | ||
|
|
b1b67c497e | ||
|
|
237fb95b4b | ||
|
|
c1b7c6ba6c | ||
|
|
d8add9291f | ||
|
|
a93edf158e | ||
|
|
fdadf3ba07 | ||
|
|
3e26f1113d | ||
|
|
822652cac3 | ||
|
|
1447687ebe | ||
|
|
12150f775d | ||
|
|
5f2f179581 | ||
|
|
407134bab1 | ||
|
|
de5b895fad | ||
|
|
80e3f01562 | ||
|
|
6904dcfed0 | ||
|
|
21863e8de6 | ||
|
|
d75be372cb | ||
|
|
edaf999bf5 | ||
|
|
3e98485c8b | ||
|
|
cc292886a6 | ||
|
|
0c1b36d0d4 | ||
|
|
a06957e9fa | ||
|
|
390bc59d99 | ||
|
|
85464f0fbb | ||
|
|
42f7a68ba5 | ||
|
|
e3397a7c90 | ||
|
|
46b4a21617 | ||
|
|
fc0aba6311 | ||
|
|
0b96a79c41 | ||
|
|
a5929ebb29 | ||
|
|
ce9ec0d738 | ||
|
|
961178fd82 | ||
|
|
49c73a9590 | ||
|
|
92c80e7833 | ||
|
|
6d5bce0078 | ||
|
|
112cbb9039 | ||
|
|
812c5f4993 | ||
|
|
921f303404 | ||
|
|
e0a9f8120c | ||
|
|
8ecc241a4b | ||
|
|
30e34151ed | ||
|
|
d734578f74 | ||
|
|
37c8328eed | ||
|
|
e71f6bb528 | ||
|
|
f7ae52f86e | ||
|
|
067d1cc41c | ||
|
|
b97af7efb9 | ||
|
|
fd0ecc05b2 | ||
|
|
5b934c3f9a | ||
|
|
c7a2f499e0 | ||
|
|
713f7e7bc9 | ||
|
|
09078e4c6a | ||
|
|
1f66ec2af5 | ||
|
|
936e5b3b86 | ||
|
|
99f28b569b | ||
|
|
0c83dea8b7 | ||
|
|
30edfdbdc5 | ||
|
|
60ef98b836 | ||
|
|
73c8b53882 | ||
|
|
425d8f0a3f | ||
|
|
92a83b82a0 | ||
|
|
d1ec15febf | ||
|
|
dd345c82ea | ||
|
|
2bf3e6a13b | ||
|
|
0b04476c99 | ||
|
|
229dc93132 | ||
|
|
0952c488be | ||
|
|
c4f28b3a32 | ||
|
|
201f25e0ad | ||
|
|
0c3523c34a | ||
|
|
0d7a0ee9ea | ||
|
|
931bdb0cd7 | ||
|
|
8807a78463 | ||
|
|
d832133410 | ||
|
|
cdde59b543 | ||
|
|
463dfe9729 | ||
|
|
805c8c87ba | ||
|
|
7ba2cfc010 | ||
|
|
40794c476f | ||
|
|
c3ab871366 | ||
|
|
42a5296f93 | ||
|
|
183db4ff80 | ||
|
|
0bc9bd9281 | ||
|
|
9bed7ef156 | ||
|
|
8f68e4b9f5 | ||
|
|
6589c8fce6 | ||
|
|
38b313a25d | ||
|
|
dab0ebeb99 | ||
|
|
27bf7220b9 | ||
|
|
e68ef87c66 | ||
|
|
29b747c192 | ||
|
|
2047d6b772 | ||
|
|
71e7938b7a | ||
|
|
6bce219eb3 | ||
|
|
dfcac525bc | ||
|
|
da307aee0a | ||
|
|
edf2b5b4c2 | ||
|
|
f41d947cf7 | ||
|
|
54bc169525 | ||
|
|
05d55c4000 | ||
|
|
739f5eb421 | ||
|
|
0aab1bdc4e | ||
|
|
47f99cf6cc | ||
|
|
55c9773a02 | ||
|
|
4b66aaba5c | ||
|
|
4223408090 | ||
|
|
58e6b0b683 | ||
|
|
891438c672 | ||
|
|
910864eaaf | ||
|
|
598c0757be | ||
|
|
01e0a95e14 | ||
|
|
f459a99e7e | ||
|
|
85e18a4754 | ||
|
|
1650499a38 | ||
|
|
51f243995a | ||
|
|
aeafb244d9 | ||
|
|
142417dda1 | ||
|
|
da658185c3 | ||
|
|
ef82158368 | ||
|
|
083ccd36b7 | ||
|
|
d61c79da84 | ||
|
|
8f76c3e202 | ||
|
|
23aa7a015c | ||
|
|
674a4416cf | ||
|
|
db85915c2f | ||
|
|
dfc8e8d74e | ||
|
|
b2b424a4ed | ||
|
|
3433899577 | ||
|
|
b1f814e118 | ||
|
|
7aa6afeb30 | ||
|
|
d414496a3c | ||
|
|
d4684fd01f | ||
|
|
bb444a02fe | ||
|
|
e980a8d121 | ||
|
|
f493baaf2b | ||
|
|
28f26920dd | ||
|
|
69e994c067 | ||
|
|
656083cb6f | ||
|
|
ab9ea887d2 | ||
|
|
9ac6a50e66 | ||
|
|
acc9cb94b5 | ||
|
|
01829c82ee | ||
|
|
9c02ea8799 | ||
|
|
d202538581 | ||
|
|
a84b642ba5 | ||
|
|
74176c298f | ||
|
|
91e21441f7 | ||
|
|
896b7f2d73 | ||
|
|
66ed152358 | ||
|
|
257134cd80 | ||
|
|
a4373aee91 | ||
|
|
7442905873 | ||
|
|
d3af51f684 | ||
|
|
04419a7242 | ||
|
|
a45d6e6b44 | ||
|
|
37b1306eb3 | ||
|
|
cff6573767 | ||
|
|
a2f34e02ad | ||
|
|
796543d194 | ||
|
|
3b25fb27fe | ||
|
|
3b20f955ff | ||
|
|
c81ae9c40d | ||
|
|
7ceae7af87 | ||
|
|
5e02cfe375 | ||
|
|
6e836b5fd9 | ||
|
|
8753e3a77f | ||
|
|
6a2227efc5 | ||
|
|
1fbcea7a06 | ||
|
|
168c839cf1 | ||
|
|
162e913cc4 | ||
|
|
5aaf50d68e | ||
|
|
d2f5be1d18 | ||
|
|
36ab455a49 | ||
|
|
ee8cab8455 | ||
|
|
bd884e85d4 | ||
|
|
5ceb6fb740 | ||
|
|
0d6155e8bc | ||
|
|
a78c59c11a | ||
|
|
173420c608 | ||
|
|
10b0ec301b | ||
|
|
1706a869d9 | ||
|
|
d0393799d2 | ||
|
|
739433ba8b | ||
|
|
a15e9c29c8 | ||
|
|
d58f89aa26 | ||
|
|
b7671f70da | ||
|
|
52366b9dd4 | ||
|
|
32417e40cb | ||
|
|
4cb44be9a0 | ||
|
|
a484455b0b | ||
|
|
4b3ed2b7ba | ||
|
|
e2986a7b4c | ||
|
|
82e04800aa | ||
|
|
5d367da626 | ||
|
|
59de5a5f55 | ||
|
|
0855104068 | ||
|
|
8c6f97c4e2 | ||
|
|
2d16856582 | ||
|
|
41e903cf26 | ||
|
|
4872bd3a92 | ||
|
|
8b675f55cc | ||
|
|
acda7f02c6 | ||
|
|
184ff90b9f | ||
|
|
d8be3c28cb | ||
|
|
3d358ab046 | ||
|
|
960bdfc232 | ||
|
|
101b4daff4 | ||
|
|
13431ff8cf | ||
|
|
4cdcad29df | ||
|
|
a4c34ff7be | ||
|
|
2b7b5e9a8f | ||
|
|
58db902084 | ||
|
|
983e3c9eaa | ||
|
|
dbe35cf567 | ||
|
|
8298f9d491 | ||
|
|
16a951b938 | ||
|
|
51fcbfb3c2 | ||
|
|
e01e370d16 | ||
|
|
736ac8ba90 | ||
|
|
d07104b8d9 | ||
|
|
cad53e397a | ||
|
|
3608a6d068 | ||
|
|
92ddd2eebe | ||
|
|
bf0b58b344 | ||
|
|
ff543b151c | ||
|
|
d842025835 | ||
|
|
230e56370a | ||
|
|
a8514a9ae4 | ||
|
|
148f7a9cfe | ||
|
|
29d50cabc2 | ||
|
|
a8f8297131 | ||
|
|
cd4b632d75 | ||
|
|
843754b7e7 | ||
|
|
847cc2bc50 | ||
|
|
751bd15785 | ||
|
|
c12db7567e | ||
|
|
e8069a10ba | ||
|
|
9742bf13e4 | ||
|
|
6441707c76 | ||
|
|
23bcba4fd9 | ||
|
|
9049a205b7 | ||
|
|
8cfa0b595c | ||
|
|
4b958e8b87 | ||
|
|
bcd5d2848d | ||
|
|
b59cbeceac | ||
|
|
46f948a584 | ||
|
|
14bf3a134b | ||
|
|
1557438fdf | ||
|
|
27b680e0cd | ||
|
|
14314ef939 | ||
|
|
bf5c168d7d | ||
|
|
1e0791416d | ||
|
|
ab8d42b609 | ||
|
|
96dbdbe7c9 | ||
|
|
6f135ad6ab | ||
|
|
5b9a1e1978 | ||
|
|
4ba3522e79 | ||
|
|
d3faa22b78 | ||
|
|
1daad334a5 | ||
|
|
3dda49dab4 | ||
|
|
c6c4e5580b | ||
|
|
3f808e3813 | ||
|
|
e5107c40f9 | ||
|
|
0871ca884e | ||
|
|
62ce9311bf | ||
|
|
70b15a7ab0 | ||
|
|
708bff20f0 | ||
|
|
369628ee95 | ||
|
|
0c6f8f1136 | ||
|
|
9f9d011d46 | ||
|
|
e28b73c130 | ||
|
|
56f953ab2f | ||
|
|
3ad8be175c | ||
|
|
f5f990511c | ||
|
|
1e3ccba503 | ||
|
|
a842b5b7cd | ||
|
|
909e42b0be | ||
|
|
c8acb5de68 | ||
|
|
53b9e3ddc1 | ||
|
|
68e1c61e7f | ||
|
|
8605b35b57 | ||
|
|
36680e82aa | ||
|
|
83b7d5a5f1 | ||
|
|
fe41e9d573 | ||
|
|
d76e6647d2 | ||
|
|
6f17f70137 | ||
|
|
ef01754ad5 | ||
|
|
eab9347522 | ||
|
|
59bcd62717 | ||
|
|
3f01fad12f | ||
|
|
c7f0d14c1b | ||
|
|
2408829627 | ||
|
|
8d244c8d34 | ||
|
|
42af057316 | ||
|
|
8f68078835 | ||
|
|
0c34032fd3 | ||
|
|
20f457a3e9 | ||
|
|
39693ca1fe | ||
|
|
784908420e | ||
|
|
9685929824 | ||
|
|
fe4b2c4ae4 | ||
|
|
5f87bb13f8 | ||
|
|
a87f6c6709 | ||
|
|
da3ee6b65e | ||
|
|
c5eda37bda | ||
|
|
1966367caf | ||
|
|
eed7b6e565 | ||
|
|
0e54ed691d | ||
|
|
997289da02 | ||
|
|
c841e57db5 | ||
|
|
f5138385be | ||
|
|
63ceba199d | ||
|
|
e6ee4ceae2 | ||
|
|
19a9d815eb | ||
|
|
5b78b363f0 | ||
|
|
b078c00492 | ||
|
|
e712efd008 | ||
|
|
ab27c0ce53 | ||
|
|
d97cabbe79 | ||
|
|
c3c7ffad25 | ||
|
|
fe4329d730 | ||
|
|
c53ba7b119 | ||
|
|
025eec6c70 | ||
|
|
40e1670314 | ||
|
|
2bca260627 | ||
|
|
463d8e8950 | ||
|
|
e2eed8a728 | ||
|
|
f97effcfe0 | ||
|
|
2cf21ab3bd | ||
|
|
7daa602630 | ||
|
|
7b637d6a61 | ||
|
|
a4f979be08 | ||
|
|
8852739111 | ||
|
|
2099ea16ec | ||
|
|
a739eb6d60 | ||
|
|
529ddacafe | ||
|
|
f71c95b74a | ||
|
|
8260a0843b | ||
|
|
bfbeb7b1fb | ||
|
|
df70810aa6 | ||
|
|
aca5804f98 | ||
|
|
b7f7288a4b | ||
|
|
d54a2bde0f | ||
|
|
679bb8d357 | ||
|
|
ca515998e4 | ||
|
|
c5b6d203f5 | ||
|
|
86159c5d86 | ||
|
|
846802c003 | ||
|
|
e9ec32b3c3 | ||
|
|
4882bec118 | ||
|
|
89ff259be0 | ||
|
|
60ece7fbf7 | ||
|
|
0c110f574a | ||
|
|
dbca5b2a7e | ||
|
|
3088298e6b | ||
|
|
a9c6a12182 | ||
|
|
fa5b512629 | ||
|
|
5c2061a6e6 | ||
|
|
cf0fc956c9 | ||
|
|
a0517dfbeb | ||
|
|
39c71638e6 | ||
|
|
672b728379 | ||
|
|
750a546faf | ||
|
|
a41835573b | ||
|
|
2650cb89b5 | ||
|
|
4a122e0209 | ||
|
|
ce4bf62d75 | ||
|
|
40bbcb3250 | ||
|
|
905f51fbd0 | ||
|
|
cd4fe4362b | ||
|
|
ed7be6eb99 | ||
|
|
555007ab16 | ||
|
|
bd31b99324 | ||
|
|
60237c3c0b | ||
|
|
eb21833d94 | ||
|
|
763002ae14 | ||
|
|
ae2dc39a78 | ||
|
|
fe4ced2709 | ||
|
|
9075d68b7c | ||
|
|
759c0ea957 | ||
|
|
67b393d4a3 | ||
|
|
de71821759 | ||
|
|
0c2bcaee34 | ||
|
|
1613975e0e | ||
|
|
be82204df2 | ||
|
|
14c2ff5545 | ||
|
|
d7d0e11f2c | ||
|
|
6654f45cb8 | ||
|
|
23f92179ad | ||
|
|
7377917642 | ||
|
|
0f796859f2 | ||
|
|
6383230678 | ||
|
|
51536f8746 | ||
|
|
e3b6c061c4 | ||
|
|
4bd3fa74d1 | ||
|
|
71553988d5 | ||
|
|
761b24e614 | ||
|
|
10974902b5 | ||
|
|
474407dbc2 | ||
|
|
95d84f354d | ||
|
|
db47a9a253 | ||
|
|
709a4639b3 | ||
|
|
28b9cd02ef | ||
|
|
af9ea13933 | ||
|
|
bd2cd18916 | ||
|
|
23138dc0b4 | ||
|
|
a2f9742cfc | ||
|
|
6378e614b0 | ||
|
|
b116a57aa7 | ||
|
|
a03f32f521 | ||
|
|
b9180be685 | ||
|
|
334aee64ad | ||
|
|
16bd368a58 | ||
|
|
4d082a87a1 | ||
|
|
1b6512fc8d | ||
|
|
9e32886f60 |
@@ -1,3 +1,4 @@
|
||||
bin/rr
|
||||
config/autoload/*local*
|
||||
data/infra
|
||||
data/cache/*
|
||||
@@ -22,4 +23,4 @@ infection*
|
||||
**/test*
|
||||
build*
|
||||
**/.*
|
||||
bin/helper
|
||||
!config/roadrunner/.rr.yml
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
-->
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/Bug.md
vendored
8
.github/ISSUE_TEMPLATE/Bug.md
vendored
@@ -7,18 +7,18 @@ labels: bug
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set-up
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Docker image
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
@@ -31,7 +31,7 @@ With that said, please fill in the information requested next. More information
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
<!-- How did you expected to behave? -->
|
||||
<!-- How did you expect it to behave? -->
|
||||
|
||||
#### How to reproduce
|
||||
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
2
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
@@ -7,7 +7,7 @@ labels: feature
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
6
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
@@ -7,18 +7,18 @@ labels: question
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set-up
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Docker image
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
47
.github/actions/ci-setup/action.yml
vendored
Normal file
47
.github/actions/ci-setup/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: CI setup
|
||||
description: 'Sets up the environment to run CI actions for Shlink'
|
||||
|
||||
inputs:
|
||||
install-deps:
|
||||
description: 'Tells if dependencies should be installed with composer. Default value is "yes"'
|
||||
required: true
|
||||
default: 'yes'
|
||||
php-version:
|
||||
description: 'The PHP version to be setup'
|
||||
required: true
|
||||
php-extensions:
|
||||
description: 'The PHP extensions to install'
|
||||
required: false
|
||||
default: ''
|
||||
extensions-cache-key:
|
||||
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Setup cache environment
|
||||
id: extcache
|
||||
uses: shivammathur/cache-extensions@v1
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
key: ${{ inputs.extensions-cache-key }}
|
||||
- name: Cache extensions
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ steps.extcache.outputs.dir }}
|
||||
key: ${{ steps.extcache.outputs.key }}
|
||||
restore-keys: ${{ steps.extcache.outputs.key }}
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
tools: composer
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
shell: bash
|
||||
44
.github/workflows/ci-db-tests.yml
vendored
Normal file
44
.github/workflows/ci-db-tests.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Database tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
platform:
|
||||
type: string
|
||||
required: true
|
||||
description: One of sqlite:ci, mysql, maria, postgres or ms
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ inputs.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0, pdo_sqlsrv-5.10.1
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
build/coverage-db
|
||||
build/coverage-db.cov
|
||||
14
.github/workflows/ci-docker-image-build.yml
vendored
Normal file
14
.github/workflows/ci-docker-image-build.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Build docker image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- run: docker build -t shlink-docker-image:temp .
|
||||
45
.github/workflows/ci-mutation-tests.yml
vendored
Normal file
45
.github/workflows/ci-mutation-tests.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Mutation tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
test-group:
|
||||
type: string
|
||||
required: true
|
||||
description: One of unit, db, api or cli
|
||||
|
||||
jobs:
|
||||
mutation-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: build
|
||||
- name: Resolve infection args
|
||||
id: infection_args
|
||||
run: echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
||||
# run: |
|
||||
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
||||
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
||||
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
||||
# else
|
||||
# echo "args=--logger-github=false --git-diff-lines --git-diff-base=develop" >> $GITHUB_OUTPUT
|
||||
# fi;
|
||||
shell: bash
|
||||
- if: ${{ inputs.test-group == 'unit' }}
|
||||
run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }}
|
||||
env:
|
||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
||||
- if: ${{ inputs.test-group != 'unit' }}
|
||||
run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }}
|
||||
37
.github/workflows/ci-tests.yml
vendored
Normal file
37
.github/workflows/ci-tests.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
test-group:
|
||||
type: string
|
||||
required: true
|
||||
description: One of unit, api or cli
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start postgres database server
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Start maria database server
|
||||
if: ${{ inputs.test-group == 'cli' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
build/coverage-${{ inputs.test-group }}
|
||||
build/coverage-${{ inputs.test-group }}.cov
|
||||
239
.github/workflows/ci.yml
vendored
239
.github/workflows/ci.yml
vendored
@@ -1,12 +1,28 @@
|
||||
name: Continuous integration
|
||||
|
||||
on:
|
||||
pull_request: null
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- '.*'
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- 2.x
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- '.*'
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
|
||||
jobs:
|
||||
static-analysis:
|
||||
@@ -16,148 +32,128 @@ jobs:
|
||||
php-version: ['8.1']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: none
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
php-extensions: openswoole-22.0.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||
- run: composer ${{ matrix.command }}
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
test-group: ['unit', 'api']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Start database server
|
||||
if: ${{ matrix.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- run: composer test:${{ matrix.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
with:
|
||||
name: coverage-${{ matrix.test-group }}
|
||||
path: |
|
||||
build/coverage-${{ matrix.test-group }}
|
||||
build/coverage-${{ matrix.test-group }}.cov
|
||||
unit-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: unit
|
||||
|
||||
db-tests:
|
||||
cli-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: cli
|
||||
|
||||
openswoole-api-tests:
|
||||
uses: './.github/workflows/ci-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
roadrunner-api-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
||||
php-version: ['8.1', '8.2']
|
||||
env:
|
||||
LC_ALL: C
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ matrix.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ matrix.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ matrix.platform }}
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- name: Create test database
|
||||
if: ${{ matrix.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ matrix.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
build/coverage-db
|
||||
build/coverage-db.cov
|
||||
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole
|
||||
- run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:api:rr
|
||||
|
||||
mutation-tests:
|
||||
sqlite-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'sqlite:ci'
|
||||
|
||||
mysql-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'mysql'
|
||||
|
||||
maria-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'maria'
|
||||
|
||||
postgres-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'postgres'
|
||||
|
||||
ms-db-tests:
|
||||
uses: './.github/workflows/ci-db-tests.yml'
|
||||
with:
|
||||
platform: 'ms'
|
||||
|
||||
unit-mutation-tests:
|
||||
needs:
|
||||
- tests
|
||||
- db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
test-group: ['unit', 'db', 'api']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: build
|
||||
- if: ${{ matrix.test-group == 'unit' }}
|
||||
run: composer infect:ci:unit
|
||||
env:
|
||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
||||
- if: ${{ matrix.test-group != 'unit' }}
|
||||
run: composer infect:ci:${{ matrix.test-group }}
|
||||
- unit-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: unit
|
||||
|
||||
db-mutation-tests:
|
||||
needs:
|
||||
- sqlite-db-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: db
|
||||
|
||||
api-mutation-tests:
|
||||
needs:
|
||||
- openswoole-api-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: api
|
||||
|
||||
cli-mutation-tests:
|
||||
needs:
|
||||
- cli-tests
|
||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||
with:
|
||||
test-group: cli
|
||||
|
||||
upload-coverage:
|
||||
needs:
|
||||
- tests
|
||||
- db-tests
|
||||
- unit-tests
|
||||
- openswoole-api-tests
|
||||
- cli-tests
|
||||
- sqlite-db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
||||
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
|
||||
- run: php phpcov-8.2.1.phar merge build --clover build/clover.xml
|
||||
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
|
||||
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
|
||||
- name: Publish coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
@@ -165,29 +161,14 @@ jobs:
|
||||
|
||||
delete-artifacts:
|
||||
needs:
|
||||
- mutation-tests
|
||||
- unit-mutation-tests
|
||||
- db-mutation-tests
|
||||
- api-mutation-tests
|
||||
- cli-mutation-tests
|
||||
- upload-coverage
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v1
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: |
|
||||
coverage-unit
|
||||
coverage-db
|
||||
coverage-api
|
||||
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 100
|
||||
- uses: marceloprado/has-changed-path@v1
|
||||
id: changed-dockerfile
|
||||
with:
|
||||
paths: ./Dockerfile
|
||||
- if: ${{ steps.changed-dockerfile.outputs.changed == 'true' }}
|
||||
run: docker build -t shlink-docker-image:temp .
|
||||
- if: ${{ steps.changed-dockerfile.outputs.changed != 'true' }}
|
||||
run: echo "Dockerfile didn't change. Skipped"
|
||||
coverage-*
|
||||
|
||||
28
.github/workflows/docker-image-build.yml
vendored
28
.github/workflows/docker-image-build.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Build docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to docker hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build the image
|
||||
run: bash ./docker/build
|
||||
44
.github/workflows/publish-docker-image.yml
vendored
Normal file
44
.github/workflows/publish-docker-image.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Build and publish docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- '.*'
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runtime: 'rr'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
- runtime: 'rr'
|
||||
tag-suffix: 'roadrunner'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
- runtime: 'openswoole'
|
||||
tag-suffix: 'openswoole'
|
||||
platforms: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
||||
- runtime: 'rr'
|
||||
tag-suffix: 'non-root'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
user-id: '1001'
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
image-name: shlinkio/shlink
|
||||
version-arg-name: SHLINK_VERSION
|
||||
platforms: ${{ matrix.platforms }}
|
||||
tags-suffix: ${{ matrix.tag-suffix }}
|
||||
extra-build-args: |
|
||||
SHLINK_RUNTIME=${{ matrix.runtime }}
|
||||
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}
|
||||
28
.github/workflows/publish-release.yml
vendored
28
.github/workflows/publish-release.yml
vendored
@@ -10,22 +10,21 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.1', '8.2']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
php-extensions: openswoole-22.0.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
install-deps: 'no'
|
||||
- if: ${{ matrix.swoole == 'yes' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||
- if: ${{ matrix.swoole == 'no' }}
|
||||
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
||||
- uses: actions/upload-artifact@v2
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
||||
path: build
|
||||
@@ -34,9 +33,8 @@ jobs:
|
||||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
- name: Publish release with assets
|
||||
@@ -51,11 +49,7 @@ jobs:
|
||||
delete-artifacts:
|
||||
needs: ['publish']
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v1
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
||||
name: dist-files-*
|
||||
|
||||
16
.github/workflows/publish-swagger-spec.yml
vendored
16
.github/workflows/publish-swagger-spec.yml
vendored
@@ -12,25 +12,21 @@ jobs:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Determine version
|
||||
id: determine_version
|
||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
extensions: openswoole-4.11.1
|
||||
coverage: none
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
php-extensions: openswoole-22.0.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
- run: composer swagger:inline
|
||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||
- name: Publish spec
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.7
|
||||
uses: JamesIves/github-pages-deploy-action@4.4.1
|
||||
with:
|
||||
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
|
||||
repository-name: 'shlinkio/shlink-open-api-specs'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.idea
|
||||
bin/rr
|
||||
config/roadrunner/.pid
|
||||
build
|
||||
!docker/build
|
||||
composer.lock
|
||||
|
||||
365
CHANGELOG.md
365
CHANGELOG.md
@@ -4,6 +4,359 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [3.6.3] - 2023-06-14
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1817](https://github.com/shlinkio/shlink/issues/1817) Fix Shlink trying to create SQLite database tables even if they already exist.
|
||||
|
||||
|
||||
## [3.6.2] - 2023-06-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1808](https://github.com/shlinkio/shlink/issues/1808) Fix `rr` binary downloading during Shlink update.
|
||||
|
||||
|
||||
## [3.6.1] - 2023-06-04
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1413](https://github.com/shlinkio/shlink/issues/1413) Fix error when creating initial DB in Postgres in a cluster where a default `postgres` db does not exist or the credentials do not grant permissions to connect.
|
||||
* [#1803](https://github.com/shlinkio/shlink/issues/1803) Fix default RoadRunner port when not using docker image.
|
||||
|
||||
|
||||
## [3.6.0] - 2023-05-24
|
||||
### Added
|
||||
* [#1148](https://github.com/shlinkio/shlink/issues/1148) Add support to delete short URL visits.
|
||||
|
||||
This can be done via `DELETE /short-urls/{shortCode}/visits` REST endpoint or via `short-url:visits-delete` console command.
|
||||
|
||||
The CLI command includes a warning and requires the user to confirm before proceeding.
|
||||
|
||||
* [#1681](https://github.com/shlinkio/shlink/issues/1681) Add support to delete orphan visits.
|
||||
|
||||
This can be done via `DELETE /visits/orphan` REST endpoint or via `visit:orphan-delete` console command.
|
||||
|
||||
The CLI command includes a warning and requires the user to confirm before proceeding.
|
||||
|
||||
* [#1753](https://github.com/shlinkio/shlink/issues/1753) Add a new `vendor/bin/shlink-installer init` command that can be used to automate Shlink installations.
|
||||
|
||||
This command can create the initial database, update it, create proxies, clean cache, download initial GeoLite db files, etc
|
||||
|
||||
The official docker image also uses it on its entry point script.
|
||||
|
||||
* [#1656](https://github.com/shlinkio/shlink/issues/1656) Add support for openswoole 22
|
||||
* [#1784](https://github.com/shlinkio/shlink/issues/1784) Add new docker tag where the container runs as a non-root user.
|
||||
* [#953](https://github.com/shlinkio/shlink/issues/953) Add locks that prevent errors on duplicated keys when creating short URLs in parallel that depend on the same new tag or domain.
|
||||
|
||||
### Changed
|
||||
* [#1755](https://github.com/shlinkio/shlink/issues/1755) Update to roadrunner 2023
|
||||
* [#1745](https://github.com/shlinkio/shlink/issues/1745) Roadrunner is now the default docker runtime.
|
||||
|
||||
There are now three different docker images published:
|
||||
|
||||
* Versions without suffix (like `3.6.0`) will contain the default runtime, whichever it is.
|
||||
* Versions with `-roadrunner` suffix (like `3.6.0-roadrunner`) will always use roadrunner as the runtime, even if default one changes in the future.
|
||||
* Versions with `-openswoole` suffix (like `3.6.0-openswoole`) will always use openswoole as the runtime, even if default one changes in the future.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1760](https://github.com/shlinkio/shlink/issues/1760) Fix domain not being set to null when importing short URLs with default domain.
|
||||
* [#953](https://github.com/shlinkio/shlink/issues/953) Fix duplicated key errors and short URL creation failing when creating short URLs in parallel that depend on the same new tag or domain.
|
||||
* [#1741](https://github.com/shlinkio/shlink/issues/1741) Fix randomly using 100% CPU in task workers when trying to download GeoLite DB files.
|
||||
* Fix Shlink trying to connect to RabbitMQ even if configuration set to not connect.
|
||||
|
||||
|
||||
## [3.5.4] - 2023-04-12
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1742](https://github.com/shlinkio/shlink/issues/1742) Fix URLs using schemas which do not contain `//`, like `mailto:`, to no longer be considered valid.
|
||||
* [#1743](https://github.com/shlinkio/shlink/issues/1743) Fix Error when trying to create short URLs from CLI on an openswoole context.
|
||||
|
||||
Unfortunately the reason are real-time updates do not work with openswoole when outside an openswoole request, so the feature has been disabled for that context.
|
||||
|
||||
|
||||
## [3.5.3] - 2023-03-31
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1715](https://github.com/shlinkio/shlink/issues/1715) Fix short URL creation/edition allowing long URLs without schema. Now a validation error is thrown.
|
||||
* [#1537](https://github.com/shlinkio/shlink/issues/1537) Fix incorrect list of tags being returned for some author-only API keys.
|
||||
* [#1738](https://github.com/shlinkio/shlink/issues/1738) Fix memory leak when importing short URLs with many visits.
|
||||
|
||||
|
||||
## [3.5.2] - 2023-02-16
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1696](https://github.com/shlinkio/shlink/issues/1696) Migrated to PHPUnit 10.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1698](https://github.com/shlinkio/shlink/issues/1698) Fixed error 500 in `robots.txt`.
|
||||
* [#1688](https://github.com/shlinkio/shlink/issues/1688) Fixed huge performance degradation on `/tags/stats` endpoint.
|
||||
* [#1693](https://github.com/shlinkio/shlink/issues/1693) Fixed Shlink thinking database already exists if it finds foreign tables.
|
||||
|
||||
|
||||
## [3.5.1] - 2023-02-04
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1685](https://github.com/shlinkio/shlink/issues/1685) Changed `loosely` mode to `loose`, as it was a typo. The old one keeps working and maps to the new one, but it's considered deprecated.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1682](https://github.com/shlinkio/shlink/issues/1682) Fixed incorrect case-insensitive checks in short URLs when using Microsoft SQL server.
|
||||
* [#1684](https://github.com/shlinkio/shlink/issues/1684) Fixed entities metadata cache not being cleared at docker container start-up when using redis with replication.
|
||||
|
||||
|
||||
## [3.5.0] - 2023-01-28
|
||||
### Added
|
||||
* [#1557](https://github.com/shlinkio/shlink/issues/1557) Added support to dynamically redirect to different long URLs based on the visitor's device type.
|
||||
|
||||
For the moment, only `android`, `ios` and `desktop` can have their own specific long URL, and when the visitor cannot be matched against any of them, the regular long URL will be used.
|
||||
|
||||
In the future, more granular device types could be added if appropriate (iOS tablet, android table, tablet, mobile phone, Linux, Mac, Windows, etc).
|
||||
|
||||
In order to match the visitor's device, the `User-Agent` header is used.
|
||||
|
||||
* [#1632](https://github.com/shlinkio/shlink/issues/1632) Added amount of bots, non-bots and total visits to the visits summary endpoint.
|
||||
* [#1633](https://github.com/shlinkio/shlink/issues/1633) Added amount of bots, non-bots and total visits to the tag stats endpoint.
|
||||
* [#1653](https://github.com/shlinkio/shlink/issues/1653) Added support for all HTTP methods in short URLs, together with two new redirect status codes, 307 and 308.
|
||||
|
||||
Existing Shlink instances will continue to work the same. However, if you decide to set the redirect status codes as 307 or 308, Shlink will also return a redirect for short URLs even when the request method is different from `GET`.
|
||||
|
||||
The status 308 is equivalent to 301, and 307 is equivalent to 302. The difference is that the spec requires the client to respect the original HTTP method when performing the redirect. With 301 and 302, some old clients might perform a `GET` request during the redirect, regardless the original request method.
|
||||
|
||||
* [#1662](https://github.com/shlinkio/shlink/issues/1662) Added support to provide openswoole-specific config options via env vars prefixed with `OPENSWOOLE_`.
|
||||
* [#1389](https://github.com/shlinkio/shlink/issues/1389) and [#706](https://github.com/shlinkio/shlink/issues/706) Added support for case-insensitive short URLs.
|
||||
|
||||
In order to achieve this, a new env var/config option has been implemented (`SHORT_URL_MODE`), which allows either `strict` or ~~`loosely`~~ `loose`.
|
||||
|
||||
Default value is `strict`, but if `loose` is provided, then short URLs will be matched in a case-insensitive way, and new short URLs will be generated with short-codes in lowercase only.
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* [#1676](https://github.com/shlinkio/shlink/issues/1676) Deprecated `GET /short-urls/shorten` endpoint. Use `POST /short-urls` to create short URLs instead.
|
||||
* [#1678](https://github.com/shlinkio/shlink/issues/1678) Deprecated `validateUrl` option on URL creation/edition.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1639](https://github.com/shlinkio/shlink/issues/1639) Fixed 500 error returned when request body is not valid JSON, instead of a proper descriptive error.
|
||||
|
||||
|
||||
## [3.4.0] - 2022-12-16
|
||||
### Added
|
||||
* [#1612](https://github.com/shlinkio/shlink/issues/1612) Allowed to filter short URLs out of lists, when `validUntil` date is in the past or have reached their maximum amount of visits.
|
||||
|
||||
This can be done by:
|
||||
|
||||
* Providing `excludeMaxVisitsReached=true` and/or `excludePastValidUntil=true` to the `GET /short-urls` endpoint.
|
||||
* Providing `--exclude-max-visits-reached` and/or `--exclude-past-valid-until` to the `short-urls:list` command.
|
||||
|
||||
* [#1613](https://github.com/shlinkio/shlink/issues/1613) Added amount of visits coming from bots, non-bots and total to every short URL in the short URLs list.
|
||||
|
||||
Additionally, added option to order by non-bot visits, by passing `nonBotVisits-DESC` or `nonBotVisits-ASC`.
|
||||
|
||||
* [#1599](https://github.com/shlinkio/shlink/issues/1599) Added support for credentials on redis DSNs, either only password, or both username and password.
|
||||
* [#1616](https://github.com/shlinkio/shlink/issues/1616) Added support to import orphan visits when importing short URLs from another Shlink instance.
|
||||
* [#1519](https://github.com/shlinkio/shlink/issues/1519) Allowing to search short URLs by default domain.
|
||||
* [#1555](https://github.com/shlinkio/shlink/issues/1555) and [#1625](https://github.com/shlinkio/shlink/issues/1625) Added full support for PHP 8.2, updating the docker image to this version.
|
||||
|
||||
### Changed
|
||||
* [#1563](https://github.com/shlinkio/shlink/issues/1563) Moved logic to reuse command options to option classes instead of base abstract command classes.
|
||||
* [#1569](https://github.com/shlinkio/shlink/issues/1569) Migrated test doubles from phpspec/prophecy to PHPUnit mocks.
|
||||
* [#1329](https://github.com/shlinkio/shlink/issues/1329) Split some logic from `VisitRepository` and `ShortUrlRepository` into separated repository classes.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1618](https://github.com/shlinkio/shlink/issues/1618) Fixed imported short URLs and visits dates not being set to the target server timezone.
|
||||
* [#1578](https://github.com/shlinkio/shlink/issues/1578) Fixed short URL allowing an empty string as the domain during creation.
|
||||
* [#1580](https://github.com/shlinkio/shlink/issues/1580) Fixed `FLUSHDB` being run on Shlink docker start-up when using redis, causing full cache to be flushed.
|
||||
|
||||
|
||||
## [3.3.2] - 2022-10-18
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1576](https://github.com/shlinkio/shlink/issues/1576) Fixed error when trying to retry visits location from CLI.
|
||||
|
||||
|
||||
## [3.3.1] - 2022-09-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1474](https://github.com/shlinkio/shlink/issues/1474) Added preliminary support for PHP 8.2 during CI workflow.
|
||||
* [#1551](https://github.com/shlinkio/shlink/issues/1551) Moved services related to geolocating visits to the `Visit\Geolocation` namespace.
|
||||
* [#1550](https://github.com/shlinkio/shlink/issues/1550) Reorganized main namespaces from Core module.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1556](https://github.com/shlinkio/shlink/issues/1556) Fixed trailing slash not working when enabling multi-segment slashes.
|
||||
|
||||
|
||||
## [3.3.0] - 2022-09-18
|
||||
### Added
|
||||
* [#1221](https://github.com/shlinkio/shlink/issues/1221) Added experimental support to run Shlink with [RoadRunner](https://roadrunner.dev) instead of openswoole.
|
||||
* [#1531](https://github.com/shlinkio/shlink/issues/1531) and [#1090](https://github.com/shlinkio/shlink/issues/1090) Added support for trailing slashes in short URLs.
|
||||
* [#1406](https://github.com/shlinkio/shlink/issues/1406) Added new REST API version 3.
|
||||
|
||||
When making requests to the REST API with `/rest/v3/...` and an error occurs, all error types will be different, with the next correlation:
|
||||
|
||||
* `INVALID_ARGUMENT` -> `https://shlink.io/api/error/invalid-data`
|
||||
* `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion`
|
||||
* `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found`
|
||||
* `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation`
|
||||
* `INVALID_URL` -> `https://shlink.io/api/error/invalid-url`
|
||||
* `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug`
|
||||
* `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found`
|
||||
* `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict`
|
||||
* `TAG_NOT_FOUND` -> `https://shlink.io/api/error/tag-not-found`
|
||||
* `MERCURE_NOT_CONFIGURED` -> `https://shlink.io/api/error/mercure-not-configured`
|
||||
* `INVALID_AUTHORIZATION` -> `https://shlink.io/api/error/missing-authentication`
|
||||
* `INVALID_API_KEY` -> `https://shlink.io/api/error/invalid-api-key`
|
||||
|
||||
If you make a request to the API with v2 or v1, the old error types will be returned, until Shlink 4 is released, when only the new ones will be used.
|
||||
|
||||
Non-error responses are not affected.
|
||||
|
||||
* [#1513](https://github.com/shlinkio/shlink/issues/1513) Added publishing of the docker image in GHCR.
|
||||
* [#1114](https://github.com/shlinkio/shlink/issues/1114) Added support to provide an initial API key via `INITIAL_API_KEY` env var, when running Shlink with openswoole or RoadRunner.
|
||||
|
||||
Also, the installer tool now allows to generate an initial API key that can be copy-pasted (this tool is run interactively), in case you use php-fpm or you don't want to use env vars.
|
||||
|
||||
* [#1528](https://github.com/shlinkio/shlink/issues/1528) Added support to delay when the GeoLite2 DB file is downloaded in docker images, speeding up its startup time.
|
||||
|
||||
In order to do it, pass `SKIP_INITIAL_GEOLITE_DOWNLOAD=true` when creating the container.
|
||||
|
||||
### Changed
|
||||
* [#1339](https://github.com/shlinkio/shlink/issues/1339) Added new test suite for CLI E2E tests.
|
||||
* [#1503](https://github.com/shlinkio/shlink/issues/1503) Drastically improved build time in GitHub Actions, by optimizing parallelization and adding php extensions cache.
|
||||
* [#1525](https://github.com/shlinkio/shlink/issues/1525) Migrated to custom doctrine CLI entry point.
|
||||
* [#1492](https://github.com/shlinkio/shlink/issues/1492) Migrated to immutable options objects, mapped with [cuyz/valinor](https://github.com/CuyZ/Valinor).
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [3.2.1] - 2022-08-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1495](https://github.com/shlinkio/shlink/issues/1495) Centralized how routes are configured to support multi-segment slugs.
|
||||
* [#1497](https://github.com/shlinkio/shlink/issues/1497) Updated to latest shlink dependencies with support for PHP 8.1 only.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1499](https://github.com/shlinkio/shlink/issues/1499) Fixed loading of config options as env vars, which was making all default configurations to be loaded unless env vars were explicitly provided.
|
||||
|
||||
|
||||
## [3.2.0] - 2022-08-05
|
||||
### Added
|
||||
* [#854](https://github.com/shlinkio/shlink/issues/854) Added support for multi-segment custom slugs.
|
||||
@@ -1289,7 +1642,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
|
||||
Endpoints and commands which create short URLs support providing the `domain` now (via query param or CLI flag). If not provided, the short URLs will still be "attached" to the default domain.
|
||||
|
||||
Custom slugs can be created on multiple domains, allowing to share links like `https://doma.in/my-campaign` and `https://example.com/my-campaign`, under the same shlink instance.
|
||||
Custom slugs can be created on multiple domains, allowing to share links like `https://s.test/my-campaign` and `https://example.com/my-campaign`, under the same shlink instance.
|
||||
|
||||
When resolving a short URL to redirect end users, the following rules are applied:
|
||||
|
||||
@@ -1752,7 +2105,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
```json
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
@@ -1819,7 +2172,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* [#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`
|
||||
For example: `https://s.test/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.
|
||||
|
||||
@@ -1891,7 +2244,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
|
||||
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.
|
||||
With this feature, a simple request to a URL like `https://s.test/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*
|
||||
@@ -1927,7 +2280,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
### 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.
|
||||
Useful to track emails. Just add an image pointing to a URL like `https://s.test/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
|
||||
|
||||
@@ -2208,7 +2561,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
### 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`
|
||||
In order to get the QR code URL, use a pattern like `https://s.test/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
|
||||
|
||||
@@ -102,7 +102,9 @@ In order to ensure stability and no regressions are introduced while developing
|
||||
|
||||
Since the app instance is run on a process different from the one running the tests, when a test fails it might not be obvious why. To help debugging that, the app will dump all its logs inside `data/log/api-tests`, where you will find the `shlink.log` and `access.log` files.
|
||||
|
||||
* **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line*
|
||||
* **CLI tests**: These are E2E tests too, but they test console commands instead of REST endpoints.
|
||||
|
||||
They use Maria DB as the database engine, and include the same fixtures as the API tests, that ensure the same data exists at the beginning of the execution.
|
||||
|
||||
Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better.
|
||||
|
||||
@@ -119,9 +121,9 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
||||
For example, `test:db:postgres`.
|
||||
|
||||
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
||||
* Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used.
|
||||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration.
|
||||
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
||||
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||
|
||||
## Pull request process
|
||||
|
||||
@@ -133,7 +135,7 @@ Once everything is clear, to provide a pull request to this project, you should
|
||||
|
||||
The base branch should always be `develop`, and the target branch for the pull request should also be `develop`.
|
||||
|
||||
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci:parallel`, or wait for the build to be run automatically after the pull request is created.
|
||||
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci`, or wait for the build to be run automatically after the pull request is created.
|
||||
|
||||
## Architectural Decision Records
|
||||
|
||||
|
||||
55
Dockerfile
55
Dockerfile
@@ -1,18 +1,24 @@
|
||||
FROM php:8.1.9-alpine3.16 as base
|
||||
FROM php:8.2-alpine3.17 as base
|
||||
|
||||
ARG SHLINK_VERSION=latest
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
ENV OPENSWOOLE_VERSION 4.11.1
|
||||
ARG SHLINK_RUNTIME=rr
|
||||
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||
ARG SHLINK_USER_ID='root'
|
||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||
ENV LC_ALL "C"
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV LC_ALL 'C'
|
||||
|
||||
WORKDIR /etc/shlink
|
||||
|
||||
# Install required PHP extensions
|
||||
RUN \
|
||||
# Temp install dev dependencies needed to compile the extensions
|
||||
apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev libpng-dev && \
|
||||
apk add --no-cache --virtual .dev-deps sqlite-dev postgresql-dev icu-dev libzip-dev zlib-dev libpng-dev linux-headers && \
|
||||
docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql intl calendar sockets bcmath zip gd && \
|
||||
apk add --no-cache sqlite-libs && \
|
||||
docker-php-ext-install -j"$(nproc)" pdo_sqlite && \
|
||||
@@ -22,14 +28,16 @@ RUN \
|
||||
|
||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole ; \
|
||||
fi; \
|
||||
if [ $(uname -m) == "x86_64" ]; then \
|
||||
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} && \
|
||||
docker-php-ext-enable pdo_sqlsrv && \
|
||||
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk ; \
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk ; \
|
||||
fi; \
|
||||
apk del .phpize-deps
|
||||
|
||||
@@ -38,7 +46,13 @@ FROM base as builder
|
||||
COPY . .
|
||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||
RUN apk add --no-cache git && \
|
||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
||||
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
||||
fi; \
|
||||
php composer.phar clear-cache && \
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
@@ -48,10 +62,13 @@ RUN apk add --no-cache git && \
|
||||
FROM base
|
||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||
|
||||
COPY --from=builder /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
||||
COPY --from=builder --chown=${SHLINK_USER_ID} /etc/shlink .
|
||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
||||
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
|
||||
fi;
|
||||
|
||||
# Expose default openswoole port
|
||||
# Expose default port
|
||||
EXPOSE 8080
|
||||
|
||||
# Copy config specific for the image
|
||||
@@ -59,14 +76,6 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||
|
||||
# Change the ownership of /etc/shlink/data to be writable, then change the user to non-root
|
||||
# FIXME Disabled for now, as it conflicts with ENABLE_PERIODIC_VISIT_LOCATE, which is used to configure a cron as root.
|
||||
# Ref: https://github.com/shlinkio/shlink/issues/1132
|
||||
#RUN chown 1001 /etc/shlink/data
|
||||
#RUN chown 1001 /etc/shlink/data/locks
|
||||
#RUN chown 1001 /etc/shlink/data/proxies
|
||||
#RUN chown 1001 /etc/shlink/data/cache
|
||||
#RUN chown 1001 /etc/shlink/data/log
|
||||
#USER 1001
|
||||
USER ${SHLINK_USER_ID}
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016-2021 Alejandro Celaya
|
||||
Copyright (c) 2016-2023 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
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||

|
||||
|
||||
[](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Continuous+integration%22)
|
||||
[](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
|
||||
[](https://app.codecov.io/gh/shlinkio/shlink)
|
||||
[](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
||||
[](https://twitter.com/shlinkio)
|
||||
[](https://twitter.com/shlinkio)
|
||||
[](https://fosstodon.org/@shlinkio)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.
|
||||
@@ -15,7 +16,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
|
||||
|
||||
- [Full documentation](#full-documentation)
|
||||
- [Docker image](#docker-image)
|
||||
- [Self hosted](#self-hosted)
|
||||
- [Self-hosted](#self-hosted)
|
||||
- [Download](#download)
|
||||
- [Configure](#configure)
|
||||
- [Using shlink](#using-shlink)
|
||||
@@ -35,7 +36,7 @@ The idea is that you can just generate a container using the image and provide t
|
||||
|
||||
First, make sure the host where you are going to run shlink fulfills these requirements:
|
||||
|
||||
* PHP 8.1
|
||||
* PHP 8.1 or 8.2
|
||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
||||
* apcu extension is recommended if you don't plan to use openswoole.
|
||||
* xml extension is required if you want to generate QR codes in svg format.
|
||||
|
||||
12
bin/doctrine
Executable file
12
bin/doctrine
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||
|
||||
/** @var EntityManager $app */
|
||||
$em = require __DIR__ . '/../config/entity-manager.php';
|
||||
ConsoleRunner::run(new SingleManagerProvider($em));
|
||||
32
bin/roadrunner-worker.php
Normal file
32
bin/roadrunner-worker.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mezzio\Application;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
|
||||
(static function (): void {
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
$rrMode = env('RR_MODE');
|
||||
|
||||
if ($rrMode === 'http') {
|
||||
// This was spin-up as a web worker
|
||||
$app = $container->get(Application::class);
|
||||
$worker = $container->get(PSR7Worker::class);
|
||||
|
||||
while ($req = $worker->waitRequest()) {
|
||||
try {
|
||||
$worker->respond($app->handle($req));
|
||||
} catch (Throwable $e) {
|
||||
$worker->getWorker()->error((string) $e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks();
|
||||
}
|
||||
})();
|
||||
@@ -1,25 +1,38 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
export APP_ENV=test
|
||||
export DB_DRIVER=postgres
|
||||
export TEST_ENV=api
|
||||
export GENERATE_COVERAGE=${GENERATE_COVERAGE:-"no"}
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
# Reset logs
|
||||
OUTPUT_LOGS=data/log/api-tests/output.log
|
||||
rm -rf data/log/api-tests
|
||||
mkdir data/log/api-tests
|
||||
touch data/log/api-tests/output.log
|
||||
touch $OUTPUT_LOGS
|
||||
|
||||
# Try to stop server just in case it hanged in last execution
|
||||
vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
|
||||
|
||||
echo 'Starting server...'
|
||||
vendor/bin/laminas mezzio:swoole:start -d
|
||||
sleep 2
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \
|
||||
-o=http.address=0.0.0.0:9999 \
|
||||
-o=logs.encoding=json \
|
||||
-o=logs.channels.http.encoding=json \
|
||||
-o=logs.channels.server.encoding=json \
|
||||
-o=logs.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
|
||||
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
|
||||
sleep 2 # Let's give the server a couple of seconds to start
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
|
||||
testsExitCode=$?
|
||||
|
||||
vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999
|
||||
|
||||
# Exit this script with the same code as the tests. If tests failed, this script has to fail
|
||||
exit $testsExitCode
|
||||
|
||||
4
build.sh
4
build.sh
@@ -24,6 +24,7 @@ rsync -av * "${builtContent}" \
|
||||
--exclude=*docker* \
|
||||
--exclude=Dockerfile \
|
||||
--include=.htaccess \
|
||||
--include=config/roadrunner/.rr.yml \
|
||||
--exclude-from=./.dockerignore
|
||||
cd "${builtContent}"
|
||||
|
||||
@@ -36,6 +37,9 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags
|
||||
if [[ $noSwoole ]]; then
|
||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# If generating a dist for openswoole, uninstall RoadRunner
|
||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
||||
fi
|
||||
|
||||
# Delete development files
|
||||
|
||||
148
composer.json
148
composer.json
@@ -13,65 +13,71 @@
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "^2.3",
|
||||
"doctrine/migrations": "^3.5",
|
||||
"doctrine/orm": "^2.12",
|
||||
"endroid/qr-code": "^4.4",
|
||||
"geoip2/geoip2": "^2.12",
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"doctrine/orm": "^2.14",
|
||||
"endroid/qr-code": "^4.7",
|
||||
"geoip2/geoip2": "^2.13",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.2.110",
|
||||
"laminas/laminas-config": "^3.7",
|
||||
"laminas/laminas-config-aggregator": "^1.8",
|
||||
"laminas/laminas-diactoros": "^2.14",
|
||||
"laminas/laminas-inputfilter": "^2.19",
|
||||
"laminas/laminas-servicemanager": "^3.16",
|
||||
"laminas/laminas-stdlib": "^3.11",
|
||||
"lcobucci/jwt": "^4.1",
|
||||
"league/uri": "^6.7",
|
||||
"jaybizzle/crawler-detect": "^1.2.112",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-diactoros": "^2.24",
|
||||
"laminas/laminas-inputfilter": "^2.24",
|
||||
"laminas/laminas-servicemanager": "^3.20",
|
||||
"laminas/laminas-stdlib": "^3.16",
|
||||
"league/uri": "^6.8",
|
||||
"lstrojny/functional-php": "^1.17",
|
||||
"mezzio/mezzio": "^3.11",
|
||||
"mezzio/mezzio-fastroute": "^3.5",
|
||||
"mezzio/mezzio-problem-details": "^1.6",
|
||||
"mezzio/mezzio-swoole": "^4.3",
|
||||
"mezzio/mezzio": "^3.15",
|
||||
"mezzio/mezzio-fastroute": "^3.8",
|
||||
"mezzio/mezzio-problem-details": "^1.11",
|
||||
"mezzio/mezzio-swoole": "^4.6",
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^3.74",
|
||||
"ocramius/proxy-manager": "^2.14",
|
||||
"pagerfanta/core": "^3.6",
|
||||
"pagerfanta/core": "^3.7",
|
||||
"php-middleware/request-id": "^4.1",
|
||||
"pugx/shortid-php": "^1.0",
|
||||
"ramsey/uuid": "^4.3",
|
||||
"shlinkio/shlink-common": "^4.5",
|
||||
"shlinkio/shlink-config": "^1.6",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.4",
|
||||
"shlinkio/shlink-importer": "^3.0",
|
||||
"shlinkio/shlink-installer": "^8.0",
|
||||
"shlinkio/shlink-ip-geolocation": "^2.2",
|
||||
"symfony/console": "^6.1",
|
||||
"symfony/filesystem": "^6.1",
|
||||
"symfony/lock": "^6.1",
|
||||
"symfony/process": "^6.1",
|
||||
"symfony/string": "^6.1"
|
||||
"pugx/shortid-php": "^1.1",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/shlink-common": "^5.5",
|
||||
"shlinkio/shlink-config": "^2.4",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.0",
|
||||
"shlinkio/shlink-importer": "^5.1",
|
||||
"shlinkio/shlink-installer": "^8.4.1",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||
"shlinkio/shlink-json": "^1.0",
|
||||
"spiral/roadrunner": "^2023.1",
|
||||
"spiral/roadrunner-cli": "^2.5",
|
||||
"spiral/roadrunner-http": "^3.0",
|
||||
"spiral/roadrunner-jobs": "^4.0",
|
||||
"symfony/console": "^6.2",
|
||||
"symfony/filesystem": "^6.2",
|
||||
"symfony/lock": "^6.2",
|
||||
"symfony/process": "^6.2",
|
||||
"symfony/string": "^6.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"cebe/php-openapi": "^1.7",
|
||||
"devster/ubench": "^2.1",
|
||||
"dms/phpunit-arraysubset-asserts": "^0.3.0",
|
||||
"infection/infection": "^0.26.5",
|
||||
"openswoole/ide-helper": "~4.11.1",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"infection/infection": "^0.27",
|
||||
"openswoole/ide-helper": "~22.0.0",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpstan/phpstan-doctrine": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-symfony": "^1.2",
|
||||
"phpunit/php-code-coverage": "^9.2",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"phpunit/php-code-coverage": "^10.0",
|
||||
"phpunit/phpunit": "~10.1.0",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^3.0.1",
|
||||
"symfony/var-dumper": "^6.1",
|
||||
"veewee/composer-run-parallel": "^1.1"
|
||||
"shlinkio/shlink-test-utils": "~3.6.0",
|
||||
"symfony/var-dumper": "^6.2",
|
||||
"veewee/composer-run-parallel": "^1.2"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -87,10 +93,13 @@
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||
"ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli",
|
||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
|
||||
"ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db",
|
||||
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
||||
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
|
||||
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db",
|
||||
"ShlinkioApiTest\\Shlink\\Core\\": "module/Core/test-api"
|
||||
},
|
||||
"files": [
|
||||
"config/test/constants.php"
|
||||
@@ -98,31 +107,18 @@
|
||||
},
|
||||
"scripts": {
|
||||
"ci": [
|
||||
"@cs",
|
||||
"@stan",
|
||||
"@swagger:validate",
|
||||
"@test:ci",
|
||||
"@infect:ci"
|
||||
],
|
||||
"ci:parallel": [
|
||||
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"@parallel infect:test:api infect:ci:unit infect:ci:db"
|
||||
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
||||
],
|
||||
"cs": "phpcs",
|
||||
"cs": "phpcs -s",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/config config docker/config data/migrations --level=8",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:db",
|
||||
"@test:api"
|
||||
"@parallel test:unit test:db",
|
||||
"@parallel test:api test:cli"
|
||||
],
|
||||
"test:ci": [
|
||||
"@test:unit:ci",
|
||||
"@test:db",
|
||||
"@test:api:ci"
|
||||
],
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
@@ -132,12 +128,18 @@
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh",
|
||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
|
||||
"infect:ci:base": "infection --threads=4 --log-verbosity=default --only-covered --only-covering-test-cases --skip-initial-tests",
|
||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84",
|
||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json",
|
||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json",
|
||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api",
|
||||
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
|
||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
||||
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
||||
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
|
||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
|
||||
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
|
||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
|
||||
"infect:test": [
|
||||
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
|
||||
"@infect:ci"
|
||||
@@ -150,18 +152,20 @@
|
||||
"@test:api:ci",
|
||||
"@infect:ci:api"
|
||||
],
|
||||
"infect:test:cli": [
|
||||
"@test:cli:ci",
|
||||
"@infect:ci:cli"
|
||||
],
|
||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json",
|
||||
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"</>",
|
||||
"ci:parallel": "<fg=blue;options=bold>Same as \"ci\", but parallelizing tasks as much as possible</>",
|
||||
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
||||
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
|
||||
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||
@@ -173,7 +177,11 @@
|
||||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||
"test:db:ms": "<fg=blue;options=bold>Runs database test suites on a Microsoft SQL Server database</>",
|
||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage reports</>",
|
||||
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage for CI</>",
|
||||
"test:api:pretty": "<fg=blue;options=bold>Runs API test suites, and generates code coverage in HTML format</>",
|
||||
"test:cli": "<fg=blue;options=bold>Runs CLI test suites</>",
|
||||
"test:cli:ci": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage for CI</>",
|
||||
"test:cli:pretty": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage in HTML format</>",
|
||||
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
||||
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
|
||||
|
||||
24
config/autoload/cache.global.php
Normal file
24
config/autoload/cache.global.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
|
||||
$redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false)];
|
||||
$cacheRedisBlock = $redisServers === null ? [] : [
|
||||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'cache' => [
|
||||
'namespace' => 'Shlink',
|
||||
...$cacheRedisBlock,
|
||||
],
|
||||
'redis' => $redis,
|
||||
];
|
||||
})();
|
||||
@@ -8,8 +8,8 @@ return [
|
||||
|
||||
'debug' => false,
|
||||
|
||||
// Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't
|
||||
// generate a cache file that's then used by non-openswoole web executions
|
||||
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||
|
||||
];
|
||||
|
||||
@@ -3,14 +3,25 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Mezzio\Application;
|
||||
use Mezzio\Container;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
use Spiral\RoadRunner\WorkerInterface;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
PSR7Worker::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
Mezzio\Application::class => [
|
||||
Application::class => [
|
||||
Container\ApplicationConfigInjectionDelegator::class,
|
||||
],
|
||||
],
|
||||
@@ -26,4 +37,13 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
PSR7Worker::class => [
|
||||
WorkerInterface::class,
|
||||
ServerRequestFactoryInterface::class,
|
||||
StreamFactoryInterface::class,
|
||||
UploadedFileFactoryInterface::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -42,6 +42,9 @@ return (static function (): array {
|
||||
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
|
||||
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
||||
'charset' => $resolveCharset(),
|
||||
'driverOptions' => $driver !== 'mssql' ? [] : [
|
||||
'TrustServerCertificate' => 'true',
|
||||
],
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -6,12 +6,40 @@ return [
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
// MySQL
|
||||
'user' => 'root',
|
||||
'password' => 'root',
|
||||
'driver' => 'pdo_mysql',
|
||||
'host' => 'shlink_db_mysql',
|
||||
'dbname' => 'shlink',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
'charset' => 'utf8mb4',
|
||||
|
||||
// MariaDB
|
||||
// 'user' => 'root',
|
||||
// 'password' => 'root',
|
||||
// 'driver' => 'pdo_mysql',
|
||||
// 'host' => 'shlink_db_maria',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'charset' => 'utf8mb4',
|
||||
|
||||
// Postgres
|
||||
// 'user' => 'postgres',
|
||||
// 'password' => 'root',
|
||||
// 'driver' => 'pdo_pgsql',
|
||||
// 'host' => 'shlink_db_postgres',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'charset' => 'utf8',
|
||||
|
||||
// MSSQL
|
||||
// 'user' => 'sa',
|
||||
// 'password' => 'Passw0rd!',
|
||||
// 'driver' => 'pdo_sqlsrv',
|
||||
// 'host' => 'shlink_db_ms',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'driverOptions' => [
|
||||
// 'TrustServerCertificate' => 'true',
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -6,12 +6,14 @@ use Laminas\Stratigility\Middleware\ErrorHandler;
|
||||
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
|
||||
use Shlinkio\Shlink\Common\Logger;
|
||||
|
||||
use function Shlinkio\Shlink\Core\toProblemDetailsType;
|
||||
|
||||
return [
|
||||
|
||||
'problem-details' => [
|
||||
'default_types_map' => [
|
||||
404 => 'NOT_FOUND',
|
||||
500 => 'INTERNAL_SERVER_ERROR',
|
||||
404 => toProblemDetailsType('not-found'),
|
||||
500 => toProblemDetailsType('internal-server-error'),
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -44,6 +44,8 @@ return [
|
||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
|
||||
Option\UrlShortener\ShortUrlModeConfigOption::class,
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
@@ -72,9 +74,18 @@ return [
|
||||
InstallationCommand::DB_MIGRATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
||||
],
|
||||
InstallationCommand::ORM_PROXIES->value => [
|
||||
'command' => 'bin/doctrine orm:generate-proxies',
|
||||
],
|
||||
InstallationCommand::ORM_CLEAR_CACHE->value => [
|
||||
'command' => 'bin/doctrine orm:clear-cache:metadata',
|
||||
],
|
||||
InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [
|
||||
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
||||
],
|
||||
InstallationCommand::API_KEY_GENERATE->value => [
|
||||
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -4,51 +4,63 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
use PhpMiddleware\RequestId;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
|
||||
$common = [
|
||||
'level' => Level::Info->value,
|
||||
'processors' => [RequestId\MonologProcessor::class],
|
||||
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
||||
];
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
return (static function (): array {
|
||||
$common = [
|
||||
'level' => Level::Info->value,
|
||||
'processors' => [RequestId\MonologProcessor::class],
|
||||
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
||||
];
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::FILE->value,
|
||||
...$common,
|
||||
],
|
||||
'Access' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
...$common,
|
||||
],
|
||||
],
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||
],
|
||||
'aliases' => [
|
||||
'logger' => 'Logger_Shlink',
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
],
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger-name' => 'Logger_Access',
|
||||
'format' => '%u "%r" %>s %B',
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::FILE->value,
|
||||
...$common,
|
||||
],
|
||||
'Access' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
'add_new_line' => ! runningInRoadRunner(),
|
||||
...$common,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||
NullLogger::class => InvokableFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'logger' => 'Logger_Shlink',
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
AccessLogMiddleware::LOGGER_SERVICE_NAME => 'Logger_Access',
|
||||
],
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
// Let's disable mezio-swoole access logging, so that we can provide our own implementation,
|
||||
// consistent for roadrunner and openswoole
|
||||
'logger-name' => NullLogger::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
|
||||
@@ -5,14 +5,12 @@ declare(strict_types=1);
|
||||
use Monolog\Level;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
|
||||
$isSwoole = extension_loaded('openswoole');
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
// For swoole, send logs as stream
|
||||
'type' => $isSwoole ? LoggerType::STREAM->value : LoggerType::FILE->value,
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
'level' => Level::Debug->value,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
'mercure' => [
|
||||
'public_hub_url' => 'http://localhost:8001',
|
||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||
'jwt_secret' => 'mercure_jwt_key',
|
||||
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -9,6 +9,7 @@ use Mezzio\ProblemDetails;
|
||||
use Mezzio\Router;
|
||||
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
||||
use RKA\Middleware\IpAddress;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
||||
|
||||
return [
|
||||
@@ -16,6 +17,7 @@ return [
|
||||
'middleware_pipeline' => [
|
||||
'error-handler' => [
|
||||
'middleware' => [
|
||||
AccessLogMiddleware::class,
|
||||
ContentLengthMiddleware::class,
|
||||
RequestIdMiddleware::class,
|
||||
ErrorHandler::class,
|
||||
@@ -45,6 +47,7 @@ return [
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
|
||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
|
||||
@@ -16,7 +16,7 @@ return [
|
||||
],
|
||||
|
||||
'redirects' => [
|
||||
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE),
|
||||
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE->value),
|
||||
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(
|
||||
DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||
),
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
|
||||
$pubSub = [
|
||||
'redis' => [
|
||||
'pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false),
|
||||
],
|
||||
];
|
||||
|
||||
return match ($redisServers) {
|
||||
null => $pubSub,
|
||||
default => [
|
||||
'cache' => [
|
||||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||
],
|
||||
],
|
||||
...$pubSub,
|
||||
],
|
||||
};
|
||||
})();
|
||||
@@ -7,6 +7,8 @@ return [
|
||||
'cache' => [
|
||||
'redis' => [
|
||||
'servers' => 'tcp://shlink_redis:6379',
|
||||
// 'servers' => 'tcp://barbar@shlink_redis_acl:6379',
|
||||
// 'servers' => 'tcp://foo:bar@shlink_redis_acl:6379',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Fig\Http\Message\RequestMethodInterface;
|
||||
use RKA\Middleware\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Action as CoreAction;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Action;
|
||||
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||
use Shlinkio\Shlink\Rest\Middleware;
|
||||
@@ -15,15 +16,17 @@ use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
// The order of the routes defined here matters. Changing it might cause path conflicts
|
||||
return (static function (): array {
|
||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
$multiSegment = (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false);
|
||||
|
||||
// TODO This should be based on config, not the env var
|
||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
||||
|
||||
return [
|
||||
|
||||
// The order of the routes defined here matters. Changing it might cause path conflicts
|
||||
'routes' => [
|
||||
// Rest
|
||||
...ConfigProvider::applyRoutesPrefix([
|
||||
@@ -35,6 +38,7 @@ return (static function (): array {
|
||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
|
||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||
|
||||
// Short URLs
|
||||
@@ -50,6 +54,7 @@ return (static function (): array {
|
||||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
@@ -64,7 +69,7 @@ return (static function (): array {
|
||||
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||
|
||||
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||
], $multiSegment),
|
||||
]),
|
||||
|
||||
// Non-rest
|
||||
[
|
||||
@@ -77,7 +82,7 @@ return (static function (): array {
|
||||
],
|
||||
[
|
||||
'name' => CoreAction\PixelAction::class,
|
||||
'path' => sprintf('/{shortCode%s}/track', $multiSegment ? ':.+' : ''),
|
||||
'path' => '/{shortCode}/track',
|
||||
'middleware' => [
|
||||
IpAddress::class,
|
||||
CoreAction\PixelAction::class,
|
||||
@@ -86,7 +91,7 @@ return (static function (): array {
|
||||
],
|
||||
[
|
||||
'name' => CoreAction\QrCodeAction::class,
|
||||
'path' => sprintf('/{shortCode%s}/qr-code', $multiSegment ? ':.+' : ''),
|
||||
'path' => '/{shortCode}/qr-code',
|
||||
'middleware' => [
|
||||
CoreAction\QrCodeAction::class,
|
||||
],
|
||||
@@ -94,9 +99,10 @@ return (static function (): array {
|
||||
],
|
||||
[
|
||||
'name' => CoreAction\RedirectAction::class,
|
||||
'path' => sprintf('/{shortCode%s}', $multiSegment ? ':.+' : ''),
|
||||
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
||||
'middleware' => [
|
||||
IpAddress::class,
|
||||
TrimTrailingSlashMiddleware::class,
|
||||
CoreAction\RedirectAction::class,
|
||||
],
|
||||
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use function Shlinkio\Shlink\Config\getOpenswooleConfigFromEnv;
|
||||
|
||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
||||
|
||||
return (static function (): array {
|
||||
@@ -21,6 +23,7 @@ return (static function (): array {
|
||||
'process-name' => 'shlink',
|
||||
|
||||
'options' => [
|
||||
...getOpenswooleConfigFromEnv(),
|
||||
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
|
||||
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
|
||||
],
|
||||
|
||||
@@ -4,33 +4,40 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
return (static function (): array {
|
||||
/** @var string|null $disableTrackingFrom */
|
||||
$disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv();
|
||||
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
return [
|
||||
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(),
|
||||
],
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
|
||||
];
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => $disableTrackingFrom === null
|
||||
? []
|
||||
: array_map(trim(...), explode(',', $disableTrackingFrom)),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
|
||||
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
||||
@@ -12,6 +13,8 @@ return (static function (): array {
|
||||
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
|
||||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
|
||||
return [
|
||||
|
||||
@@ -24,6 +27,8 @@ return (static function (): array {
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||
'mode' => $mode,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -2,16 +2,23 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$isSwoole = extension_loaded('openswoole');
|
||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => sprintf('localhost:%s', $isSwoole ? '8080' : '8000'),
|
||||
'hostname' => sprintf('localhost:%s', match (true) {
|
||||
runningInRoadRunner() => '8800',
|
||||
runningInOpenswoole() => '8080',
|
||||
default => '8000',
|
||||
}),
|
||||
],
|
||||
'auto_resolve_titles' => true,
|
||||
// 'multi_segment_slugs_enabled' => true,
|
||||
// 'trailing_slash_enabled' => true,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -13,20 +13,23 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
||||
|
||||
use function class_exists;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
use const PHP_SAPI;
|
||||
|
||||
$isCli = PHP_SAPI === 'cli';
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', Core\Config\EnvVars::cases())
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
$isCli && class_exists(Swoole\ConfigProvider::class)
|
||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||
? Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
@@ -46,5 +49,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
], 'data/cache/app_config.php', [
|
||||
Core\Config\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
|
||||
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
|
||||
]))->getMergedConfig();
|
||||
|
||||
@@ -4,19 +4,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
||||
|
||||
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
|
||||
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
||||
const MIN_SHORT_CODES_LENGTH = 4;
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
|
||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||
const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema.
|
||||
const DEFAULT_QR_CODE_SIZE = 300;
|
||||
const DEFAULT_QR_CODE_MARGIN = 0;
|
||||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
||||
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||
const MIN_TASK_WORKERS = 4;
|
||||
const MIGRATIONS_TABLE = 'migrations';
|
||||
|
||||
@@ -12,6 +12,16 @@ chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// Workaround to make this compatible with both openswoole 22 and earlier versions.
|
||||
if (! function_exists('swoole_set_process_name')) {
|
||||
// phpcs:disable
|
||||
function swoole_set_process_name(string $name): void
|
||||
{
|
||||
OpenSwoole\Util::setProcessName($name);
|
||||
}
|
||||
// phpcs:enable
|
||||
}
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone here
|
||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||
|
||||
@@ -21,7 +31,6 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
|
||||
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
|
||||
}
|
||||
|
||||
// Build container
|
||||
return (static function (): ServiceManager {
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['dependencies']);
|
||||
|
||||
40
config/roadrunner/.rr.dev.yml
Normal file
40
config/roadrunner/.rr.dev.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
version: '3.0'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
||||
server:
|
||||
command: 'php ../../bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:8080'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: '../../public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: true
|
||||
|
||||
jobs:
|
||||
pool:
|
||||
num_workers: 1
|
||||
debug: true
|
||||
timeout: 300
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
driver: memory
|
||||
config:
|
||||
priority: 10
|
||||
prefetch: 10
|
||||
|
||||
logs:
|
||||
mode: development
|
||||
channels:
|
||||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
level: debug
|
||||
metrics:
|
||||
level: debug
|
||||
36
config/roadrunner/.rr.yml
Normal file
36
config/roadrunner/.rr.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
version: '3.0'
|
||||
|
||||
rpc:
|
||||
listen: tcp://127.0.0.1:6001
|
||||
|
||||
server:
|
||||
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
|
||||
|
||||
http:
|
||||
address: '0.0.0.0:${PORT:-8080}'
|
||||
middleware: ['static']
|
||||
static:
|
||||
dir: '../../public'
|
||||
forbid: ['.php', '.htaccess']
|
||||
pool:
|
||||
num_workers: ${WEB_WORKER_NUM:-0}
|
||||
|
||||
jobs:
|
||||
timeout: 300 # 5 minutes
|
||||
pool:
|
||||
num_workers: ${TASK_WORKER_NUM:-0}
|
||||
consume: ['shlink']
|
||||
pipelines:
|
||||
shlink:
|
||||
driver: memory
|
||||
config:
|
||||
priority: 10
|
||||
prefetch: 10
|
||||
|
||||
logs:
|
||||
mode: production
|
||||
channels:
|
||||
http:
|
||||
mode: 'off' # Disable logging as Shlink handles it internally
|
||||
server:
|
||||
level: debug # Everything written to worker stderr is logged
|
||||
@@ -10,8 +10,8 @@ use Psr\Container\ContainerInterface;
|
||||
use function register_shutdown_function;
|
||||
use function sprintf;
|
||||
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
@@ -24,10 +24,15 @@ $httpClient = $container->get('shlink_test_api_client');
|
||||
register_shutdown_function(function () use ($httpClient): void {
|
||||
$httpClient->request(
|
||||
'GET',
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
|
||||
);
|
||||
});
|
||||
|
||||
$testHelper->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']);
|
||||
$testHelper->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
ApiTest\ApiTestCase::setApiClient($httpClient);
|
||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
||||
|
||||
33
config/test/bootstrap_cli_tests.php
Normal file
33
config/test/bootstrap_cli_tests.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\TestUtils;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function unlink;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$testHelper = $container->get(Helper\TestHelper::class);
|
||||
$config = $container->get('config');
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
// Delete old coverage in PHP, to avoid merging older executions with current one
|
||||
$covFile = __DIR__ . '/../../build/coverage-cli.cov';
|
||||
if (file_exists($covFile)) {
|
||||
unlink($covFile);
|
||||
}
|
||||
|
||||
$testHelper->createTestDb(
|
||||
createDbCommand: ['bin/cli', 'db:create'],
|
||||
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
||||
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
CliTest\CliTestCase::setSeedFixturesCallback(
|
||||
static fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []),
|
||||
);
|
||||
@@ -8,5 +8,10 @@ use Psr\Container\ContainerInterface;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = require __DIR__ . '/../container.php';
|
||||
$container->get(Helper\TestHelper::class)->createTestDb(['bin/cli', 'db:create'], ['bin/cli', 'db:migrate']);
|
||||
$container->get(Helper\TestHelper::class)->createTestDb(
|
||||
['bin/cli', 'db:create'],
|
||||
['bin/cli', 'db:migrate'],
|
||||
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||
['bin/doctrine', 'dbal:run-sql'],
|
||||
);
|
||||
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||
|
||||
@@ -4,5 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink;
|
||||
|
||||
const SWOOLE_TESTING_HOST = '127.0.0.1';
|
||||
const SWOOLE_TESTING_PORT = 9999;
|
||||
const API_TESTS_HOST = '127.0.0.1';
|
||||
const API_TESTS_PORT = 9999;
|
||||
|
||||
const ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) '
|
||||
. 'Chrome/109.0.5414.86 Mobile Safari/537.36';
|
||||
const IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 '
|
||||
. '(KHTML, like Gecko) FxiOS/109.0 Mobile/15E148 Safari/605.1.15';
|
||||
const DESKTOP_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like '
|
||||
. 'Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61';
|
||||
|
||||
@@ -8,8 +8,10 @@ use GuzzleHttp\Client;
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Laminas\Diactoros\Response\EmptyResponse;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use League\Event\EventDispatcher;
|
||||
use Monolog\Level;
|
||||
use PHPUnit\Runner\Version;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
@@ -20,24 +22,60 @@ use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
||||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
use function file_exists;
|
||||
use function Functional\contains;
|
||||
use function Laminas\Stratigility\middleware;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||
|
||||
$isApiTest = env('TEST_ENV') === 'api';
|
||||
$generateCoverage = env('GENERATE_COVERAGE') === 'yes';
|
||||
if ($isApiTest && $generateCoverage) {
|
||||
$isCliTest = env('TEST_ENV') === 'cli';
|
||||
$isE2eTest = $isApiTest || $isCliTest;
|
||||
$coverageType = env('GENERATE_COVERAGE');
|
||||
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
|
||||
|
||||
$coverage = null;
|
||||
if ($isE2eTest && $generateCoverage) {
|
||||
$filter = new Filter();
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/Rest/src');
|
||||
$filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src');
|
||||
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param 'api'|'cli' $type
|
||||
*/
|
||||
$exportCoverage = static function (string $type = 'api') use (&$coverage, $coverageType): void {
|
||||
if ($coverage === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$basePath = __DIR__ . '/../../build/coverage-' . $type;
|
||||
$covPath = $basePath . '.cov';
|
||||
|
||||
// Every CLI test runs on its own process and dumps the coverage afterwards.
|
||||
// Try to load it and merge it, so that we end up with the whole coverage at the end.
|
||||
if ($type === 'cli' && file_exists($covPath)) {
|
||||
$coverage->merge(require $covPath);
|
||||
}
|
||||
|
||||
if ($coverageType === 'pretty') {
|
||||
(new Html())->process($coverage, $basePath . '/coverage-html');
|
||||
} else {
|
||||
(new PHP())->process($coverage, $covPath);
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
}
|
||||
};
|
||||
|
||||
$buildDbConnection = static function (): array {
|
||||
$driver = env('DB_DRIVER', 'sqlite');
|
||||
$isCi = env('CI', false);
|
||||
@@ -46,7 +84,7 @@ $buildDbConnection = static function (): array {
|
||||
return match ($driver) {
|
||||
'sqlite' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => sys_get_temp_dir() . '/shlink-tests.db',
|
||||
'memory' => true,
|
||||
],
|
||||
'postgres' => [
|
||||
'driver' => 'pdo_pgsql',
|
||||
@@ -63,6 +101,9 @@ $buildDbConnection = static function (): array {
|
||||
'user' => 'sa',
|
||||
'password' => 'Passw0rd!',
|
||||
'dbname' => 'shlink_test',
|
||||
'driverOptions' => [
|
||||
'TrustServerCertificate' => 'true',
|
||||
],
|
||||
],
|
||||
default => [ // mysql and maria
|
||||
'driver' => 'pdo_mysql',
|
||||
@@ -80,6 +121,7 @@ $buildTestLoggerConfig = static fn (string $filename) => [
|
||||
'level' => Level::Debug->value,
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => sprintf('data/log/api-tests/%s', $filename),
|
||||
'add_new_line' => true,
|
||||
];
|
||||
|
||||
return [
|
||||
@@ -90,16 +132,15 @@ return [
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
'hostname' => 's.test',
|
||||
],
|
||||
'validate_url' => true,
|
||||
],
|
||||
|
||||
'mezzio-swoole' => [
|
||||
'enable_coroutine' => false,
|
||||
'swoole-http-server' => [
|
||||
'host' => SWOOLE_TESTING_HOST,
|
||||
'port' => SWOOLE_TESTING_PORT,
|
||||
'host' => API_TESTS_HOST,
|
||||
'port' => API_TESTS_PORT,
|
||||
'process-name' => 'shlink_test',
|
||||
'options' => [
|
||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||
@@ -113,17 +154,10 @@ return [
|
||||
[
|
||||
'name' => 'dump_coverage',
|
||||
'path' => '/api-tests/stop-coverage',
|
||||
'middleware' => middleware(static function () use (&$coverage) {
|
||||
'middleware' => middleware(static function () use ($exportCoverage) {
|
||||
// TODO I have tried moving this block to a listener so that it's invoked automatically,
|
||||
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
||||
if ($coverage) { // @phpstan-ignore-line
|
||||
$basePath = __DIR__ . '/../../build/coverage-api';
|
||||
|
||||
(new PHP())->process($coverage, $basePath . '.cov');
|
||||
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
|
||||
(new Html())->process($coverage, $basePath . '/coverage-html');
|
||||
}
|
||||
|
||||
$exportCoverage();
|
||||
return new EmptyResponse();
|
||||
}),
|
||||
'allowed_methods' => ['GET'],
|
||||
@@ -157,13 +191,69 @@ return [
|
||||
'dependencies' => [
|
||||
'services' => [
|
||||
'shlink_test_api_client' => new Client([
|
||||
'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
|
||||
'base_uri' => sprintf('http://%s:%s/', API_TESTS_HOST, API_TESTS_PORT),
|
||||
'http_errors' => false,
|
||||
]),
|
||||
],
|
||||
'factories' => [
|
||||
TestUtils\Helper\TestHelper::class => InvokableFactory::class,
|
||||
],
|
||||
'delegators' => $isCliTest ? [
|
||||
Application::class => [
|
||||
static function (
|
||||
ContainerInterface $c,
|
||||
string $serviceName,
|
||||
callable $callback,
|
||||
) use (
|
||||
&$coverage,
|
||||
$exportCoverage,
|
||||
) {
|
||||
/** @var Application $app */
|
||||
$app = $callback();
|
||||
$wrappedEventDispatcher = new EventDispatcher();
|
||||
|
||||
// When the command starts, start collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleCommandEvent::class,
|
||||
static function () use (&$coverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->start($id);
|
||||
},
|
||||
);
|
||||
// When the command ends, stop collecting coverage
|
||||
$wrappedEventDispatcher->subscribeTo(
|
||||
ConsoleTerminateEvent::class,
|
||||
static function () use (&$coverage, $exportCoverage): void {
|
||||
$id = env('COVERAGE_ID');
|
||||
if ($id === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$coverage?->stop();
|
||||
$exportCoverage('cli');
|
||||
},
|
||||
);
|
||||
|
||||
$app->setDispatcher(new class ($wrappedEventDispatcher) implements EventDispatcherInterface {
|
||||
public function __construct(private EventDispatcher $wrappedDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
public function dispatch(object $event, ?string $eventName = null): object
|
||||
{
|
||||
$this->wrappedDispatcher->dispatch($event);
|
||||
return $event;
|
||||
}
|
||||
});
|
||||
|
||||
return $app;
|
||||
},
|
||||
],
|
||||
] : [],
|
||||
],
|
||||
|
||||
'entity_manager' => [
|
||||
@@ -172,6 +262,7 @@ return [
|
||||
|
||||
'data_fixtures' => [
|
||||
'paths' => [
|
||||
// TODO These are used for CLI tests too, so maybe should be somewhere else
|
||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -ex
|
||||
|
||||
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
|
||||
curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||
apt-get update
|
||||
ACCEPT_EULA=Y apt-get install msodbcsql17
|
||||
apt-get install unixodbc-dev
|
||||
ACCEPT_EULA=Y apt-get install msodbcsql18
|
||||
# apt-get install unixodbc-dev
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
ServerName s.test
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server {
|
||||
server_name doma.in;
|
||||
server_name s.test;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM php:8.1.9-fpm-alpine3.16
|
||||
FROM php:8.2-fpm-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -30,7 +31,9 @@ RUN docker-php-ext-install gd
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN docker-php-ext-install sockets
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
|
||||
docker-php-ext-install sockets && \
|
||||
apk del .phpize-deps
|
||||
RUN docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
@@ -44,13 +47,13 @@ RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install pcov and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
2
data/infra/redis/redis-acl.conf
Normal file
2
data/infra/redis/redis-acl.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
user foo allcommands allkeys on >bar
|
||||
requirepass barbar
|
||||
76
data/infra/roadrunner.Dockerfile
Normal file
76
data/infra/roadrunner.Dockerfile
Normal file
@@ -0,0 +1,76 @@
|
||||
FROM php:8.2-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache oniguruma-dev
|
||||
RUN docker-php-ext-install mbstring
|
||||
|
||||
RUN apk add --no-cache sqlite-libs
|
||||
RUN apk add --no-cache sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
|
||||
RUN apk add --no-cache icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache libzip-dev zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
|
||||
docker-php-ext-install sockets && \
|
||||
apk del .phpize-deps
|
||||
RUN docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \
|
||||
&& docker-php-ext-configure apcu \
|
||||
&& docker-php-ext-install apcu \
|
||||
&& rm /tmp/apcu.tar.gz \
|
||||
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install pcov and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
||||
VOLUME /home/shlink
|
||||
WORKDIR /home/shlink
|
||||
|
||||
# Expose roadrunner port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD \
|
||||
# Install dependencies if the vendor dir does not exist
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# Download roadrunner binary
|
||||
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; fi && \
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done
|
||||
@@ -1,11 +1,12 @@
|
||||
FROM php:8.1.9-alpine3.16
|
||||
FROM php:8.2-alpine3.17
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV INOTIFY_VERSION 3.0.0
|
||||
ENV OPENSWOOLE_VERSION 4.11.1
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -32,7 +33,9 @@ RUN docker-php-ext-install gd
|
||||
RUN apk add --no-cache postgresql-dev
|
||||
RUN docker-php-ext-install pdo_pgsql
|
||||
|
||||
RUN docker-php-ext-install sockets
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
|
||||
docker-php-ext-install sockets && \
|
||||
apk del .phpize-deps
|
||||
RUN docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
@@ -54,13 +57,13 @@ RUN mkdir -p /usr/src/php/ext/inotify \
|
||||
&& rm /tmp/inotify.tar.gz
|
||||
|
||||
# Install openswoole, pcov and mssql driver
|
||||
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
# Install composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
|
||||
@@ -8,7 +8,7 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||
|
||||
final class Version20210207100807 extends AbstractMigration
|
||||
|
||||
53
data/migrations/Version20230103105343.php
Normal file
53
data/migrations/Version20230103105343.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230103105343 extends AbstractMigration
|
||||
{
|
||||
private const TABLE_NAME = 'device_long_urls';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->skipIf($schema->hasTable(self::TABLE_NAME));
|
||||
|
||||
$table = $schema->createTable(self::TABLE_NAME);
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
|
||||
$table->addColumn('device_type', Types::STRING, ['length' => 255]);
|
||||
$table->addColumn('long_url', Types::STRING, ['length' => 2048]);
|
||||
$table->addColumn('short_url_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
|
||||
$table->addUniqueIndex(['device_type', 'short_url_id'], 'UQ_device_type_per_short_url');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->skipIf(! $schema->hasTable(self::TABLE_NAME));
|
||||
$schema->dropTable(self::TABLE_NAME);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
50
data/migrations/Version20230130090946.php
Normal file
50
data/migrations/Version20230130090946.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLServerPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230130090946 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->skipIf(! $this->isMsSql(), 'This only sets MsSQL-specific database options');
|
||||
|
||||
$shortUrls = $schema->getTable('short_urls');
|
||||
$shortCode = $shortUrls->getColumn('short_code');
|
||||
// Drop the unique index before changing the collation, as the field is part of this index
|
||||
$shortUrls->dropIndex('unique_short_code_plus_domain');
|
||||
$shortCode->setPlatformOption('collation', 'Latin1_General_CS_AS');
|
||||
}
|
||||
|
||||
public function postUp(Schema $schema): void
|
||||
{
|
||||
if ($this->isMsSql()) {
|
||||
// The index needs to be re-created in postUp, but here, we can only use statements run against the
|
||||
// connection directly
|
||||
$this->connection->executeStatement(
|
||||
'CREATE INDEX unique_short_code_plus_domain ON short_urls (domain_id, short_code);',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// No down
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
|
||||
private function isMsSql(): bool
|
||||
{
|
||||
return $this->connection->getDatabasePlatform() instanceof SQLServerPlatform;
|
||||
}
|
||||
}
|
||||
27
data/migrations/Version20230211171904.php
Normal file
27
data/migrations/Version20230211171904.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230211171904 extends AbstractMigration
|
||||
{
|
||||
private const INDEX_NAME = 'IDX_visits_potential_bot';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
|
||||
|
||||
$visits->addIndex(['short_url_id', 'potential_bot'], self::INDEX_NAME);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
28
data/migrations/Version20230303164233.php
Normal file
28
data/migrations/Version20230303164233.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20230303164233 extends AbstractMigration
|
||||
{
|
||||
private const INDEX_NAME = 'visits_potential_bot_IDX';
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visits = $schema->getTable('visits');
|
||||
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
|
||||
|
||||
$visits->dropIndex('IDX_visits_potential_bot'); // Old index
|
||||
$visits->addIndex(['potential_bot'], self::INDEX_NAME);
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,12 @@ services:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_roadrunner:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_mysql:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
|
||||
@@ -29,6 +29,7 @@ services:
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
- shlink_redis_acl
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
@@ -65,6 +66,32 @@ services:
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
- shlink_redis_acl
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_roadrunner:
|
||||
container_name: shlink_roadrunner
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/roadrunner.Dockerfile
|
||||
ports:
|
||||
- "8800:8080"
|
||||
volumes:
|
||||
- ./:/home/shlink
|
||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
||||
links:
|
||||
- shlink_db_mysql
|
||||
- shlink_db_postgres
|
||||
- shlink_db_maria
|
||||
- shlink_db_ms
|
||||
- shlink_redis
|
||||
- shlink_redis_acl
|
||||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
@@ -75,7 +102,7 @@ services:
|
||||
|
||||
shlink_db_mysql:
|
||||
container_name: shlink_db_mysql
|
||||
image: mysql:5.7
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
@@ -122,10 +149,19 @@ services:
|
||||
|
||||
shlink_redis:
|
||||
container_name: shlink_redis
|
||||
image: redis:6.0-alpine
|
||||
image: redis:6.2-alpine
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
shlink_redis_acl:
|
||||
container_name: shlink_redis_acl
|
||||
image: redis:6.2-alpine
|
||||
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
||||
ports:
|
||||
- "6382:6379"
|
||||
volumes:
|
||||
- ./data/infra/redis/redis-acl.conf:/usr/local/etc/redis/redis.conf
|
||||
|
||||
shlink_mercure_proxy:
|
||||
container_name: shlink_mercure_proxy
|
||||
image: nginx:1.19.6-alpine
|
||||
@@ -139,13 +175,13 @@ services:
|
||||
|
||||
shlink_mercure:
|
||||
container_name: shlink_mercure
|
||||
image: dunglas/mercure:v0.13
|
||||
image: dunglas/mercure:v0.14
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
SERVER_NAME: ":80"
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
|
||||
|
||||
shlink_rabbitmq:
|
||||
|
||||
@@ -11,7 +11,7 @@ It exposes a shlink instance served with [openswoole](https://openswoole.com/),
|
||||
|
||||
The most basic way to run Shlink's docker image is by providing these mandatory env vars.
|
||||
|
||||
* `DEFAULT_DOMAIN`: The default short domain used for this shlink instance. For example **doma.in**.
|
||||
* `DEFAULT_DOMAIN`: The default short domain used for this shlink instance. For example **s.test**.
|
||||
* `IS_HTTPS_ENABLED`: Either **true** or **false**. Tells if Shlink is being served with HTTPs or not.
|
||||
* `GEOLITE_LICENSE_KEY`: Your GeoLite2 license key. [Learn more](https://shlink.io/documentation/geolite-license-key/) about this.
|
||||
|
||||
@@ -21,7 +21,7 @@ To run shlink on top of a local docker service, and using an internal SQLite dat
|
||||
docker run \
|
||||
--name shlink \
|
||||
-p 8080:8080 \
|
||||
-e DEFAULT_DOMAIN=doma.in \
|
||||
-e DEFAULT_DOMAIN=s.test \
|
||||
-e IS_HTTPS_ENABLED=true \
|
||||
-e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \
|
||||
shlinkio/shlink:stable
|
||||
|
||||
25
docker/build
25
docker/build
@@ -1,25 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -ex
|
||||
|
||||
PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
|
||||
DOCKER_IMAGE="shlinkio/shlink"
|
||||
|
||||
# If ref is not develop, then this is a tag. Build that docker tag and also "stable"
|
||||
if [[ "$GITHUB_REF" != *"develop"* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
TAGS="-t ${DOCKER_IMAGE}:${VERSION}"
|
||||
# Push stable tag only if this is not an alpha or beta tag
|
||||
[[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
|
||||
|
||||
docker buildx build --push \
|
||||
--build-arg SHLINK_VERSION=${VERSION} \
|
||||
--platform ${PLATFORMS} \
|
||||
${TAGS} .
|
||||
|
||||
# If build branch is develop, build latest
|
||||
elif [[ "$GITHUB_REF" == *"develop"* ]]; then
|
||||
docker buildx build --push \
|
||||
--platform ${PLATFORMS} \
|
||||
-t ${DOCKER_IMAGE}:latest .
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
memory_limit=256M
|
||||
memory_limit=512M
|
||||
|
||||
@@ -11,6 +11,7 @@ return [
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,36 +1,29 @@
|
||||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
# If SHELL_VERBOSITY was not explicitly provided, run commands in quite mode (-q)
|
||||
[ $SHELL_VERBOSITY ] && flags="" || flags="-q"
|
||||
|
||||
cd /etc/shlink
|
||||
|
||||
echo "Creating fresh database if needed..."
|
||||
php bin/cli db:create -n ${flags}
|
||||
flags="--clear-db-cache"
|
||||
|
||||
echo "Updating database..."
|
||||
php bin/cli db:migrate -n ${flags}
|
||||
|
||||
echo "Generating proxies..."
|
||||
php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n ${flags}
|
||||
|
||||
echo "Clearing entities cache..."
|
||||
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n ${flags}
|
||||
|
||||
# Try to download GeoLite2 db file only if the license key env var was defined
|
||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
||||
echo "Downloading GeoLite2 db file..."
|
||||
php bin/cli visit:download-db -n ${flags}
|
||||
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" == "true" ]; then
|
||||
flags="${flags} --skip-download-geolite"
|
||||
fi
|
||||
|
||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
||||
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
|
||||
php vendor/bin/shlink-installer init ${flags}
|
||||
|
||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
|
||||
# ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
|
||||
echo "Configuring periodic visit location..."
|
||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||
/usr/sbin/crond &
|
||||
fi
|
||||
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
||||
77
docs/adr/2023-01-06-support-any-http-method-in-short-urls.md
Normal file
77
docs/adr/2023-01-06-support-any-http-method-in-short-urls.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Support any HTTP method in short URLs
|
||||
|
||||
* Status: Accepted
|
||||
* Date: 2023-01-06
|
||||
|
||||
## Context and problem statement
|
||||
|
||||
There has been a report that Shlink behaves as if a short URL was not found when the request HTTP method is not `GET`.
|
||||
|
||||
They want it to accept other methods so that they can do things like POSTing stuff that then gets "redirected" to the original URL.
|
||||
|
||||
This presents two main problems:
|
||||
|
||||
* Changing this could be considered a breaking change, in case someone is relying on this behavior (Shlink to only redirect on `GET`).
|
||||
* Shlink currently supports two redirect statuses ([301](https://httpwg.org/specs/rfc9110.html#status.301) and [302](https://httpwg.org/specs/rfc9110.html#status.302)), which can be configured by the server admin.
|
||||
|
||||
For historical reasons, a client might switch from the original method to `GET` when any of these is returned, not resulting in the desired behavior anyway.
|
||||
|
||||
Instead, statuses [308](https://httpwg.org/specs/rfc9110.html#status.308) and [307](https://httpwg.org/specs/rfc9110.html#status.307) should be used.
|
||||
|
||||
## Considered options
|
||||
|
||||
There's actually two problems to solve here. Some combinations are implicitly required:
|
||||
|
||||
* **To support other HTTP methods in short URLs**
|
||||
* Start supporting all HTTP methods.
|
||||
* Introduce a feature flag to allow users decide if they want to support all methods or just `GET`.
|
||||
* **To support other redirects statuses (308 and 307)**
|
||||
* Switch to status 308 and 307 and stop using 301 and 302.
|
||||
* Allow users to configure which of the 4 status codes they want to use, insteadof just supporting 301 and 302.
|
||||
* Allow users to configure between two combinations: 301+308 and 302+307, using 301 or 302 for `GET` requests, and 308 or 307 for the rest.
|
||||
|
||||
> **Note**
|
||||
> I asked on social networks, and these were the results (not too many answers though):
|
||||
> * https://fosstodon.org/@shlinkio/109626773392324128
|
||||
> * https://twitter.com/shlinkio/status/1610347091741507585
|
||||
|
||||
## Decision outcome
|
||||
|
||||
Because of backwards compatibility, it feels like the bets option is allowing to configure between 301, 302, 308 and 307.
|
||||
|
||||
This has the benefit that we can keep existing behavior intact. Existing instances will continue working only on `GET`, with statuses 301 or 302.
|
||||
|
||||
Anyone who wants to opt-in, can switch to 308 or 307, and the short URLs will transparently work on other HTTP methods in that case.
|
||||
|
||||
The only drawback is that this difference in the behavior when 308 or 307 are configured needs to be documented, and explained in shlink-installer.
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Start supporting all HTTP methods
|
||||
|
||||
* Good: Because the change in code is pretty simple.
|
||||
* Bad: Because it would be potentially a breaking change for anyone trusting current behavior for anything.
|
||||
|
||||
### Support HTTP methods via feature flag
|
||||
|
||||
* Good: because it would be safer for existing instances and opt-in for anyone interested in this change of behavior.
|
||||
* Bad: Because it requires more changes in code.
|
||||
* Bad: Because it requires a new config entry in the shlink-installer.
|
||||
|
||||
### Switch to statuses 308 and 307
|
||||
|
||||
* Good: Because we keep supporting just two status codes.
|
||||
* Bad: Because it requires applying mapping/transformation to convert old configurations.
|
||||
* Bad: Because it requires changes in shlink-installer.
|
||||
|
||||
### Allow users to configure between 301, 302, 308 and 307
|
||||
|
||||
* Good: Because it's fully backwards compatible with existing configs.
|
||||
* Good: Because it would implicitly allow enabling all HTTP methods if 308 or 307 are selected, and keep only `GET` for 301 and 302, without the need for a separated feature flag.
|
||||
* Bad: Because it requires dynamically supporting only `GET` or all methods, depending on the selected status.
|
||||
|
||||
### Allow users to configure between 301+308 or 302+307
|
||||
|
||||
* Good: Because it would allow a more explicit redirects config, where values are not 301 and 302, but something like "permanent" and "temporary".
|
||||
* Bad: Because it implicitly changes the behavior of existing instances, making them respond to redirects with a method other than `GET`, and with a status code other than the one they explicitly configured.
|
||||
* Bad: because existing `REDIRECT_STATUS_CODE` env var might not make sense anymore, requiring a new one and logic to map from one to another.
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
|
||||
|
||||
* [2023-01-06 Support any HTTP method in short URLs](2023-01-06-support-any-http-method-in-short-urls.md)
|
||||
* [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md)
|
||||
* [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md)
|
||||
* [2021-08-05 Migrate to a new caching library](2021-08-05-migrate-to-a-new-caching-library.md)
|
||||
|
||||
@@ -111,12 +111,19 @@
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "#/components/schemas/DeviceLongUrls"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date in which the short URL was created in ISO format."
|
||||
},
|
||||
"visitsSummary": {
|
||||
"$ref": "#/components/schemas/VisitsSummary"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "integer",
|
||||
"description": "The number of visits that this short URL has received."
|
||||
},
|
||||
@@ -146,10 +153,19 @@
|
||||
},
|
||||
"example": {
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": "https://store.steampowered.com/android",
|
||||
"ios": "https://store.steampowered.com/ios",
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 328,
|
||||
"visitsSummary": {
|
||||
"total": 328,
|
||||
"nonBots": 285,
|
||||
"bots": 43
|
||||
},
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
@@ -189,6 +205,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"VisitsSummary": {
|
||||
"type": "object",
|
||||
"required": ["total", "nonBots", "bots"],
|
||||
"properties": {
|
||||
"total": {
|
||||
"description": "The total amount of visits",
|
||||
"type": "number"
|
||||
},
|
||||
"nonBots": {
|
||||
"description": "The amount of visits which were not identified as bots",
|
||||
"type": "number"
|
||||
},
|
||||
"bots": {
|
||||
"description": "The amount of visits that were identified as potential bots",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeviceLongUrls": {
|
||||
"type": "object",
|
||||
"required": ["android", "ios", "desktop"],
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string"
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string"
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Visit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -266,7 +318,7 @@
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://doma.in",
|
||||
"visitedUrl": "https://s.test",
|
||||
"type": "base_url"
|
||||
}
|
||||
},
|
||||
|
||||
20
docs/swagger/definitions/DeviceLongUrls.json
Normal file
20
docs/swagger/definitions/DeviceLongUrls.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
17
docs/swagger/definitions/DeviceLongUrlsEdit.json
Normal file
17
docs/swagger/definitions/DeviceLongUrlsEdit.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"type": "object",
|
||||
"allOf": [{
|
||||
"$ref": "./DeviceLongUrls.json"
|
||||
}],
|
||||
"properties": {
|
||||
"android": {
|
||||
"nullable": true
|
||||
},
|
||||
"ios": {
|
||||
"nullable": true
|
||||
},
|
||||
"desktop": {
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
7
docs/swagger/definitions/DeviceLongUrlsResp.json
Normal file
7
docs/swagger/definitions/DeviceLongUrlsResp.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["android", "ios", "desktop"],
|
||||
"allOf": [{
|
||||
"$ref": "./DeviceLongUrlsEdit.json"
|
||||
}]
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
"shortCode",
|
||||
"shortUrl",
|
||||
"longUrl",
|
||||
"deviceLongUrls",
|
||||
"dateCreated",
|
||||
"visitsCount",
|
||||
"visitsSummary",
|
||||
"tags",
|
||||
"meta",
|
||||
"domain",
|
||||
@@ -26,14 +28,21 @@
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "./DeviceLongUrlsResp.json"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "The date in which the short URL was created in ISO format."
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "integer",
|
||||
"description": "The number of visits that this short URL has received."
|
||||
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
|
||||
},
|
||||
"visitsSummary": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "The long URL this short URL will redirect to",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "./DeviceLongUrlsEdit.json"
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
@@ -21,7 +24,8 @@
|
||||
"nullable": true
|
||||
},
|
||||
"validateUrl": {
|
||||
"description": "Tells if the long URL (if provided) should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
"deprecated": true,
|
||||
"description": "**[DEPRECATED]** Tells if the long URL should or should not be validated as a reachable URL. Defaults to `false`",
|
||||
"type": "boolean"
|
||||
},
|
||||
"tags": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["tag", "shortUrlsCount", "visitsSummary", "visitsCount"],
|
||||
"properties": {
|
||||
"tag": {
|
||||
"type": "string",
|
||||
@@ -9,9 +10,13 @@
|
||||
"type": "number",
|
||||
"description": "The amount of short URLs using this tag"
|
||||
},
|
||||
"userAgent": {
|
||||
"visitsSummary": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "The combined amount of visits received by short URLs with this tag"
|
||||
"description": "**[DEPRECATED]** Use visitsSummary.total instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["visitsCount", "orphanVisitsCount"],
|
||||
"required": ["nonOrphanVisits", "orphanVisits", "visitsCount", "orphanVisitsCount"],
|
||||
"properties": {
|
||||
"nonOrphanVisits": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"orphanVisits": {
|
||||
"$ref": "./VisitsSummary.json"
|
||||
},
|
||||
"visitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "The total amount of visits received on any short URL."
|
||||
"description": "**[DEPRECATED]** Use nonOrphanVisits.total instead"
|
||||
},
|
||||
"orphanVisitsCount": {
|
||||
"deprecated": true,
|
||||
"type": "number",
|
||||
"description": "The total amount of visits that could not be matched to a short URL (visits to the base URL, an invalid short URL or any other kind of 404)."
|
||||
"description": "**[DEPRECATED]** Use orphanVisits.total instead"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
docs/swagger/definitions/VisitsSummary.json
Normal file
18
docs/swagger/definitions/VisitsSummary.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["total", "nonBots", "bots"],
|
||||
"properties": {
|
||||
"total": {
|
||||
"description": "The total amount of visits.",
|
||||
"type": "integer"
|
||||
},
|
||||
"nonBots": {
|
||||
"description": "The amount of visits which were not identified as bots.",
|
||||
"type": "integer"
|
||||
},
|
||||
"bots": {
|
||||
"description": "The amount of visits that were identified as potential bots.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
docs/swagger/examples/short-url-invalid-args-v3.json
Normal file
9
docs/swagger/examples/short-url-invalid-args-v3.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["maxVisits", "validSince"]
|
||||
}
|
||||
}
|
||||
9
docs/swagger/examples/short-url-not-found-v3.json
Normal file
9
docs/swagger/examples/short-url-not-found-v3.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"value": {
|
||||
"detail": "No URL found with short code \"abc123\"",
|
||||
"title": "Short URL not found",
|
||||
"type": "https://shlink.io/api/error/short-url-not-found",
|
||||
"status": 404,
|
||||
"shortCode": "abc123"
|
||||
}
|
||||
}
|
||||
9
docs/swagger/examples/tag-not-found-v3.json
Normal file
9
docs/swagger/examples/tag-not-found-v3.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"value": {
|
||||
"detail": "Tag with name \"foo\" could not be found",
|
||||
"title": "Tag not found",
|
||||
"type": "https://shlink.io/api/error/tag-not-found",
|
||||
"status": 404,
|
||||
"tag": "foo"
|
||||
}
|
||||
}
|
||||
9
docs/swagger/parameters/shortCode.json
Normal file
9
docs/swagger/parameters/shortCode.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code for the short URL.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"3",
|
||||
"2",
|
||||
"1"
|
||||
]
|
||||
|
||||
@@ -73,10 +73,12 @@
|
||||
"shortCode-DESC",
|
||||
"dateCreated-ASC",
|
||||
"dateCreated-DESC",
|
||||
"title-ASC",
|
||||
"title-DESC",
|
||||
"visits-ASC",
|
||||
"visits-DESC",
|
||||
"title-ASC",
|
||||
"title-DESC"
|
||||
"nonBotVisits-ASC",
|
||||
"nonBotVisits-DESC"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -97,6 +99,32 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "excludeMaxVisitsReached",
|
||||
"in": "query",
|
||||
"description": "If true, short URLs which already reached their maximum amount of visits will be excluded.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"true",
|
||||
"false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "excludePastValidUntil",
|
||||
"in": "query",
|
||||
"description": "If true, short URLs which validUntil date is on the past will be excluded.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"true",
|
||||
"false"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -133,10 +161,19 @@
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 328,
|
||||
"visitsSummary": {
|
||||
"total": 328,
|
||||
"nonBots": 328,
|
||||
"bots": 0
|
||||
},
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
@@ -152,10 +189,19 @@
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": "https://shlink.io/ios",
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
"nonBots": 900,
|
||||
"bots": 129
|
||||
},
|
||||
"tags": [
|
||||
"shlink"
|
||||
],
|
||||
@@ -172,8 +218,17 @@
|
||||
"shortCode": "123bA",
|
||||
"shortUrl": "https://example.com/123bA",
|
||||
"longUrl": "https://www.google.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
"visitsSummary": {
|
||||
"total": 25,
|
||||
"nonBots": 0,
|
||||
"bots": 25
|
||||
},
|
||||
"tags": [],
|
||||
"meta": {
|
||||
"validSince": "2017-01-21T00:00:00+02:00",
|
||||
@@ -241,6 +296,9 @@
|
||||
"type": "object",
|
||||
"required": ["longUrl"],
|
||||
"properties": {
|
||||
"deviceLongUrls": {
|
||||
"$ref": "../definitions/DeviceLongUrls.json"
|
||||
},
|
||||
"customSlug": {
|
||||
"description": "A unique custom slug to be used instead of the generated short code",
|
||||
"type": "string"
|
||||
@@ -256,10 +314,6 @@
|
||||
"shortCodeLength": {
|
||||
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
|
||||
"type": "number"
|
||||
},
|
||||
"validateUrl": {
|
||||
"description": "Tells if the long URL should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,10 +332,19 @@
|
||||
},
|
||||
"example": {
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://doma.in/12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 0,
|
||||
"visitsSummary": {
|
||||
"total": 0,
|
||||
"nonBots": 0,
|
||||
"bots": 0
|
||||
},
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
@@ -327,11 +390,11 @@
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "A URL that could not be verified, if the error type is INVALID_URL"
|
||||
"description": "A URL that could not be verified, if the error type is https://shlink.io/api/error/invalid-url"
|
||||
},
|
||||
"customSlug": {
|
||||
"type": "string",
|
||||
"description": "Provided custom slug when the error type is INVALID_SLUG"
|
||||
"description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
@@ -342,10 +405,31 @@
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Invalid arguments": {
|
||||
"$ref": "../examples/short-url-invalid-args.json"
|
||||
"Invalid arguments with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||
},
|
||||
"Invalid long URL": {
|
||||
"Invalid long URL with API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "https://shlink.io/api/error/invalid-url",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Non-unique slug with API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid custom slug",
|
||||
"type": "https://shlink.io/api/error/non-unique-slug",
|
||||
"detail": "Provided slug \"my-slug\" is already in use.",
|
||||
"status": 400,
|
||||
"customSlug": "my-slug"
|
||||
}
|
||||
},
|
||||
"Invalid arguments previous to API v3": {
|
||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
||||
},
|
||||
"Invalid long URL previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
@@ -354,7 +438,7 @@
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Non-unique slug": {
|
||||
"Non-unique slug previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid custom slug",
|
||||
"type": "INVALID_SLUG",
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortenUrl",
|
||||
"deprecated": true,
|
||||
"tags": [
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create a short URL",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.",
|
||||
"description": "**[Deprecated]** Use [Create short URL](#/Short%20URLs/createShortUrl) instead",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
@@ -52,10 +53,19 @@
|
||||
},
|
||||
"example": {
|
||||
"longUrl": "https://github.com/shlinkio/shlink",
|
||||
"shortUrl": "https://doma.in/abc123",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"shortUrl": "https://s.test/abc123",
|
||||
"shortCode": "abc123",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 0,
|
||||
"visitsSummary": {
|
||||
"total": 0,
|
||||
"nonBots": 0,
|
||||
"bots": 0
|
||||
},
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
@@ -74,7 +84,7 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "https://doma.in/abc123"
|
||||
"example": "https://s.test/abc123"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -85,19 +95,39 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "https://shlink.io/api/error/invalid-url",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid URL",
|
||||
"type": "INVALID_URL",
|
||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
||||
"status": 400,
|
||||
"url": "https://invalid-url.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"example": "INVALID_URL"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": "https://shlink.io/api/error/invalid-url"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": "INVALID_URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,13 +11,7 @@
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to resolve.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
@@ -38,10 +32,19 @@
|
||||
},
|
||||
"example": {
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
"nonBots": 820,
|
||||
"bots": 209
|
||||
},
|
||||
"tags": [
|
||||
"shlink"
|
||||
],
|
||||
@@ -83,8 +86,11 @@
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,13 +121,7 @@
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
@@ -153,10 +153,19 @@
|
||||
},
|
||||
"example": {
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://doma.in/12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": "https://shlink.io/android",
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
"nonBots": 900,
|
||||
"bots": 129
|
||||
},
|
||||
"tags": [
|
||||
"shlink"
|
||||
],
|
||||
@@ -203,8 +212,11 @@
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Invalid arguments": {
|
||||
"$ref": "../examples/short-url-invalid-args.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,8 +248,11 @@
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,13 +283,7 @@
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The short code to edit.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
@@ -318,13 +327,27 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"example": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORT_URL_DELETION",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "https://shlink.io/api/error/invalid-short-url-deletion",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Cannot delete short URL",
|
||||
"type": "INVALID_SHORT_URL_DELETION",
|
||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||
"status": 422,
|
||||
"shortCode": "abc123",
|
||||
"threshold": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,8 +378,11 @@
|
||||
]
|
||||
},
|
||||
"examples": {
|
||||
"Not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,7 @@
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
@@ -151,8 +145,85 @@
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Short URL not found": {
|
||||
"$ref": "../examples/short-url-not-found.json"
|
||||
"Short URL not found with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Short URL not found previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected error.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteShortUrlVisits",
|
||||
"tags": [
|
||||
"Visits"
|
||||
],
|
||||
"summary": "Delete visits for short URL",
|
||||
"description": "Delete all existing visits on the short URL behind provided short code.",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/version.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/shortCode.json"
|
||||
},
|
||||
{
|
||||
"$ref": "../parameters/domain.json"
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Deleted visits",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deletedVisits": {
|
||||
"description": "Amount of affected visits",
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"deletedVisits": 536
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "The short code does not belong to any short URL.",
|
||||
"content": {
|
||||
"application/problem+json": {
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Short URL not found with API v3 and newer": {
|
||||
"$ref": "../examples/short-url-not-found-v3.json"
|
||||
},
|
||||
"Short URL not found previous to API v3": {
|
||||
"$ref": "../examples/short-url-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +188,25 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["oldName", "newName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,11 +218,23 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "https://shlink.io/api/error/forbidden-tag-operation",
|
||||
"status": 403
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to rename tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,8 +247,11 @@
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"examples": {
|
||||
"Tag not found": {
|
||||
"$ref": "../examples/tag-not-found.json"
|
||||
"API v3 and newer": {
|
||||
"$ref": "../examples/tag-not-found-v3.json"
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"$ref": "../examples/tag-not-found-v2.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,13 +264,27 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "TAG_CONFLICT",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "https://shlink.io/api/error/tag-conflict",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You cannot rename tag foo, because it already exists",
|
||||
"title": "Tag conflict",
|
||||
"type": "TAG_CONFLICT",
|
||||
"status": 409,
|
||||
"oldName": "bar",
|
||||
"newName": "foo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,11 +342,23 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "https://shlink.io/api/error/forbidden-tag-operation",
|
||||
"status": 403
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "You are not allowed to delete tags",
|
||||
"title": "Forbidden tag operation",
|
||||
"type": "FORBIDDEN_OPERATION",
|
||||
"status": 403
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,12 +94,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"example": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "https://shlink.io/api/error/invalid-data",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"title": "Invalid data",
|
||||
"type": "INVALID_ARGUMENT",
|
||||
"detail": "Provided data is not valid",
|
||||
"status": 400,
|
||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,12 +147,25 @@
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
},
|
||||
"example": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "DOMAIN_NOT_FOUND",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
"examples": {
|
||||
"API v3 and newer": {
|
||||
"value": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "https://shlink.io/api/error/domain-not-found",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
}
|
||||
},
|
||||
"Previous to API v3": {
|
||||
"value": {
|
||||
"detail": "Domain with authority \"example.com\" could not be found",
|
||||
"title": "Domain not found",
|
||||
"type": "DOMAIN_NOT_FOUND",
|
||||
"status": 404,
|
||||
"authority": "example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user