mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 15:23:12 +08:00
Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4d082a87a1 | ||
|
|
1b6512fc8d | ||
|
|
9e32886f60 |
@@ -1,3 +1,4 @@
|
|||||||
|
bin/rr
|
||||||
config/autoload/*local*
|
config/autoload/*local*
|
||||||
data/infra
|
data/infra
|
||||||
data/cache/*
|
data/cache/*
|
||||||
@@ -22,4 +23,4 @@ infection*
|
|||||||
**/test*
|
**/test*
|
||||||
build*
|
build*
|
||||||
**/.*
|
**/.*
|
||||||
bin/helper
|
!config/roadrunner/.rr.yml
|
||||||
|
|||||||
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@v2
|
||||||
|
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-4.12.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
|
||||||
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-4.12.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 "::set-output name=args::--logger-github=false"
|
||||||
|
# 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 "::set-output name=args::--logger-github=false"
|
||||||
|
# else
|
||||||
|
# echo "::set-output name=args::--logger-github=false --git-diff-lines --git-diff-base=develop"
|
||||||
|
# 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-4.12.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
|
||||||
198
.github/workflows/ci.yml
vendored
198
.github/workflows/ci.yml
vendored
@@ -16,146 +16,124 @@ jobs:
|
|||||||
php-version: ['8.1']
|
php-version: ['8.1']
|
||||||
command: ['cs', 'stan', 'swagger:validate']
|
command: ['cs', 'stan', 'swagger:validate']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: './.github/actions/ci-setup'
|
||||||
- name: Use PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
php-extensions: openswoole-4.12.0
|
||||||
extensions: openswoole-4.11.1
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||||
coverage: none
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-interaction --prefer-dist
|
|
||||||
- run: composer ${{ matrix.command }}
|
- run: composer ${{ matrix.command }}
|
||||||
|
|
||||||
tests:
|
unit-tests:
|
||||||
|
uses: './.github/workflows/ci-tests.yml'
|
||||||
|
with:
|
||||||
|
test-group: unit
|
||||||
|
|
||||||
|
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
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.1']
|
php-version: ['8.1', '8.2']
|
||||||
test-group: ['unit', 'api']
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||||
- name: Start database server
|
- uses: shivammathur/setup-php@v2
|
||||||
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:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
tools: composer
|
||||||
extensions: openswoole-4.11.1
|
- run: composer install --no-interaction --prefer-dist
|
||||||
coverage: pcov
|
- run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr
|
||||||
ini-values: pcov.directory=module
|
- run: composer test:api:rr
|
||||||
- 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
|
|
||||||
|
|
||||||
db-tests:
|
sqlite-db-tests:
|
||||||
runs-on: ubuntu-22.04
|
uses: './.github/workflows/ci-db-tests.yml'
|
||||||
strategy:
|
with:
|
||||||
matrix:
|
platform: 'sqlite:ci'
|
||||||
php-version: ['8.1']
|
|
||||||
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
|
||||||
env:
|
|
||||||
LC_ALL: C
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
mutation-tests:
|
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:
|
needs:
|
||||||
- tests
|
- unit-tests
|
||||||
- db-tests
|
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||||
runs-on: ubuntu-22.04
|
with:
|
||||||
strategy:
|
test-group: unit
|
||||||
matrix:
|
|
||||||
php-version: ['8.1']
|
db-mutation-tests:
|
||||||
test-group: ['unit', 'db', 'api']
|
needs:
|
||||||
steps:
|
- sqlite-db-tests
|
||||||
- name: Checkout code
|
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||||
uses: actions/checkout@v2
|
with:
|
||||||
- name: Use PHP
|
test-group: db
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
api-mutation-tests:
|
||||||
php-version: ${{ matrix.php-version }}
|
needs:
|
||||||
tools: composer
|
- openswoole-api-tests
|
||||||
extensions: openswoole-4.11.1
|
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||||
coverage: pcov
|
with:
|
||||||
ini-values: pcov.directory=module
|
test-group: api
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-interaction --prefer-dist
|
cli-mutation-tests:
|
||||||
- uses: actions/download-artifact@v2
|
needs:
|
||||||
with:
|
- cli-tests
|
||||||
path: build
|
uses: './.github/workflows/ci-mutation-tests.yml'
|
||||||
- if: ${{ matrix.test-group == 'unit' }}
|
with:
|
||||||
run: composer infect:ci:unit
|
test-group: cli
|
||||||
env:
|
|
||||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
|
||||||
- if: ${{ matrix.test-group != 'unit' }}
|
|
||||||
run: composer infect:ci:${{ matrix.test-group }}
|
|
||||||
|
|
||||||
upload-coverage:
|
upload-coverage:
|
||||||
needs:
|
needs:
|
||||||
- tests
|
- unit-tests
|
||||||
- db-tests
|
- openswoole-api-tests
|
||||||
|
- cli-tests
|
||||||
|
- sqlite-db-tests
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.1']
|
php-version: ['8.1']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Use PHP
|
- name: Use PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: build
|
path: build
|
||||||
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
- 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-db/coverage-db.cov build/coverage-db.cov
|
||||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||||
|
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||||
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
|
- 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: php phpcov-8.2.1.phar merge build --clover build/clover.xml
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
@@ -165,7 +143,10 @@ jobs:
|
|||||||
|
|
||||||
delete-artifacts:
|
delete-artifacts:
|
||||||
needs:
|
needs:
|
||||||
- mutation-tests
|
- unit-mutation-tests
|
||||||
|
- db-mutation-tests
|
||||||
|
- api-mutation-tests
|
||||||
|
- cli-mutation-tests
|
||||||
- upload-coverage
|
- upload-coverage
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
@@ -175,12 +156,13 @@ jobs:
|
|||||||
coverage-unit
|
coverage-unit
|
||||||
coverage-db
|
coverage-db
|
||||||
coverage-api
|
coverage-api
|
||||||
|
coverage-cli
|
||||||
|
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 100
|
fetch-depth: 100
|
||||||
- uses: marceloprado/has-changed-path@v1
|
- uses: marceloprado/has-changed-path@v1
|
||||||
|
|||||||
37
.github/workflows/docker-image-build.yml
vendored
37
.github/workflows/docker-image-build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build docker image
|
name: Build and publish docker image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -8,21 +8,20 @@ on:
|
|||||||
- 'v*'
|
- 'v*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-openswoole:
|
||||||
runs-on: ubuntu-22.04
|
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||||
steps:
|
secrets: inherit
|
||||||
- name: Checkout code
|
with:
|
||||||
uses: actions/checkout@v2
|
image-name: shlinkio/shlink
|
||||||
- name: Set up QEMU
|
version-arg-name: SHLINK_VERSION
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
- name: Set up Docker Buildx
|
build-roadrunner:
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||||
with:
|
secrets: inherit
|
||||||
version: latest
|
with:
|
||||||
- name: Login to docker hub
|
image-name: shlinkio/shlink
|
||||||
uses: docker/login-action@v1
|
version-arg-name: SHLINK_VERSION
|
||||||
with:
|
platforms: 'linux/arm64/v8,linux/amd64'
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
tags-suffix: roadrunner
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
extra-build-args: |
|
||||||
- name: Build the image
|
SHLINK_RUNTIME=rr
|
||||||
run: bash ./docker/build
|
|
||||||
|
|||||||
22
.github/workflows/publish-release.yml
vendored
22
.github/workflows/publish-release.yml
vendored
@@ -10,22 +10,21 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.1']
|
php-version: ['8.1', '8.2']
|
||||||
swoole: ['yes', 'no']
|
swoole: ['yes', 'no']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: './.github/actions/ci-setup'
|
||||||
- name: Use PHP
|
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
php-extensions: openswoole-4.12.0
|
||||||
extensions: openswoole-4.11.1
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
|
install-deps: 'no'
|
||||||
- if: ${{ matrix.swoole == 'yes' }}
|
- if: ${{ matrix.swoole == 'yes' }}
|
||||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||||
- if: ${{ matrix.swoole == 'no' }}
|
- if: ${{ matrix.swoole == 'no' }}
|
||||||
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
||||||
path: build
|
path: build
|
||||||
@@ -34,9 +33,8 @@ jobs:
|
|||||||
needs: ['build']
|
needs: ['build']
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
- uses: actions/download-artifact@v3
|
||||||
- uses: actions/download-artifact@v2
|
|
||||||
with:
|
with:
|
||||||
path: build
|
path: build
|
||||||
- name: Publish release with assets
|
- name: Publish release with assets
|
||||||
@@ -53,7 +51,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.1']
|
php-version: ['8.1', '8.2']
|
||||||
swoole: ['yes', 'no']
|
swoole: ['yes', 'no']
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v1
|
||||||
|
|||||||
12
.github/workflows/publish-swagger-spec.yml
vendored
12
.github/workflows/publish-swagger-spec.yml
vendored
@@ -12,20 +12,16 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.1']
|
php-version: ['8.1']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v3
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: determine_version
|
id: determine_version
|
||||||
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
run: echo "::set-output name=version::${GITHUB_REF#refs/tags/}"
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Use PHP
|
- uses: './.github/actions/ci-setup'
|
||||||
uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
php-extensions: openswoole-4.12.0
|
||||||
extensions: openswoole-4.11.1
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
coverage: none
|
|
||||||
- run: composer install --no-interaction --prefer-dist
|
|
||||||
- run: composer swagger:inline
|
- run: composer swagger:inline
|
||||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
|||||||
.idea
|
.idea
|
||||||
|
bin/.rr.*
|
||||||
|
bin/rr
|
||||||
|
config/roadrunner/.pid
|
||||||
build
|
build
|
||||||
!docker/build
|
!docker/build
|
||||||
composer.lock
|
composer.lock
|
||||||
|
|||||||
121
CHANGELOG.md
121
CHANGELOG.md
@@ -4,6 +4,127 @@ 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).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [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
|
## [3.2.1] - 2022-08-08
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@@ -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.
|
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.
|
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`.
|
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: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 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` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||||
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
|
||||||
|
|
||||||
## Pull request process
|
## 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`.
|
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
|
## Architectural Decision Records
|
||||||
|
|
||||||
|
|||||||
37
Dockerfile
37
Dockerfile
@@ -1,10 +1,13 @@
|
|||||||
FROM php:8.1.9-alpine3.16 as base
|
FROM php:8.2-alpine3.17 as base
|
||||||
|
|
||||||
ARG SHLINK_VERSION=latest
|
ARG SHLINK_VERSION=latest
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ENV OPENSWOOLE_VERSION 4.11.1
|
ARG SHLINK_RUNTIME=openswoole
|
||||||
|
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||||
|
ENV OPENSWOOLE_VERSION 4.12.0
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
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
|
||||||
ENV LC_ALL "C"
|
ENV LC_ALL "C"
|
||||||
|
|
||||||
WORKDIR /etc/shlink
|
WORKDIR /etc/shlink
|
||||||
@@ -12,7 +15,7 @@ WORKDIR /etc/shlink
|
|||||||
# Install required PHP extensions
|
# Install required PHP extensions
|
||||||
RUN \
|
RUN \
|
||||||
# Temp install dev dependencies needed to compile the extensions
|
# 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 && \
|
docker-php-ext-install -j"$(nproc)" pdo_mysql pdo_pgsql intl calendar sockets bcmath zip gd && \
|
||||||
apk add --no-cache sqlite-libs && \
|
apk add --no-cache sqlite-libs && \
|
||||||
docker-php-ext-install -j"$(nproc)" pdo_sqlite && \
|
docker-php-ext-install -j"$(nproc)" pdo_sqlite && \
|
||||||
@@ -22,14 +25,16 @@ RUN \
|
|||||||
|
|
||||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||||
docker-php-ext-enable openswoole && \
|
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||||
|
docker-php-ext-enable openswoole ; \
|
||||||
|
fi; \
|
||||||
if [ $(uname -m) == "x86_64" ]; then \
|
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 && \
|
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||||
apk add --no-cache --allow-untrusted msodbcsql17_${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} && \
|
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} && \
|
||||||
docker-php-ext-enable pdo_sqlsrv && \
|
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; \
|
fi; \
|
||||||
apk del .phpize-deps
|
apk del .phpize-deps
|
||||||
|
|
||||||
@@ -38,7 +43,12 @@ FROM base as builder
|
|||||||
COPY . .
|
COPY . .
|
||||||
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
COPY --from=composer:2 /usr/bin/composer ./composer.phar
|
||||||
RUN apk add --no-cache git && \
|
RUN apk add --no-cache git && \
|
||||||
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
|
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||||
|
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||||
|
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \
|
||||||
|
elif [ $SHLINK_RUNTIME == 'rr' ]; then \
|
||||||
|
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||||
|
fi; \
|
||||||
php composer.phar clear-cache && \
|
php composer.phar clear-cache && \
|
||||||
rm -r docker composer.* && \
|
rm -r docker composer.* && \
|
||||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||||
@@ -49,9 +59,12 @@ FROM base
|
|||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
|
|
||||||
COPY --from=builder /etc/shlink .
|
COPY --from=builder /etc/shlink .
|
||||||
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
|
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
|
||||||
|
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||||
|
php ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; \
|
||||||
|
fi;
|
||||||
|
|
||||||
# Expose default openswoole port
|
# Expose default port
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Copy config specific for the image
|
# Copy config specific for the image
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
[](https://hub.docker.com/r/shlinkio/shlink/)
|
[](https://hub.docker.com/r/shlinkio/shlink/)
|
||||||
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
||||||
[](https://twitter.com/shlinkio)
|
[](https://twitter.com/shlinkio)
|
||||||
|
[](https://fosstodon.org/@shlinkio)
|
||||||
[](https://slnk.to/donate)
|
[](https://slnk.to/donate)
|
||||||
|
|
||||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.
|
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)
|
- [Full documentation](#full-documentation)
|
||||||
- [Docker image](#docker-image)
|
- [Docker image](#docker-image)
|
||||||
- [Self hosted](#self-hosted)
|
- [Self-hosted](#self-hosted)
|
||||||
- [Download](#download)
|
- [Download](#download)
|
||||||
- [Configure](#configure)
|
- [Configure](#configure)
|
||||||
- [Using shlink](#using-shlink)
|
- [Using shlink](#using-shlink)
|
||||||
|
|||||||
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
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
export APP_ENV=test
|
export APP_ENV=test
|
||||||
export DB_DRIVER=postgres
|
|
||||||
export TEST_ENV=api
|
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
|
# Reset logs
|
||||||
|
OUTPUT_LOGS=data/log/api-tests/output.log
|
||||||
rm -rf data/log/api-tests
|
rm -rf data/log/api-tests
|
||||||
mkdir 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
|
# 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...'
|
echo 'Starting server...'
|
||||||
vendor/bin/laminas mezzio:swoole:start -d
|
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
|
||||||
sleep 2
|
[ "$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 $*
|
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
|
||||||
testsExitCode=$?
|
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 this script with the same code as the tests. If tests failed, this script has to fail
|
||||||
exit $testsExitCode
|
exit $testsExitCode
|
||||||
|
|||||||
4
build.sh
4
build.sh
@@ -24,6 +24,7 @@ rsync -av * "${builtContent}" \
|
|||||||
--exclude=*docker* \
|
--exclude=*docker* \
|
||||||
--exclude=Dockerfile \
|
--exclude=Dockerfile \
|
||||||
--include=.htaccess \
|
--include=.htaccess \
|
||||||
|
--include=config/roadrunner/.rr.yml \
|
||||||
--exclude-from=./.dockerignore
|
--exclude-from=./.dockerignore
|
||||||
cd "${builtContent}"
|
cd "${builtContent}"
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ ${composerBin} install --no-dev --prefer-dist $composerFlags
|
|||||||
if [[ $noSwoole ]]; then
|
if [[ $noSwoole ]]; then
|
||||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
${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 --with-all-dependencies --update-no-dev $composerFlags
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Delete development files
|
# Delete development files
|
||||||
|
|||||||
117
composer.json
117
composer.json
@@ -13,42 +13,46 @@
|
|||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
|
"ext-curl": "*",
|
||||||
|
"ext-gd": "*",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"akrabat/ip-address-middleware": "^2.1",
|
"akrabat/ip-address-middleware": "^2.1",
|
||||||
"cakephp/chronos": "^2.3",
|
"cakephp/chronos": "^2.3",
|
||||||
"doctrine/migrations": "^3.5",
|
"doctrine/migrations": "^3.5",
|
||||||
"doctrine/orm": "^2.12",
|
"doctrine/orm": "^2.13.3",
|
||||||
"endroid/qr-code": "^4.4",
|
"endroid/qr-code": "^4.6",
|
||||||
"geoip2/geoip2": "^2.12",
|
"geoip2/geoip2": "^2.13",
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
"happyr/doctrine-specification": "^2.0",
|
"happyr/doctrine-specification": "^2.0",
|
||||||
"jaybizzle/crawler-detect": "^1.2.110",
|
"jaybizzle/crawler-detect": "^1.2.112",
|
||||||
"laminas/laminas-config": "^3.7",
|
"laminas/laminas-config": "^3.7",
|
||||||
"laminas/laminas-config-aggregator": "^1.8",
|
"laminas/laminas-config-aggregator": "^1.11",
|
||||||
"laminas/laminas-diactoros": "^2.14",
|
"laminas/laminas-diactoros": "^2.19",
|
||||||
"laminas/laminas-inputfilter": "^2.19",
|
"laminas/laminas-inputfilter": "^2.22",
|
||||||
"laminas/laminas-servicemanager": "^3.16",
|
"laminas/laminas-servicemanager": "^3.19",
|
||||||
"laminas/laminas-stdlib": "^3.11",
|
"laminas/laminas-stdlib": "^3.15",
|
||||||
"lcobucci/jwt": "^4.1",
|
"lcobucci/jwt": "^4.2",
|
||||||
"league/uri": "^6.7",
|
"league/uri": "^6.8",
|
||||||
"lstrojny/functional-php": "^1.17",
|
"lstrojny/functional-php": "^1.17",
|
||||||
"mezzio/mezzio": "^3.11",
|
"mezzio/mezzio": "^3.13",
|
||||||
"mezzio/mezzio-fastroute": "^3.5",
|
"mezzio/mezzio-fastroute": "^3.7",
|
||||||
"mezzio/mezzio-problem-details": "^1.6",
|
"mezzio/mezzio-problem-details": "^1.7",
|
||||||
"mezzio/mezzio-swoole": "^4.3",
|
"mezzio/mezzio-swoole": "^4.5",
|
||||||
"mlocati/ip-lib": "^1.18",
|
"mlocati/ip-lib": "^1.18",
|
||||||
"ocramius/proxy-manager": "^2.14",
|
"ocramius/proxy-manager": "^2.14",
|
||||||
"pagerfanta/core": "^3.6",
|
"pagerfanta/core": "^3.6",
|
||||||
"php-middleware/request-id": "^4.1",
|
"php-middleware/request-id": "^4.1",
|
||||||
"pugx/shortid-php": "^1.0",
|
"pugx/shortid-php": "^1.1",
|
||||||
"ramsey/uuid": "^4.3",
|
"ramsey/uuid": "^4.5",
|
||||||
"shlinkio/shlink-common": "^5.0",
|
"shlinkio/shlink-common": "^5.2",
|
||||||
"shlinkio/shlink-config": "^2.0",
|
"shlinkio/shlink-config": "^2.3",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.5",
|
"shlinkio/shlink-event-dispatcher": "^2.6",
|
||||||
"shlinkio/shlink-importer": "^4.0",
|
"shlinkio/shlink-importer": "^5.0",
|
||||||
"shlinkio/shlink-installer": "^8.1",
|
"shlinkio/shlink-installer": "^8.2",
|
||||||
"shlinkio/shlink-ip-geolocation": "^3.0",
|
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||||
|
"spiral/roadrunner": "^2.11",
|
||||||
|
"spiral/roadrunner-jobs": "^2.5",
|
||||||
"symfony/console": "^6.1",
|
"symfony/console": "^6.1",
|
||||||
"symfony/filesystem": "^6.1",
|
"symfony/filesystem": "^6.1",
|
||||||
"symfony/lock": "^6.1",
|
"symfony/lock": "^6.1",
|
||||||
@@ -58,18 +62,18 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"cebe/php-openapi": "^1.7",
|
"cebe/php-openapi": "^1.7",
|
||||||
"devster/ubench": "^2.1",
|
"devster/ubench": "^2.1",
|
||||||
"dms/phpunit-arraysubset-asserts": "^0.3.0",
|
"dms/phpunit-arraysubset-asserts": "^0.4.0",
|
||||||
"infection/infection": "^0.26.5",
|
"infection/infection": "^0.26.15",
|
||||||
"openswoole/ide-helper": "~4.11.1",
|
"openswoole/ide-helper": "~4.11.5",
|
||||||
"phpspec/prophecy-phpunit": "^2.0",
|
|
||||||
"phpstan/phpstan": "^1.8",
|
"phpstan/phpstan": "^1.8",
|
||||||
"phpstan/phpstan-doctrine": "^1.3",
|
"phpstan/phpstan-doctrine": "^1.3",
|
||||||
|
"phpstan/phpstan-phpunit": "^1.1",
|
||||||
"phpstan/phpstan-symfony": "^1.2",
|
"phpstan/phpstan-symfony": "^1.2",
|
||||||
"phpunit/php-code-coverage": "^9.2",
|
"phpunit/php-code-coverage": "^9.2",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~2.3.0",
|
"shlinkio/php-coding-standard": "~2.3.0",
|
||||||
"shlinkio/shlink-test-utils": "^3.1.0",
|
"shlinkio/shlink-test-utils": "^3.3",
|
||||||
"symfony/var-dumper": "^6.1",
|
"symfony/var-dumper": "^6.1",
|
||||||
"veewee/composer-run-parallel": "^1.1"
|
"veewee/composer-run-parallel": "^1.1"
|
||||||
},
|
},
|
||||||
@@ -87,8 +91,10 @@
|
|||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||||
|
"ShlinkioCliTest\\Shlink\\CLI\\": "module/CLI/test-cli",
|
||||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||||
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
|
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
|
||||||
|
"ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db",
|
||||||
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
||||||
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
|
"ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
|
||||||
},
|
},
|
||||||
@@ -98,31 +104,18 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ci": [
|
"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 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",
|
||||||
"cs:fix": "phpcbf",
|
"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": [
|
||||||
"@test:unit",
|
"@parallel test:unit test:db",
|
||||||
"@test:db",
|
"@parallel test:api test:cli"
|
||||||
"@test:api"
|
|
||||||
],
|
],
|
||||||
"test:ci": [
|
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||||
"@test:unit:ci",
|
"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: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:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
"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": "@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",
|
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||||
@@ -132,12 +125,18 @@
|
|||||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
|
||||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
|
||||||
"test:api": "bin/test/run-api-tests.sh",
|
"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",
|
"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",
|
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
|
||||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=84",
|
"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",
|
||||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json",
|
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
||||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json",
|
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
||||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api",
|
"infect:ci:base": "infection --threads=max --only-covered --only-covering-test-cases --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": [
|
"infect:test": [
|
||||||
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
|
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
|
||||||
"@infect:ci"
|
"@infect:ci"
|
||||||
@@ -150,18 +149,20 @@
|
|||||||
"@test:api:ci",
|
"@test:api:ci",
|
||||||
"@infect:ci:api"
|
"@infect:ci:api"
|
||||||
],
|
],
|
||||||
|
"infect:test:cli": [
|
||||||
|
"@test:cli:ci",
|
||||||
|
"@infect:ci:cli"
|
||||||
|
],
|
||||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||||
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.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"
|
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
|
||||||
},
|
},
|
||||||
"scripts-descriptions": {
|
"scripts-descriptions": {
|
||||||
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"</>",
|
"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": "<fg=blue;options=bold>Checks coding styles</>",
|
||||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||||
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
"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": "<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: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</>",
|
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||||
@@ -173,7 +174,11 @@
|
|||||||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
"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: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": "<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": "<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: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</>",
|
"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,
|
'debug' => false,
|
||||||
|
|
||||||
// Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't
|
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
||||||
// generate a cache file that's then used by non-openswoole web executions
|
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,12 +3,22 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Mezzio\Container;
|
use Mezzio\Container;
|
||||||
use Psr\Http\Client\ClientInterface;
|
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 [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
|
'factories' => [
|
||||||
|
PSR7Worker::class => ConfigAbstractFactory::class,
|
||||||
|
],
|
||||||
|
|
||||||
'delegators' => [
|
'delegators' => [
|
||||||
Mezzio\Application::class => [
|
Mezzio\Application::class => [
|
||||||
Container\ApplicationConfigInjectionDelegator::class,
|
Container\ApplicationConfigInjectionDelegator::class,
|
||||||
@@ -26,4 +36,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()),
|
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
|
||||||
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
||||||
'charset' => $resolveCharset(),
|
'charset' => $resolveCharset(),
|
||||||
|
'driverOptions' => $driver !== 'mssql' ? [] : [
|
||||||
|
'TrustServerCertificate' => 'true',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ use Laminas\Stratigility\Middleware\ErrorHandler;
|
|||||||
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
|
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
|
||||||
use Shlinkio\Shlink\Common\Logger;
|
use Shlinkio\Shlink\Common\Logger;
|
||||||
|
|
||||||
|
use function Shlinkio\Shlink\Core\toProblemDetailsType;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'problem-details' => [
|
'problem-details' => [
|
||||||
'default_types_map' => [
|
'default_types_map' => [
|
||||||
404 => 'NOT_FOUND',
|
404 => toProblemDetailsType('not-found'),
|
||||||
500 => 'INTERNAL_SERVER_ERROR',
|
500 => toProblemDetailsType('internal-server-error'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ return [
|
|||||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||||
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||||
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||||
|
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
|
||||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||||
@@ -72,9 +73,18 @@ return [
|
|||||||
InstallationCommand::DB_MIGRATE->value => [
|
InstallationCommand::DB_MIGRATE->value => [
|
||||||
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
'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 => [
|
InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [
|
||||||
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
||||||
],
|
],
|
||||||
|
InstallationCommand::API_KEY_GENERATE->value => [
|
||||||
|
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ return [
|
|||||||
'mercure' => [
|
'mercure' => [
|
||||||
'public_hub_url' => 'http://localhost:8001',
|
'public_hub_url' => 'http://localhost:8001',
|
||||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||||
'jwt_secret' => 'mercure_jwt_key',
|
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ return [
|
|||||||
'rest' => [
|
'rest' => [
|
||||||
'path' => '/rest',
|
'path' => '/rest',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
|
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
|
||||||
Router\Middleware\ImplicitOptionsMiddleware::class,
|
Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||||
Rest\Middleware\BodyParserMiddleware::class,
|
Rest\Middleware\BodyParserMiddleware::class,
|
||||||
Rest\Middleware\AuthenticationMiddleware::class,
|
Rest\Middleware\AuthenticationMiddleware::class,
|
||||||
|
|||||||
@@ -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' => [
|
'cache' => [
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'servers' => 'tcp://shlink_redis:6379',
|
'servers' => 'tcp://shlink_redis:6379',
|
||||||
|
// 'servers' => 'tcp://barbar@shlink_redis_acl:6379',
|
||||||
|
// 'servers' => 'tcp://foo:bar@shlink_redis_acl:6379',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,23 @@ namespace Shlinkio\Shlink;
|
|||||||
use Fig\Http\Message\RequestMethodInterface;
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
use RKA\Middleware\IpAddress;
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Action as CoreAction;
|
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\Action;
|
||||||
use Shlinkio\Shlink\Rest\ConfigProvider;
|
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||||
use Shlinkio\Shlink\Rest\Middleware;
|
use Shlinkio\Shlink\Rest\Middleware;
|
||||||
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||||
|
|
||||||
|
// TODO This should be based on config, not the env var
|
||||||
|
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
// The order of the routes defined here matters. Changing it might cause path conflicts
|
// The order of the routes defined here matters. Changing it might cause path conflicts
|
||||||
@@ -90,9 +97,10 @@ return (static function (): array {
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => CoreAction\RedirectAction::class,
|
'name' => CoreAction\RedirectAction::class,
|
||||||
'path' => '/{shortCode}',
|
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
IpAddress::class,
|
IpAddress::class,
|
||||||
|
TrimTrailingSlashMiddleware::class,
|
||||||
CoreAction\RedirectAction::class,
|
CoreAction\RedirectAction::class,
|
||||||
],
|
],
|
||||||
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
|||||||
@@ -4,33 +4,40 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
return [
|
return (static function (): array {
|
||||||
|
/** @var string|null $disableTrackingFrom */
|
||||||
|
$disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv();
|
||||||
|
|
||||||
'tracking' => [
|
return [
|
||||||
// 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),
|
|
||||||
|
|
||||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
'tracking' => [
|
||||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
// 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
|
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||||
|
|
||||||
// If true, visits will not be tracked at all
|
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
'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
|
// If true, visits will not be tracked at all
|
||||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// If true, the referrer will not be tracked
|
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// If true, the user agent will not be tracked
|
// If true, the referrer will not be tracked
|
||||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
'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
|
// If true, the user agent will not be tracked
|
||||||
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(),
|
'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)),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ return (static function (): array {
|
|||||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
||||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->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),
|
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||||
|
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$isSwoole = extension_loaded('openswoole');
|
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||||
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'url_shortener' => [
|
'url_shortener' => [
|
||||||
'domain' => [
|
'domain' => [
|
||||||
'schema' => 'http',
|
'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,
|
'auto_resolve_titles' => true,
|
||||||
// 'multi_segment_slugs_enabled' => true,
|
// 'multi_segment_slugs_enabled' => true,
|
||||||
|
// 'trailing_slash_enabled' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
|||||||
|
|
||||||
use function class_exists;
|
use function class_exists;
|
||||||
use function Shlinkio\Shlink\Config\env;
|
use function Shlinkio\Shlink\Config\env;
|
||||||
|
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
|
||||||
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
|
|
||||||
use const PHP_SAPI;
|
use const PHP_SAPI;
|
||||||
|
|
||||||
$isCli = PHP_SAPI === 'cli';
|
|
||||||
$isTestEnv = env('APP_ENV') === 'test';
|
$isTestEnv = env('APP_ENV') === 'test';
|
||||||
|
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||||
|
|
||||||
return (new ConfigAggregator\ConfigAggregator([
|
return (new ConfigAggregator\ConfigAggregator([
|
||||||
! $isTestEnv
|
! $isTestEnv
|
||||||
@@ -26,7 +28,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||||||
Mezzio\ConfigProvider::class,
|
Mezzio\ConfigProvider::class,
|
||||||
Mezzio\Router\ConfigProvider::class,
|
Mezzio\Router\ConfigProvider::class,
|
||||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||||
$isCli && class_exists(Swoole\ConfigProvider::class)
|
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||||
? Swoole\ConfigProvider::class
|
? Swoole\ConfigProvider::class
|
||||||
: new ConfigAggregator\ArrayProvider([]),
|
: new ConfigAggregator\ArrayProvider([]),
|
||||||
ProblemDetails\ConfigProvider::class,
|
ProblemDetails\ConfigProvider::class,
|
||||||
|
|||||||
49
config/roadrunner/.rr.dev.yml
Normal file
49
config/roadrunner/.rr.dev.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: '2.7'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pool:
|
||||||
|
num_workers: 1
|
||||||
|
timeout: 300
|
||||||
|
consume: ['shlink']
|
||||||
|
pipelines:
|
||||||
|
shlink:
|
||||||
|
driver: memory
|
||||||
|
config:
|
||||||
|
priority: 10
|
||||||
|
prefetch: 10
|
||||||
|
|
||||||
|
logs:
|
||||||
|
mode: development
|
||||||
|
channels:
|
||||||
|
http:
|
||||||
|
level: debug
|
||||||
|
server:
|
||||||
|
level: debug
|
||||||
|
metrics:
|
||||||
|
level: debug
|
||||||
|
|
||||||
|
reload:
|
||||||
|
interval: 1s
|
||||||
|
patterns: ['.php']
|
||||||
|
services:
|
||||||
|
http:
|
||||||
|
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||||
|
recursive: true
|
||||||
|
jobs:
|
||||||
|
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
|
||||||
|
recursive: true
|
||||||
36
config/roadrunner/.rr.yml
Normal file
36
config/roadrunner/.rr.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
version: '2.7'
|
||||||
|
|
||||||
|
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}'
|
||||||
|
middleware: ['static']
|
||||||
|
static:
|
||||||
|
dir: '../../public'
|
||||||
|
forbid: ['.php', '.htaccess']
|
||||||
|
pool:
|
||||||
|
num_workers: ${WEB_WORKER_NUM}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
timeout: 300 # 5 minutes
|
||||||
|
pool:
|
||||||
|
num_workers: ${TASK_WORKER_NUM}
|
||||||
|
consume: ['shlink']
|
||||||
|
pipelines:
|
||||||
|
shlink:
|
||||||
|
driver: memory
|
||||||
|
config:
|
||||||
|
priority: 10
|
||||||
|
prefetch: 10
|
||||||
|
|
||||||
|
logs:
|
||||||
|
mode: production
|
||||||
|
channels:
|
||||||
|
http:
|
||||||
|
level: info # Log all http requests, set to info to disable
|
||||||
|
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 register_shutdown_function;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||||
|
|
||||||
/** @var ContainerInterface $container */
|
/** @var ContainerInterface $container */
|
||||||
$container = require __DIR__ . '/../container.php';
|
$container = require __DIR__ . '/../container.php';
|
||||||
@@ -24,10 +24,15 @@ $httpClient = $container->get('shlink_test_api_client');
|
|||||||
register_shutdown_function(function () use ($httpClient): void {
|
register_shutdown_function(function () use ($httpClient): void {
|
||||||
$httpClient->request(
|
$httpClient->request(
|
||||||
'GET',
|
'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::setApiClient($httpClient);
|
||||||
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
|
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(
|
||||||
|
['bin/cli', 'db:create'],
|
||||||
|
['bin/cli', 'db:migrate'],
|
||||||
|
['bin/doctrine', 'orm:schema-tool:drop'],
|
||||||
|
['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 */
|
/** @var ContainerInterface $container */
|
||||||
$container = require __DIR__ . '/../container.php';
|
$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'));
|
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink;
|
namespace ShlinkioTest\Shlink;
|
||||||
|
|
||||||
const SWOOLE_TESTING_HOST = '127.0.0.1';
|
const API_TESTS_HOST = '127.0.0.1';
|
||||||
const SWOOLE_TESTING_PORT = 9999;
|
const API_TESTS_PORT = 9999;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ use GuzzleHttp\Client;
|
|||||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
|
use League\Event\EventDispatcher;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use PHPUnit\Runner\Version;
|
use PHPUnit\Runner\Version;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
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\PHP;
|
||||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
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 Laminas\Stratigility\middleware;
|
||||||
use function Shlinkio\Shlink\Config\env;
|
use function Shlinkio\Shlink\Config\env;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
use function sys_get_temp_dir;
|
use function sys_get_temp_dir;
|
||||||
|
|
||||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
|
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||||
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
|
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||||
|
|
||||||
$isApiTest = env('TEST_ENV') === 'api';
|
$isApiTest = env('TEST_ENV') === 'api';
|
||||||
$generateCoverage = env('GENERATE_COVERAGE') === 'yes';
|
$isCliTest = env('TEST_ENV') === 'cli';
|
||||||
if ($isApiTest && $generateCoverage) {
|
$isE2eTest = $isApiTest || $isCliTest;
|
||||||
|
$coverageType = env('GENERATE_COVERAGE');
|
||||||
|
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
|
||||||
|
|
||||||
|
$coverage = null;
|
||||||
|
if ($isE2eTest && $generateCoverage) {
|
||||||
$filter = new Filter();
|
$filter = new Filter();
|
||||||
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
|
$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);
|
$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 {
|
$buildDbConnection = static function (): array {
|
||||||
$driver = env('DB_DRIVER', 'sqlite');
|
$driver = env('DB_DRIVER', 'sqlite');
|
||||||
$isCi = env('CI', false);
|
$isCi = env('CI', false);
|
||||||
@@ -63,6 +101,9 @@ $buildDbConnection = static function (): array {
|
|||||||
'user' => 'sa',
|
'user' => 'sa',
|
||||||
'password' => 'Passw0rd!',
|
'password' => 'Passw0rd!',
|
||||||
'dbname' => 'shlink_test',
|
'dbname' => 'shlink_test',
|
||||||
|
'driverOptions' => [
|
||||||
|
'TrustServerCertificate' => 'true',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
default => [ // mysql and maria
|
default => [ // mysql and maria
|
||||||
'driver' => 'pdo_mysql',
|
'driver' => 'pdo_mysql',
|
||||||
@@ -92,14 +133,13 @@ return [
|
|||||||
'schema' => 'http',
|
'schema' => 'http',
|
||||||
'hostname' => 'doma.in',
|
'hostname' => 'doma.in',
|
||||||
],
|
],
|
||||||
'validate_url' => true,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
'mezzio-swoole' => [
|
'mezzio-swoole' => [
|
||||||
'enable_coroutine' => false,
|
'enable_coroutine' => false,
|
||||||
'swoole-http-server' => [
|
'swoole-http-server' => [
|
||||||
'host' => SWOOLE_TESTING_HOST,
|
'host' => API_TESTS_HOST,
|
||||||
'port' => SWOOLE_TESTING_PORT,
|
'port' => API_TESTS_PORT,
|
||||||
'process-name' => 'shlink_test',
|
'process-name' => 'shlink_test',
|
||||||
'options' => [
|
'options' => [
|
||||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
||||||
@@ -113,17 +153,10 @@ return [
|
|||||||
[
|
[
|
||||||
'name' => 'dump_coverage',
|
'name' => 'dump_coverage',
|
||||||
'path' => '/api-tests/stop-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,
|
// TODO I have tried moving this block to a listener so that it's invoked automatically,
|
||||||
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
||||||
if ($coverage) { // @phpstan-ignore-line
|
$exportCoverage();
|
||||||
$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');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new EmptyResponse();
|
return new EmptyResponse();
|
||||||
}),
|
}),
|
||||||
'allowed_methods' => ['GET'],
|
'allowed_methods' => ['GET'],
|
||||||
@@ -157,13 +190,69 @@ return [
|
|||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'services' => [
|
'services' => [
|
||||||
'shlink_test_api_client' => new Client([
|
'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,
|
'http_errors' => false,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
'factories' => [
|
'factories' => [
|
||||||
TestUtils\Helper\TestHelper::class => InvokableFactory::class,
|
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' => [
|
'entity_manager' => [
|
||||||
@@ -172,6 +261,7 @@ return [
|
|||||||
|
|
||||||
'data_fixtures' => [
|
'data_fixtures' => [
|
||||||
'paths' => [
|
'paths' => [
|
||||||
|
// TODO These are used for CLI tests too, so maybe should be somewhere else
|
||||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
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
|
RUN apk update
|
||||||
|
|
||||||
@@ -30,7 +31,9 @@ RUN docker-php-ext-install gd
|
|||||||
RUN apk add --no-cache postgresql-dev
|
RUN apk add --no-cache postgresql-dev
|
||||||
RUN docker-php-ext-install pdo_pgsql
|
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
|
RUN docker-php-ext-install bcmath
|
||||||
|
|
||||||
# Install APCu extension
|
# 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
|
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||||
|
|
||||||
# Install pcov and sqlsrv driver
|
# 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 && \
|
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||||
apk add --allow-untrusted msodbcsql17_${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 && \
|
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||||
apk del .phpize-deps && \
|
apk del .phpize-deps && \
|
||||||
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||||
|
|
||||||
# Install composer
|
# Install composer
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/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 --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>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV INOTIFY_VERSION 3.0.0
|
ENV INOTIFY_VERSION 3.0.0
|
||||||
ENV OPENSWOOLE_VERSION 4.11.1
|
ENV OPENSWOOLE_VERSION 4.12.0
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
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
|
RUN apk update
|
||||||
|
|
||||||
@@ -32,7 +33,9 @@ RUN docker-php-ext-install gd
|
|||||||
RUN apk add --no-cache postgresql-dev
|
RUN apk add --no-cache postgresql-dev
|
||||||
RUN docker-php-ext-install pdo_pgsql
|
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
|
RUN docker-php-ext-install bcmath
|
||||||
|
|
||||||
# Install APCu extension
|
# Install APCu extension
|
||||||
@@ -54,13 +57,13 @@ RUN mkdir -p /usr/src/php/ext/inotify \
|
|||||||
&& rm /tmp/inotify.tar.gz
|
&& rm /tmp/inotify.tar.gz
|
||||||
|
|
||||||
# Install openswoole, pcov and mssql driver
|
# 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 && \
|
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||||
apk add --allow-untrusted msodbcsql17_${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 && \
|
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||||
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||||
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
|
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
|
||||||
apk del .phpize-deps && \
|
apk del .phpize-deps && \
|
||||||
rm msodbcsql17_${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||||
|
|
||||||
# Install composer
|
# Install composer
|
||||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/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\Schema\Schema;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
|
|
||||||
final class Version20210207100807 extends AbstractMigration
|
final class Version20210207100807 extends AbstractMigration
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ services:
|
|||||||
- /etc/passwd:/etc/passwd:ro
|
- /etc/passwd:/etc/passwd:ro
|
||||||
- /etc/group:/etc/group: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:
|
shlink_db_mysql:
|
||||||
user: 1000:1000
|
user: 1000:1000
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ services:
|
|||||||
- shlink_db_maria
|
- shlink_db_maria
|
||||||
- shlink_db_ms
|
- shlink_db_ms
|
||||||
- shlink_redis
|
- shlink_redis
|
||||||
|
- shlink_redis_acl
|
||||||
- shlink_mercure
|
- shlink_mercure
|
||||||
- shlink_mercure_proxy
|
- shlink_mercure_proxy
|
||||||
- shlink_rabbitmq
|
- shlink_rabbitmq
|
||||||
@@ -65,6 +66,32 @@ services:
|
|||||||
- shlink_db_maria
|
- shlink_db_maria
|
||||||
- shlink_db_ms
|
- shlink_db_ms
|
||||||
- shlink_redis
|
- 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
|
||||||
- shlink_mercure_proxy
|
- shlink_mercure_proxy
|
||||||
- shlink_rabbitmq
|
- shlink_rabbitmq
|
||||||
@@ -122,10 +149,19 @@ services:
|
|||||||
|
|
||||||
shlink_redis:
|
shlink_redis:
|
||||||
container_name: shlink_redis
|
container_name: shlink_redis
|
||||||
image: redis:6.0-alpine
|
image: redis:6.2-alpine
|
||||||
ports:
|
ports:
|
||||||
- "6380:6379"
|
- "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:
|
shlink_mercure_proxy:
|
||||||
container_name: shlink_mercure_proxy
|
container_name: shlink_mercure_proxy
|
||||||
image: nginx:1.19.6-alpine
|
image: nginx:1.19.6-alpine
|
||||||
@@ -144,8 +180,8 @@ services:
|
|||||||
- "3080:80"
|
- "3080:80"
|
||||||
environment:
|
environment:
|
||||||
SERVER_NAME: ":80"
|
SERVER_NAME: ":80"
|
||||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key
|
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key
|
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"
|
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
|
||||||
|
|
||||||
shlink_rabbitmq:
|
shlink_rabbitmq:
|
||||||
|
|||||||
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
|
|
||||||
@@ -6,11 +6,14 @@ namespace Shlinkio\Shlink;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'type' => LoggerType::STREAM->value,
|
'type' => LoggerType::STREAM->value,
|
||||||
|
'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -13,24 +13,36 @@ echo "Updating database..."
|
|||||||
php bin/cli db:migrate -n ${flags}
|
php bin/cli db:migrate -n ${flags}
|
||||||
|
|
||||||
echo "Generating proxies..."
|
echo "Generating proxies..."
|
||||||
php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n ${flags}
|
php bin/doctrine orm:generate-proxies -n ${flags}
|
||||||
|
|
||||||
echo "Clearing entities cache..."
|
echo "Clearing entities cache..."
|
||||||
php vendor/doctrine/orm/bin/doctrine.php orm:clear-cache:metadata -n ${flags}
|
php bin/doctrine orm:clear-cache:metadata -n ${flags}
|
||||||
|
|
||||||
# Try to download GeoLite2 db file only if the license key env var was defined
|
# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
|
||||||
if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
|
||||||
echo "Downloading GeoLite2 db file..."
|
echo "Downloading GeoLite2 db file..."
|
||||||
php bin/cli visit:download-db -n ${flags}
|
php bin/cli visit:download-db -n ${flags}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
|
||||||
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
|
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
|
||||||
echo "Configuring periodic visit location..."
|
echo "Configuring periodic visit location..."
|
||||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||||
/usr/sbin/crond &
|
/usr/sbin/crond &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# When restarting the container, openswoole might think it is already in execution
|
# RoadRunner config needs these to have been set, so falling back to default values if not set yet
|
||||||
# This forces the app to be started every second until the exit code is 0
|
if [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
export PORT="${PORT:-"8080"}"
|
||||||
|
# Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs
|
||||||
|
export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}"
|
||||||
|
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"longUrl",
|
"longUrl",
|
||||||
"dateCreated",
|
"dateCreated",
|
||||||
"visitsCount",
|
"visitsCount",
|
||||||
|
"visitsSummary",
|
||||||
"tags",
|
"tags",
|
||||||
"meta",
|
"meta",
|
||||||
"domain",
|
"domain",
|
||||||
@@ -32,8 +33,12 @@
|
|||||||
"description": "The date in which the short URL was created in ISO format."
|
"description": "The date in which the short URL was created in ISO format."
|
||||||
},
|
},
|
||||||
"visitsCount": {
|
"visitsCount": {
|
||||||
|
"deprecated": true,
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The number of visits that this short URL has received."
|
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
|
||||||
|
},
|
||||||
|
"visitsSummary": {
|
||||||
|
"$ref": "./ShortUrlVisitsSummary.json"
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
|||||||
18
docs/swagger/definitions/ShortUrlVisitsSummary.json
Normal file
18
docs/swagger/definitions/ShortUrlVisitsSummary.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["total", "nonBots", "bots"],
|
||||||
|
"properties": {
|
||||||
|
"total": {
|
||||||
|
"description": "The total amount of visits that this short URL has received.",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
"3",
|
||||||
"2",
|
"2",
|
||||||
"1"
|
"1"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -73,10 +73,12 @@
|
|||||||
"shortCode-DESC",
|
"shortCode-DESC",
|
||||||
"dateCreated-ASC",
|
"dateCreated-ASC",
|
||||||
"dateCreated-DESC",
|
"dateCreated-DESC",
|
||||||
|
"title-ASC",
|
||||||
|
"title-DESC",
|
||||||
"visits-ASC",
|
"visits-ASC",
|
||||||
"visits-DESC",
|
"visits-DESC",
|
||||||
"title-ASC",
|
"nonBotVisits-ASC",
|
||||||
"title-DESC"
|
"nonBotVisits-DESC"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,6 +99,32 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"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": [
|
"security": [
|
||||||
@@ -136,7 +164,11 @@
|
|||||||
"shortUrl": "https://doma.in/12C18",
|
"shortUrl": "https://doma.in/12C18",
|
||||||
"longUrl": "https://store.steampowered.com",
|
"longUrl": "https://store.steampowered.com",
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsCount": 328,
|
"visitsSummary": {
|
||||||
|
"total": 328,
|
||||||
|
"nonBots": 328,
|
||||||
|
"bots": 0
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
@@ -155,7 +187,11 @@
|
|||||||
"shortUrl": "https://doma.in/12Kb3",
|
"shortUrl": "https://doma.in/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsCount": 1029,
|
"visitsSummary": {
|
||||||
|
"total": 1029,
|
||||||
|
"nonBots": 900,
|
||||||
|
"bots": 129
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
],
|
],
|
||||||
@@ -173,7 +209,11 @@
|
|||||||
"shortUrl": "https://example.com/123bA",
|
"shortUrl": "https://example.com/123bA",
|
||||||
"longUrl": "https://www.google.com",
|
"longUrl": "https://www.google.com",
|
||||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||||
"visitsCount": 25,
|
"visitsSummary": {
|
||||||
|
"total": 25,
|
||||||
|
"nonBots": 0,
|
||||||
|
"bots": 25
|
||||||
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
"validSince": "2017-01-21T00:00:00+02:00",
|
"validSince": "2017-01-21T00:00:00+02:00",
|
||||||
@@ -281,7 +321,11 @@
|
|||||||
"shortUrl": "https://doma.in/12C18",
|
"shortUrl": "https://doma.in/12C18",
|
||||||
"longUrl": "https://store.steampowered.com",
|
"longUrl": "https://store.steampowered.com",
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsCount": 0,
|
"visitsSummary": {
|
||||||
|
"total": 0,
|
||||||
|
"nonBots": 0,
|
||||||
|
"bots": 0
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
@@ -327,11 +371,11 @@
|
|||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"type": "string",
|
"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": {
|
"customSlug": {
|
||||||
"type": "string",
|
"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": {
|
"domain": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -342,10 +386,31 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Invalid arguments": {
|
"Invalid arguments with API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-invalid-args.json"
|
"$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": {
|
"value": {
|
||||||
"title": "Invalid URL",
|
"title": "Invalid URL",
|
||||||
"type": "INVALID_URL",
|
"type": "INVALID_URL",
|
||||||
@@ -354,7 +419,7 @@
|
|||||||
"url": "https://invalid-url.com"
|
"url": "https://invalid-url.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Non-unique slug": {
|
"Non-unique slug previous to API v3": {
|
||||||
"value": {
|
"value": {
|
||||||
"title": "Invalid custom slug",
|
"title": "Invalid custom slug",
|
||||||
"type": "INVALID_SLUG",
|
"type": "INVALID_SLUG",
|
||||||
|
|||||||
@@ -55,7 +55,11 @@
|
|||||||
"shortUrl": "https://doma.in/abc123",
|
"shortUrl": "https://doma.in/abc123",
|
||||||
"shortCode": "abc123",
|
"shortCode": "abc123",
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsCount": 0,
|
"visitsSummary": {
|
||||||
|
"total": 0,
|
||||||
|
"nonBots": 0,
|
||||||
|
"bots": 0
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"games",
|
"games",
|
||||||
"tech"
|
"tech"
|
||||||
@@ -85,19 +89,39 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"title": "Invalid URL",
|
"API v3 and newer": {
|
||||||
"type": "INVALID_URL",
|
"value": {
|
||||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
"title": "Invalid URL",
|
||||||
"status": 400,
|
"type": "https://shlink.io/api/error/invalid-url",
|
||||||
"url": "https://invalid-url.com"
|
"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": {
|
"text/plain": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,7 +41,11 @@
|
|||||||
"shortUrl": "https://doma.in/12Kb3",
|
"shortUrl": "https://doma.in/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsCount": 1029,
|
"visitsSummary": {
|
||||||
|
"total": 1029,
|
||||||
|
"nonBots": 820,
|
||||||
|
"bots": 209
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
],
|
],
|
||||||
@@ -83,8 +87,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Not found": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-not-found.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v2.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +163,11 @@
|
|||||||
"shortUrl": "https://doma.in/12Kb3",
|
"shortUrl": "https://doma.in/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsCount": 1029,
|
"visitsSummary": {
|
||||||
|
"total": 1029,
|
||||||
|
"nonBots": 900,
|
||||||
|
"bots": 129
|
||||||
|
},
|
||||||
"tags": [
|
"tags": [
|
||||||
"shlink"
|
"shlink"
|
||||||
],
|
],
|
||||||
@@ -203,8 +214,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Invalid arguments": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-invalid-args.json"
|
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"$ref": "../examples/short-url-invalid-args-v2.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,8 +250,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Not found": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-not-found.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v2.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,13 +335,27 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"title": "Cannot delete short URL",
|
"API v3 and newer": {
|
||||||
"type": "INVALID_SHORT_URL_DELETION",
|
"value": {
|
||||||
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
"title": "Cannot delete short URL",
|
||||||
"status": 422,
|
"type": "https://shlink.io/api/error/invalid-short-url-deletion",
|
||||||
"shortCode": "abc123",
|
"detail": "Impossible to delete short URL with short code \"abc123\", since it has more than \"15\" visits.",
|
||||||
"threshold": 15
|
"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 +386,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Not found": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-not-found.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v2.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,8 +151,11 @@
|
|||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Short URL not found": {
|
"Short URL not found with API v3 and newer": {
|
||||||
"$ref": "../examples/short-url-not-found.json"
|
"$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": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"title": "Invalid data",
|
"API v3 and newer": {
|
||||||
"type": "INVALID_ARGUMENT",
|
"value": {
|
||||||
"detail": "Provided data is not valid",
|
"title": "Invalid data",
|
||||||
"status": 400,
|
"type": "https://shlink.io/api/error/invalid-data",
|
||||||
"invalidElements": ["oldName", "newName"]
|
"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": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"detail": "You are not allowed to rename tags",
|
"API v3 and newer": {
|
||||||
"title": "Forbidden tag operation",
|
"value": {
|
||||||
"type": "FORBIDDEN_OPERATION",
|
"detail": "You are not allowed to rename tags",
|
||||||
"status": 403
|
"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"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Tag not found": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/tag-not-found.json"
|
"$ref": "../examples/tag-not-found-v3.json"
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"$ref": "../examples/tag-not-found-v2.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,13 +264,27 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"detail": "You cannot rename tag foo, because it already exists",
|
"API v3 and newer": {
|
||||||
"title": "Tag conflict",
|
"value": {
|
||||||
"type": "TAG_CONFLICT",
|
"detail": "You cannot rename tag foo, because it already exists",
|
||||||
"status": 409,
|
"title": "Tag conflict",
|
||||||
"oldName": "bar",
|
"type": "https://shlink.io/api/error/tag-conflict",
|
||||||
"newName": "foo"
|
"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": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"detail": "You are not allowed to delete tags",
|
"API v3 and newer": {
|
||||||
"title": "Forbidden tag operation",
|
"value": {
|
||||||
"type": "FORBIDDEN_OPERATION",
|
"detail": "You are not allowed to delete tags",
|
||||||
"status": 403
|
"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": {
|
"examples": {
|
||||||
"title": "Invalid data",
|
"API v3 and newer": {
|
||||||
"type": "INVALID_ARGUMENT",
|
"value": {
|
||||||
"detail": "Provided data is not valid",
|
"title": "Invalid data",
|
||||||
"status": 400,
|
"type": "https://shlink.io/api/error/invalid-data",
|
||||||
"invalidElements": ["domain", "invalidShortUrlRedirect"]
|
"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": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"detail": "Domain with authority \"example.com\" could not be found",
|
"API v3 and newer": {
|
||||||
"title": "Domain not found",
|
"value": {
|
||||||
"type": "DOMAIN_NOT_FOUND",
|
"detail": "Domain with authority \"example.com\" could not be found",
|
||||||
"status": 404,
|
"title": "Domain not found",
|
||||||
"authority": "example.com"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,23 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"example": {
|
"examples": {
|
||||||
"title": "Mercure integration not configured",
|
"API v3 and newer": {
|
||||||
"type": "MERCURE_NOT_CONFIGURED",
|
"value": {
|
||||||
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
"title": "Mercure integration not configured",
|
||||||
"status": 501
|
"type": "https://shlink.io/api/error/mercure-not-configured",
|
||||||
|
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
||||||
|
"status": 501
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"value": {
|
||||||
|
"title": "Mercure integration not configured",
|
||||||
|
"type": "MERCURE_NOT_CONFIGURED",
|
||||||
|
"detail": "This Shlink instance is not integrated with a mercure hub.",
|
||||||
|
"status": 501
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,8 +148,12 @@
|
|||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Shlink",
|
"title": "Shlink",
|
||||||
"description": "Shlink, the self-hosted URL shortener",
|
"description": "Shlink, the self-hosted URL shortener",
|
||||||
"version": "2.0"
|
"version": "3.0"
|
||||||
},
|
},
|
||||||
|
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"source": {
|
|
||||||
"directories": [
|
|
||||||
"module/*/src"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timeout": 5,
|
|
||||||
"logs": {
|
|
||||||
"text": "build/infection-api/infection-log.txt",
|
|
||||||
"html": "build/infection-api/infection-log.html",
|
|
||||||
"summary": "build/infection-api/summary-log.txt",
|
|
||||||
"debug": "build/infection-api/debug-log.txt"
|
|
||||||
},
|
|
||||||
"tmpDir": "build/infection-api/temp",
|
|
||||||
"phpUnit": {
|
|
||||||
"configDir": "."
|
|
||||||
},
|
|
||||||
"testFrameworkOptions": "--configuration=phpunit-api.xml",
|
|
||||||
"mutators": {
|
|
||||||
"@default": true,
|
|
||||||
"IdenticalEqual": false,
|
|
||||||
"NotIdenticalNotEqual": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
infection-api.json5
Normal file
24
infection-api.json5
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
source: {
|
||||||
|
directories: [
|
||||||
|
'module/*/src'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
timeout: 5,
|
||||||
|
logs: {
|
||||||
|
text: 'build/infection-api/infection-log.txt',
|
||||||
|
html: 'build/infection-api/infection-log.html',
|
||||||
|
summary: 'build/infection-api/summary-log.txt',
|
||||||
|
debug: 'build/infection-api/debug-log.txt'
|
||||||
|
},
|
||||||
|
tmpDir: 'build/infection-api/temp',
|
||||||
|
phpUnit: {
|
||||||
|
configDir: '.'
|
||||||
|
},
|
||||||
|
testFrameworkOptions: '--configuration=phpunit-api.xml',
|
||||||
|
mutators: {
|
||||||
|
'@default': true,
|
||||||
|
IdenticalEqual: false,
|
||||||
|
NotIdenticalNotEqual: false
|
||||||
|
}
|
||||||
|
}
|
||||||
24
infection-cli.json5
Normal file
24
infection-cli.json5
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
source: {
|
||||||
|
directories: [
|
||||||
|
'module/*/src'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
timeout: 5,
|
||||||
|
logs: {
|
||||||
|
text: 'build/infection-cli/infection-log.txt',
|
||||||
|
html: 'build/infection-cli/infection-log.html',
|
||||||
|
summary: 'build/infection-cli/summary-log.txt',
|
||||||
|
debug: 'build/infection-cli/debug-log.txt'
|
||||||
|
},
|
||||||
|
tmpDir: 'build/infection-cli/temp',
|
||||||
|
phpUnit: {
|
||||||
|
configDir: '.'
|
||||||
|
},
|
||||||
|
testFrameworkOptions: '--configuration=phpunit-cli.xml',
|
||||||
|
mutators: {
|
||||||
|
'@default': true,
|
||||||
|
IdenticalEqual: false,
|
||||||
|
NotIdenticalNotEqual: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
"source": {
|
|
||||||
"directories": [
|
|
||||||
"module/*/src"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timeout": 5,
|
|
||||||
"logs": {
|
|
||||||
"text": "build/infection-db/infection-log.txt",
|
|
||||||
"html": "build/infection-db/infection-log.html",
|
|
||||||
"summary": "build/infection-db/summary-log.txt",
|
|
||||||
"debug": "build/infection-db/debug-log.txt"
|
|
||||||
},
|
|
||||||
"tmpDir": "build/infection-db/temp",
|
|
||||||
"phpUnit": {
|
|
||||||
"configDir": "."
|
|
||||||
},
|
|
||||||
"testFrameworkOptions": "--configuration=phpunit-db.xml",
|
|
||||||
"mutators": {
|
|
||||||
"@default": true,
|
|
||||||
"IdenticalEqual": false,
|
|
||||||
"NotIdenticalNotEqual": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
infection-db.json5
Normal file
24
infection-db.json5
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
source: {
|
||||||
|
directories: [
|
||||||
|
'module/*/src'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
timeout: 5,
|
||||||
|
logs: {
|
||||||
|
text: 'build/infection-db/infection-log.txt',
|
||||||
|
html: 'build/infection-db/infection-log.html',
|
||||||
|
summary: 'build/infection-db/summary-log.txt',
|
||||||
|
debug: 'build/infection-db/debug-log.txt'
|
||||||
|
},
|
||||||
|
tmpDir: 'build/infection-db/temp',
|
||||||
|
phpUnit: {
|
||||||
|
configDir: '.'
|
||||||
|
},
|
||||||
|
testFrameworkOptions: '--configuration=phpunit-db.xml',
|
||||||
|
mutators: {
|
||||||
|
'@default': true,
|
||||||
|
IdenticalEqual: false,
|
||||||
|
NotIdenticalNotEqual: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"source": {
|
|
||||||
"directories": [
|
|
||||||
"module/*/src"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"timeout": 5,
|
|
||||||
"logs": {
|
|
||||||
"text": "build/infection-unit/infection-log.txt",
|
|
||||||
"html": "build/infection-unit/infection-log.html",
|
|
||||||
"summary": "build/infection-unit/summary-log.txt",
|
|
||||||
"debug": "build/infection-unit/debug-log.txt",
|
|
||||||
"stryker": {
|
|
||||||
"report": "develop"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tmpDir": "build/infection-unit/temp",
|
|
||||||
"phpUnit": {
|
|
||||||
"configDir": "."
|
|
||||||
},
|
|
||||||
"mutators": {
|
|
||||||
"@default": true,
|
|
||||||
"IdenticalEqual": false,
|
|
||||||
"NotIdenticalNotEqual": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
infection.json5
Normal file
26
infection.json5
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
source: {
|
||||||
|
directories: [
|
||||||
|
'module/*/src'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
timeout: 5,
|
||||||
|
logs: {
|
||||||
|
text: 'build/infection-unit/infection-log.txt',
|
||||||
|
html: 'build/infection-unit/infection-log.html',
|
||||||
|
summary: 'build/infection-unit/summary-log.txt',
|
||||||
|
debug: 'build/infection-unit/debug-log.txt',
|
||||||
|
stryker: {
|
||||||
|
report: 'develop'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tmpDir: 'build/infection-unit/temp',
|
||||||
|
phpUnit: {
|
||||||
|
configDir: '.'
|
||||||
|
},
|
||||||
|
mutators: {
|
||||||
|
'@default': true,
|
||||||
|
IdenticalEqual: false,
|
||||||
|
NotIdenticalNotEqual: false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,14 +12,12 @@ use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
|||||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
|
||||||
use Shlinkio\Shlink\Core\Tag\TagService;
|
use Shlinkio\Shlink\Core\Tag\TagService;
|
||||||
use Shlinkio\Shlink\Core\Visit;
|
use Shlinkio\Shlink\Core\Visit;
|
||||||
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
use Symfony\Component\Console as SymfonyCli;
|
use Symfony\Component\Console as SymfonyCli;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
@@ -35,7 +33,7 @@ return [
|
|||||||
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
|
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
|
||||||
PhpExecutableFinder::class => InvokableFactory::class,
|
PhpExecutableFinder::class => InvokableFactory::class,
|
||||||
|
|
||||||
Util\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||||
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
||||||
@@ -70,7 +68,7 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
Util\GeolocationDbUpdater::class => [
|
GeoLite\GeolocationDbUpdater::class => [
|
||||||
DbUpdater::class,
|
DbUpdater::class,
|
||||||
Reader::class,
|
Reader::class,
|
||||||
LOCAL_LOCK_FACTORY,
|
LOCAL_LOCK_FACTORY,
|
||||||
@@ -80,22 +78,22 @@ return [
|
|||||||
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
||||||
|
|
||||||
Command\ShortUrl\CreateShortUrlCommand::class => [
|
Command\ShortUrl\CreateShortUrlCommand::class => [
|
||||||
Service\UrlShortener::class,
|
ShortUrl\UrlShortener::class,
|
||||||
ShortUrlStringifier::class,
|
ShortUrlStringifier::class,
|
||||||
UrlShortenerOptions::class,
|
UrlShortenerOptions::class,
|
||||||
],
|
],
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
|
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
|
||||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||||
Service\ShortUrlService::class,
|
ShortUrl\ShortUrlListService::class,
|
||||||
ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
],
|
],
|
||||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||||
Command\Visit\LocateVisitsCommand::class => [
|
Command\Visit\LocateVisitsCommand::class => [
|
||||||
Visit\VisitLocator::class,
|
Visit\Geolocation\VisitLocator::class,
|
||||||
IpLocationResolverInterface::class,
|
Visit\Geolocation\VisitToLocationHelper::class,
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
],
|
],
|
||||||
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\CLI\ApiKey;
|
|||||||
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||||
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
use function is_string;
|
use function is_string;
|
||||||
@@ -19,8 +20,8 @@ class RoleResolver implements RoleResolverInterface
|
|||||||
|
|
||||||
public function determineRoles(InputInterface $input): array
|
public function determineRoles(InputInterface $input): array
|
||||||
{
|
{
|
||||||
$domainAuthority = $input->getOption(self::DOMAIN_ONLY_PARAM);
|
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
|
||||||
$author = $input->getOption(self::AUTHOR_ONLY_PARAM);
|
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
|
||||||
|
|
||||||
$roleDefinitions = [];
|
$roleDefinitions = [];
|
||||||
if ($author) {
|
if ($author) {
|
||||||
|
|||||||
@@ -9,9 +9,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
|
|
||||||
interface RoleResolverInterface
|
interface RoleResolverInterface
|
||||||
{
|
{
|
||||||
public const AUTHOR_ONLY_PARAM = 'author-only';
|
|
||||||
public const DOMAIN_ONLY_PARAM = 'domain-only';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RoleDefinition[]
|
* @return RoleDefinition[]
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ class GenerateKeyCommand extends Command
|
|||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$authorOnly = RoleResolverInterface::AUTHOR_ONLY_PARAM;
|
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
|
||||||
$domainOnly = RoleResolverInterface::DOMAIN_ONLY_PARAM;
|
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
|
||||||
$help = <<<HELP
|
$help = <<<HELP
|
||||||
The <info>%command.name%</info> generates a new valid API key.
|
The <info>%command.name%</info> generates a new valid API key.
|
||||||
|
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ class ListKeysCommand extends Command
|
|||||||
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||||
fn (Role $role, array $meta) =>
|
fn (Role $role, array $meta) =>
|
||||||
empty($meta)
|
empty($meta)
|
||||||
? Role::toFriendlyName($role)
|
? $role->toFriendlyName()
|
||||||
: sprintf('%s: %s', Role::toFriendlyName($role), Role::domainAuthorityFromMeta($meta)),
|
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
|
||||||
));
|
));
|
||||||
|
|
||||||
return $rowData;
|
return $rowData;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use const Shlinkio\Shlink\MIGRATIONS_TABLE;
|
|||||||
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||||
{
|
{
|
||||||
public const NAME = 'db:create';
|
public const NAME = 'db:create';
|
||||||
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
public const DOCTRINE_SCRIPT = 'bin/doctrine';
|
||||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\Domain;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -25,7 +25,7 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand
|
|||||||
parent::__construct($visitsHelper);
|
parent::__construct($visitsHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
|||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -40,7 +40,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
private readonly UrlShortenerOptions $options,
|
private readonly UrlShortenerOptions $options,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->defaultDomain = $this->options->domain()['hostname'] ?? '';
|
$this->defaultDomain = $this->options->domain['hostname'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -158,11 +158,11 @@ class CreateShortUrlCommand extends Command
|
|||||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||||
$customSlug = $input->getOption('custom-slug');
|
$customSlug = $input->getOption('custom-slug');
|
||||||
$maxVisits = $input->getOption('max-visits');
|
$maxVisits = $input->getOption('max-visits');
|
||||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength();
|
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
|
||||||
$doValidateUrl = $input->getOption('validate-url');
|
$doValidateUrl = $input->getOption('validate-url');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$shortUrl = $this->urlShortener->shorten(ShortUrlMeta::fromRawData([
|
$shortUrl = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
|
||||||
ShortUrlInputFilter::LONG_URL => $longUrl,
|
ShortUrlInputFilter::LONG_URL => $longUrl,
|
||||||
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
||||||
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
||||||
@@ -175,7 +175,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
ShortUrlInputFilter::TAGS => $tags,
|
ShortUrlInputFilter::TAGS => $tags,
|
||||||
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
||||||
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
||||||
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled(),
|
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled,
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$io->writeln([
|
$io->writeln([
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -21,7 +21,7 @@ class DeleteShortUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:delete';
|
public const NAME = 'short-url:delete';
|
||||||
|
|
||||||
public function __construct(private DeleteShortUrlServiceInterface $deleteShortUrlService)
|
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
@@ -20,7 +20,7 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:visits';
|
public const NAME = 'short-url:visits';
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
|
|||||||
@@ -4,17 +4,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
||||||
|
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@@ -27,20 +29,25 @@ use function Functional\map;
|
|||||||
use function implode;
|
use function implode;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
class ListShortUrlsCommand extends Command
|
||||||
{
|
{
|
||||||
use PagerfantaUtilsTrait;
|
use PagerfantaUtilsTrait;
|
||||||
|
|
||||||
public const NAME = 'short-url:list';
|
public const NAME = 'short-url:list';
|
||||||
|
|
||||||
|
private readonly StartDateOption $startDateOption;
|
||||||
|
private readonly EndDateOption $endDateOption;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ShortUrlServiceInterface $shortUrlService,
|
private readonly ShortUrlListServiceInterface $shortUrlService,
|
||||||
private DataTransformerInterface $transformer,
|
private readonly DataTransformerInterface $transformer,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->startDateOption = new StartDateOption($this, 'short URLs');
|
||||||
|
$this->endDateOption = new EndDateOption($this, 'short URLs');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
@@ -70,6 +77,18 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
'If tags is provided, returns only short URLs having ALL tags.',
|
'If tags is provided, returns only short URLs having ALL tags.',
|
||||||
)
|
)
|
||||||
|
->addOption(
|
||||||
|
'exclude-max-visits-reached',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Excludes short URLs which reached their max amount of visits.',
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'exclude-past-valid-until',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
'Excludes short URLs which have a "validUntil" date in the past.',
|
||||||
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'order-by',
|
'order-by',
|
||||||
'o',
|
'o',
|
||||||
@@ -104,16 +123,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getStartDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter short URLs, returning only those created after "%s".', $optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getEndDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter short URLs, returning only those created before "%s".', $optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
@@ -124,8 +133,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||||
$all = $input->getOption('all');
|
$all = $input->getOption('all');
|
||||||
$startDate = $this->getStartDateOption($input, $output);
|
$startDate = $this->startDateOption->get($input, $output);
|
||||||
$endDate = $this->getEndDateOption($input, $output);
|
$endDate = $this->endDateOption->get($input, $output);
|
||||||
$orderBy = $this->processOrderBy($input);
|
$orderBy = $this->processOrderBy($input);
|
||||||
$columnsMap = $this->resolveColumnsMap($input);
|
$columnsMap = $this->resolveColumnsMap($input);
|
||||||
|
|
||||||
@@ -136,6 +145,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
ShortUrlsParamsInputFilter::ORDER_BY => $orderBy,
|
ShortUrlsParamsInputFilter::ORDER_BY => $orderBy,
|
||||||
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
|
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
|
||||||
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
|
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
|
||||||
|
ShortUrlsParamsInputFilter::EXCLUDE_MAX_VISITS_REACHED => $input->getOption('exclude-max-visits-reached'),
|
||||||
|
ShortUrlsParamsInputFilter::EXCLUDE_PAST_VALID_UNTIL => $input->getOption('exclude-past-valid-until'),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($all) {
|
if ($all) {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -21,7 +21,7 @@ class ResolveUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:parse';
|
public const NAME = 'short-url:parse';
|
||||||
|
|
||||||
public function __construct(private ShortUrlResolverInterface $urlResolver)
|
public function __construct(private readonly ShortUrlResolverInterface $urlResolver)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -25,7 +25,7 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
|
|||||||
parent::__construct($visitsHelper);
|
parent::__construct($visitsHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Util;
|
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
use function is_string;
|
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
abstract class AbstractWithDateRangeCommand extends Command
|
|
||||||
{
|
|
||||||
private const START_DATE = 'start-date';
|
|
||||||
private const END_DATE = 'end-date';
|
|
||||||
|
|
||||||
final protected function configure(): void
|
|
||||||
{
|
|
||||||
$this->doConfigure();
|
|
||||||
$this
|
|
||||||
->addOption(self::START_DATE, 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc(self::START_DATE))
|
|
||||||
->addOption(self::END_DATE, 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc(self::END_DATE));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getStartDateOption(InputInterface $input, OutputInterface $output): ?Chronos
|
|
||||||
{
|
|
||||||
return $this->getDateOption($input, $output, self::START_DATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getEndDateOption(InputInterface $input, OutputInterface $output): ?Chronos
|
|
||||||
{
|
|
||||||
return $this->getDateOption($input, $output, self::END_DATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
|
|
||||||
{
|
|
||||||
$value = $input->getOption($key);
|
|
||||||
if (empty($value) || ! is_string($value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return Chronos::parse($value);
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$output->writeln(sprintf(
|
|
||||||
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
|
|
||||||
$key,
|
|
||||||
$value,
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($output->isVeryVerbose()) {
|
|
||||||
$this->getApplication()?->renderThrowable($e, $output);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected function doConfigure(): void;
|
|
||||||
|
|
||||||
abstract protected function getStartDateDesc(string $optionName): string;
|
|
||||||
|
|
||||||
abstract protected function getEndDateDesc(string $optionName): string;
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,15 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
use Shlinkio\Shlink\CLI\Option\EndDateOption;
|
||||||
|
use Shlinkio\Shlink\CLI\Option\StartDateOption;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
@@ -19,29 +21,23 @@ use function Functional\map;
|
|||||||
use function Functional\select_keys;
|
use function Functional\select_keys;
|
||||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||||
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand
|
abstract class AbstractVisitsListCommand extends Command
|
||||||
{
|
{
|
||||||
|
private readonly StartDateOption $startDateOption;
|
||||||
|
private readonly EndDateOption $endDateOption;
|
||||||
|
|
||||||
public function __construct(protected readonly VisitsStatsHelperInterface $visitsHelper)
|
public function __construct(protected readonly VisitsStatsHelperInterface $visitsHelper)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
$this->startDateOption = new StartDateOption($this, 'visits');
|
||||||
|
$this->endDateOption = new EndDateOption($this, 'visits');
|
||||||
final protected function getStartDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
final protected function getEndDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$startDate = $this->getStartDateOption($input, $output);
|
$startDate = $this->startDateOption->get($input, $output);
|
||||||
$endDate = $this->getEndDateOption($input, $output);
|
$endDate = $this->endDateOption->get($input, $output);
|
||||||
$paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate));
|
$paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate));
|
||||||
[$rows, $headers] = $this->resolveRowsAndHeaders($paginator);
|
[$rows, $headers] = $this->resolveRowsAndHeaders($paginator);
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdaterInterface;
|
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\ProgressBar;
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
|
|||||||
parent::__construct($visitsHelper);
|
parent::__construct($visitsHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||||
{
|
{
|
||||||
public const NAME = 'visit:orphan';
|
public const NAME = 'visit:orphan';
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ use Shlinkio\Shlink\CLI\Command\Util\AbstractLockedCommand;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
use Shlinkio\Shlink\CLI\Command\Util\LockedCommandConfig;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitGeolocationHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitLocatorInterface;
|
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitGeolocationHelperInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocatorInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\UnlocatableIpType;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
|
||||||
use Symfony\Component\Console\Exception\RuntimeException;
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
use Symfony\Component\Console\Input\ArrayInput;
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -34,8 +34,8 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
private SymfonyStyle $io;
|
private SymfonyStyle $io;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private VisitLocatorInterface $visitLocator,
|
private readonly VisitLocatorInterface $visitLocator,
|
||||||
private IpLocationResolverInterface $ipLocationResolver,
|
private readonly VisitToLocationHelperInterface $visitToLocation,
|
||||||
LockFactory $locker,
|
LockFactory $locker,
|
||||||
) {
|
) {
|
||||||
parent::__construct($locker);
|
parent::__construct($locker);
|
||||||
@@ -132,39 +132,33 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
*/
|
*/
|
||||||
public function geolocateVisit(Visit $visit): Location
|
public function geolocateVisit(Visit $visit): Location
|
||||||
{
|
{
|
||||||
if (! $visit->hasRemoteAddr()) {
|
$ipAddr = $visit->getRemoteAddr() ?? '?';
|
||||||
$this->io->writeln(
|
|
||||||
'<comment>Ignored visit with no IP address</comment>',
|
|
||||||
OutputInterface::VERBOSITY_VERBOSE,
|
|
||||||
);
|
|
||||||
throw IpCannotBeLocatedException::forEmptyAddress();
|
|
||||||
}
|
|
||||||
|
|
||||||
$ipAddr = $visit->getRemoteAddr() ?? '';
|
|
||||||
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
|
||||||
$this->io->writeln(' [<comment>Ignored localhost address</comment>]');
|
|
||||||
throw IpCannotBeLocatedException::forLocalhost();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
return $this->visitToLocation->resolveVisitLocation($visit);
|
||||||
} catch (WrongIpException $e) {
|
} catch (IpCannotBeLocatedException $e) {
|
||||||
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
$this->io->writeln(match ($e->type) {
|
||||||
if ($this->io->isVerbose()) {
|
UnlocatableIpType::EMPTY_ADDRESS => ' [<comment>Ignored visit with no IP address</comment>]',
|
||||||
|
UnlocatableIpType::LOCALHOST => ' [<comment>Ignored localhost address</comment>]',
|
||||||
|
UnlocatableIpType::ERROR => ' [<fg=red>An error occurred while locating IP. Skipped</>]',
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($e->type === UnlocatableIpType::ERROR && $this->io->isVerbose()) {
|
||||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IpCannotBeLocatedException::forError($e);
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
||||||
{
|
{
|
||||||
$message = ! $visitLocation->isEmpty()
|
if (! $visitLocation->isEmpty()) {
|
||||||
? sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName())
|
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName()));
|
||||||
: ' [<comment>Address not found</comment>]';
|
} elseif ($visit->hasRemoteAddr() && $visit->getRemoteAddr() !== IpAddress::LOCALHOST) {
|
||||||
$this->io->writeln($message);
|
$this->io->writeln(' <comment>[Could not locate address]</comment>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkDbUpdate(): void
|
private function checkDbUpdate(): void
|
||||||
|
|||||||
@@ -13,16 +13,15 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||||||
{
|
{
|
||||||
private bool $olderDbExists;
|
private bool $olderDbExists;
|
||||||
|
|
||||||
private function __construct(string $message, int $code, ?Throwable $previous)
|
private function __construct(string $message, ?Throwable $previous = null)
|
||||||
{
|
{
|
||||||
parent::__construct($message, $code, $previous);
|
parent::__construct($message, 0, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function withOlderDb(?Throwable $prev = null): self
|
public static function withOlderDb(?Throwable $prev = null): self
|
||||||
{
|
{
|
||||||
$e = new self(
|
$e = new self(
|
||||||
'An error occurred while updating geolocation database, but an older DB is already present.',
|
'An error occurred while updating geolocation database, but an older DB is already present.',
|
||||||
0,
|
|
||||||
$prev,
|
$prev,
|
||||||
);
|
);
|
||||||
$e->olderDbExists = true;
|
$e->olderDbExists = true;
|
||||||
@@ -34,7 +33,6 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||||||
{
|
{
|
||||||
$e = new self(
|
$e = new self(
|
||||||
'An error occurred while updating geolocation database, and an older version could not be found.',
|
'An error occurred while updating geolocation database, and an older version could not be found.',
|
||||||
0,
|
|
||||||
$prev,
|
$prev,
|
||||||
);
|
);
|
||||||
$e->olderDbExists = false;
|
$e->olderDbExists = false;
|
||||||
@@ -47,7 +45,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
|||||||
$e = new self(sprintf(
|
$e = new self(sprintf(
|
||||||
'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.',
|
'Build epoch with value "%s" from existing geolocation database, could not be parsed to integer.',
|
||||||
$buildEpoch,
|
$buildEpoch,
|
||||||
), 0, null);
|
));
|
||||||
$e->olderDbExists = true;
|
$e->olderDbExists = true;
|
||||||
|
|
||||||
return $e;
|
return $e;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class ApplicationFactory
|
|||||||
$appOptions = $container->get(AppOptions::class);
|
$appOptions = $container->get(AppOptions::class);
|
||||||
|
|
||||||
$commands = $config['commands'] ?? [];
|
$commands = $config['commands'] ?? [];
|
||||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
$app = new CliApp($appOptions->name, $appOptions->version);
|
||||||
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
|
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
|
||||||
|
|
||||||
return $app;
|
return $app;
|
||||||
|
|||||||
@@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Util;
|
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
use Cake\Chronos\Chronos;
|
use Cake\Chronos\Chronos;
|
||||||
use GeoIp2\Database\Reader;
|
use GeoIp2\Database\Reader;
|
||||||
use MaxMind\Db\Reader\Metadata;
|
use MaxMind\Db\Reader\Metadata;
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
|
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
use Symfony\Component\Lock\LockFactory;
|
use Symfony\Component\Lock\LockFactory;
|
||||||
|
|
||||||
@@ -20,27 +22,27 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
private const LOCK_NAME = 'geolocation-db-update';
|
private const LOCK_NAME = 'geolocation-db-update';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DbUpdaterInterface $dbUpdater,
|
private readonly DbUpdaterInterface $dbUpdater,
|
||||||
private Reader $geoLiteDbReader,
|
private readonly Reader $geoLiteDbReader,
|
||||||
private LockFactory $locker,
|
private readonly LockFactory $locker,
|
||||||
private TrackingOptions $trackingOptions,
|
private readonly TrackingOptions $trackingOptions,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void
|
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): GeolocationResult
|
||||||
{
|
{
|
||||||
if ($this->trackingOptions->disableTracking() || $this->trackingOptions->disableIpTracking()) {
|
if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) {
|
||||||
return;
|
return GeolocationResult::CHECK_SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
$lock = $this->locker->createLock(self::LOCK_NAME);
|
$lock = $this->locker->createLock(self::LOCK_NAME);
|
||||||
$lock->acquire(true); // Block until lock is released
|
$lock->acquire(true); // Block until lock is released
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->downloadIfNeeded($beforeDownload, $handleProgress);
|
return $this->downloadIfNeeded($beforeDownload, $handleProgress);
|
||||||
} finally {
|
} finally {
|
||||||
$lock->release();
|
$lock->release();
|
||||||
}
|
}
|
||||||
@@ -49,17 +51,18 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): void
|
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult
|
||||||
{
|
{
|
||||||
if (! $this->dbUpdater->databaseFileExists()) {
|
if (! $this->dbUpdater->databaseFileExists()) {
|
||||||
$this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$meta = $this->geoLiteDbReader->metadata();
|
$meta = $this->geoLiteDbReader->metadata();
|
||||||
if ($this->buildIsTooOld($meta)) {
|
if ($this->buildIsTooOld($meta)) {
|
||||||
$this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
return $this->downloadNewDb(true, $beforeDownload, $handleProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return GeolocationResult::DB_IS_UP_TO_DATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildIsTooOld(Metadata $meta): bool
|
private function buildIsTooOld(Metadata $meta): bool
|
||||||
@@ -92,15 +95,22 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
private function downloadNewDb(bool $olderDbExists, ?callable $beforeDownload, ?callable $handleProgress): void
|
private function downloadNewDb(
|
||||||
{
|
bool $olderDbExists,
|
||||||
|
?callable $beforeDownload,
|
||||||
|
?callable $handleProgress,
|
||||||
|
): GeolocationResult {
|
||||||
if ($beforeDownload !== null) {
|
if ($beforeDownload !== null) {
|
||||||
$beforeDownload($olderDbExists);
|
$beforeDownload($olderDbExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||||
} catch (RuntimeException $e) {
|
return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
|
||||||
|
} catch (MissingLicenseException) {
|
||||||
|
// If there's no license key, just ignore the error
|
||||||
|
return GeolocationResult::CHECK_SKIPPED;
|
||||||
|
} catch (DbUpdateException | WrongIpException $e) {
|
||||||
throw $olderDbExists
|
throw $olderDbExists
|
||||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||||
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
: GeolocationDbUpdateFailedException::withoutOlderDb($e);
|
||||||
@@ -113,6 +123,6 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
return static fn (int $total, int $downloaded) => $handleProgress($total, $downloaded, $olderDbExists);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Util;
|
namespace Shlinkio\Shlink\CLI\GeoLite;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||||
|
|
||||||
@@ -11,5 +11,8 @@ interface GeolocationDbUpdaterInterface
|
|||||||
/**
|
/**
|
||||||
* @throws GeolocationDbUpdateFailedException
|
* @throws GeolocationDbUpdateFailedException
|
||||||
*/
|
*/
|
||||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): void;
|
public function checkDbUpdate(
|
||||||
|
?callable $beforeDownload = null,
|
||||||
|
?callable $handleProgress = null,
|
||||||
|
): GeolocationResult;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user