mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Compare commits
131 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b5a5296d | ||
|
|
febca6d441 | ||
|
|
bf29abc468 | ||
|
|
97cb30565c | ||
|
|
9809f050ef | ||
|
|
7ecfb24584 | ||
|
|
4aa65f750e | ||
|
|
63c533fa62 | ||
|
|
8751d6c315 | ||
|
|
eb40dc2d5d | ||
|
|
c9d1a955b9 | ||
|
|
c346fd0602 | ||
|
|
a45550b0c6 | ||
|
|
a843c59d77 | ||
|
|
3bfb29a51c | ||
|
|
d8ede3263f | ||
|
|
c36e43e249 | ||
|
|
52150b3067 | ||
|
|
e7796cc917 | ||
|
|
7f560e6a65 | ||
|
|
8f233221e5 | ||
|
|
f700abd65d | ||
|
|
f9e4d6d617 | ||
|
|
d9286765e1 | ||
|
|
a7cde9364a | ||
|
|
070d74830b | ||
|
|
23c07c4e82 | ||
|
|
ab7824aa85 | ||
|
|
67bafbe44e | ||
|
|
c4805b8152 | ||
|
|
33729289c7 | ||
|
|
721e3d9ef9 | ||
|
|
a72e22e046 | ||
|
|
36749658da | ||
|
|
4ad3dc0bc7 | ||
|
|
73864b923d | ||
|
|
71277e979a | ||
|
|
60fef3de74 | ||
|
|
0fe503fa0e | ||
|
|
db02d9f1ba | ||
|
|
89a987d03a | ||
|
|
3284cea6f2 | ||
|
|
df5ad554c1 | ||
|
|
07ae92943d | ||
|
|
175712d4a9 | ||
|
|
3f1b253c31 | ||
|
|
202d0b86b3 | ||
|
|
4e87affb0b | ||
|
|
7f83d37b3c | ||
|
|
09e81b00c5 | ||
|
|
68b77e22c5 | ||
|
|
c5ddd8302a | ||
|
|
1a0fe0429a | ||
|
|
6646232311 | ||
|
|
c1e88c3e83 | ||
|
|
c91a534d1a | ||
|
|
752100f1ce | ||
|
|
dae083c540 | ||
|
|
857c3a4f8d | ||
|
|
acc4c4756e | ||
|
|
0bacb215c5 | ||
|
|
d1a6e60b01 | ||
|
|
8f954151ca | ||
|
|
145d4eaaed | ||
|
|
7673232793 | ||
|
|
f08951a9b9 | ||
|
|
ff963a9df4 | ||
|
|
f30c74b987 | ||
|
|
467dbdd183 | ||
|
|
0e78deb8f2 | ||
|
|
50cc7ae632 | ||
|
|
512d765d60 | ||
|
|
7b9331bd14 | ||
|
|
4f5ce9fb43 | ||
|
|
83f73eb631 | ||
|
|
3f1b89d665 | ||
|
|
8f6fc97fc8 | ||
|
|
a463e6f9d7 | ||
|
|
2a0364ca8f | ||
|
|
23e9ed93bb | ||
|
|
689343d1c9 | ||
|
|
d01dc334d7 | ||
|
|
58a3791a5c | ||
|
|
1a133af141 | ||
|
|
938fb6509e | ||
|
|
d3bfd99210 | ||
|
|
3a1740fdca | ||
|
|
e3de403c6c | ||
|
|
5c1ab02753 | ||
|
|
e5713df008 | ||
|
|
95ea64980b | ||
|
|
c0a77b790d | ||
|
|
e073b4331a | ||
|
|
e919901487 | ||
|
|
13f9f106be | ||
|
|
e9c7053ef5 | ||
|
|
62051c8809 | ||
|
|
0a6a794e23 | ||
|
|
01846657d1 | ||
|
|
dd7545afdf | ||
|
|
9296013596 | ||
|
|
8015c6cc88 | ||
|
|
8c93444286 | ||
|
|
96ed7cae0d | ||
|
|
72c4628b79 | ||
|
|
1117631717 | ||
|
|
60176060cb | ||
|
|
d949b54ef4 | ||
|
|
720db64a03 | ||
|
|
37e0978bfc | ||
|
|
cf355b0b69 | ||
|
|
f2edb54b8b | ||
|
|
13ec27039d | ||
|
|
ad3805a560 | ||
|
|
cc4afa7b62 | ||
|
|
7a6bfed445 | ||
|
|
f2a7b687a9 | ||
|
|
522d021264 | ||
|
|
14a0db1f34 | ||
|
|
430883987a | ||
|
|
f17b641d46 | ||
|
|
48a8290e92 | ||
|
|
46acf4de1c | ||
|
|
17792a1603 | ||
|
|
a8611f5d80 | ||
|
|
deef938e97 | ||
|
|
e014cfa72a | ||
|
|
aa242eba25 | ||
|
|
0ac5569d60 | ||
|
|
7c3e3442c2 | ||
|
|
0f894dcdfe |
@@ -19,7 +19,6 @@ indocker
|
|||||||
docker-*
|
docker-*
|
||||||
phpstan.neon
|
phpstan.neon
|
||||||
php*xml*
|
php*xml*
|
||||||
infection*
|
|
||||||
**/test*
|
**/test*
|
||||||
build*
|
build*
|
||||||
**/.*
|
**/.*
|
||||||
|
|||||||
4
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
4
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
@@ -20,10 +20,8 @@ body:
|
|||||||
options:
|
options:
|
||||||
- Self-hosted Apache
|
- Self-hosted Apache
|
||||||
- Self-hosted nginx
|
- Self-hosted nginx
|
||||||
- Self-hosted openswoole
|
|
||||||
- Self-hosted RoadRunner
|
- Self-hosted RoadRunner
|
||||||
- Openswoole Docker image
|
- Docker image
|
||||||
- RoadRunner Docker image
|
|
||||||
- Other (explain in summary)
|
- Other (explain in summary)
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,2 +1,2 @@
|
|||||||
github: ['acelaya']
|
github: ['acelaya']
|
||||||
custom: ['https://acel.me/donate']
|
custom: ['https://slnk.to/donate']
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/Bug.yml
vendored
4
.github/ISSUE_TEMPLATE/Bug.yml
vendored
@@ -22,10 +22,8 @@ body:
|
|||||||
options:
|
options:
|
||||||
- Self-hosted Apache
|
- Self-hosted Apache
|
||||||
- Self-hosted nginx
|
- Self-hosted nginx
|
||||||
- Self-hosted openswoole
|
|
||||||
- Self-hosted RoadRunner
|
- Self-hosted RoadRunner
|
||||||
- Openswoole Docker image
|
- Docker image
|
||||||
- RoadRunner Docker image
|
|
||||||
- Other (explain in summary)
|
- Other (explain in summary)
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
7
.github/actions/ci-setup/action.yml
vendored
7
.github/actions/ci-setup/action.yml
vendored
@@ -12,7 +12,6 @@ inputs:
|
|||||||
php-extensions:
|
php-extensions:
|
||||||
description: 'The PHP extensions to install'
|
description: 'The PHP extensions to install'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
|
||||||
extensions-cache-key:
|
extensions-cache-key:
|
||||||
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
|
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
|
||||||
required: true
|
required: true
|
||||||
@@ -21,6 +20,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Setup cache environment
|
- name: Setup cache environment
|
||||||
|
if: ${{ inputs.php-extensions }}
|
||||||
id: extcache
|
id: extcache
|
||||||
uses: shivammathur/cache-extensions@v1
|
uses: shivammathur/cache-extensions@v1
|
||||||
with:
|
with:
|
||||||
@@ -28,7 +28,8 @@ runs:
|
|||||||
extensions: ${{ inputs.php-extensions }}
|
extensions: ${{ inputs.php-extensions }}
|
||||||
key: ${{ inputs.extensions-cache-key }}
|
key: ${{ inputs.extensions-cache-key }}
|
||||||
- name: Cache extensions
|
- name: Cache extensions
|
||||||
uses: actions/cache@v3
|
if: ${{ inputs.php-extensions }}
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.extcache.outputs.dir }}
|
path: ${{ steps.extcache.outputs.dir }}
|
||||||
key: ${{ steps.extcache.outputs.key }}
|
key: ${{ steps.extcache.outputs.key }}
|
||||||
@@ -43,5 +44,5 @@ runs:
|
|||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: ${{ inputs.install-deps == 'yes' }}
|
if: ${{ inputs.install-deps == 'yes' }}
|
||||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
run: composer install --no-interaction --prefer-dist
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
3
.github/workflows/ci-db-tests.yml
vendored
3
.github/workflows/ci-db-tests.yml
vendored
@@ -14,7 +14,6 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.2', '8.3']
|
php-version: ['8.2', '8.3']
|
||||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
|
||||||
env:
|
env:
|
||||||
LC_ALL: C
|
LC_ALL: C
|
||||||
steps:
|
steps:
|
||||||
@@ -28,7 +27,7 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
|
php-extensions: pdo_sqlsrv-5.12.0
|
||||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||||
- name: Create test database
|
- name: Create test database
|
||||||
if: ${{ inputs.platform == 'ms' }}
|
if: ${{ inputs.platform == 'ms' }}
|
||||||
|
|||||||
46
.github/workflows/ci-mutation-tests.yml
vendored
46
.github/workflows/ci-mutation-tests.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
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.2', '8.3']
|
|
||||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: './.github/actions/ci-setup'
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php-version }}
|
|
||||||
php-extensions: openswoole-22.1.0
|
|
||||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: coverage-${{ inputs.test-group }}
|
|
||||||
path: build
|
|
||||||
- name: Resolve infection args
|
|
||||||
id: infection_args
|
|
||||||
run: echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
|
||||||
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
|
|
||||||
# run: |
|
|
||||||
# BRANCH="${GITHUB_REF#refs/heads/}" |
|
|
||||||
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
|
|
||||||
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
|
|
||||||
# else
|
|
||||||
# echo "args=--logger-github=false --git-diff-lines --git-diff-base=develop" >> $GITHUB_OUTPUT
|
|
||||||
# fi;
|
|
||||||
shell: bash
|
|
||||||
- if: ${{ inputs.test-group == 'unit' }}
|
|
||||||
run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }}
|
|
||||||
env:
|
|
||||||
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
|
|
||||||
- if: ${{ inputs.test-group != 'unit' }}
|
|
||||||
run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }}
|
|
||||||
7
.github/workflows/ci-tests.yml
vendored
7
.github/workflows/ci-tests.yml
vendored
@@ -14,7 +14,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.2', '8.3']
|
php-version: ['8.2', '8.3']
|
||||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Start postgres database server
|
- name: Start postgres database server
|
||||||
@@ -26,8 +27,10 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-22.1.0
|
|
||||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||||
|
- name: Download RoadRunner binary
|
||||||
|
if: ${{ inputs.test-group == 'api' }}
|
||||||
|
run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||||
- run: composer test:${{ inputs.test-group }}:ci
|
- run: composer test:${{ inputs.test-group }}:ci
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
if: ${{ matrix.php-version == '8.2' }}
|
if: ${{ matrix.php-version == '8.2' }}
|
||||||
|
|||||||
93
.github/workflows/ci.yml
vendored
93
.github/workflows/ci.yml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
- '*.md'
|
- '*.md'
|
||||||
- '*.xml'
|
- '*.xml'
|
||||||
- '*.yml*'
|
- '*.yml*'
|
||||||
- '*.json5'
|
|
||||||
- '*.neon'
|
- '*.neon'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -21,7 +20,6 @@ on:
|
|||||||
- '*.md'
|
- '*.md'
|
||||||
- '*.xml'
|
- '*.xml'
|
||||||
- '*.yml*'
|
- '*.yml*'
|
||||||
- '*.json5'
|
|
||||||
- '*.neon'
|
- '*.neon'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -36,7 +34,6 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-22.1.0
|
|
||||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||||
- run: composer ${{ matrix.command }}
|
- run: composer ${{ matrix.command }}
|
||||||
|
|
||||||
@@ -50,89 +47,25 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
test-group: cli
|
test-group: cli
|
||||||
|
|
||||||
openswoole-api-tests:
|
api-tests:
|
||||||
uses: './.github/workflows/ci-tests.yml'
|
uses: './.github/workflows/ci-tests.yml'
|
||||||
with:
|
with:
|
||||||
test-group: api
|
test-group: api
|
||||||
|
|
||||||
roadrunner-api-tests:
|
db-tests:
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.2', '8.3']
|
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
||||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
|
||||||
- uses: shivammathur/setup-php@v2
|
|
||||||
with:
|
|
||||||
php-version: ${{ matrix.php-version }}
|
|
||||||
tools: composer
|
|
||||||
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole ${{ matrix.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
|
||||||
- run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
|
||||||
- run: composer test:api:rr
|
|
||||||
|
|
||||||
sqlite-db-tests:
|
|
||||||
uses: './.github/workflows/ci-db-tests.yml'
|
uses: './.github/workflows/ci-db-tests.yml'
|
||||||
with:
|
with:
|
||||||
platform: 'sqlite:ci'
|
platform: ${{ matrix.platform }}
|
||||||
|
|
||||||
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:
|
|
||||||
- unit-tests
|
|
||||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
|
||||||
with:
|
|
||||||
test-group: unit
|
|
||||||
|
|
||||||
db-mutation-tests:
|
|
||||||
needs:
|
|
||||||
- sqlite-db-tests
|
|
||||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
|
||||||
with:
|
|
||||||
test-group: db
|
|
||||||
|
|
||||||
api-mutation-tests:
|
|
||||||
needs:
|
|
||||||
- openswoole-api-tests
|
|
||||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
|
||||||
with:
|
|
||||||
test-group: api
|
|
||||||
|
|
||||||
cli-mutation-tests:
|
|
||||||
needs:
|
|
||||||
- cli-tests
|
|
||||||
uses: './.github/workflows/ci-mutation-tests.yml'
|
|
||||||
with:
|
|
||||||
test-group: cli
|
|
||||||
|
|
||||||
upload-coverage:
|
upload-coverage:
|
||||||
needs:
|
needs:
|
||||||
- unit-tests
|
- unit-tests
|
||||||
- openswoole-api-tests
|
- api-tests
|
||||||
- cli-tests
|
- cli-tests
|
||||||
- sqlite-db-tests
|
- db-tests
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -141,11 +74,10 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Use PHP
|
- name: Use PHP
|
||||||
uses: shivammathur/setup-php@v2
|
uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
coverage: pcov
|
extensions-cache-key: tests-extensions-${{ matrix.php-version }}
|
||||||
ini-values: pcov.directory=module
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: build
|
path: build
|
||||||
@@ -153,19 +85,14 @@ jobs:
|
|||||||
- 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: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
|
||||||
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
|
- run: vendor/bin/phpcov merge build --clover build/clover.xml
|
||||||
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
|
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
file: ./build/clover.xml
|
file: ./build/clover.xml
|
||||||
|
|
||||||
delete-artifacts:
|
delete-artifacts:
|
||||||
needs:
|
needs:
|
||||||
- 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:
|
||||||
|
|||||||
8
.github/workflows/publish-docker-image.yml
vendored
8
.github/workflows/publish-docker-image.yml
vendored
@@ -15,13 +15,6 @@ jobs:
|
|||||||
- runtime: 'rr'
|
- runtime: 'rr'
|
||||||
tag-suffix: 'roadrunner'
|
tag-suffix: 'roadrunner'
|
||||||
platforms: 'linux/arm64/v8,linux/amd64'
|
platforms: 'linux/arm64/v8,linux/amd64'
|
||||||
- runtime: 'openswoole'
|
|
||||||
tag-suffix: 'openswoole'
|
|
||||||
platforms: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
|
||||||
- runtime: 'rr'
|
|
||||||
tag-suffix: 'non-root'
|
|
||||||
platforms: 'linux/arm64/v8,linux/amd64'
|
|
||||||
user-id: '1001'
|
|
||||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
@@ -31,4 +24,3 @@ jobs:
|
|||||||
tags-suffix: ${{ matrix.tag-suffix }}
|
tags-suffix: ${{ matrix.tag-suffix }}
|
||||||
extra-build-args: |
|
extra-build-args: |
|
||||||
SHLINK_RUNTIME=${{ matrix.runtime }}
|
SHLINK_RUNTIME=${{ matrix.runtime }}
|
||||||
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}
|
|
||||||
|
|||||||
9
.github/workflows/publish-release.yml
vendored
9
.github/workflows/publish-release.yml
vendored
@@ -11,22 +11,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.2', '8.3']
|
php-version: ['8.2', '8.3']
|
||||||
swoole: ['yes', 'no']
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-22.1.0
|
|
||||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
install-deps: 'no'
|
install-deps: 'no'
|
||||||
- if: ${{ matrix.swoole == 'yes' }}
|
- run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
||||||
run: ./build.sh ${GITHUB_REF#refs/tags/v}
|
|
||||||
- if: ${{ matrix.swoole == 'no' }}
|
|
||||||
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
|
name: dist-files-${{ matrix.php-version }}
|
||||||
path: build
|
path: build
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
|
|||||||
1
.github/workflows/publish-swagger-spec.yml
vendored
1
.github/workflows/publish-swagger-spec.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
|||||||
- uses: './.github/actions/ci-setup'
|
- uses: './.github/actions/ci-setup'
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
php-extensions: openswoole-22.1.0
|
|
||||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||||
- run: composer swagger:inline
|
- run: composer swagger:inline
|
||||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
.idea
|
.idea
|
||||||
bin/rr
|
bin/rr
|
||||||
config/roadrunner/.pid
|
.pid
|
||||||
build
|
build
|
||||||
!docker/build
|
!docker/build
|
||||||
composer.lock
|
composer.lock
|
||||||
@@ -15,3 +15,4 @@ docs/mercure.html
|
|||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
docs/swagger/swagger-inlined.json
|
docs/swagger/swagger-inlined.json
|
||||||
|
phpcov*
|
||||||
|
|||||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -4,6 +4,52 @@ 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).
|
||||||
|
|
||||||
|
## [4.0.0] - 2024-03-03
|
||||||
|
### Added
|
||||||
|
* [#1914](https://github.com/shlinkio/shlink/issues/1914) Add new dynamic redirects engine based on rules. Rules are conditions checked against the visitor's request, and when matching, they can result in a redirect to a different long URL.
|
||||||
|
|
||||||
|
Rules can be based on things like the presence of specific params, headers, locations, etc. This version ships with three initial rule condition types: device, query param and language.
|
||||||
|
|
||||||
|
* [#1902](https://github.com/shlinkio/shlink/issues/1902) Add dynamic redirects based on query parameters.
|
||||||
|
|
||||||
|
This is implemented on top of the new [rule-based redirects](https://github.com/shlinkio/shlink/discussions/1912).
|
||||||
|
|
||||||
|
* [#1915](https://github.com/shlinkio/shlink/issues/1915) Add dynamic redirects based on accept language.
|
||||||
|
|
||||||
|
This is implemented on top of the new [rule-based redirects](https://github.com/shlinkio/shlink/discussions/1912).
|
||||||
|
|
||||||
|
* [#1868](https://github.com/shlinkio/shlink/issues/1868) Add support for [docker compose secrets](https://docs.docker.com/compose/use-secrets/) to the docker image.
|
||||||
|
* [#1979](https://github.com/shlinkio/shlink/issues/1979) Allow orphan visits lists to be filtered by type.
|
||||||
|
|
||||||
|
This is supported both by the `GET /visits/orphan` API endpoint via `type=...` query param, and by the `visit:orphan` CLI command, via `--type` flag.
|
||||||
|
|
||||||
|
* [#1904](https://github.com/shlinkio/shlink/issues/1904) Allow to customize QR codes foreground color, background color and logo.
|
||||||
|
* [#1884](https://github.com/shlinkio/shlink/issues/1884) Allow a path prefix to be provided during short URL creation.
|
||||||
|
|
||||||
|
This can be useful to let Shlink generate partially random URLs, but with a known prefix.
|
||||||
|
|
||||||
|
Path prefixes are validated and filtered taking multi-segment slugs into consideration, which means slashes are replaced with dashes as long as multi-segment slugs are disabled.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1935](https://github.com/shlinkio/shlink/issues/1935) Replace dependency on abandoned `php-middleware/request-id` with userland simple middleware.
|
||||||
|
* [#1988](https://github.com/shlinkio/shlink/issues/1988) Remove dependency on `league\uri` package.
|
||||||
|
* [#1909](https://github.com/shlinkio/shlink/issues/1909) Update docker image to PHP 8.3.
|
||||||
|
* [#1786](https://github.com/shlinkio/shlink/issues/1786) Run API tests with RoadRunner by default.
|
||||||
|
* [#2008](https://github.com/shlinkio/shlink/issues/2008) Update to Doctrine ORM 3.0.
|
||||||
|
* [#2010](https://github.com/shlinkio/shlink/issues/2010) Update to Symfony 7.0 components.
|
||||||
|
* [#2016](https://github.com/shlinkio/shlink/issues/2016) Simplify and improve how code coverage is generated in API and CLI tests.
|
||||||
|
* [#1674](https://github.com/shlinkio/shlink/issues/1674) Database columns persisting long URLs have now `TEXT` type, which allows for much longer values.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* [#1908](https://github.com/shlinkio/shlink/issues/1908) Remove support for openswoole (and swoole).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#2000](https://github.com/shlinkio/shlink/issues/2000) Fix short URL creation/edition getting stuck when trying to resolve the title of a long URL which never returns a response.
|
||||||
|
|
||||||
|
|
||||||
## [3.7.3] - 2024-01-04
|
## [3.7.3] - 2024-01-04
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Then you will have to follow these steps:
|
|||||||
* Run `./indocker bin/cli db:migrate` to get database migrations up to date.
|
* Run `./indocker bin/cli db:migrate` to get database migrations up to date.
|
||||||
* Run `./indocker bin/cli api-key:generate` to get your first API key generated.
|
* Run `./indocker bin/cli api-key:generate` to get your first API key generated.
|
||||||
|
|
||||||
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
|
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner and `8000` through nginx+php-fpm.
|
||||||
|
|
||||||
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
|
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ The purposes of every folder are:
|
|||||||
* `data`: Common git-ignored assets, like logs, caches, lock files, GeoLite DB files, etc. It's the only location where Shlink may need to write at runtime.
|
* `data`: Common git-ignored assets, like logs, caches, lock files, GeoLite DB files, etc. It's the only location where Shlink may need to write at runtime.
|
||||||
* `docs`: Any project documentation is stored here, like API spec definitions or architectural decision records.
|
* `docs`: Any project documentation is stored here, like API spec definitions or architectural decision records.
|
||||||
* `module`: Contains a sub-folder for every module in the project. Modules contain the source code, tests and configurations for every context in the project.
|
* `module`: Contains a sub-folder for every module in the project. Modules contain the source code, tests and configurations for every context in the project.
|
||||||
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner or openswoole.
|
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner.
|
||||||
|
|
||||||
## Project tests
|
## Project tests
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ In order to ensure stability and no regressions are introduced while developing
|
|||||||
|
|
||||||
The project provides some tooling to run them against any of the supported database engines.
|
The project provides some tooling to run them against any of the supported database engines.
|
||||||
|
|
||||||
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner or openswoole, and test it from the outside by interacting with the REST API.
|
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner, and test it from the outside by interacting with the REST API.
|
||||||
|
|
||||||
These are the best tests to catch regressions, and to verify everything behaves as expected.
|
These are the best tests to catch regressions, and to verify everything behaves as expected.
|
||||||
|
|
||||||
@@ -124,7 +124,6 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
|||||||
|
|
||||||
* 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 test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used.
|
||||||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
|
||||||
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||||
|
|
||||||
## Testing endpoints
|
## Testing endpoints
|
||||||
|
|||||||
28
Dockerfile
28
Dockerfile
@@ -1,14 +1,12 @@
|
|||||||
FROM php:8.2-alpine3.17 as base
|
FROM php:8.3-alpine3.19 as base
|
||||||
|
|
||||||
ARG SHLINK_VERSION=latest
|
ARG SHLINK_VERSION=latest
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ARG SHLINK_RUNTIME=rr
|
ARG SHLINK_RUNTIME=rr
|
||||||
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||||
ARG SHLINK_USER_ID='root'
|
|
||||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
|
||||||
|
|
||||||
ENV OPENSWOOLE_VERSION 22.1.0
|
ENV USER_ID '1001'
|
||||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||||
ENV LC_ALL 'C'
|
ENV LC_ALL 'C'
|
||||||
@@ -26,13 +24,8 @@ RUN \
|
|||||||
apk del .dev-deps && \
|
apk del .dev-deps && \
|
||||||
apk add --no-cache postgresql icu libzip libpng
|
apk add --no-cache postgresql icu libzip libpng
|
||||||
|
|
||||||
# Install openswoole and sqlsrv driver for x86_64 builds
|
# Install 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 && \
|
||||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
|
||||||
# Openswoole is deprecated. Remove in v4.0.0
|
|
||||||
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/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||||
@@ -47,14 +40,7 @@ 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 && \
|
||||||
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
|
||||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
|
||||||
# Openswoole is deprecated. Remove in v4.0.0
|
|
||||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
|
||||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
|
||||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
|
||||||
fi; \
|
|
||||||
php composer.phar clear-cache && \
|
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
|
||||||
@@ -64,7 +50,7 @@ RUN apk add --no-cache git && \
|
|||||||
FROM base
|
FROM base
|
||||||
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
|
||||||
|
|
||||||
COPY --from=builder --chown=${SHLINK_USER_ID} /etc/shlink .
|
COPY --from=builder --chown=${USER_ID} /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 \
|
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||||
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
|
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
|
||||||
@@ -78,6 +64,6 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
|||||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||||
|
|
||||||
USER ${SHLINK_USER_ID}
|
USER ${USER_ID}
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016-2023 Alejandro Celaya
|
Copyright (c) 2016-2024 Alejandro Celaya
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
[](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
|
[](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
|
||||||
[](https://app.codecov.io/gh/shlinkio/shlink)
|
[](https://app.codecov.io/gh/shlinkio/shlink)
|
||||||
[](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop)
|
|
||||||
[](https://packagist.org/packages/shlinkio/shlink)
|
[](https://packagist.org/packages/shlinkio/shlink)
|
||||||
[](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://fosstodon.org/@shlinkio)
|
[](https://fosstodon.org/@shlinkio)
|
||||||
|
[](https://bsky.app/profile/shlinkio.bsky.social)
|
||||||
|
[](https://twitter.com/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.
|
||||||
@@ -38,12 +39,11 @@ First, make sure the host where you are going to run shlink fulfills these requi
|
|||||||
|
|
||||||
* PHP 8.2 or 8.3
|
* PHP 8.2 or 8.3
|
||||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
||||||
* apcu extension is recommended if you don't plan to use openswoole.
|
* apcu extension is recommended if you don't plan to use RoadRunner.
|
||||||
* xml extension is required if you want to generate QR codes in svg format.
|
* xml extension is required if you want to generate QR codes in svg format.
|
||||||
* sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance.
|
* sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance.
|
||||||
* MySQL, MariaDB, PostgreSQL, MicrosoftSQL or SQLite.
|
* MySQL, MariaDB, PostgreSQL, MicrosoftSQL or SQLite.
|
||||||
* You will also need the corresponding pdo variation for the database you are planning to use: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv` or `pdo_sqlite`.
|
* You will also need the corresponding pdo variation for the database you are planning to use: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv` or `pdo_sqlite`.
|
||||||
* The [openswoole](https://openswoole.com/) PHP extension (if you plan to serve Shlink with openswoole) or the web server of your choice with PHP integration (like Apache or Nginx).
|
|
||||||
|
|
||||||
### Download
|
### Download
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ In order to run Shlink, you will need a built version of the project. There are
|
|||||||
|
|
||||||
The easiest way to install shlink is by using one of the pre-bundled distributable packages.
|
The easiest way to install shlink is by using one of the pre-bundled distributable packages.
|
||||||
|
|
||||||
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version and with/without openswoole integration.
|
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version.
|
||||||
|
|
||||||
Finally, decompress the file in the location of your choice.
|
Finally, decompress the file in the location of your choice.
|
||||||
|
|
||||||
|
|||||||
50
UPGRADE.md
50
UPGRADE.md
@@ -1,5 +1,55 @@
|
|||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
|
## From v3.x to v4.x
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
* Swoole and Openswoole are no longer officially supported runtimes. The recommended alternative is RoadRunner.
|
||||||
|
* Dist files for swoole/openswoole are no longer published.
|
||||||
|
* Webhooks are no longer supported. Migrate to one of the other [real-time updates](https://shlink.io/documentation/advanced/real-time-updates/) mechanisms.
|
||||||
|
* When using RoadRunner, the amount of web workers, task workers and the port number can no longer be provided via config options. Use `WEB_WORKER_NUM`, `TASK_WORKER_NUM` and `PORT` env vars instead.
|
||||||
|
|
||||||
|
### Changes in URL shortener
|
||||||
|
|
||||||
|
* The short URLs `loosely` mode is no longer supported, as it was a typo. Use `loose` mode instead.
|
||||||
|
* QR codes URLs now work by default, even for short URLs that cannot be visited due to max visits or date range limitations.
|
||||||
|
If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option.
|
||||||
|
* Long URL title resolution is now enabled by default. You can still disable it by passing `AUTO_RESOLVE_TITLES=false` or the equivalent configuration option.
|
||||||
|
* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition.
|
||||||
|
* Device long URLs have been migrated to the new Dynamic rule-based redirects system and will continue to work as expected, but the API surface has changed.
|
||||||
|
If you use shlink-web-client and rely on this feature when creating/updating short URLs, **DO NOT UPDATE YET**. Support for dynamic rule-based redirects will be added to shlink-web-client soon, in v4.1.0
|
||||||
|
|
||||||
|
### Changes in REST API
|
||||||
|
|
||||||
|
* REST API v1/v2 now behave like v3. This only affects error codes, which are now proper URIs.
|
||||||
|
* `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_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`
|
||||||
|
* Endpoints previously returning props like `"visitsCount": {number}` no longer do it. There should be an alternative `"visitsSummary": {}` object with the amount nested on it.
|
||||||
|
* It is no longer possible to order the short URLs list with `orderBy=visitsCount-ASC`/`orderBy=visitsCount-DESC`. Use `orderBy=visits-ASC`/`orderBy=visits-DESC` instead.
|
||||||
|
* It is no longer possible to get tags with stats using `GET /tags?withStats=true`. Use `GET /tags/stats` endpoint instead.
|
||||||
|
* The `deviceLongUrls` are ignored when calling `POST /short-urls` or `PATCH /short-urls/{shortCode}`. These should now be configured as dynamic rule-based redirects via `POST /short-urls/{shortCode}/redirect-rules`.
|
||||||
|
|
||||||
|
### Changes in Docker image
|
||||||
|
|
||||||
|
* Since openswoole is no longer supported, there are no longer image tags suffixed with `openswoole`. You should migrate to the default or `roadrunner` ones.
|
||||||
|
* The `non-root` docker tag is no longer published, as all docker images are now running without super-user permissions.
|
||||||
|
* Due to previous point, it is no longer possible to pass `ENABLE_PERIODIC_VISIT_LOCATE=true` in order to configure a cron job that locates visits periodically.
|
||||||
|
This was not really needed in the docker image, as visits are located on the fly.
|
||||||
|
|
||||||
|
### Changes in integrations
|
||||||
|
|
||||||
|
* Credentials in redis URLs should now be URL-encoded, as they are unconditionally url-decoded before being used. Previously, it was possible to customize this behavior via `REDIS_DECODE_CREDENTIALS=true|false`.
|
||||||
|
* Providing redis URIs in the form of `tcp://password@6.6.6.6:6379` is no longer supported. If you want to provide password with no username, do `tcp://:password@6.6.6.6:6379` instead.
|
||||||
|
|
||||||
## From v2.x to v3.x
|
## From v2.x to v3.x
|
||||||
|
|
||||||
### Changes in REST API
|
### Changes in REST API
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
export APP_ENV=test
|
export APP_ENV=test
|
||||||
export TEST_ENV=api
|
export TEST_ENV=api
|
||||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
|
export TEST_RUNTIME="${TEST_RUNTIME:-"rr"}" # rr is the only runtime currently supported
|
||||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||||
|
|
||||||
@@ -13,26 +13,19 @@ mkdir data/log/api-tests
|
|||||||
touch $OUTPUT_LOGS
|
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
|
||||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f -w .
|
||||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
|
|
||||||
|
|
||||||
echo 'Starting server...'
|
echo 'Starting server...'
|
||||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
|
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -w . -c=config/roadrunner/.rr.test.yml \
|
||||||
[ "$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.output="${PWD}/${OUTPUT_LOGS}" \
|
||||||
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
|
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
|
||||||
-o=logs.channels.server.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
|
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 $*
|
||||||
testsExitCode=$?
|
TESTS_EXIT_CODE=$?
|
||||||
|
|
||||||
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
|
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -w .
|
||||||
[ "$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 $TESTS_EXIT_CODE
|
||||||
|
|||||||
22
build.sh
22
build.sh
@@ -1,18 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ] || ([ "$#" == 2 ] && [ "$2" != "--no-swoole" ]); then
|
if [ "$#" -lt 1 ]; then
|
||||||
echo "Usage:" >&2
|
echo "Usage:" >&2
|
||||||
echo " $0 {version} [--no-swoole]" >&2
|
echo " $0 {version}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
version=$1
|
version=$1
|
||||||
noSwoole=$2
|
|
||||||
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
||||||
# Openswoole is deprecated. Remove in v4.0.0
|
distId="shlink${version}_php${phpVersion}_dist"
|
||||||
[[ $noSwoole ]] && swooleSuffix="" || swooleSuffix="_openswoole"
|
|
||||||
distId="shlink${version}_php${phpVersion}${swooleSuffix}_dist"
|
|
||||||
builtContent="./build/${distId}"
|
builtContent="./build/${distId}"
|
||||||
projectdir=$(pwd)
|
projectdir=$(pwd)
|
||||||
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
|
||||||
@@ -31,19 +28,8 @@ cd "${builtContent}"
|
|||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
echo "Installing dependencies with $composerBin..."
|
echo "Installing dependencies with $composerBin..."
|
||||||
# Deprecated. Do not ignore PHP platform req for Shlink v4.0.0
|
|
||||||
composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-req=php+"
|
|
||||||
${composerBin} self-update
|
${composerBin} self-update
|
||||||
${composerBin} install --no-dev --prefer-dist $composerFlags
|
${composerBin} install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction
|
||||||
|
|
||||||
if [[ $noSwoole ]]; then
|
|
||||||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
|
||||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
|
||||||
else
|
|
||||||
# Deprecated. Remove in Shlink v4.0.0
|
|
||||||
# If generating a dist for openswoole, uninstall RoadRunner
|
|
||||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Delete development files
|
# Delete development files
|
||||||
echo 'Deleting dev files...'
|
echo 'Deleting dev files...'
|
||||||
|
|||||||
101
composer.json
101
composer.json
@@ -19,13 +19,13 @@
|
|||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"akrabat/ip-address-middleware": "^2.1",
|
"akrabat/ip-address-middleware": "^2.1",
|
||||||
"cakephp/chronos": "^3.0.2",
|
"cakephp/chronos": "^3.0.2",
|
||||||
|
"doctrine/dbal": "^4.0",
|
||||||
"doctrine/migrations": "^3.6",
|
"doctrine/migrations": "^3.6",
|
||||||
"doctrine/orm": "^2.16",
|
"doctrine/orm": "^3.0",
|
||||||
"endroid/qr-code": "^4.8",
|
"endroid/qr-code": "^5.0",
|
||||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||||
"geoip2/geoip2": "^3.0",
|
"geoip2/geoip2": "^3.0",
|
||||||
"guzzlehttp/guzzle": "^7.5",
|
"guzzlehttp/guzzle": "^7.5",
|
||||||
"happyr/doctrine-specification": "^2.0",
|
|
||||||
"jaybizzle/crawler-detect": "^1.2.116",
|
"jaybizzle/crawler-detect": "^1.2.116",
|
||||||
"laminas/laminas-config": "^3.8",
|
"laminas/laminas-config": "^3.8",
|
||||||
"laminas/laminas-config-aggregator": "^1.13",
|
"laminas/laminas-config-aggregator": "^1.13",
|
||||||
@@ -33,50 +33,47 @@
|
|||||||
"laminas/laminas-inputfilter": "^2.27",
|
"laminas/laminas-inputfilter": "^2.27",
|
||||||
"laminas/laminas-servicemanager": "^3.21",
|
"laminas/laminas-servicemanager": "^3.21",
|
||||||
"laminas/laminas-stdlib": "^3.17",
|
"laminas/laminas-stdlib": "^3.17",
|
||||||
"league/uri": "^6.8",
|
|
||||||
"matomo/matomo-php-tracker": "^3.2",
|
"matomo/matomo-php-tracker": "^3.2",
|
||||||
"mezzio/mezzio": "^3.17",
|
"mezzio/mezzio": "^3.17",
|
||||||
"mezzio/mezzio-fastroute": "^3.10",
|
"mezzio/mezzio-fastroute": "^3.11",
|
||||||
"mezzio/mezzio-problem-details": "^1.13",
|
"mezzio/mezzio-problem-details": "^1.13",
|
||||||
"mezzio/mezzio-swoole": "^4.7",
|
|
||||||
"mlocati/ip-lib": "^1.18",
|
"mlocati/ip-lib": "^1.18",
|
||||||
"mobiledetect/mobiledetectlib": "^4.8",
|
"mobiledetect/mobiledetectlib": "^4.8",
|
||||||
"pagerfanta/core": "^3.8",
|
"pagerfanta/core": "^3.8",
|
||||||
"php-middleware/request-id": "^4.1",
|
|
||||||
"pugx/shortid-php": "^1.1",
|
"pugx/shortid-php": "^1.1",
|
||||||
"ramsey/uuid": "^4.7",
|
"ramsey/uuid": "^4.7",
|
||||||
"shlinkio/shlink-common": "^5.7.1",
|
"shlinkio/doctrine-specification": "^2.1.1",
|
||||||
"shlinkio/shlink-config": "^2.5",
|
"shlinkio/shlink-common": "^6.0",
|
||||||
"shlinkio/shlink-event-dispatcher": "^3.1",
|
"shlinkio/shlink-config": "^3.0",
|
||||||
"shlinkio/shlink-importer": "^5.2.1",
|
"shlinkio/shlink-event-dispatcher": "^4.0",
|
||||||
"shlinkio/shlink-installer": "^8.7",
|
"shlinkio/shlink-importer": "^5.3",
|
||||||
"shlinkio/shlink-ip-geolocation": "^3.4",
|
"shlinkio/shlink-installer": "^9.0",
|
||||||
|
"shlinkio/shlink-ip-geolocation": "^3.5",
|
||||||
"shlinkio/shlink-json": "^1.1",
|
"shlinkio/shlink-json": "^1.1",
|
||||||
"spiral/roadrunner": "^2023.2",
|
"spiral/roadrunner": "^2023.3",
|
||||||
"spiral/roadrunner-cli": "^2.5",
|
"spiral/roadrunner-cli": "^2.6",
|
||||||
"spiral/roadrunner-http": "^3.1",
|
"spiral/roadrunner-http": "^3.3",
|
||||||
"spiral/roadrunner-jobs": "^4.0",
|
"spiral/roadrunner-jobs": "^4.3",
|
||||||
"symfony/console": "^6.3",
|
"symfony/console": "^7.0",
|
||||||
"symfony/filesystem": "^6.3",
|
"symfony/filesystem": "^7.0",
|
||||||
"symfony/lock": "^6.3",
|
"symfony/lock": "^7.0",
|
||||||
"symfony/process": "^6.3",
|
"symfony/process": "^7.0",
|
||||||
"symfony/string": "^6.3"
|
"symfony/string": "^7.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||||
"devster/ubench": "^2.1",
|
"devster/ubench": "^2.1",
|
||||||
"infection/infection": "^0.27",
|
|
||||||
"openswoole/ide-helper": "~22.0.0",
|
|
||||||
"phpstan/phpstan": "^1.10",
|
"phpstan/phpstan": "^1.10",
|
||||||
"phpstan/phpstan-doctrine": "^1.3",
|
"phpstan/phpstan-doctrine": "^1.3",
|
||||||
"phpstan/phpstan-phpunit": "^1.3",
|
"phpstan/phpstan-phpunit": "^1.3",
|
||||||
"phpstan/phpstan-symfony": "^1.3",
|
"phpstan/phpstan-symfony": "^1.3",
|
||||||
"phpunit/php-code-coverage": "^10.1",
|
"phpunit/php-code-coverage": "^10.1",
|
||||||
|
"phpunit/phpcov": "^9.0",
|
||||||
"phpunit/phpunit": "^10.4",
|
"phpunit/phpunit": "^10.4",
|
||||||
"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.8.1",
|
"shlinkio/shlink-test-utils": "^4.1",
|
||||||
"symfony/var-dumper": "^6.3",
|
"symfony/var-dumper": "^7.0",
|
||||||
"veewee/composer-run-parallel": "^1.3"
|
"veewee/composer-run-parallel": "^1.3"
|
||||||
},
|
},
|
||||||
"conflict": {
|
"conflict": {
|
||||||
@@ -111,8 +108,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ci": [
|
"ci": [
|
||||||
"@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:postgres test:db:mysql test:db:maria test:db:ms",
|
||||||
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
|
"@parallel test:api:ci test:cli:ci"
|
||||||
],
|
],
|
||||||
"cs": "phpcs -s",
|
"cs": "phpcs -s",
|
||||||
"cs:fix": "phpcbf",
|
"cs:fix": "phpcbf",
|
||||||
@@ -122,54 +119,27 @@
|
|||||||
"@parallel test:api test:cli"
|
"@parallel test:api test:cli"
|
||||||
],
|
],
|
||||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov",
|
||||||
"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",
|
||||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
|
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov",
|
||||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
|
||||||
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
|
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
|
||||||
"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 && vendor/bin/phpcov merge build/coverage-api --php build/coverage-api.cov && rm build/coverage-api/*.cov",
|
||||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
|
"test:api:pretty": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --html build/coverage-api/coverage-html && rm build/coverage-api/*.cov",
|
||||||
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
|
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml",
|
||||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
|
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --php build/coverage-cli.cov && rm build/coverage-cli/*.cov",
|
||||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
|
"test:cli:pretty": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --html build/coverage-cli/coverage-html && rm build/coverage-cli/*.cov",
|
||||||
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
|
|
||||||
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
|
|
||||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
|
||||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
|
||||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=95 --configuration=infection-api.json5",
|
|
||||||
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
|
|
||||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
|
|
||||||
"infect:test": [
|
|
||||||
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
|
|
||||||
"@infect:ci"
|
|
||||||
],
|
|
||||||
"infect:test:unit": [
|
|
||||||
"@test:unit:ci",
|
|
||||||
"@infect:ci:unit"
|
|
||||||
],
|
|
||||||
"infect:test:db": [
|
|
||||||
"@test:db:sqlite:ci",
|
|
||||||
"@infect:ci:db"
|
|
||||||
],
|
|
||||||
"infect:test:api": [
|
|
||||||
"@test:api:ci",
|
|
||||||
"@infect:ci:api"
|
|
||||||
],
|
|
||||||
"infect:test:cli": [
|
|
||||||
"@test:cli:ci",
|
|
||||||
"@infect:ci:cli"
|
|
||||||
],
|
|
||||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
"swagger: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\" and \"test:ci\"</>",
|
||||||
"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</>",
|
||||||
@@ -190,10 +160,6 @@
|
|||||||
"test:cli": "<fg=blue;options=bold>Runs CLI test suites</>",
|
"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: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</>",
|
"test:cli:pretty": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage in HTML format</>",
|
||||||
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
|
||||||
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
|
||||||
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
|
|
||||||
"infect:test": "<fg=blue;options=bold>Runs unit and db tests, then checks tests quality applying mutation testing</>",
|
|
||||||
"swagger:validate": "<fg=blue;options=bold>Validates the swagger docs, making sure they fulfil the spec</>",
|
"swagger:validate": "<fg=blue;options=bold>Validates the swagger docs, making sure they fulfil the spec</>",
|
||||||
"swagger:inline": "<fg=blue;options=bold>Inlines swagger docs in a single file</>",
|
"swagger:inline": "<fg=blue;options=bold>Inlines swagger docs in a single file</>",
|
||||||
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
|
||||||
@@ -204,7 +170,6 @@
|
|||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
"composer/package-versions-deprecated": true,
|
"composer/package-versions-deprecated": true,
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||||
"infection/extension-installer": true,
|
|
||||||
"veewee/composer-run-parallel": true
|
"veewee/composer-run-parallel": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ return (static function (): array {
|
|||||||
'redis' => [
|
'redis' => [
|
||||||
'servers' => $redisServers,
|
'servers' => $redisServers,
|
||||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||||
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
|
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ return [
|
|||||||
|
|
||||||
'debug' => false,
|
'debug' => false,
|
||||||
|
|
||||||
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||||
// commands don't generate a cache file that's then used by php-fpm 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',
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Mezzio\Application;
|
use Mezzio\Application;
|
||||||
use Mezzio\Container;
|
use Mezzio\Container;
|
||||||
use Psr\Http\Client\ClientInterface;
|
use Psr\Http\Client\ClientInterface;
|
||||||
@@ -12,12 +13,14 @@ use Psr\Http\Message\StreamFactoryInterface;
|
|||||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||||
use Spiral\RoadRunner\WorkerInterface;
|
use Spiral\RoadRunner\WorkerInterface;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
PSR7Worker::class => ConfigAbstractFactory::class,
|
PSR7Worker::class => ConfigAbstractFactory::class,
|
||||||
|
Filesystem::class => InvokableFactory::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'delegators' => [
|
'delegators' => [
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ return [
|
|||||||
Option\Database\DatabaseUnixSocketConfigOption::class,
|
Option\Database\DatabaseUnixSocketConfigOption::class,
|
||||||
Option\UrlShortener\ShortDomainHostConfigOption::class,
|
Option\UrlShortener\ShortDomainHostConfigOption::class,
|
||||||
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
|
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
|
||||||
Option\Visit\VisitsWebhooksConfigOption::class,
|
|
||||||
Option\Visit\OrphanVisitsWebhooksConfigOption::class,
|
|
||||||
Option\Redirect\BaseUrlRedirectConfigOption::class,
|
Option\Redirect\BaseUrlRedirectConfigOption::class,
|
||||||
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
|
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
|
||||||
Option\Redirect\Regular404RedirectConfigOption::class,
|
Option\Redirect\Regular404RedirectConfigOption::class,
|
||||||
@@ -30,10 +28,7 @@ return [
|
|||||||
Option\BasePathConfigOption::class,
|
Option\BasePathConfigOption::class,
|
||||||
Option\TimezoneConfigOption::class,
|
Option\TimezoneConfigOption::class,
|
||||||
Option\Cache\CacheNamespaceConfigOption::class,
|
Option\Cache\CacheNamespaceConfigOption::class,
|
||||||
Option\Worker\TaskWorkerNumConfigOption::class,
|
|
||||||
Option\Worker\WebWorkerNumConfigOption::class,
|
|
||||||
Option\Redis\RedisServersConfigOption::class,
|
Option\Redis\RedisServersConfigOption::class,
|
||||||
Option\Redis\RedisDecodeCredentialsConfigOption::class,
|
|
||||||
Option\Redis\RedisSentinelServiceConfigOption::class,
|
Option\Redis\RedisSentinelServiceConfigOption::class,
|
||||||
Option\Redis\RedisPubSubConfigOption::class,
|
Option\Redis\RedisPubSubConfigOption::class,
|
||||||
Option\UrlShortener\ShortCodeLengthOption::class,
|
Option\UrlShortener\ShortCodeLengthOption::class,
|
||||||
@@ -62,6 +57,9 @@ return [
|
|||||||
Option\QrCode\DefaultFormatConfigOption::class,
|
Option\QrCode\DefaultFormatConfigOption::class,
|
||||||
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
|
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
|
||||||
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
|
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
|
||||||
|
Option\QrCode\DefaultColorConfigOption::class,
|
||||||
|
Option\QrCode\DefaultBgColorConfigOption::class,
|
||||||
|
Option\QrCode\DefaultLogoUrlConfigOption::class,
|
||||||
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
|
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
|
||||||
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
|
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
|
||||||
Option\RabbitMq\RabbitMqHostConfigOption::class,
|
Option\RabbitMq\RabbitMqHostConfigOption::class,
|
||||||
|
|||||||
@@ -7,20 +7,21 @@ namespace Shlinkio\Shlink;
|
|||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use PhpMiddleware\RequestId;
|
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$common = [
|
$common = [
|
||||||
'level' => Level::Info->value,
|
'level' => Level::Info->value,
|
||||||
'processors' => [RequestId\MonologProcessor::class],
|
'processors' => [RequestIdMiddleware::class],
|
||||||
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
'line_format' =>
|
||||||
|
'[%datetime%] [%extra.' . RequestIdMiddleware::ATTRIBUTE . '%] %channel%.%level_name% - %message%',
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -52,16 +53,5 @@ return (static function (): array {
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
// Deprecated. Remove in Shlink 4.0.0
|
|
||||||
'mezzio-swoole' => [
|
|
||||||
'swoole-http-server' => [
|
|
||||||
'logger' => [
|
|
||||||
// Let's disable mezio-swoole access logging, so that we can provide our own implementation,
|
|
||||||
// consistent for roadrunner and openswoole
|
|
||||||
'logger-name' => NullLogger::class,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'mercure' => [
|
'mercure' => [
|
||||||
'public_hub_url' => 'http://localhost:8001',
|
'public_hub_url' => 'http://localhost:8002',
|
||||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||||
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ namespace Shlinkio\Shlink;
|
|||||||
use Laminas\Stratigility\Middleware\ErrorHandler;
|
use Laminas\Stratigility\Middleware\ErrorHandler;
|
||||||
use Mezzio\ProblemDetails;
|
use Mezzio\ProblemDetails;
|
||||||
use Mezzio\Router;
|
use Mezzio\Router;
|
||||||
use PhpMiddleware\RequestId\RequestIdMiddleware;
|
|
||||||
use RKA\Middleware\IpAddress;
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||||
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
||||||
|
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -47,7 +47,6 @@ 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,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
|
use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR;
|
||||||
|
use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR;
|
||||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS;
|
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS;
|
||||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
|
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
|
||||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||||
@@ -26,6 +28,9 @@ return [
|
|||||||
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
|
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
|
||||||
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
|
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
|
||||||
),
|
),
|
||||||
|
'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(DEFAULT_QR_CODE_COLOR),
|
||||||
|
'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(DEFAULT_QR_CODE_BG_COLOR),
|
||||||
|
'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ return [
|
|||||||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||||
|
|
||||||
// Deprecated
|
|
||||||
'legacy_visits_publishing' => (bool) EnvVars::RABBITMQ_LEGACY_VISITS_PUBLISHING->loadFromEnv(false),
|
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ return [
|
|||||||
'rabbitmq' => [
|
'rabbitmq' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'host' => 'shlink_rabbitmq',
|
'host' => 'shlink_rabbitmq',
|
||||||
|
'port' => '5673',
|
||||||
'user' => 'rabbit',
|
'user' => 'rabbit',
|
||||||
'password' => 'rabbit',
|
'password' => 'rabbit',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
|
||||||
use PhpMiddleware\RequestId;
|
|
||||||
use Shlinkio\Shlink\Common\Logger\Processor\BackwardsCompatibleMonologProcessorDelegator;
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'request_id' => [
|
|
||||||
'allow_override' => true,
|
|
||||||
'header_name' => 'X-Request-Id',
|
|
||||||
],
|
|
||||||
|
|
||||||
'dependencies' => [
|
|
||||||
'factories' => [
|
|
||||||
RequestId\Generator\RamseyUuid4StaticGenerator::class => InvokableFactory::class,
|
|
||||||
RequestId\RequestIdProviderFactory::class => ConfigAbstractFactory::class,
|
|
||||||
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
|
|
||||||
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
|
|
||||||
],
|
|
||||||
'delegators' => [
|
|
||||||
RequestId\MonologProcessor::class => [
|
|
||||||
BackwardsCompatibleMonologProcessorDelegator::class,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
|
||||||
RequestId\RequestIdProviderFactory::class => [
|
|
||||||
RequestId\Generator\RamseyUuid4StaticGenerator::class,
|
|
||||||
'config.request_id.allow_override',
|
|
||||||
'config.request_id.header_name',
|
|
||||||
],
|
|
||||||
RequestId\RequestIdMiddleware::class => [
|
|
||||||
RequestId\RequestIdProviderFactory::class,
|
|
||||||
'config.request_id.header_name',
|
|
||||||
],
|
|
||||||
RequestId\MonologProcessor::class => [RequestId\RequestIdMiddleware::class],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -11,7 +11,7 @@ return [
|
|||||||
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
|
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
|
||||||
|
|
||||||
'fastroute' => [
|
'fastroute' => [
|
||||||
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
|
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||||
FastRouteRouter::CONFIG_CACHE_ENABLED => PHP_SAPI !== 'cli',
|
FastRouteRouter::CONFIG_CACHE_ENABLED => PHP_SAPI !== 'cli',
|
||||||
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
|
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
|||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$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;
|
||||||
|
|
||||||
@@ -32,9 +31,10 @@ return (static function (): array {
|
|||||||
...ConfigProvider::applyRoutesPrefix([
|
...ConfigProvider::applyRoutesPrefix([
|
||||||
Action\HealthAction::getRouteDef(),
|
Action\HealthAction::getRouteDef(),
|
||||||
|
|
||||||
|
// Visits and rules routes must go first, as they have a more specific path, otherwise, when
|
||||||
|
// multi-segment slugs are enabled, routes with a less-specific path might match first
|
||||||
|
|
||||||
// Visits.
|
// Visits.
|
||||||
// These routes must go first, as they have a more specific path, otherwise, when multi-segment slugs
|
|
||||||
// are enabled, routes with a less-specific path might match first
|
|
||||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||||
@@ -44,15 +44,18 @@ return (static function (): array {
|
|||||||
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
|
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
|
||||||
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||||
|
|
||||||
|
//Redirect rules
|
||||||
|
Action\RedirectRule\ListRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\RedirectRule\SetRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
|
||||||
// Short URLs
|
// Short URLs
|
||||||
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||||
$contentNegotiationMiddleware,
|
|
||||||
$dropDomainMiddleware,
|
$dropDomainMiddleware,
|
||||||
$overrideDomainMiddleware,
|
$overrideDomainMiddleware,
|
||||||
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||||
]),
|
]),
|
||||||
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||||
$contentNegotiationMiddleware,
|
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class,
|
||||||
$overrideDomainMiddleware,
|
$overrideDomainMiddleware,
|
||||||
]),
|
]),
|
||||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Config\getOpenswooleConfigFromEnv;
|
|
||||||
|
|
||||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
|
||||||
|
|
||||||
return (static function (): array {
|
|
||||||
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16);
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'mezzio-swoole' => [
|
|
||||||
// Setting this to true can have unexpected behaviors when running several concurrent slow DB queries
|
|
||||||
'enable_coroutine' => false,
|
|
||||||
|
|
||||||
'swoole-http-server' => [
|
|
||||||
'host' => '0.0.0.0',
|
|
||||||
'port' => (int) EnvVars::PORT->loadFromEnv(8080),
|
|
||||||
'process-name' => 'shlink',
|
|
||||||
|
|
||||||
'options' => [
|
|
||||||
...getOpenswooleConfigFromEnv(),
|
|
||||||
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
|
|
||||||
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'mezzio-swoole' => [
|
|
||||||
'hot-code-reload' => [
|
|
||||||
'enable' => true,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -14,7 +14,7 @@ return (static function (): array {
|
|||||||
MIN_SHORT_CODES_LENGTH,
|
MIN_SHORT_CODES_LENGTH,
|
||||||
);
|
);
|
||||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||||
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
|
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ return (static function (): array {
|
|||||||
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
|
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
|
||||||
],
|
],
|
||||||
'default_short_codes_length' => $shortCodesLength,
|
'default_short_codes_length' => $shortCodesLength,
|
||||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(true),
|
||||||
'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),
|
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
|
||||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -12,11 +11,9 @@ return [
|
|||||||
'schema' => 'http',
|
'schema' => 'http',
|
||||||
'hostname' => sprintf('localhost:%s', match (true) {
|
'hostname' => sprintf('localhost:%s', match (true) {
|
||||||
runningInRoadRunner() => '8800',
|
runningInRoadRunner() => '8800',
|
||||||
runningInOpenswoole() => '8080',
|
|
||||||
default => '8000',
|
default => '8000',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
'auto_resolve_titles' => true,
|
|
||||||
// 'multi_segment_slugs_enabled' => true,
|
// 'multi_segment_slugs_enabled' => true,
|
||||||
// 'trailing_slash_enabled' => true,
|
// 'trailing_slash_enabled' => true,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
|
||||||
|
|
||||||
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
|
|
||||||
return (static function (): array {
|
|
||||||
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'visits_webhooks' => [
|
|
||||||
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
|
|
||||||
'notify_orphan_visits_to_webhooks' =>
|
|
||||||
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
@@ -8,19 +8,12 @@ use Laminas\ConfigAggregator;
|
|||||||
use Laminas\Diactoros;
|
use Laminas\Diactoros;
|
||||||
use Mezzio;
|
use Mezzio;
|
||||||
use Mezzio\ProblemDetails;
|
use Mezzio\ProblemDetails;
|
||||||
use Mezzio\Swoole;
|
|
||||||
use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
||||||
|
|
||||||
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 function Shlinkio\Shlink\Core\enumValues;
|
use function Shlinkio\Shlink\Core\enumValues;
|
||||||
|
|
||||||
use const PHP_SAPI;
|
|
||||||
|
|
||||||
$isTestEnv = env('APP_ENV') === 'test';
|
$isTestEnv = env('APP_ENV') === 'test';
|
||||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
|
||||||
|
|
||||||
return (new ConfigAggregator\ConfigAggregator(
|
return (new ConfigAggregator\ConfigAggregator(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -30,9 +23,6 @@ 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,
|
||||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
|
||||||
? Swoole\ConfigProvider::class
|
|
||||||
: new ConfigAggregator\ArrayProvider([]),
|
|
||||||
ProblemDetails\ConfigProvider::class,
|
ProblemDetails\ConfigProvider::class,
|
||||||
Diactoros\ConfigProvider::class,
|
Diactoros\ConfigProvider::class,
|
||||||
Common\ConfigProvider::class,
|
Common\ConfigProvider::class,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
|||||||
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
|
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
|
||||||
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
const DEFAULT_SHORT_CODES_LENGTH = 5;
|
||||||
const MIN_SHORT_CODES_LENGTH = 4;
|
const MIN_SHORT_CODES_LENGTH = 4;
|
||||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
|
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
|
||||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||||
@@ -19,6 +19,6 @@ const DEFAULT_QR_CODE_MARGIN = 0;
|
|||||||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||||
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
|
||||||
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||||
// Deprecated. Shlink 4.0.0 should change default value to `true`
|
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true;
|
||||||
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = false;
|
const DEFAULT_QR_CODE_COLOR = '#000000'; // Black
|
||||||
const MIN_TASK_WORKERS = 4;
|
const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White
|
||||||
|
|||||||
@@ -12,17 +12,6 @@ chdir(dirname(__DIR__));
|
|||||||
|
|
||||||
require 'vendor/autoload.php';
|
require 'vendor/autoload.php';
|
||||||
|
|
||||||
// Workaround to make this compatible with both openswoole 22 and earlier versions.
|
|
||||||
// Openswoole support is deprecated. Remove in v4.0.0
|
|
||||||
if (! function_exists('swoole_set_process_name')) {
|
|
||||||
// phpcs:disable
|
|
||||||
function swoole_set_process_name(string $name): void
|
|
||||||
{
|
|
||||||
OpenSwoole\Util::setProcessName($name);
|
|
||||||
}
|
|
||||||
// phpcs:enable
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is one of the first files loaded. Configure the timezone here
|
// This is one of the first files loaded. Configure the timezone here
|
||||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||||
|
|
||||||
|
|||||||
49
config/roadrunner/.rr.test.yml
Normal file
49
config/roadrunner/.rr.test.yml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
############################################################################################
|
||||||
|
# Routes here need to be relative to the project root, as API tests are run with `-w .` #
|
||||||
|
# See https://github.com/orgs/roadrunner-server/discussions/1440#discussioncomment-8486186 #
|
||||||
|
############################################################################################
|
||||||
|
|
||||||
|
rpc:
|
||||||
|
listen: tcp://127.0.0.1:6001
|
||||||
|
|
||||||
|
server:
|
||||||
|
command: 'php ./bin/roadrunner-worker.php'
|
||||||
|
|
||||||
|
http:
|
||||||
|
address: '0.0.0.0:9999'
|
||||||
|
middleware: ['static']
|
||||||
|
static:
|
||||||
|
dir: './public'
|
||||||
|
forbid: ['.php', '.htaccess']
|
||||||
|
pool:
|
||||||
|
num_workers: 1
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pool:
|
||||||
|
num_workers: 1
|
||||||
|
debug: false
|
||||||
|
timeout: 300
|
||||||
|
consume: ['shlink']
|
||||||
|
pipelines:
|
||||||
|
shlink:
|
||||||
|
driver: memory
|
||||||
|
config:
|
||||||
|
priority: 10
|
||||||
|
prefetch: 10
|
||||||
|
|
||||||
|
logs:
|
||||||
|
encoding: json
|
||||||
|
mode: development
|
||||||
|
channels:
|
||||||
|
http:
|
||||||
|
mode: 'off' # Disable logging as Shlink handles it internally
|
||||||
|
server:
|
||||||
|
encoding: json
|
||||||
|
level: info
|
||||||
|
metrics:
|
||||||
|
level: panic
|
||||||
|
jobs:
|
||||||
|
level: panic
|
||||||
@@ -7,12 +7,6 @@ namespace Shlinkio\Shlink\TestUtils;
|
|||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Psr\Container\ContainerInterface;
|
use Psr\Container\ContainerInterface;
|
||||||
|
|
||||||
use function register_shutdown_function;
|
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
|
||||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
|
||||||
|
|
||||||
/** @var ContainerInterface $container */
|
/** @var ContainerInterface $container */
|
||||||
$container = require __DIR__ . '/../container.php';
|
$container = require __DIR__ . '/../container.php';
|
||||||
$testHelper = $container->get(Helper\TestHelper::class);
|
$testHelper = $container->get(Helper\TestHelper::class);
|
||||||
@@ -20,14 +14,6 @@ $config = $container->get('config');
|
|||||||
$em = $container->get(EntityManager::class);
|
$em = $container->get(EntityManager::class);
|
||||||
$httpClient = $container->get('shlink_test_api_client');
|
$httpClient = $container->get('shlink_test_api_client');
|
||||||
|
|
||||||
// Dump code coverage when process shuts down
|
|
||||||
register_shutdown_function(function () use ($httpClient): void {
|
|
||||||
$httpClient->request(
|
|
||||||
'GET',
|
|
||||||
sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
$testHelper->createTestDb(
|
$testHelper->createTestDb(
|
||||||
createDbCommand: ['bin/cli', 'db:create'],
|
createDbCommand: ['bin/cli', 'db:create'],
|
||||||
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
migrateDbCommand: ['bin/cli', 'db:migrate'],
|
||||||
|
|||||||
@@ -6,75 +6,38 @@ namespace Shlinkio\Shlink;
|
|||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\HtmlResponse;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use League\Event\EventDispatcher;
|
use Mezzio\Router\FastRouteRouter;
|
||||||
use Monolog\Level;
|
use Monolog\Level;
|
||||||
use PHPUnit\Runner\Version;
|
|
||||||
use Psr\Container\ContainerInterface;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
use SebastianBergmann\CodeCoverage\CodeCoverage;
|
|
||||||
use SebastianBergmann\CodeCoverage\Driver\Selector;
|
|
||||||
use SebastianBergmann\CodeCoverage\Filter;
|
|
||||||
use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
|
||||||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
|
||||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
use Shlinkio\Shlink\TestUtils\ApiTest\CoverageMiddleware;
|
||||||
|
use Shlinkio\Shlink\TestUtils\CliTest\CliCoverageDelegator;
|
||||||
|
use Shlinkio\Shlink\TestUtils\Helper\CoverageHelper;
|
||||||
use Symfony\Component\Console\Application;
|
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 Laminas\Stratigility\middleware;
|
use function Laminas\Stratigility\middleware;
|
||||||
use function Shlinkio\Shlink\Config\env;
|
use function Shlinkio\Shlink\Config\env;
|
||||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
use function sleep;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
use function sys_get_temp_dir;
|
|
||||||
|
|
||||||
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
use const ShlinkioTest\Shlink\API_TESTS_HOST;
|
||||||
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
use const ShlinkioTest\Shlink\API_TESTS_PORT;
|
||||||
|
|
||||||
$isApiTest = env('TEST_ENV') === 'api';
|
$testEnv = env('TEST_ENV');
|
||||||
$isCliTest = env('TEST_ENV') === 'cli';
|
$isApiTest = $testEnv === 'api';
|
||||||
|
$isCliTest = $testEnv === 'cli';
|
||||||
$isE2eTest = $isApiTest || $isCliTest;
|
$isE2eTest = $isApiTest || $isCliTest;
|
||||||
|
|
||||||
$coverageType = env('GENERATE_COVERAGE');
|
$coverageType = env('GENERATE_COVERAGE');
|
||||||
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
|
$generateCoverage = $coverageType === 'yes';
|
||||||
|
$coverage = $isE2eTest && $generateCoverage ? CoverageHelper::createCoverageForDirectories(
|
||||||
$coverage = null;
|
[
|
||||||
if ($isE2eTest && $generateCoverage) {
|
__DIR__ . '/../../module/Core/src',
|
||||||
$filter = new Filter();
|
__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src',
|
||||||
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
|
],
|
||||||
$filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src');
|
__DIR__ . '/../../build/coverage-' . $testEnv,
|
||||||
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
|
) : null;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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');
|
||||||
@@ -89,7 +52,7 @@ $buildDbConnection = static function (): array {
|
|||||||
'postgres' => [
|
'postgres' => [
|
||||||
'driver' => 'pdo_pgsql',
|
'driver' => 'pdo_pgsql',
|
||||||
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
|
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
|
||||||
'port' => $isCi ? '5433' : '5432',
|
'port' => $isCi ? '5434' : '5432',
|
||||||
'user' => 'postgres',
|
'user' => 'postgres',
|
||||||
'password' => 'root',
|
'password' => 'root',
|
||||||
'dbname' => 'shlink_test',
|
'dbname' => 'shlink_test',
|
||||||
@@ -128,6 +91,7 @@ return [
|
|||||||
|
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
ConfigAggregator::ENABLE_CACHE => false,
|
ConfigAggregator::ENABLE_CACHE => false,
|
||||||
|
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||||
|
|
||||||
'url_shortener' => [
|
'url_shortener' => [
|
||||||
'domain' => [
|
'domain' => [
|
||||||
@@ -136,52 +100,27 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'mezzio-swoole' => [
|
'routes' => [
|
||||||
'enable_coroutine' => false,
|
// This route is used to test that title resolution is skipped if the long URL times out
|
||||||
'swoole-http-server' => [
|
|
||||||
'host' => API_TESTS_HOST,
|
|
||||||
'port' => API_TESTS_PORT,
|
|
||||||
'process-name' => 'shlink_test',
|
|
||||||
'options' => [
|
|
||||||
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
|
|
||||||
'log_file' => __DIR__ . '/../../data/log/api-tests/output.log',
|
|
||||||
'enable_coroutine' => false,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
'routes' => !$isApiTest ? [] : [
|
|
||||||
[
|
[
|
||||||
'name' => 'dump_coverage',
|
'name' => 'long_url_with_timeout',
|
||||||
'path' => '/api-tests/stop-coverage',
|
'path' => '/api-tests/long-url-with-timeout',
|
||||||
'middleware' => middleware(static function () use ($exportCoverage) {
|
|
||||||
// TODO I have tried moving this block to a listener so that it's invoked automatically,
|
|
||||||
// but then the coverage is generated empty ¯\_(ツ)_/¯
|
|
||||||
$exportCoverage();
|
|
||||||
return new EmptyResponse();
|
|
||||||
}),
|
|
||||||
'allowed_methods' => ['GET'],
|
'allowed_methods' => ['GET'],
|
||||||
|
'middleware' => middleware(static function () {
|
||||||
|
sleep(5); // Title resolution times out at 3 seconds
|
||||||
|
return new HtmlResponse('<title>The title</title>');
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'middleware_pipeline' => !$isApiTest ? [] : [
|
'middleware_pipeline' => !$isApiTest ? [] : [
|
||||||
'capture_code_coverage' => [
|
'capture_code_coverage' => [
|
||||||
'middleware' => middleware(static function (
|
'middleware' => new CoverageMiddleware($coverage),
|
||||||
ServerRequestInterface $req,
|
|
||||||
RequestHandlerInterface $handler,
|
|
||||||
) use (&$coverage): ResponseInterface {
|
|
||||||
$coverage?->start($req->getHeaderLine('x-coverage-id'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
return $handler->handle($req);
|
|
||||||
} finally {
|
|
||||||
$coverage?->stop();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
'priority' => 9999,
|
'priority' => 9999,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// Disable mercure integration during E2E tests
|
||||||
'mercure' => [
|
'mercure' => [
|
||||||
'public_hub_url' => null,
|
'public_hub_url' => null,
|
||||||
'internal_hub_url' => null,
|
'internal_hub_url' => null,
|
||||||
@@ -200,58 +139,7 @@ return [
|
|||||||
],
|
],
|
||||||
'delegators' => $isCliTest ? [
|
'delegators' => $isCliTest ? [
|
||||||
Application::class => [
|
Application::class => [
|
||||||
static function (
|
new CliCoverageDelegator($coverage),
|
||||||
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;
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
] : [],
|
] : [],
|
||||||
],
|
],
|
||||||
@@ -262,7 +150,7 @@ return [
|
|||||||
|
|
||||||
'data_fixtures' => [
|
'data_fixtures' => [
|
||||||
'paths' => [
|
'paths' => [
|
||||||
// TODO These are used for CLI tests too, so maybe should be somewhere else
|
// TODO These are used for other module's tests, so maybe should be somewhere else
|
||||||
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
__DIR__ . '/../../module/Rest/test-api/Fixtures',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ server {
|
|||||||
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
include fastcgi.conf;
|
include fastcgi.conf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
/var/log/shlink/shlink_openswoole.log {
|
|
||||||
su root root
|
|
||||||
daily
|
|
||||||
missingok
|
|
||||||
rotate 120
|
|
||||||
compress
|
|
||||||
delaycompress
|
|
||||||
notifempty
|
|
||||||
create 0640 root root
|
|
||||||
postrotate
|
|
||||||
/etc/init.d/shlink_openswoole restart
|
|
||||||
endscript
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
### BEGIN INIT INFO
|
|
||||||
# Provides: shlink_openswoole
|
|
||||||
# Required-Start: $local_fs $network $named $time $syslog
|
|
||||||
# Required-Stop: $local_fs $network $named $time $syslog
|
|
||||||
# Default-Start: 2 3 4 5
|
|
||||||
# Default-Stop: 0 1 6
|
|
||||||
# Description: Shlink non-blocking server with openswoole
|
|
||||||
### END INIT INFO
|
|
||||||
|
|
||||||
SCRIPT=/path/to/shlink/vendor/bin/laminas\ mezzio:swoole:start
|
|
||||||
RUNAS=root
|
|
||||||
|
|
||||||
PIDFILE=/var/run/shlink_openswoole.pid
|
|
||||||
LOGDIR=/var/log/shlink
|
|
||||||
LOGFILE=${LOGDIR}/shlink_openswoole.log
|
|
||||||
|
|
||||||
start() {
|
|
||||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
|
||||||
echo 'Shlink with openswoole already running' >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
echo 'Starting shlink with openswoole' >&2
|
|
||||||
mkdir -p "$LOGDIR"
|
|
||||||
touch "$LOGFILE"
|
|
||||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
|
||||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
|
||||||
echo 'Shlink started' >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
|
||||||
echo 'Shlink with openswoole not running' >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
echo 'Stopping shlink with openswoole' >&2
|
|
||||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
|
||||||
echo 'Shlink stopped' >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$1" in
|
|
||||||
start)
|
|
||||||
start
|
|
||||||
;;
|
|
||||||
stop)
|
|
||||||
stop
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
stop
|
|
||||||
start
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Usage: $0 {start|stop|restart}"
|
|
||||||
esac
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM php:8.2-fpm-alpine3.17
|
FROM php:8.3-fpm-alpine3.19
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.23
|
||||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM php:8.2-alpine3.17
|
FROM php:8.3-alpine3.19
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.23
|
||||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
FROM php:8.2-alpine3.17
|
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
|
||||||
ENV INOTIFY_VERSION 3.0.0
|
|
||||||
ENV OPENSWOOLE_VERSION 22.1.0
|
|
||||||
ENV PDO_SQLSRV_VERSION 5.11.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 inotify extension
|
|
||||||
ADD https://pecl.php.net/get/inotify-$INOTIFY_VERSION.tgz /tmp/inotify.tar.gz
|
|
||||||
RUN mkdir -p /usr/src/php/ext/inotify \
|
|
||||||
&& tar xf /tmp/inotify.tar.gz -C /usr/src/php/ext/inotify --strip-components=1 \
|
|
||||||
&& docker-php-ext-configure inotify \
|
|
||||||
&& docker-php-ext-install inotify \
|
|
||||||
&& rm /tmp/inotify.tar.gz
|
|
||||||
|
|
||||||
# Install openswoole, pcov and mssql 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 openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
|
||||||
docker-php-ext-enable openswoole 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 openswoole port
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD \
|
|
||||||
# Install dependencies if the vendor dir does not exist
|
|
||||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
|
||||||
# When restarting the container, openswoole might think it is already in execution
|
|
||||||
# This forces the app to be started every second until the exit code is 0
|
|
||||||
until php ./vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
|
|
||||||
error_log /home/shlink/www/data/infra/nginx/swoole_proxy.error.log;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_pass http://shlink_swoole:8080;
|
|
||||||
proxy_read_timeout 90s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,12 +7,6 @@ services:
|
|||||||
- /etc/passwd:/etc/passwd:ro
|
- /etc/passwd:/etc/passwd:ro
|
||||||
- /etc/group:/etc/group:ro
|
- /etc/group:/etc/group:ro
|
||||||
|
|
||||||
shlink_swoole:
|
|
||||||
user: 1000:1000
|
|
||||||
volumes:
|
|
||||||
- /etc/passwd:/etc/passwd:ro
|
|
||||||
- /etc/group:/etc/group:ro
|
|
||||||
|
|
||||||
shlink_roadrunner:
|
shlink_roadrunner:
|
||||||
user: 1000:1000
|
user: 1000:1000
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -39,44 +39,6 @@ services:
|
|||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'host.docker.internal:host-gateway'
|
- 'host.docker.internal:host-gateway'
|
||||||
|
|
||||||
shlink_swoole_proxy:
|
|
||||||
container_name: shlink_swoole_proxy
|
|
||||||
image: nginx:1.25-alpine
|
|
||||||
ports:
|
|
||||||
- "8002:80"
|
|
||||||
volumes:
|
|
||||||
- ./:/home/shlink/www
|
|
||||||
- ./data/infra/swoole_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
|
|
||||||
links:
|
|
||||||
- shlink_swoole
|
|
||||||
|
|
||||||
shlink_swoole:
|
|
||||||
container_name: shlink_swoole
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./data/infra/swoole.Dockerfile
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
- "9001:9001"
|
|
||||||
volumes:
|
|
||||||
- ./:/home/shlink
|
|
||||||
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
|
|
||||||
links:
|
|
||||||
- shlink_db_mysql
|
|
||||||
- shlink_db_postgres
|
|
||||||
- shlink_db_maria
|
|
||||||
- shlink_db_ms
|
|
||||||
- shlink_redis
|
|
||||||
- shlink_redis_acl
|
|
||||||
- shlink_mercure
|
|
||||||
- shlink_mercure_proxy
|
|
||||||
- shlink_rabbitmq
|
|
||||||
- shlink_matomo
|
|
||||||
environment:
|
|
||||||
LC_ALL: C
|
|
||||||
extra_hosts:
|
|
||||||
- 'host.docker.internal:host-gateway'
|
|
||||||
|
|
||||||
shlink_roadrunner:
|
shlink_roadrunner:
|
||||||
container_name: shlink_roadrunner
|
container_name: shlink_roadrunner
|
||||||
build:
|
build:
|
||||||
@@ -119,7 +81,7 @@ services:
|
|||||||
container_name: shlink_db_postgres
|
container_name: shlink_db_postgres
|
||||||
image: postgres:12.2-alpine
|
image: postgres:12.2-alpine
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5434:5432"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/shlink/www
|
- ./:/home/shlink/www
|
||||||
- ./data/infra/database_pg:/var/lib/postgresql/data
|
- ./data/infra/database_pg:/var/lib/postgresql/data
|
||||||
@@ -169,7 +131,7 @@ services:
|
|||||||
container_name: shlink_mercure_proxy
|
container_name: shlink_mercure_proxy
|
||||||
image: nginx:1.25-alpine
|
image: nginx:1.25-alpine
|
||||||
ports:
|
ports:
|
||||||
- "8001:80"
|
- "8002:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/home/shlink/www
|
- ./:/home/shlink/www
|
||||||
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
|
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
|
||||||
@@ -191,15 +153,15 @@ services:
|
|||||||
container_name: shlink_rabbitmq
|
container_name: shlink_rabbitmq
|
||||||
image: rabbitmq:3.11-management-alpine
|
image: rabbitmq:3.11-management-alpine
|
||||||
ports:
|
ports:
|
||||||
- "15672:15672"
|
- "15673:15672"
|
||||||
- "5672:5672"
|
- "5673:5672"
|
||||||
environment:
|
environment:
|
||||||
RABBITMQ_DEFAULT_USER: "rabbit"
|
RABBITMQ_DEFAULT_USER: "rabbit"
|
||||||
RABBITMQ_DEFAULT_PASS: "rabbit"
|
RABBITMQ_DEFAULT_PASS: "rabbit"
|
||||||
|
|
||||||
shlink_swagger_ui:
|
shlink_swagger_ui:
|
||||||
container_name: shlink_swagger_ui
|
container_name: shlink_swagger_ui
|
||||||
image: swaggerapi/swagger-ui:v5.10.3
|
image: swaggerapi/swagger-ui:v5.11.3
|
||||||
ports:
|
ports:
|
||||||
- "8005:8080"
|
- "8005:8080"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime.
|
This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime.
|
||||||
|
|
||||||
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev) or [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
|
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev), which can be linked to external databases to persist data.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
@@ -20,19 +20,6 @@ fi
|
|||||||
|
|
||||||
php vendor/bin/shlink-installer init ${flags}
|
php vendor/bin/shlink-installer init ${flags}
|
||||||
|
|
||||||
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
|
if [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||||
# FIXME: ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
|
|
||||||
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
|
|
||||||
echo "Configuring periodic visit location..."
|
|
||||||
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
|
||||||
/usr/sbin/crond &
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHLINK_RUNTIME" = 'openswoole' ]; then
|
|
||||||
# Openswoole is deprecated. Remove in Shlink 4.0.0
|
|
||||||
# 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
|
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -111,9 +111,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The original long URL."
|
"description": "The original long URL."
|
||||||
},
|
},
|
||||||
"deviceLongUrls": {
|
|
||||||
"$ref": "#/components/schemas/DeviceLongUrls"
|
|
||||||
},
|
|
||||||
"dateCreated": {
|
"dateCreated": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
@@ -122,11 +119,6 @@
|
|||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"$ref": "#/components/schemas/VisitsSummary"
|
"$ref": "#/components/schemas/VisitsSummary"
|
||||||
},
|
},
|
||||||
"visitsCount": {
|
|
||||||
"deprecated": true,
|
|
||||||
"type": "integer",
|
|
||||||
"description": "The number of visits that this short URL has received."
|
|
||||||
},
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -155,11 +147,6 @@
|
|||||||
"shortCode": "12C18",
|
"shortCode": "12C18",
|
||||||
"shortUrl": "https://s.test/12C18",
|
"shortUrl": "https://s.test/12C18",
|
||||||
"longUrl": "https://store.steampowered.com",
|
"longUrl": "https://store.steampowered.com",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": "https://store.steampowered.com/android",
|
|
||||||
"ios": "https://store.steampowered.com/ios",
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 328,
|
"total": 328,
|
||||||
@@ -223,24 +210,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DeviceLongUrls": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["android", "ios", "desktop"],
|
|
||||||
"properties": {
|
|
||||||
"android": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"desktop": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Visit": {
|
"Visit": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"android": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
|
||||||
"type": ["string"]
|
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
|
||||||
"type": ["string"]
|
|
||||||
},
|
|
||||||
"desktop": {
|
|
||||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
|
||||||
"type": ["string"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"allOf": [{
|
|
||||||
"$ref": "./DeviceLongUrls.json"
|
|
||||||
}],
|
|
||||||
"properties": {
|
|
||||||
"android": {
|
|
||||||
"type": ["null"]
|
|
||||||
},
|
|
||||||
"ios": {
|
|
||||||
"type": ["null"]
|
|
||||||
},
|
|
||||||
"desktop": {
|
|
||||||
"type": ["null"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"required": ["android", "ios", "desktop"],
|
|
||||||
"allOf": [{
|
|
||||||
"$ref": "./DeviceLongUrlsEdit.json"
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
31
docs/swagger/definitions/SetShortUrlRedirectRule.json
Normal file
31
docs/swagger/definitions/SetShortUrlRedirectRule.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["longUrl", "conditions"],
|
||||||
|
"properties": {
|
||||||
|
"longUrl": {
|
||||||
|
"description": "Long URL to redirect to when this condition matches",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"conditions": {
|
||||||
|
"description": "List of conditions that need to match in order to consider this rule matches",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type", "matchKey", "matchValue"],
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["device", "language", "query"],
|
||||||
|
"description": "The type of the condition, which will condition the logic used to match it"
|
||||||
|
},
|
||||||
|
"matchKey": {
|
||||||
|
"type": ["string", "null"]
|
||||||
|
},
|
||||||
|
"matchValue": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,9 +4,7 @@
|
|||||||
"shortCode",
|
"shortCode",
|
||||||
"shortUrl",
|
"shortUrl",
|
||||||
"longUrl",
|
"longUrl",
|
||||||
"deviceLongUrls",
|
|
||||||
"dateCreated",
|
"dateCreated",
|
||||||
"visitsCount",
|
|
||||||
"visitsSummary",
|
"visitsSummary",
|
||||||
"tags",
|
"tags",
|
||||||
"meta",
|
"meta",
|
||||||
@@ -28,19 +26,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The original long URL."
|
"description": "The original long URL."
|
||||||
},
|
},
|
||||||
"deviceLongUrls": {
|
|
||||||
"$ref": "./DeviceLongUrlsResp.json"
|
|
||||||
},
|
|
||||||
"dateCreated": {
|
"dateCreated": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"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": {
|
|
||||||
"deprecated": true,
|
|
||||||
"type": "integer",
|
|
||||||
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
|
|
||||||
},
|
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"$ref": "./VisitsSummary.json"
|
"$ref": "./VisitsSummary.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
"description": "The long URL this short URL will redirect to",
|
"description": "The long URL this short URL will redirect to",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"deviceLongUrls": {
|
|
||||||
"$ref": "./DeviceLongUrlsEdit.json"
|
|
||||||
},
|
|
||||||
"validSince": {
|
"validSince": {
|
||||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||||
"type": ["string", "null"]
|
"type": ["string", "null"]
|
||||||
@@ -20,11 +17,6 @@
|
|||||||
"description": "The maximum number of allowed visits for this short code",
|
"description": "The maximum number of allowed visits for this short code",
|
||||||
"type": ["number", "null"]
|
"type": ["number", "null"]
|
||||||
},
|
},
|
||||||
"validateUrl": {
|
|
||||||
"deprecated": true,
|
|
||||||
"description": "**[DEPRECATED]** Tells if the long URL should or should not be validated as a reachable URL. Defaults to `false`",
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|||||||
13
docs/swagger/definitions/ShortUrlRedirectRule.json
Normal file
13
docs/swagger/definitions/ShortUrlRedirectRule.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["priority"],
|
||||||
|
"properties": {
|
||||||
|
"priority": {
|
||||||
|
"description": "Order in which attempting to match the rule. Lower goes first",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [{
|
||||||
|
"$ref": "./SetShortUrlRedirectRule.json"
|
||||||
|
}]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["tag", "shortUrlsCount", "visitsSummary", "visitsCount"],
|
"required": ["tag", "shortUrlsCount", "visitsSummary"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"tag": {
|
"tag": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -12,11 +12,6 @@
|
|||||||
},
|
},
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"$ref": "./VisitsSummary.json"
|
"$ref": "./VisitsSummary.json"
|
||||||
},
|
|
||||||
"visitsCount": {
|
|
||||||
"deprecated": true,
|
|
||||||
"type": "number",
|
|
||||||
"description": "**[DEPRECATED]** Use visitsSummary.total instead"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["nonOrphanVisits", "orphanVisits", "visitsCount", "orphanVisitsCount"],
|
"required": ["nonOrphanVisits", "orphanVisits"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"nonOrphanVisits": {
|
"nonOrphanVisits": {
|
||||||
"$ref": "./VisitsSummary.json"
|
"$ref": "./VisitsSummary.json"
|
||||||
},
|
},
|
||||||
"orphanVisits": {
|
"orphanVisits": {
|
||||||
"$ref": "./VisitsSummary.json"
|
"$ref": "./VisitsSummary.json"
|
||||||
},
|
|
||||||
"visitsCount": {
|
|
||||||
"deprecated": true,
|
|
||||||
"type": "number",
|
|
||||||
"description": "**[DEPRECATED]** Use nonOrphanVisits.total instead"
|
|
||||||
},
|
|
||||||
"orphanVisitsCount": {
|
|
||||||
"deprecated": true,
|
|
||||||
"type": "number",
|
|
||||||
"description": "**[DEPRECATED]** Use orphanVisits.total instead"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"value": {
|
|
||||||
"title": "Invalid data",
|
|
||||||
"type": "INVALID_ARGUMENT",
|
|
||||||
"detail": "Provided data is not valid",
|
|
||||||
"status": 400,
|
|
||||||
"invalidElements": ["maxVisits", "validSince"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"value": {
|
|
||||||
"detail": "No URL found with short code \"abc123\"",
|
|
||||||
"title": "Short URL not found",
|
|
||||||
"type": "INVALID_SHORTCODE",
|
|
||||||
"status": 404,
|
|
||||||
"shortCode": "abc123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"value": {
|
|
||||||
"detail": "Tag with name \"foo\" could not be found",
|
|
||||||
"title": "Tag not found",
|
|
||||||
"type": "TAG_NOT_FOUND",
|
|
||||||
"status": 404,
|
|
||||||
"tag": "foo"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -163,11 +163,6 @@
|
|||||||
"shortCode": "12C18",
|
"shortCode": "12C18",
|
||||||
"shortUrl": "https://s.test/12C18",
|
"shortUrl": "https://s.test/12C18",
|
||||||
"longUrl": "https://store.steampowered.com",
|
"longUrl": "https://store.steampowered.com",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 328,
|
"total": 328,
|
||||||
@@ -191,11 +186,6 @@
|
|||||||
"shortCode": "12Kb3",
|
"shortCode": "12Kb3",
|
||||||
"shortUrl": "https://s.test/12Kb3",
|
"shortUrl": "https://s.test/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": "https://shlink.io/ios",
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 1029,
|
"total": 1029,
|
||||||
@@ -218,11 +208,6 @@
|
|||||||
"shortCode": "123bA",
|
"shortCode": "123bA",
|
||||||
"shortUrl": "https://example.com/123bA",
|
"shortUrl": "https://example.com/123bA",
|
||||||
"longUrl": "https://www.google.com",
|
"longUrl": "https://www.google.com",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 25,
|
"total": 25,
|
||||||
@@ -296,13 +281,14 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["longUrl"],
|
"required": ["longUrl"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"deviceLongUrls": {
|
|
||||||
"$ref": "../definitions/DeviceLongUrls.json"
|
|
||||||
},
|
|
||||||
"customSlug": {
|
"customSlug": {
|
||||||
"description": "A unique custom slug to be used instead of the generated short code",
|
"description": "A unique custom slug to be used instead of the generated short code",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"pathPrefix": {
|
||||||
|
"description": "A prefix that will be prepended to provided custom slug or auto-generated short code",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"findIfExists": {
|
"findIfExists": {
|
||||||
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
|
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@@ -334,11 +320,6 @@
|
|||||||
"shortCode": "12C18",
|
"shortCode": "12C18",
|
||||||
"shortUrl": "https://s.test/12C18",
|
"shortUrl": "https://s.test/12C18",
|
||||||
"longUrl": "https://store.steampowered.com",
|
"longUrl": "https://store.steampowered.com",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 0,
|
"total": 0,
|
||||||
@@ -382,16 +363,13 @@
|
|||||||
"validSince",
|
"validSince",
|
||||||
"validUntil",
|
"validUntil",
|
||||||
"customSlug",
|
"customSlug",
|
||||||
|
"pathPrefix",
|
||||||
"maxVisits",
|
"maxVisits",
|
||||||
"findIfExists",
|
"findIfExists",
|
||||||
"domain"
|
"domain"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"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 https://shlink.io/api/error/non-unique-slug"
|
"description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug"
|
||||||
@@ -405,19 +383,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Invalid arguments with API v3 and newer": {
|
"Invalid arguments": {
|
||||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||||
},
|
},
|
||||||
"Invalid long URL with API v3 and newer": {
|
"Non-unique slug": {
|
||||||
"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": {
|
"value": {
|
||||||
"title": "Invalid custom slug",
|
"title": "Invalid custom slug",
|
||||||
"type": "https://shlink.io/api/error/non-unique-slug",
|
"type": "https://shlink.io/api/error/non-unique-slug",
|
||||||
@@ -425,27 +394,6 @@
|
|||||||
"status": 400,
|
"status": 400,
|
||||||
"customSlug": "my-slug"
|
"customSlug": "my-slug"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Invalid arguments previous to API v3": {
|
|
||||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
|
||||||
},
|
|
||||||
"Invalid long URL previous to API v3": {
|
|
||||||
"value": {
|
|
||||||
"title": "Invalid URL",
|
|
||||||
"type": "INVALID_URL",
|
|
||||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
|
||||||
"status": 400,
|
|
||||||
"url": "https://invalid-url.com"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Non-unique slug previous to API v3": {
|
|
||||||
"value": {
|
|
||||||
"title": "Invalid custom slug",
|
|
||||||
"type": "INVALID_SLUG",
|
|
||||||
"detail": "Provided slug \"my-slug\" is already in use.",
|
|
||||||
"status": 400,
|
|
||||||
"customSlug": "my-slug"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,11 +53,6 @@
|
|||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
"longUrl": "https://github.com/shlinkio/shlink",
|
"longUrl": "https://github.com/shlinkio/shlink",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"shortUrl": "https://s.test/abc123",
|
"shortUrl": "https://s.test/abc123",
|
||||||
"shortCode": "abc123",
|
"shortCode": "abc123",
|
||||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
@@ -88,49 +83,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
|
||||||
"description": "The long URL was not provided or is invalid.",
|
|
||||||
"content": {
|
|
||||||
"application/problem+json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "../definitions/Error.json"
|
|
||||||
},
|
|
||||||
"examples": {
|
|
||||||
"API v3 and newer": {
|
|
||||||
"value": {
|
|
||||||
"title": "Invalid URL",
|
|
||||||
"type": "https://shlink.io/api/error/invalid-url",
|
|
||||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
|
||||||
"status": 400,
|
|
||||||
"url": "https://invalid-url.com"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"value": {
|
|
||||||
"title": "Invalid URL",
|
|
||||||
"type": "INVALID_URL",
|
|
||||||
"detail": "Provided URL foo is invalid. Try with a different one.",
|
|
||||||
"status": 400,
|
|
||||||
"url": "https://invalid-url.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text/plain": {
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"examples": {
|
|
||||||
"API v3 and newer": {
|
|
||||||
"value": "https://shlink.io/api/error/invalid-url"
|
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"value": "INVALID_URL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"default": {
|
"default": {
|
||||||
"description": "Unexpected error.",
|
"description": "Unexpected error.",
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
@@ -34,11 +34,6 @@
|
|||||||
"shortCode": "12Kb3",
|
"shortCode": "12Kb3",
|
||||||
"shortUrl": "https://s.test/12Kb3",
|
"shortUrl": "https://s.test/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": null,
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 1029,
|
"total": 1029,
|
||||||
@@ -86,11 +81,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"API v3 and newer": {
|
"Short URL not found": {
|
||||||
"$ref": "../examples/short-url-not-found-v3.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/short-url-not-found-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,11 +147,6 @@
|
|||||||
"shortCode": "12Kb3",
|
"shortCode": "12Kb3",
|
||||||
"shortUrl": "https://s.test/12Kb3",
|
"shortUrl": "https://s.test/12Kb3",
|
||||||
"longUrl": "https://shlink.io",
|
"longUrl": "https://shlink.io",
|
||||||
"deviceLongUrls": {
|
|
||||||
"android": "https://shlink.io/android",
|
|
||||||
"ios": null,
|
|
||||||
"desktop": null
|
|
||||||
},
|
|
||||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||||
"visitsSummary": {
|
"visitsSummary": {
|
||||||
"total": 1029,
|
"total": 1029,
|
||||||
@@ -212,11 +199,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"API v3 and newer": {
|
"Invalid arguments": {
|
||||||
"$ref": "../examples/short-url-invalid-args-v3.json"
|
"$ref": "../examples/short-url-invalid-args-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/short-url-invalid-args-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,11 +232,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"API v3 and newer": {
|
"Short URL not found": {
|
||||||
"$ref": "../examples/short-url-not-found-v3.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/short-url-not-found-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,11 +359,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"API v3 and newer": {
|
"Short URL not found": {
|
||||||
"$ref": "../examples/short-url-not-found-v3.json"
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/short-url-not-found-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,11 +145,8 @@
|
|||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Short URL not found with API v3 and newer": {
|
"Short URL not found": {
|
||||||
"$ref": "../examples/short-url-not-found-v3.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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,11 +216,8 @@
|
|||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
"Short URL not found with API v3 and newer": {
|
"Short URL not found": {
|
||||||
"$ref": "../examples/short-url-not-found-v3.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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,20 +15,6 @@
|
|||||||
{
|
{
|
||||||
"$ref": "../parameters/version.json"
|
"$ref": "../parameters/version.json"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "withStats",
|
|
||||||
"deprecated": true,
|
|
||||||
"description": "**[Deprecated]** Use [GET /tags/stats](#/Tags/tagsWithStats) endpoint to get tags with their stats.",
|
|
||||||
"in": "query",
|
|
||||||
"required": false,
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"true",
|
|
||||||
"false"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "page",
|
"name": "page",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
@@ -88,13 +74,6 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
|
||||||
"description": "The tag stats will be returned only if the withStats param was provided with value 'true'",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "../definitions/TagInfo.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pagination": {
|
"pagination": {
|
||||||
"$ref": "../definitions/Pagination.json"
|
"$ref": "../definitions/Pagination.json"
|
||||||
}
|
}
|
||||||
@@ -249,9 +228,6 @@
|
|||||||
"examples": {
|
"examples": {
|
||||||
"API v3 and newer": {
|
"API v3 and newer": {
|
||||||
"$ref": "../examples/tag-not-found-v3.json"
|
"$ref": "../examples/tag-not-found-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/tag-not-found-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,12 +148,8 @@
|
|||||||
"$ref": "../definitions/Error.json"
|
"$ref": "../definitions/Error.json"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": {
|
||||||
|
"Tag not found": {
|
||||||
"API v3 and newer": {
|
|
||||||
"$ref": "../examples/tag-not-found-v3.json"
|
"$ref": "../examples/tag-not-found-v3.json"
|
||||||
},
|
|
||||||
"Previous to API v3": {
|
|
||||||
"$ref": "../examples/tag-not-found-v2.json"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["true"]
|
"enum": ["true"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The type of visits to return. All visits are returned when not provided.",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["invalid_short_url", "base_url", "regular_404"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
@@ -137,6 +147,54 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Provided query arguments are invalid.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["invalidElements"],
|
||||||
|
"properties": {
|
||||||
|
"invalidElements": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["type"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"API v3 and newer": {
|
||||||
|
"value": {
|
||||||
|
"title": "Invalid data",
|
||||||
|
"type": "https://shlink.io/api/error/invalid-data",
|
||||||
|
"detail": "Provided data is not valid",
|
||||||
|
"status": 400,
|
||||||
|
"invalidElements": ["type"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Previous to API v3": {
|
||||||
|
"value": {
|
||||||
|
"title": "Invalid data",
|
||||||
|
"type": "INVALID_ARGUMENT",
|
||||||
|
"detail": "Provided data is not valid",
|
||||||
|
"status": 400,
|
||||||
|
"invalidElements": ["type"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"default": {
|
"default": {
|
||||||
"description": "Unexpected error.",
|
"description": "Unexpected error.",
|
||||||
"content": {
|
"content": {
|
||||||
|
|||||||
344
docs/swagger/paths/v3_short-urls_{shortCode}_redirect-rules.json
Normal file
344
docs/swagger/paths/v3_short-urls_{shortCode}_redirect-rules.json
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
{
|
||||||
|
"get": {
|
||||||
|
"operationId": "listShortUrlRedirectRules",
|
||||||
|
"tags": [
|
||||||
|
"Redirect rules"
|
||||||
|
],
|
||||||
|
"summary": "List short URL redirect rules",
|
||||||
|
"description": "Returns the list of redirect rules for a short URL.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/version.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/shortCode.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/domain.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The list of rules",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["defaultLongUrl", "redirectRules"],
|
||||||
|
"properties": {
|
||||||
|
"defaultLongUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"redirectRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "../definitions/ShortUrlRedirectRule.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"defaultLongUrl": "https://example.com",
|
||||||
|
"redirectRules": [
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/android-en-us",
|
||||||
|
"priority": 1,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "device",
|
||||||
|
"matchValue": "android",
|
||||||
|
"matchKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "en-US",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/fr",
|
||||||
|
"priority": 2,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "fr",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/query-foo-bar-hello-world",
|
||||||
|
"priority": 3,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "foo",
|
||||||
|
"matchValue": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "hello",
|
||||||
|
"matchValue": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "No URL was found for provided short code.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["shortCode"],
|
||||||
|
"properties": {
|
||||||
|
"shortCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The short code with which we tried to find the short URL"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain with which we tried to find the short URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"Short URL not found": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "Unexpected error.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"post": {
|
||||||
|
"operationId": "setShortUrlRedirectRules",
|
||||||
|
"tags": [
|
||||||
|
"Redirect rules"
|
||||||
|
],
|
||||||
|
"summary": "Set short URL redirect rules",
|
||||||
|
"description": "Sets redirect rules for a short URL, with priorities matching the order in which they are provided.",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/version.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/shortCode.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "../parameters/domain.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"description": "Request body.",
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"redirectRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "../definitions/SetShortUrlRedirectRule.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"redirectRules": [
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/android-en-us",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "device",
|
||||||
|
"matchValue": "android",
|
||||||
|
"matchKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "en-US",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/fr",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "fr",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/query-foo-bar-hello-world",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "foo",
|
||||||
|
"matchValue": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "hello",
|
||||||
|
"matchValue": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "The list of rules",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["defaultLongUrl", "redirectRules"],
|
||||||
|
"properties": {
|
||||||
|
"defaultLongUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"redirectRules": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "../definitions/ShortUrlRedirectRule.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"defaultLongUrl": "https://example.com",
|
||||||
|
"redirectRules": [
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/android-en-us",
|
||||||
|
"priority": 1,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "device",
|
||||||
|
"matchValue": "android",
|
||||||
|
"matchKey": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "en-US",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/fr",
|
||||||
|
"priority": 2,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "language",
|
||||||
|
"matchValue": "fr",
|
||||||
|
"matchKey": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"longUrl": "https://example.com/query-foo-bar-hello-world",
|
||||||
|
"priority": 3,
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "foo",
|
||||||
|
"matchValue": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "query",
|
||||||
|
"matchKey": "hello",
|
||||||
|
"matchValue": "world"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "No URL was found for provided short code.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["shortCode"],
|
||||||
|
"properties": {
|
||||||
|
"shortCode": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The short code with which we tried to find the short URL"
|
||||||
|
},
|
||||||
|
"domain": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The domain with which we tried to find the short URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"Short URL not found": {
|
||||||
|
"$ref": "../examples/short-url-not-found-v3.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"description": "Unexpected error.",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "../definitions/Error.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,6 +42,10 @@
|
|||||||
"name": "Short URLs",
|
"name": "Short URLs",
|
||||||
"description": "Operations that can be performed on short URLs"
|
"description": "Operations that can be performed on short URLs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Redirect rules",
|
||||||
|
"description": "Handle dynamic rule-based redirects"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Tags",
|
"name": "Tags",
|
||||||
"description": "Let you handle the list of available tags"
|
"description": "Let you handle the list of available tags"
|
||||||
@@ -79,6 +83,10 @@
|
|||||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"/rest/v{version}/short-urls/{shortCode}/redirect-rules": {
|
||||||
|
"$ref": "paths/v3_short-urls_{shortCode}_redirect-rules.json"
|
||||||
|
},
|
||||||
|
|
||||||
"/rest/v{version}/tags": {
|
"/rest/v{version}/tags": {
|
||||||
"$ref": "paths/v1_tags.json"
|
"$ref": "paths/v1_tags.json"
|
||||||
},
|
},
|
||||||
|
|||||||
4
indocker
4
indocker
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Run docker containers if they are not up yet
|
# Run docker containers if they are not up yet
|
||||||
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
|
if ! [[ $(docker ps | grep shlink_roadrunner) ]]; then
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker exec -it shlink_swoole /bin/sh -c "$*"
|
docker exec -it shlink_roadrunner /bin/sh -c "$*"
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,9 @@ return [
|
|||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||||
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
||||||
|
|
||||||
|
Command\RedirectRule\ManageRedirectRulesCommand::NAME =>
|
||||||
|
Command\RedirectRule\ManageRedirectRulesCommand::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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\RedirectRule\ShortUrlRedirectRuleService;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
use Shlinkio\Shlink\Core\Tag\TagService;
|
use Shlinkio\Shlink\Core\Tag\TagService;
|
||||||
@@ -33,6 +34,7 @@ return [
|
|||||||
PhpExecutableFinder::class => InvokableFactory::class,
|
PhpExecutableFinder::class => InvokableFactory::class,
|
||||||
|
|
||||||
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
|
||||||
|
RedirectRule\RedirectRuleHandler::class => InvokableFactory::class,
|
||||||
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
Util\ProcessRunner::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
||||||
@@ -66,6 +68,8 @@ return [
|
|||||||
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
|
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class,
|
Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
|
Command\RedirectRule\ManageRedirectRulesCommand::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -117,6 +121,12 @@ return [
|
|||||||
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
||||||
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
||||||
|
|
||||||
|
Command\RedirectRule\ManageRedirectRulesCommand::class => [
|
||||||
|
ShortUrl\ShortUrlResolver::class,
|
||||||
|
ShortUrlRedirectRuleService::class,
|
||||||
|
RedirectRule\RedirectRuleHandler::class,
|
||||||
|
],
|
||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::class => [
|
Command\Db\CreateDatabaseCommand::class => [
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
Util\ProcessRunner::class,
|
Util\ProcessRunner::class,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class DisableKeyCommand extends Command
|
|||||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
|
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$apiKey = $input->getArgument('apiKey');
|
$apiKey = $input->getArgument('apiKey');
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class GenerateKeyCommand extends Command
|
|||||||
->setHelp($help);
|
->setHelp($help);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$expirationDate = $input->getOption('expiration-date');
|
$expirationDate = $input->getOption('expiration-date');
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class InitialApiKeyCommand extends Command
|
|||||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
|
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$key = $input->getArgument('apiKey');
|
$key = $input->getArgument('apiKey');
|
||||||
$result = $this->apiKeyService->createInitial($key);
|
$result = $this->apiKeyService->createInitial($key);
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class ListKeysCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$enabledOnly = $input->getOption('enabled-only');
|
$enabledOnly = $input->getOption('enabled-only');
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class DomainRedirectsCommand extends Command
|
|||||||
$input->setArgument('domain', str_contains($selectedOption, 'New domain') ? $askNewDomain() : $selectedOption);
|
$input->setArgument('domain', str_contains($selectedOption, 'New domain') ? $askNewDomain() : $selectedOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
$domainAuthority = $input->getArgument('domain');
|
$domainAuthority = $input->getArgument('domain');
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ListDomainsCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$domains = $this->domainService->listDomains();
|
$domains = $this->domainService->listDomains();
|
||||||
$showRedirects = $input->getOption('show-redirects');
|
$showRedirects = $input->getOption('show-redirects');
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\RedirectRule;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||||
|
use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandlerInterface;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
|
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class ManageRedirectRulesCommand extends Command
|
||||||
|
{
|
||||||
|
public const NAME = 'short-url:manage-rules';
|
||||||
|
|
||||||
|
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected readonly ShortUrlResolverInterface $shortUrlResolver,
|
||||||
|
protected readonly ShortUrlRedirectRuleServiceInterface $ruleService,
|
||||||
|
protected readonly RedirectRuleHandlerInterface $ruleHandler,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||||
|
$this,
|
||||||
|
shortCodeDesc: 'The short code which rules we want to set.',
|
||||||
|
domainDesc: 'The domain for the short code.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Set redirect rules for a short URL');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$shortUrl = $this->shortUrlResolver->resolveShortUrl($identifier);
|
||||||
|
} catch (ShortUrlNotFoundException) {
|
||||||
|
$io->error(sprintf('Short URL for %s not found', $identifier->__toString()));
|
||||||
|
return ExitCode::EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rulesToSave = $this->ruleHandler->manageRules($io, $shortUrl, $this->ruleService->rulesForShortUrl($shortUrl));
|
||||||
|
if ($rulesToSave !== null) {
|
||||||
|
$this->ruleService->saveRulesForShortUrl($shortUrl, $rulesToSave);
|
||||||
|
$io->success('Rules properly saved');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitCode::EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
@@ -71,6 +70,12 @@ class CreateShortUrlCommand extends Command
|
|||||||
InputOption::VALUE_REQUIRED,
|
InputOption::VALUE_REQUIRED,
|
||||||
'If provided, this slug will be used instead of generating a short code',
|
'If provided, this slug will be used instead of generating a short code',
|
||||||
)
|
)
|
||||||
|
->addOption(
|
||||||
|
'path-prefix',
|
||||||
|
'p',
|
||||||
|
InputOption::VALUE_REQUIRED,
|
||||||
|
'Prefix to prepend before the generated short code or provided custom slug',
|
||||||
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'max-visits',
|
'max-visits',
|
||||||
'm',
|
'm',
|
||||||
@@ -95,12 +100,6 @@ class CreateShortUrlCommand extends Command
|
|||||||
InputOption::VALUE_REQUIRED,
|
InputOption::VALUE_REQUIRED,
|
||||||
'The length for generated short code (it will be ignored if --custom-slug was provided).',
|
'The length for generated short code (it will be ignored if --custom-slug was provided).',
|
||||||
)
|
)
|
||||||
->addOption(
|
|
||||||
'validate-url',
|
|
||||||
null,
|
|
||||||
InputOption::VALUE_NONE,
|
|
||||||
'[DEPRECATED] Makes the URL to be validated as publicly accessible.',
|
|
||||||
)
|
|
||||||
->addOption(
|
->addOption(
|
||||||
'crawlable',
|
'crawlable',
|
||||||
'r',
|
'r',
|
||||||
@@ -134,7 +133,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$io = $this->getIO($input, $output);
|
$io = $this->getIO($input, $output);
|
||||||
$longUrl = $input->getArgument('longUrl');
|
$longUrl = $input->getArgument('longUrl');
|
||||||
@@ -145,22 +144,20 @@ class CreateShortUrlCommand extends Command
|
|||||||
|
|
||||||
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
|
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
|
||||||
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||||
$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');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
|
$result = $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'),
|
||||||
ShortUrlInputFilter::CUSTOM_SLUG => $customSlug,
|
|
||||||
ShortUrlInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
|
ShortUrlInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
|
||||||
|
ShortUrlInputFilter::CUSTOM_SLUG => $input->getOption('custom-slug'),
|
||||||
|
ShortUrlInputFilter::PATH_PREFIX => $input->getOption('path-prefix'),
|
||||||
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'),
|
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'),
|
||||||
ShortUrlInputFilter::DOMAIN => $input->getOption('domain'),
|
ShortUrlInputFilter::DOMAIN => $input->getOption('domain'),
|
||||||
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||||
ShortUrlInputFilter::VALIDATE_URL => $doValidateUrl,
|
|
||||||
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'),
|
||||||
@@ -176,7 +173,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
|
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
|
||||||
]);
|
]);
|
||||||
return ExitCode::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (InvalidUrlException | NonUniqueSlugException $e) {
|
} catch (NonUniqueSlugException $e) {
|
||||||
$io->error($e->getMessage());
|
$io->error($e->getMessage());
|
||||||
return ExitCode::EXIT_FAILURE;
|
return ExitCode::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
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\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;
|
||||||
@@ -21,9 +21,16 @@ class DeleteShortUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:delete';
|
public const NAME = 'short-url:delete';
|
||||||
|
|
||||||
|
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||||
|
|
||||||
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
|
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||||
|
$this,
|
||||||
|
shortCodeDesc: 'The short code for the short URL to be deleted',
|
||||||
|
domainDesc: 'The domain if the short code does not belong to the default one',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -31,26 +38,19 @@ class DeleteShortUrlCommand extends Command
|
|||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Deletes a short URL')
|
->setDescription('Deletes a short URL')
|
||||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
|
|
||||||
->addOption(
|
->addOption(
|
||||||
'ignore-threshold',
|
'ignore-threshold',
|
||||||
'i',
|
'i',
|
||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||||
. 'accidentally deleted',
|
. 'accidentally deleted',
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'domain',
|
|
||||||
'd',
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'The domain if the short code does not belong to the default one',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||||
$ignoreThreshold = $input->getOption('ignore-threshold');
|
$ignoreThreshold = $input->getOption('ignore-threshold');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||||
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\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
@@ -20,32 +18,28 @@ class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:visits-delete';
|
public const NAME = 'short-url:visits-delete';
|
||||||
|
|
||||||
|
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||||
|
|
||||||
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
|
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||||
|
$this,
|
||||||
|
shortCodeDesc: 'The short code for the short URL which visits will be deleted',
|
||||||
|
domainDesc: 'The domain if the short code does not belong to the default one',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Deletes visits from a short URL')
|
->setDescription('Deletes visits from a short URL');
|
||||||
->addArgument(
|
|
||||||
'shortCode',
|
|
||||||
InputArgument::REQUIRED,
|
|
||||||
'The short code for the short URL which visits will be deleted',
|
|
||||||
)
|
|
||||||
->addOption(
|
|
||||||
'domain',
|
|
||||||
'd',
|
|
||||||
InputOption::VALUE_REQUIRED,
|
|
||||||
'The domain if the short code does not belong to the default one',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
|
protected function doExecute(InputInterface $input, SymfonyStyle $io): int
|
||||||
{
|
{
|
||||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||||
try {
|
try {
|
||||||
$result = $this->deleter->deleteShortUrlVisits($identifier);
|
$result = $this->deleter->deleteShortUrlVisits($identifier);
|
||||||
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
|
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||||
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\ShortUrl\Model\ShortUrlIdentifier;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
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\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
@@ -20,18 +18,23 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:visits';
|
public const NAME = 'short-url:visits';
|
||||||
|
|
||||||
|
private ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Returns the detailed visits information for provided short code')
|
->setDescription('Returns the detailed visits information for provided short code');
|
||||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.')
|
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.');
|
$this,
|
||||||
|
shortCodeDesc: 'The short code which visits we want to get.',
|
||||||
|
domainDesc: 'The domain for the short code.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$shortCode = $input->getArgument('shortCode');
|
$shortCode = $this->shortUrlIdentifierInput->shortCode($input);
|
||||||
if (! empty($shortCode)) {
|
if (! empty($shortCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -45,7 +48,7 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
|
|||||||
|
|
||||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
{
|
{
|
||||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||||
return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange));
|
return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class ListShortUrlsCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ class ListShortUrlsCommand extends Command
|
|||||||
'Short URL' => $pickProp('shortUrl'),
|
'Short URL' => $pickProp('shortUrl'),
|
||||||
'Long URL' => $pickProp('longUrl'),
|
'Long URL' => $pickProp('longUrl'),
|
||||||
'Date created' => $pickProp('dateCreated'),
|
'Date created' => $pickProp('dateCreated'),
|
||||||
'Visits count' => $pickProp('visitsCount'),
|
'Visits count' => static fn (array $shortUrl) => $shortUrl['visitsSummary']->total,
|
||||||
];
|
];
|
||||||
if ($input->getOption('show-tags')) {
|
if ($input->getOption('show-tags')) {
|
||||||
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
|
||||||
use Shlinkio\Shlink\Core\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\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
@@ -21,23 +19,28 @@ class ResolveUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:parse';
|
public const NAME = 'short-url:parse';
|
||||||
|
|
||||||
|
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||||
|
|
||||||
public function __construct(private readonly ShortUrlResolverInterface $urlResolver)
|
public function __construct(private readonly ShortUrlResolverInterface $urlResolver)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||||
|
$this,
|
||||||
|
shortCodeDesc: 'The short code to parse',
|
||||||
|
domainDesc: 'The domain to which the short URL is attached.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription('Returns the long URL behind a short code')
|
->setDescription('Returns the long URL behind a short code');
|
||||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse')
|
|
||||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain to which the short URL is attached.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$shortCode = $input->getArgument('shortCode');
|
$shortCode = $this->shortUrlIdentifierInput->shortCode($input);
|
||||||
if (! empty($shortCode)) {
|
if (! empty($shortCode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -49,12 +52,12 @@ class ResolveUrlCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
|
$url = $this->urlResolver->resolveShortUrl($this->shortUrlIdentifierInput->toShortUrlIdentifier($input));
|
||||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||||
return ExitCode::EXIT_SUCCESS;
|
return ExitCode::EXIT_SUCCESS;
|
||||||
} catch (ShortUrlNotFoundException $e) {
|
} catch (ShortUrlNotFoundException $e) {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class DeleteTagsCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
$tagNames = $input->getOption('name');
|
$tagNames = $input->getOption('name');
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user