mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa7969c746 | ||
|
|
aef04af4f0 | ||
|
|
f118ea252c | ||
|
|
d514f39a82 | ||
|
|
e17556a7ae | ||
|
|
d79f11eeb8 | ||
|
|
1ec950ee1e | ||
|
|
14ba9fd6a4 | ||
|
|
83e8801827 | ||
|
|
be822646e4 | ||
|
|
3a4a27a60c | ||
|
|
1773e6ecae | ||
|
|
a8e4b2fceb | ||
|
|
15b53ef43c | ||
|
|
11a4702b10 | ||
|
|
6b15cd6d51 | ||
|
|
00169a5729 | ||
|
|
94702791d9 | ||
|
|
447cccacdf | ||
|
|
0413399102 | ||
|
|
9afc7876c4 | ||
|
|
187c17319a | ||
|
|
7310ecd886 | ||
|
|
620cd92d11 | ||
|
|
f9658c8da1 | ||
|
|
613b1d3045 | ||
|
|
d39711ec51 | ||
|
|
69dcab96f8 | ||
|
|
d76c96ad41 | ||
|
|
133efff2cd | ||
|
|
c10f0db170 | ||
|
|
037cd8a389 | ||
|
|
1d24750f43 | ||
|
|
b52ceaff9a | ||
|
|
6b0b52853c | ||
|
|
64d7ac7093 | ||
|
|
b9ba1246d4 | ||
|
|
7f9dc10f6a | ||
|
|
a1afc90150 | ||
|
|
df94c68e2e | ||
|
|
65ea1e00a6 | ||
|
|
5bccdded8a | ||
|
|
8917ed5c2e | ||
|
|
fabc752398 | ||
|
|
38d8086516 | ||
|
|
ae0ff5f23c | ||
|
|
7c659699f3 | ||
|
|
9e6cdcb838 | ||
|
|
7e2f755dfd | ||
|
|
ce2ed237c7 | ||
|
|
626caa4afa | ||
|
|
f4a7712ded | ||
|
|
bab6a3951e | ||
|
|
f49d98f2ea | ||
|
|
1312ea61f4 | ||
|
|
8d90661d0a | ||
|
|
b6b2530cb6 | ||
|
|
e4f66b7ce6 | ||
|
|
4b52c92e97 | ||
|
|
76c42bc17c | ||
|
|
c4f8da5f02 | ||
|
|
80bdeb280a | ||
|
|
99010b6eae | ||
|
|
b2dabf06bf | ||
|
|
67ae05f473 | ||
|
|
fb4fecf411 | ||
|
|
c855f011d1 | ||
|
|
02717eb2fb | ||
|
|
de70ebe769 | ||
|
|
c6109fd396 | ||
|
|
83584a3175 | ||
|
|
f5dcc52b3b | ||
|
|
1901964de1 | ||
|
|
80e9c2452b | ||
|
|
5ad4b39160 | ||
|
|
89b73a9cfa | ||
|
|
e2d8334d69 | ||
|
|
9b16d7acc0 | ||
|
|
6836840746 | ||
|
|
4084d301ca | ||
|
|
added21b18 | ||
|
|
8cd77391cc | ||
|
|
05ebfccc63 | ||
|
|
cb3a690294 | ||
|
|
194a7b0e57 | ||
|
|
98e4d01feb | ||
|
|
c22e3895b5 | ||
|
|
9a76c19615 | ||
|
|
59fa088975 | ||
|
|
163244f40f | ||
|
|
a89b53af4f | ||
|
|
35508e253d | ||
|
|
e586fec338 | ||
|
|
93fa27bdba | ||
|
|
048856c333 | ||
|
|
986f1162dd | ||
|
|
dc8dfa9f0c | ||
|
|
82e7094f3a | ||
|
|
f0e62004d5 | ||
|
|
bbdbafd8db | ||
|
|
6121efec59 | ||
|
|
4fdbcc25a0 | ||
|
|
ca42425b33 | ||
|
|
ce0f61b66d | ||
|
|
13ee71f351 | ||
|
|
c57494d7cd | ||
|
|
d2e74ab330 | ||
|
|
850dde1a06 | ||
|
|
5e83f301ff | ||
|
|
5e74dd7a6d | ||
|
|
8a273e01e9 | ||
|
|
75f6f8dd18 | ||
|
|
e1cf0c4ea7 | ||
|
|
cc134abd12 | ||
|
|
b7db676cba | ||
|
|
3881996560 | ||
|
|
527d28ad81 | ||
|
|
f2371e8a80 | ||
|
|
fd882834d3 | ||
|
|
f92a720d63 | ||
|
|
d6f58698b7 | ||
|
|
d090260b17 | ||
|
|
cd43c1c01f | ||
|
|
284b28e8d9 | ||
|
|
b50547d868 | ||
|
|
401046fbe5 | ||
|
|
6e82509964 | ||
|
|
ab6fa490e5 | ||
|
|
55e2780f50 | ||
|
|
f4803c675c | ||
|
|
90514c603f | ||
|
|
7f4137e7cc | ||
|
|
071cb9af2b | ||
|
|
6ce1550457 | ||
|
|
8cb5d44dc9 | ||
|
|
1331b3f87c | ||
|
|
ab96297e58 | ||
|
|
c4fd3a74c5 | ||
|
|
da922fb2a7 | ||
|
|
4a05c4be40 | ||
|
|
cef30c8e2d | ||
|
|
8417498f08 | ||
|
|
10e941cea6 | ||
|
|
3d7b1ca799 | ||
|
|
b236354fc7 | ||
|
|
6fbb5a380d | ||
|
|
054eb42613 | ||
|
|
6074f4475d | ||
|
|
7afd3fd6a2 | ||
|
|
7d415e40b2 | ||
|
|
3c89d252d2 | ||
|
|
f678873e9f | ||
|
|
17d37a062a | ||
|
|
14702063f2 | ||
|
|
c599d8a0ed | ||
|
|
207d5adceb | ||
|
|
b4c46ce222 | ||
|
|
6fe269193a | ||
|
|
d948543d5c | ||
|
|
a327e6c0a7 | ||
|
|
fbd35b7974 | ||
|
|
b94a22e6a7 | ||
|
|
63ea9e4a21 | ||
|
|
e028d8ea31 | ||
|
|
457a7a14e5 | ||
|
|
cd387328be | ||
|
|
5524476787 | ||
|
|
78526fb405 | ||
|
|
b2dee43bb0 | ||
|
|
60e9443b12 | ||
|
|
ab8fa52ca4 | ||
|
|
16f64f6247 | ||
|
|
98992c656f | ||
|
|
053e026982 | ||
|
|
74180a4381 | ||
|
|
293725f933 | ||
|
|
c33f8d0ea2 | ||
|
|
0f2cd3cb7f | ||
|
|
2441ac5e77 |
13
.github/ISSUE_TEMPLATE/Bug.yml
vendored
13
.github/ISSUE_TEMPLATE/Bug.yml
vendored
@@ -58,5 +58,14 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
value: '<!-- Provide steps to reproduce the bug. -->'
|
||||
label: Minimum steps to reproduce
|
||||
value: |
|
||||
<!--
|
||||
Simple but detailed way to reproduce the bug:
|
||||
|
||||
* Avoid things like "create a kubernetes cluster", or anything related with cloud providers, as that is rarely the root cause.
|
||||
* Avoid too vague steps or one-liners like "Update from v1 to v2".
|
||||
* Providing the reproduction in the form of a self-contained docker-composer is desirable.
|
||||
|
||||
Failing in any of these will cause the issue to be closed as "not reproducible".
|
||||
-->
|
||||
|
||||
2
.github/actions/ci-setup/action.yml
vendored
2
.github/actions/ci-setup/action.yml
vendored
@@ -44,5 +44,5 @@ runs:
|
||||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.4' && '--ignore-platform-req=php' || '' }}
|
||||
shell: bash
|
||||
|
||||
11
.github/workflows/ci-db-tests.yml
vendored
11
.github/workflows/ci-db-tests.yml
vendored
@@ -10,10 +10,11 @@ on:
|
||||
|
||||
jobs:
|
||||
db-tests:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
@@ -23,7 +24,7 @@ jobs:
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
- name: Start database server
|
||||
if: ${{ inputs.platform != 'sqlite:ci' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_${{ inputs.platform }}
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
@@ -31,12 +32,12 @@ jobs:
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
run: docker compose exec -T shlink_db_ms /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||
- name: Run tests
|
||||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.php-version == '8.2' && inputs.platform == 'sqlite:ci' }}
|
||||
if: ${{ matrix.php-version == '8.3' && inputs.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
|
||||
2
.github/workflows/ci-docker-image-build.yml
vendored
2
.github/workflows/ci-docker-image-build.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
11
.github/workflows/ci-tests.yml
vendored
11
.github/workflows/ci-tests.yml
vendored
@@ -10,20 +10,21 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Start postgres database server
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- name: Start maria database server
|
||||
if: ${{ inputs.test-group == 'cli' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
@@ -33,7 +34,7 @@ jobs:
|
||||
run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.php-version == '8.2' }}
|
||||
if: ${{ matrix.php-version == '8.3' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -24,10 +24,10 @@ on:
|
||||
|
||||
jobs:
|
||||
static-analysis:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
php-version: ['8.3']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -66,10 +66,10 @@ jobs:
|
||||
- api-tests
|
||||
- cli-tests
|
||||
- db-tests
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
php-version: ['8.3']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
delete-artifacts:
|
||||
needs:
|
||||
- upload-coverage
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-docker-image.yml
vendored
2
.github/workflows/publish-docker-image.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
- runtime: 'rr'
|
||||
tag-suffix: 'roadrunner'
|
||||
platforms: 'linux/arm64/v8,linux/amd64'
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-publish-image.yml@main
|
||||
secrets: inherit
|
||||
with:
|
||||
image-name: shlinkio/shlink
|
||||
|
||||
8
.github/workflows/publish-release.yml
vendored
8
.github/workflows/publish-release.yml
vendored
@@ -7,10 +7,10 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3']
|
||||
php-version: ['8.2', '8.3'] # TODO 8.4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
publish:
|
||||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
delete-artifacts:
|
||||
needs: ['publish']
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-swagger-spec.yml
vendored
2
.github/workflows/publish-swagger-spec.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2']
|
||||
|
||||
2213
CHANGELOG.md
2213
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
use Mezzio\Application;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\EventDispatcher\RoadRunner\RoadRunnerTaskConsumerToListener;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
|
||||
@@ -27,6 +28,9 @@ use function Shlinkio\Shlink\Config\env;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks();
|
||||
$requestIdMiddleware = $container->get(RequestIdMiddleware::class);
|
||||
$container->get(RoadRunnerTaskConsumerToListener::class)->listenForTasks(
|
||||
fn (string $requestId) => $requestIdMiddleware->setCurrentRequestId($requestId),
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -22,7 +22,7 @@ echo 'Starting server...'
|
||||
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
|
||||
sleep 2 # Let's give the server a couple of seconds to start
|
||||
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
|
||||
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --testdox-summary $*
|
||||
TESTS_EXIT_CODE=$?
|
||||
|
||||
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -w .
|
||||
|
||||
@@ -16,19 +16,21 @@
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "^3.0.2",
|
||||
"doctrine/dbal": "^4.0",
|
||||
"doctrine/dbal": "^4.1",
|
||||
"doctrine/migrations": "^3.6",
|
||||
"doctrine/orm": "^3.0",
|
||||
"doctrine/orm": "^3.2",
|
||||
"endroid/qr-code": "^5.0",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"hidehalo/nanoid-php": "^1.1",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-config-aggregator": "^1.15",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
@@ -40,20 +42,19 @@
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"pagerfanta/core": "^3.8",
|
||||
"pugx/shortid-php": "^1.1",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/doctrine-specification": "^2.1.1",
|
||||
"shlinkio/shlink-common": "^6.0",
|
||||
"shlinkio/shlink-config": "^3.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^4.0",
|
||||
"shlinkio/shlink-importer": "^5.3",
|
||||
"shlinkio/shlink-installer": "^9.0",
|
||||
"shlinkio/shlink-common": "^6.3",
|
||||
"shlinkio/shlink-config": "^3.2",
|
||||
"shlinkio/shlink-event-dispatcher": "^4.1",
|
||||
"shlinkio/shlink-importer": "^5.3.2",
|
||||
"shlinkio/shlink-installer": "^9.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^4.0",
|
||||
"shlinkio/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2023.3",
|
||||
"spiral/roadrunner": "^2024.1",
|
||||
"spiral/roadrunner-cli": "^2.6",
|
||||
"spiral/roadrunner-http": "^3.3",
|
||||
"spiral/roadrunner-jobs": "^4.3",
|
||||
"spiral/roadrunner-http": "^3.5",
|
||||
"spiral/roadrunner-jobs": "^4.5",
|
||||
"symfony/console": "^7.0",
|
||||
"symfony/filesystem": "^7.0",
|
||||
"symfony/lock": "^7.0",
|
||||
@@ -63,13 +64,13 @@
|
||||
"require-dev": {
|
||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||
"devster/ubench": "^2.1",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-doctrine": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-symfony": "^1.3",
|
||||
"phpunit/php-code-coverage": "^10.1",
|
||||
"phpunit/phpcov": "^9.0",
|
||||
"phpunit/phpunit": "^10.4",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpstan/phpstan-doctrine": "^1.4",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-symfony": "^1.4",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpcov": "^10.0",
|
||||
"phpunit/phpunit": "^11.3",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^4.1",
|
||||
@@ -113,25 +114,29 @@
|
||||
],
|
||||
"cs": "phpcs -s",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config module/*/migrations config docker/config --level=8",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse",
|
||||
"test": [
|
||||
"@parallel test:unit test:db",
|
||||
"@parallel test:api test:cli"
|
||||
],
|
||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
|
||||
"test:unit": "COLUMNS=120 vendor/bin/phpunit --order-by=random --testdox --testdox-summary",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov",
|
||||
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-db.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:maria": "DB_DRIVER=maria 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:mysql": "DB_DRIVER=mysql 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:ms": "DB_DRIVER=mssql composer test:db:sqlite -- $*",
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:api:sqlite": "DB_DRIVER=sqlite composer test:api -- $*",
|
||||
"test:api:mysql": "DB_DRIVER=mysql composer test:api -- $*",
|
||||
"test:api:maria": "DB_DRIVER=maria composer test:api -- $*",
|
||||
"test:api:mssql": "DB_DRIVER=mssql composer test:api -- $*",
|
||||
"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: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: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 --testdox --testdox-summary -c phpunit-cli.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: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",
|
||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||
|
||||
@@ -6,7 +6,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
|
||||
$redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false)];
|
||||
$redis = ['pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv()];
|
||||
$cacheRedisBlock = $redisServers === null ? [] : [
|
||||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
@@ -16,7 +16,7 @@ return (static function (): array {
|
||||
|
||||
return [
|
||||
'cache' => [
|
||||
'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv('Shlink'),
|
||||
'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv(),
|
||||
...$cacheRedisBlock,
|
||||
],
|
||||
'redis' => $redis,
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Events;
|
||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\Visit\Listener\OrphanVisitsCountTracker;
|
||||
use Shlinkio\Shlink\Core\Visit\Listener\ShortUrlVisitsCountTracker;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
|
||||
@@ -16,10 +19,9 @@ return (static function (): array {
|
||||
'mssql' => 'pdo_sqlsrv',
|
||||
default => 'pdo_mysql',
|
||||
};
|
||||
$resolveDefaultPort = static fn () => match ($driver) {
|
||||
'postgres' => '5432',
|
||||
'mssql' => '1433',
|
||||
default => '3306',
|
||||
$readCredentialAsString = static function (EnvVars $envVar): string|null {
|
||||
$value = $envVar->loadFromEnv();
|
||||
return $value === null ? null : (string) $value;
|
||||
};
|
||||
$resolveCharset = static fn () => match ($driver) {
|
||||
// This does not determine charsets or collations in tables or columns, but the charset used in the data
|
||||
@@ -28,6 +30,7 @@ return (static function (): array {
|
||||
'postgres' => 'utf8',
|
||||
default => null,
|
||||
};
|
||||
|
||||
$resolveConnection = static fn () => match ($driver) {
|
||||
null, 'sqlite' => [
|
||||
'driver' => 'pdo_sqlite',
|
||||
@@ -35,11 +38,11 @@ return (static function (): array {
|
||||
],
|
||||
default => [
|
||||
'driver' => $resolveDriver(),
|
||||
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
|
||||
'user' => EnvVars::DB_USER->loadFromEnv(),
|
||||
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
|
||||
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
|
||||
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
|
||||
'dbname' => EnvVars::DB_NAME->loadFromEnv(),
|
||||
'user' => $readCredentialAsString(EnvVars::DB_USER),
|
||||
'password' => $readCredentialAsString(EnvVars::DB_PASSWORD),
|
||||
'host' => EnvVars::DB_HOST->loadFromEnv(),
|
||||
'port' => EnvVars::DB_PORT->loadFromEnv(),
|
||||
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
||||
'charset' => $resolveCharset(),
|
||||
'driverOptions' => $driver !== 'mssql' ? [] : [
|
||||
@@ -55,6 +58,10 @@ return (static function (): array {
|
||||
'proxies_dir' => 'data/proxies',
|
||||
'load_mappings_using_functional_style' => true,
|
||||
'default_repository_classname' => EntitySpecificationRepository::class,
|
||||
'listeners' => [
|
||||
Events::onFlush => [ShortUrlVisitsCountTracker::class, OrphanVisitsCountTracker::class],
|
||||
Events::postFlush => [ShortUrlVisitsCountTracker::class, OrphanVisitsCountTracker::class],
|
||||
],
|
||||
],
|
||||
'connection' => $resolveConnection(),
|
||||
],
|
||||
|
||||
@@ -12,6 +12,7 @@ return [
|
||||
'installer' => [
|
||||
'enabled_options' => [
|
||||
Option\Server\RuntimeConfigOption::class,
|
||||
Option\Server\MemoryLimitConfigOption::class,
|
||||
Option\Database\DatabaseDriverConfigOption::class,
|
||||
Option\Database\DatabaseNameConfigOption::class,
|
||||
Option\Database\DatabaseHostConfigOption::class,
|
||||
@@ -44,6 +45,8 @@ return [
|
||||
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
|
||||
Option\UrlShortener\ShortUrlModeConfigOption::class,
|
||||
Option\Robots\RobotsAllowAllShortUrlsConfigOption::class,
|
||||
Option\Robots\RobotsUserAgentsConfigOption::class,
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Monolog\Level;
|
||||
use Monolog\Logger;
|
||||
@@ -13,6 +14,8 @@ use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Helper\RequestIdProvider;
|
||||
use Shlinkio\Shlink\EventDispatcher\Util\RequestIdProviderInterface;
|
||||
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
@@ -44,14 +47,20 @@ return (static function (): array {
|
||||
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||
NullLogger::class => InvokableFactory::class,
|
||||
RequestIdProvider::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'logger' => 'Logger_Shlink',
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
AccessLogMiddleware::LOGGER_SERVICE_NAME => 'Logger_Access',
|
||||
RequestIdProviderInterface::class => RequestIdProvider::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
RequestIdProvider::class => [RequestIdMiddleware::class],
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
|
||||
@@ -7,7 +7,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false),
|
||||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(),
|
||||
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
|
||||
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
|
||||
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
|
||||
@@ -15,7 +15,7 @@ return (static function (): array {
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => $publicUrl,
|
||||
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv($publicUrl),
|
||||
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv(),
|
||||
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(),
|
||||
'jwt_issuer' => 'Shlink',
|
||||
],
|
||||
|
||||
@@ -4,32 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
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_ERROR_CORRECTION;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ROUND_BLOCK_SIZE;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
|
||||
|
||||
return [
|
||||
|
||||
'qr_codes' => [
|
||||
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(DEFAULT_QR_CODE_SIZE),
|
||||
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
|
||||
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
|
||||
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(
|
||||
DEFAULT_QR_CODE_ERROR_CORRECTION,
|
||||
),
|
||||
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
|
||||
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
|
||||
),
|
||||
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
|
||||
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),
|
||||
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(),
|
||||
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(),
|
||||
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(),
|
||||
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(),
|
||||
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(),
|
||||
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(),
|
||||
'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(),
|
||||
'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(),
|
||||
'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(),
|
||||
],
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
return [
|
||||
|
||||
'rabbitmq' => [
|
||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
|
||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(),
|
||||
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
|
||||
'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(false),
|
||||
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
|
||||
'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(),
|
||||
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv(),
|
||||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@ return [
|
||||
'rabbitmq' => [
|
||||
'enabled' => true,
|
||||
'host' => 'shlink_rabbitmq',
|
||||
'port' => '5673',
|
||||
'port' => 5672,
|
||||
'user' => 'rabbit',
|
||||
'password' => 'rabbit',
|
||||
],
|
||||
|
||||
@@ -4,9 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||
use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
|
||||
|
||||
return [
|
||||
|
||||
'not_found_redirects' => [
|
||||
@@ -16,10 +13,8 @@ return [
|
||||
],
|
||||
|
||||
'redirects' => [
|
||||
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE->value),
|
||||
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(
|
||||
DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||
),
|
||||
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(),
|
||||
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
14
config/autoload/robots.global.php
Normal file
14
config/autoload/robots.global.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
return [
|
||||
|
||||
'robots' => [
|
||||
'allow-all-short-urls' => (bool) Config\EnvVars::ROBOTS_ALLOW_ALL_SHORT_URLS->loadFromEnv(),
|
||||
'user-agents' => splitByComma(Config\EnvVars::ROBOTS_USER_AGENTS->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
return [
|
||||
|
||||
'router' => [
|
||||
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
|
||||
'base_path' => EnvVars::BASE_PATH->loadFromEnv(),
|
||||
|
||||
'fastroute' => [
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
|
||||
@@ -21,7 +21,7 @@ return (static function (): array {
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
|
||||
// TODO This should be based on config, not the env var
|
||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false) ? '[/]' : '';
|
||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv() ? '[/]' : '';
|
||||
|
||||
return [
|
||||
|
||||
|
||||
@@ -4,40 +4,35 @@ declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
/** @var string|null $disableTrackingFrom */
|
||||
$disableTrackingFrom = EnvVars::DISABLE_TRACKING_FROM->loadFromEnv();
|
||||
use function Shlinkio\Shlink\Core\splitByComma;
|
||||
|
||||
return [
|
||||
return [
|
||||
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(),
|
||||
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(),
|
||||
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(),
|
||||
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(),
|
||||
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(),
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => $disableTrackingFrom === null
|
||||
? []
|
||||
: array_map(trim(...), explode(',', $disableTrackingFrom)),
|
||||
],
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => splitByComma(EnvVars::DISABLE_TRACKING_FROM->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
];
|
||||
|
||||
@@ -5,29 +5,28 @@ declare(strict_types=1);
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
|
||||
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
return (static function (): array {
|
||||
$shortCodesLength = max(
|
||||
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
|
||||
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(),
|
||||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv();
|
||||
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv(true)) ? 'https' : 'http',
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
|
||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv()) ? 'https' : 'http',
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(),
|
||||
],
|
||||
'default_short_codes_length' => $shortCodesLength,
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(true),
|
||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(),
|
||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(),
|
||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(),
|
||||
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(),
|
||||
'mode' => $mode,
|
||||
],
|
||||
|
||||
|
||||
@@ -8,18 +8,13 @@ use Laminas\ConfigAggregator;
|
||||
use Laminas\Diactoros;
|
||||
use Mezzio;
|
||||
use Mezzio\ProblemDetails;
|
||||
use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
|
||||
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator(
|
||||
providers: [
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
|
||||
@@ -12,7 +12,6 @@ const MIN_SHORT_CODES_LENGTH = 4;
|
||||
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
|
||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||
const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema.
|
||||
const DEFAULT_QR_CODE_SIZE = 300;
|
||||
const DEFAULT_QR_CODE_MARGIN = 0;
|
||||
|
||||
@@ -6,14 +6,21 @@ use Laminas\ServiceManager\ServiceManager;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Symfony\Component\Lock;
|
||||
|
||||
use function Shlinkio\Shlink\Config\loadEnvVarsFromConfig;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
use const Shlinkio\Shlink\LOCAL_LOCK_FACTORY;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone here
|
||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||
// Promote env vars from installer config
|
||||
loadEnvVarsFromConfig('config/params/generated_config.php', enumValues(EnvVars::class));
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone and memory limit here
|
||||
ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv());
|
||||
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv());
|
||||
|
||||
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
|
||||
// It needs to be placed here as individual config files will not be loaded once config is cached
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
display_errors=On
|
||||
error_reporting=-1
|
||||
memory_limit=-1
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_db_mysql:
|
||||
environment:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_php:
|
||||
user: 1000:1000
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
@@ -79,7 +77,7 @@ services:
|
||||
|
||||
shlink_db_postgres:
|
||||
container_name: shlink_db_postgres
|
||||
image: postgres:12.2-alpine
|
||||
image: postgres:16.3-alpine
|
||||
ports:
|
||||
- "5434:5432"
|
||||
volumes:
|
||||
@@ -105,7 +103,7 @@ services:
|
||||
|
||||
shlink_db_ms:
|
||||
container_name: shlink_db_ms
|
||||
image: mcr.microsoft.com/mssql/server:2019-latest
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
ports:
|
||||
- "1433:1433"
|
||||
environment:
|
||||
@@ -147,7 +145,7 @@ services:
|
||||
SERVER_NAME: ":80"
|
||||
MERCURE_PUBLISHER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_SUBSCRIBER_JWT_KEY: mercure_jwt_key_long_enough_to_avoid_error
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000"
|
||||
MERCURE_EXTRA_DIRECTIVES: "cors_origins https://app.shlink.io http://localhost:3000 http://127.0.0.1:3000 http://localhost:3002 http://127.0.0.1:3002 http://localhost:3005 http://127.0.0.1:3005"
|
||||
|
||||
shlink_rabbitmq:
|
||||
container_name: shlink_rabbitmq
|
||||
@@ -169,7 +167,7 @@ services:
|
||||
|
||||
shlink_matomo:
|
||||
container_name: shlink_matomo
|
||||
image: matomo:4.15-apache
|
||||
image: matomo:5.0-apache
|
||||
ports:
|
||||
- "8003:80"
|
||||
volumes:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
memory_limit=512M
|
||||
|
||||
@@ -8,14 +8,19 @@ mkdir -p data/cache data/locks data/log data/proxies
|
||||
|
||||
flags="--no-interaction --clear-db-cache"
|
||||
|
||||
# Read env vars through Shlink command, so that it applies the `_FILE` env var fallback logic
|
||||
geolite_license_key=$(bin/cli env-var:read GEOLITE_LICENSE_KEY)
|
||||
skip_initial_geolite_download=$(bin/cli env-var:read SKIP_INITIAL_GEOLITE_DOWNLOAD)
|
||||
initial_api_key=$(bin/cli env-var:read INITIAL_API_KEY)
|
||||
|
||||
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" = "true" ]; then
|
||||
if [ -z "${geolite_license_key}" ] || [ "${skip_initial_geolite_download}" = "true" ]; then
|
||||
flags="${flags} --skip-download-geolite"
|
||||
fi
|
||||
|
||||
# If INITIAL_API_KEY was provided, create an initial API key
|
||||
if [ -n "${INITIAL_API_KEY}" ]; then
|
||||
flags="${flags} --initial-api-key=${INITIAL_API_KEY}"
|
||||
if [ -n "${initial_api_key}" ]; then
|
||||
flags="${flags} --initial-api-key=${initial_api_key}"
|
||||
fi
|
||||
|
||||
php vendor/bin/shlink-installer init ${flags}
|
||||
|
||||
@@ -232,6 +232,11 @@
|
||||
"potentialBot": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if Shlink thinks this visit comes potentially from a bot or crawler"
|
||||
},
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
@@ -247,7 +252,8 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
},
|
||||
"OrphanVisit": {
|
||||
@@ -256,11 +262,6 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
1167
docs/changelog-archive/CHANGELOG-1.x.md
Normal file
1167
docs/changelog-archive/CHANGELOG-1.x.md
Normal file
File diff suppressed because it is too large
Load Diff
912
docs/changelog-archive/CHANGELOG-2.x.md
Normal file
912
docs/changelog-archive/CHANGELOG-2.x.md
Normal file
@@ -0,0 +1,912 @@
|
||||
# CHANGELOG 2.x
|
||||
|
||||
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).
|
||||
|
||||
## [2.10.3] - 2022-01-23
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1349](https://github.com/shlinkio/shlink/issues/1349) Fixed memory leak in cache implementation.
|
||||
|
||||
|
||||
## [2.10.2] - 2022-01-07
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1293](https://github.com/shlinkio/shlink/issues/1293) Fixed error when trying to create/import short URLs with a too long title.
|
||||
* [#1306](https://github.com/shlinkio/shlink/issues/1306) Ensured remote IP address is not logged when using swoole/openswoole.
|
||||
* [#1308](https://github.com/shlinkio/shlink/issues/1308) Fixed memory leak when using redis due to the amount of non-expiring keys created by doctrine. Now they have a 24h expiration by default.
|
||||
|
||||
|
||||
## [2.10.1] - 2021-12-21
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1285](https://github.com/shlinkio/shlink/issues/1285) Fixed error caused by database connections expiring after some hours of inactivity.
|
||||
* [#1286](https://github.com/shlinkio/shlink/issues/1286) Fixed `x-request-id` header not being generated during non-rest requests.
|
||||
|
||||
|
||||
## [2.10.0] - 2021-12-12
|
||||
### Added
|
||||
* [#1163](https://github.com/shlinkio/shlink/issues/1163) Allowed setting not-found redirects for default domain in the same way it's done for any other domain.
|
||||
|
||||
This implies a few non-breaking changes:
|
||||
|
||||
* The domains list no longer has the values of `INVALID_SHORT_URL_REDIRECT_TO`, `REGULAR_404_REDIRECT_TO` and `BASE_URL_REDIRECT_TO` on the default domain redirects.
|
||||
* The `GET /domains` endpoint includes a new `defaultRedirects` property in the response, with the default redirects set via config or env vars.
|
||||
* The `INVALID_SHORT_URL_REDIRECT_TO`, `REGULAR_404_REDIRECT_TO` and `BASE_URL_REDIRECT_TO` env vars are now deprecated, and should be replaced by `DEFAULT_INVALID_SHORT_URL_REDIRECT`, `DEFAULT_REGULAR_404_REDIRECT` and `DEFAULT_BASE_URL_REDIRECT` respectively. Deprecated ones will continue to work until v3.0.0, where they will be removed.
|
||||
|
||||
* [#868](https://github.com/shlinkio/shlink/issues/868) Added support to publish real-time updates in a RabbitMQ server.
|
||||
|
||||
Shlink will create new exchanges and queues for every topic documented in the [Async API spec](https://api-spec.shlink.io/async-api/), meaning, you will have one queue for orphan visits, one for regular visits, and one queue for every short URL with its visits.
|
||||
|
||||
The RabbitMQ server config can be provided via installer config options, or via environment variables.
|
||||
|
||||
* [#1204](https://github.com/shlinkio/shlink/issues/1204) Added support for `openswoole` and migrated official docker image to `openswoole`.
|
||||
* [#1242](https://github.com/shlinkio/shlink/issues/1242) Added support to import urls and visits from YOURLS.
|
||||
|
||||
In order to do it, you need to first install this [dedicated plugin](https://slnk.to/yourls-import) in YOURLS, and then run the `short-url:import yourls` command, as with any other source.
|
||||
|
||||
* [#1235](https://github.com/shlinkio/shlink/issues/1235) Added support to disable rounding QR codes block sizing via config option, env var or query param.
|
||||
* [#1188](https://github.com/shlinkio/shlink/issues/1188) Added support for PHP 8.1.
|
||||
|
||||
The official docker image has also been updated to use PHP 8.1 by default.
|
||||
|
||||
### Changed
|
||||
* [#844](https://github.com/shlinkio/shlink/issues/844) Added mutation checks to API tests.
|
||||
* [#1218](https://github.com/shlinkio/shlink/issues/1218) Updated to symfony/mercure 0.6.
|
||||
* [#1223](https://github.com/shlinkio/shlink/issues/1223) Updated to phpstan 1.0.
|
||||
* [#1258](https://github.com/shlinkio/shlink/issues/1258) Updated to Symfony 6 components, except symfony/console.
|
||||
* Added `domain` field to `DeleteShortUrlException` exception.
|
||||
|
||||
### Deprecated
|
||||
* [#1260](https://github.com/shlinkio/shlink/issues/1260) Deprecated `USE_HTTPS` env var that was added in previous release, in favor of the new `IS_HTTPS_ENABLED`.
|
||||
|
||||
The old one proved to be confusing and misleading, making people think it was used to actually enable HTTPS transparently, instead of its actual purpose, which is just telling Shlink it is being served with HTTPS.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1206](https://github.com/shlinkio/shlink/issues/1206) Fixed debugging of the docker image, so that it does not run the commands with `-q` when the `SHELL_VERBOSITY` env var has been provided.
|
||||
* [#1254](https://github.com/shlinkio/shlink/issues/1254) Fixed examples in swagger docs.
|
||||
|
||||
|
||||
## [2.9.3] - 2021-11-15
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1232](https://github.com/shlinkio/shlink/issues/1232) Solved potential SQL injection by enforcing `doctrine/dbal` 3.1.4.
|
||||
|
||||
|
||||
## [2.9.2] - 2021-10-23
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1210](https://github.com/shlinkio/shlink/issues/1210) Fixed real time updates not being notified due to an incorrect handling of db transactions on multi-process tasks.
|
||||
* [#1211](https://github.com/shlinkio/shlink/issues/1211) Fixed `There is no active transaction` error when running migrations in MySQL/Mariadb after updating to doctrine-migrations 3.3.
|
||||
* [#1197](https://github.com/shlinkio/shlink/issues/1197) Fixed amount of task workers provided via config option or env var not being validated to ensure enough workers to process all parallel tasks.
|
||||
|
||||
|
||||
## [2.9.1] - 2021-10-11
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1201](https://github.com/shlinkio/shlink/issues/1201) Fixed crash when using the new `USE_HTTPS`, as it's boolean raw value was being used instead of resolving "https" or "http".
|
||||
|
||||
|
||||
## [2.9.0] - 2021-10-10
|
||||
### Added
|
||||
* [#1015](https://github.com/shlinkio/shlink/issues/1015) Shlink now accepts configuration via env vars even when not using docker.
|
||||
|
||||
The config generated with the installing tool still has precedence over the env vars, so it cannot be combined. Either you use the tool, or use env vars.
|
||||
|
||||
* [#1149](https://github.com/shlinkio/shlink/issues/1149) Allowed to set custom defaults for the QR codes.
|
||||
* [#1112](https://github.com/shlinkio/shlink/issues/1112) Added new option to define if the query string should be forwarded on a per-short URL basis.
|
||||
|
||||
The new `forwardQuery=true|false` param can be provided during short URL creation or edition, via REST API or CLI command, allowing to override the default behavior which makes the query string to always be forwarded.
|
||||
|
||||
* [#1105](https://github.com/shlinkio/shlink/issues/1105) Added support to define placeholders on not-found redirects, so that the redirected URL receives the originally visited path and/or domain.
|
||||
|
||||
Currently, `{DOMAIN}` and `{ORIGINAL_PATH}` placeholders are supported, and they can be used both in the redirected URL's path or query.
|
||||
|
||||
When they are used in the query, the values are URL encoded.
|
||||
|
||||
* [#1119](https://github.com/shlinkio/shlink/issues/1119) Added support to provide redis sentinel when using redis cache.
|
||||
* [#1016](https://github.com/shlinkio/shlink/issues/1016) Added new option to send orphan visits to webhooks, via `NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS` env var or installer tool.
|
||||
|
||||
The option is disabled by default, as the payload is backwards incompatible. You will need to adapt your webhooks to treat the `shortUrl` property as optional before enabling this option.
|
||||
|
||||
* [#1104](https://github.com/shlinkio/shlink/issues/1104) Added ability to disable tracking based on IP addresses.
|
||||
|
||||
IP addresses can be provided in the form of fixed addresses, CIDR blocks, or wildcard patterns (192.168.*.*).
|
||||
|
||||
### Changed
|
||||
* [#1142](https://github.com/shlinkio/shlink/issues/1142) Replaced `doctrine/cache` package with `symfony/cache`.
|
||||
* [#1157](https://github.com/shlinkio/shlink/issues/1157) All routes now support CORS, not only rest ones.
|
||||
* [#1144](https://github.com/shlinkio/shlink/issues/1144) Added experimental builds under PHP 8.1.
|
||||
|
||||
### Deprecated
|
||||
* [#1164](https://github.com/shlinkio/shlink/issues/1164) Deprecated `SHORT_DOMAIN_HOST` and `SHORT_DOMAIN_SCHEMA` env vars. Use `DEFAULT_DOMAIN` and `USE_HTTPS=true|false` instead.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1165](https://github.com/shlinkio/shlink/issues/1165) Fixed warning displayed when trying to locate visits and there are none pending.
|
||||
* [#1172](https://github.com/shlinkio/shlink/pull/1172) Removed unneeded explicitly defined volumes in docker image.
|
||||
|
||||
|
||||
## [2.8.1] - 2021-08-15
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1155](https://github.com/shlinkio/shlink/issues/1155) Fixed numeric query params in long URLs being replaced by `0`.
|
||||
|
||||
|
||||
## [2.8.0] - 2021-08-04
|
||||
### Added
|
||||
* [#1089](https://github.com/shlinkio/shlink/issues/1089) Added new `ENABLE_PERIODIC_VISIT_LOCATE` env var to docker image which schedules the `visit:locate` command every hour when provided with value `true`.
|
||||
* [#1082](https://github.com/shlinkio/shlink/issues/1082) Added support for error correction level on QR codes.
|
||||
|
||||
Now, when calling the `GET /{shorCode}/qr-code` URL, you can pass the `errorCorrection` query param with values `L` for Low, `M` for Medium, `Q` for Quartile or `H` for High.
|
||||
|
||||
* [#1080](https://github.com/shlinkio/shlink/issues/1080) Added support to redirect to URLs as soon as the path starts with a valid short code, appending the rest of the path to the redirected long URL.
|
||||
|
||||
With this, if you have the `https://example.com/abc123` short URL redirecting to `https://www.twitter.com`, a visit to `https://example.com/abc123/shlinkio` will take you to `https://www.twitter.com/shlinkio`.
|
||||
|
||||
This behavior needs to be actively opted in, via installer config options or env vars.
|
||||
|
||||
* [#943](https://github.com/shlinkio/shlink/issues/943) Added support to define different "not-found" redirects for every domain handled by Shlink.
|
||||
|
||||
Shlink will continue to allow defining the default values via env vars or config, but afterwards, you can use the `domain:redirects` command or the `PATCH /domains/redirects` REST endpoint to define specific values for every single domain.
|
||||
|
||||
### Changed
|
||||
* [#1118](https://github.com/shlinkio/shlink/issues/1118) Increased phpstan required level to 8.
|
||||
* [#1127](https://github.com/shlinkio/shlink/issues/1127) Updated to infection 0.24.
|
||||
* [#1139](https://github.com/shlinkio/shlink/issues/1139) Updated project dependencies, including base docker image to use PHP 8.0.9 and Alpine 3.14.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#1046](https://github.com/shlinkio/shlink/issues/1046) Dropped support for PHP 7.4.
|
||||
|
||||
### Fixed
|
||||
* [#1098](https://github.com/shlinkio/shlink/issues/1098) Fixed errors when using Redis for caching, caused by some third party lib bug that was fixed on dependencies update.
|
||||
|
||||
|
||||
## [2.7.3] - 2021-08-02
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1135](https://github.com/shlinkio/shlink/issues/1135) Fixed error when importing short URLs with no visits from another Shlink instance.
|
||||
* [#1136](https://github.com/shlinkio/shlink/issues/1136) Fixed error when fetching tag/short-url/orphan visits for a page lower than 1.
|
||||
|
||||
|
||||
## [2.7.2] - 2021-07-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1128](https://github.com/shlinkio/shlink/issues/1128) Increased memory limit reserved for the docker image, preventing it from crashing on GeoLite db download.
|
||||
|
||||
|
||||
## [2.7.1] - 2021-05-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1100](https://github.com/shlinkio/shlink/issues/1100) Fixed Shlink trying to download GeoLite2 db files even when tracking has been disabled.
|
||||
|
||||
|
||||
## [2.7.0] - 2021-05-23
|
||||
### Added
|
||||
* [#1044](https://github.com/shlinkio/shlink/issues/1044) Added ability to set names on API keys, which helps to identify them when the list grows.
|
||||
* [#819](https://github.com/shlinkio/shlink/issues/819) Visits are now always located in real time, even when not using swoole.
|
||||
|
||||
The only side effect is that a GeoLite2 db file is now installed when the docker image starts or during shlink installation or update.
|
||||
|
||||
Also, when using swoole, the file is now updated **after** tracking a visit, which means it will not apply until the next one.
|
||||
|
||||
* [#1059](https://github.com/shlinkio/shlink/issues/1059) Added ability to optionally display author API key and its name when listing short URLs from the command line.
|
||||
* [#1066](https://github.com/shlinkio/shlink/issues/1066) Added support to import short URLs and their visits from another Shlink instance using its API.
|
||||
* [#898](https://github.com/shlinkio/shlink/issues/898) Improved tracking granularity, allowing to disable visits tracking completely, or just parts of it.
|
||||
|
||||
In order to achieve it, Shlink now supports 4 new tracking-related options, that can be customized via env vars for docker, or via installer:
|
||||
|
||||
* `disable_tracking`: If true, visits will not be tracked at all.
|
||||
* `disable_ip_tracking`: If true, visits will be tracked, but neither the IP address, nor the location will be resolved.
|
||||
* `disable_referrer_tracking`: If true, the referrer will not be tracked.
|
||||
* `disable_ua_tracking`: If true, the user agent will not be tracked.
|
||||
|
||||
* [#955](https://github.com/shlinkio/shlink/issues/955) Added new option to set short URLs as crawlable, making them be listed in the robots.txt as Allowed.
|
||||
* [#900](https://github.com/shlinkio/shlink/issues/900) Shlink now tries to detect if the visit is coming from a potential bot or crawler, and allows to exclude those visits from visits lists if desired.
|
||||
|
||||
### Changed
|
||||
* [#1036](https://github.com/shlinkio/shlink/issues/1036) Updated to `happyr/doctrine-specification` 2.0.
|
||||
* [#1039](https://github.com/shlinkio/shlink/issues/1039) Updated to `endroid/qr-code` 4.0.
|
||||
* [#1008](https://github.com/shlinkio/shlink/issues/1008) Ensured all logs are sent to the filesystem while running API tests, which helps debugging the reason for tests to fail.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1041](https://github.com/shlinkio/shlink/issues/1041) Ensured the default value for the version while building the docker image is `latest`.
|
||||
* [#1067](https://github.com/shlinkio/shlink/issues/1067) Fixed exception when persisting multiple short URLs in one batch which include the same new tags/domains. This can potentially happen when importing URLs.
|
||||
|
||||
|
||||
## [2.6.2] - 2021-03-12
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1047](https://github.com/shlinkio/shlink/issues/1047) Fixed error in migrations when doing a fresh installation using PHP8 and MySQL/Mariadb databases.
|
||||
|
||||
|
||||
## [2.6.1] - 2021-02-22
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#1026](https://github.com/shlinkio/shlink/issues/1026) Removed non-inclusive terms from source code.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1024](https://github.com/shlinkio/shlink/issues/1024) Fixed migration that is incorrectly skipped due to the wrong condition being used to check it.
|
||||
* [#1031](https://github.com/shlinkio/shlink/issues/1031) Fixed shortening of twitter URLs with URL validation enabled.
|
||||
* [#1034](https://github.com/shlinkio/shlink/issues/1034) Fixed warning displayed when shlink is stopped while running it with swoole.
|
||||
|
||||
|
||||
## [2.6.0] - 2021-02-13
|
||||
### Added
|
||||
* [#856](https://github.com/shlinkio/shlink/issues/856) Added PHP 8.0 support.
|
||||
* [#941](https://github.com/shlinkio/shlink/issues/941) Added support to provide a title for every short URL.
|
||||
|
||||
The title can also be automatically resolved from the long URL, when no title was explicitly provided, but this option needs to be opted in.
|
||||
|
||||
* [#913](https://github.com/shlinkio/shlink/issues/913) Added support to import short URLs from a standard CSV file.
|
||||
|
||||
The file requires the `Long URL` and `Short code` columns, and it also accepts the optional `title`, `domain` and `tags` columns.
|
||||
|
||||
* [#1000](https://github.com/shlinkio/shlink/issues/1000) Added support to provide a `margin` query param when generating some URL's QR code.
|
||||
* [#675](https://github.com/shlinkio/shlink/issues/675) Added ability to track visits to the base URL, invalid short URLs or any other "not found" URL, as known as orphan visits.
|
||||
|
||||
This behavior is enabled by default, but you can opt out via env vars or config options.
|
||||
|
||||
This new orphan visits can be consumed in these ways:
|
||||
|
||||
* The `https://shlink.io/new-orphan-visit` mercure topic, which gets notified when an orphan visit occurs.
|
||||
* The `GET /visits/orphan` REST endpoint, which behaves like the short URL visits and tags visits endpoints, but returns only orphan visits.
|
||||
|
||||
### Changed
|
||||
* [#977](https://github.com/shlinkio/shlink/issues/977) Migrated from `laminas/laminas-paginator` to `pagerfanta/core` to handle pagination.
|
||||
* [#986](https://github.com/shlinkio/shlink/issues/986) Updated official docker image to use PHP 8.
|
||||
* [#1010](https://github.com/shlinkio/shlink/issues/1010) Increased timeout for database commands to 10 minutes.
|
||||
* [#874](https://github.com/shlinkio/shlink/issues/874) Changed how dist files are generated. Now there will be two for every supported PHP version, with and without support for swoole.
|
||||
|
||||
The dist files will have been built under the same PHP version they are meant to be run under, ensuring resolved dependencies are the proper ones.
|
||||
|
||||
### Deprecated
|
||||
* [#959](https://github.com/shlinkio/shlink/issues/959) Deprecated all command flags using camelCase format (like `--expirationDate`), adding kebab-case replacements for all of them (like `--expiration-date`).
|
||||
|
||||
All the existing camelCase flags will continue working for now, but will be removed in Shlink 3.0.0
|
||||
|
||||
* [#862](https://github.com/shlinkio/shlink/issues/862) Deprecated the endpoint to edit tags for a short URL (`PUT /short-urls/{shortCode}/tags`).
|
||||
|
||||
The short URL edition endpoint (`PATCH /short-urls/{shortCode}`) now supports setting the tags too. Use it instead.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#988](https://github.com/shlinkio/shlink/issues/988) Fixed serving zero-byte static files in apache and apache-compatible web servers.
|
||||
* [#990](https://github.com/shlinkio/shlink/issues/990) Fixed short URLs not properly composed in REST API endpoints when both custom domain and custom base path are used.
|
||||
* [#1002](https://github.com/shlinkio/shlink/issues/1002) Fixed weird behavior in which GeoLite2 metadata's `buildEpoch` is parsed as string instead of int.
|
||||
* [#851](https://github.com/shlinkio/shlink/issues/851) Fixed error when trying to schedule swoole tasks in ARM architectures (like raspberry).
|
||||
|
||||
|
||||
## [2.5.2] - 2021-01-24
|
||||
### Added
|
||||
* [#965](https://github.com/shlinkio/shlink/issues/965) Added docs section for Architectural Decision Records, including the one for API key roles.
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#979](https://github.com/shlinkio/shlink/issues/979) Added missing `itemsPerPage` query param to swagger docs for short URLs list.
|
||||
* [#980](https://github.com/shlinkio/shlink/issues/980) Fixed value used for `Access-Control-Allow-Origin`, that could not work as expected when including an IP address.
|
||||
* [#947](https://github.com/shlinkio/shlink/issues/947) Fixed incorrect value returned in `Access-Control-Allow-Methods` header, which always contained all methods.
|
||||
|
||||
|
||||
## [2.5.1] - 2021-01-21
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#968](https://github.com/shlinkio/shlink/issues/968) Fixed index error in MariaDB while updating to v2.5.0.
|
||||
* [#972](https://github.com/shlinkio/shlink/issues/972) Fixed 500 error when calling single-step short URL creation endpoint.
|
||||
|
||||
|
||||
## [2.5.0] - 2021-01-17
|
||||
### Added
|
||||
* [#795](https://github.com/shlinkio/shlink/issues/795) and [#882](https://github.com/shlinkio/shlink/issues/882) Added new roles system to API keys.
|
||||
|
||||
API keys can have any combinations of these two roles now, allowing to limit their interactions:
|
||||
|
||||
* Can interact only with short URLs created with that API key.
|
||||
* Can interact only with short URLs for a specific domain.
|
||||
|
||||
* [#833](https://github.com/shlinkio/shlink/issues/833) Added support to connect through unix socket when using an external MySQL, MariaDB or Postgres database.
|
||||
|
||||
It can be provided during the installation, or as the `DB_UNIX_SOCKET` env var for the docker image.
|
||||
|
||||
* [#869](https://github.com/shlinkio/shlink/issues/869) Added support for Mercure Hub 0.10.
|
||||
* [#896](https://github.com/shlinkio/shlink/issues/896) Added support for unicode characters in custom slugs.
|
||||
* [#930](https://github.com/shlinkio/shlink/issues/930) Added new `bin/set-option` script that allows changing individual configuration options on existing shlink instances.
|
||||
* [#877](https://github.com/shlinkio/shlink/issues/877) Improved API tests on CORS, and "refined" middleware handling it.
|
||||
|
||||
### Changed
|
||||
* [#912](https://github.com/shlinkio/shlink/issues/912) Changed error templates to be plain html files, removing the dependency on `league/plates` package.
|
||||
* [#875](https://github.com/shlinkio/shlink/issues/875) Updated to `mezzio/mezzio-swoole` v3.1.
|
||||
* [#952](https://github.com/shlinkio/shlink/issues/952) Simplified in-project docs, by keeping only the basics and linking to the websites docs for anything else.
|
||||
|
||||
### Deprecated
|
||||
* [#917](https://github.com/shlinkio/shlink/issues/917) Deprecated `/{shortCode}/qr-code/{size}` URL, in favor of providing the size in the query instead, `/{shortCode}/qr-code?size={size}`.
|
||||
* [#924](https://github.com/shlinkio/shlink/issues/924) Deprecated mechanism to provide config options to the docker image through volumes. Use the env vars instead as a direct replacement.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [2.4.2] - 2020-11-22
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#904](https://github.com/shlinkio/shlink/issues/904) Explicitly added missing "Domains" and "Integrations" tags to swagger docs.
|
||||
* [#901](https://github.com/shlinkio/shlink/issues/901) Ensured domains which are not in use on any short URL are not returned on the list of domains.
|
||||
* [#899](https://github.com/shlinkio/shlink/issues/899) Avoided filesystem errors produced while downloading geolite DB files on several shlink instances that share the same filesystem.
|
||||
* [#827](https://github.com/shlinkio/shlink/issues/827) Fixed swoole config getting loaded in config cache if a console command is run before any web execution, when swoole extension is enabled, making subsequent non-swoole web requests fail.
|
||||
|
||||
|
||||
## [2.4.1] - 2020-11-10
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#891](https://github.com/shlinkio/shlink/issues/891) Fixed error when running migrations in postgres due to incorrect return type hint.
|
||||
* [#846](https://github.com/shlinkio/shlink/issues/846) Fixed base image used for the PHP-FPM dev container.
|
||||
* [#867](https://github.com/shlinkio/shlink/issues/867) Fixed not-found redirects not using proper status (301 or 302) as configured during installation.
|
||||
|
||||
|
||||
## [2.4.0] - 2020-11-08
|
||||
### Added
|
||||
* [#829](https://github.com/shlinkio/shlink/issues/829) Added support for QR codes in SVG format, by passing `?format=svg` to the QR code URL.
|
||||
* [#820](https://github.com/shlinkio/shlink/issues/820) Added new option to force enabling or disabling URL validation on a per-URL basis.
|
||||
|
||||
Currently, there's a global config that tells if long URLs should be validated (by ensuring they are publicly accessible and return a 2xx status). However, this is either always applied or never applied.
|
||||
|
||||
Now, it is possible to enforce validation or enforce disabling validation when a new short URL is created or edited:
|
||||
|
||||
* On the `POST /short-url` and `PATCH /short-url/{shortCode}` endpoints, you can now pass `validateUrl: true/false` in order to enforce enabling or disabling validation, ignoring the global config. If the value is not provided, the global config is still normally applied.
|
||||
* On the `short-url:generate` CLI command, you can pass `--validate-url` or `--no-validate-url` flags, in order to enforce enabling or disabling validation. If none of them is provided, the global config is still normally applied.
|
||||
|
||||
* [#838](https://github.com/shlinkio/shlink/issues/838) Added new endpoint and CLI command to list existing domains.
|
||||
|
||||
It returns both default domain and specific domains that were used for some short URLs.
|
||||
|
||||
* REST endpoint: `GET /rest/v2/domains`
|
||||
* CLI Command: `domain:list`
|
||||
|
||||
* [#832](https://github.com/shlinkio/shlink/issues/832) Added support to customize the port in which the docker image listens by using the `PORT` env var or the `port` config option.
|
||||
|
||||
* [#860](https://github.com/shlinkio/shlink/issues/860) Added support to import links from bit.ly.
|
||||
|
||||
Run the command `short-urls:import bitly` and introduce requested information in order to import all your links.
|
||||
|
||||
Other sources will be supported in future releases.
|
||||
|
||||
### Changed
|
||||
* [#836](https://github.com/shlinkio/shlink/issues/836) Added support for the `<field>-<dir>` notation while determining how to order the short URLs list, as in `?orderBy=shortCode-DESC`. This effectively deprecates the array notation (`?orderBy[shortCode]=DESC`), that will be removed in Shlink 3.0.0
|
||||
* [#782](https://github.com/shlinkio/shlink/issues/782) Added code coverage to API tests.
|
||||
* [#858](https://github.com/shlinkio/shlink/issues/858) Updated to latest infection version. Updated docker images to PHP 7.4.11 and swoole 4.5.5
|
||||
* [#887](https://github.com/shlinkio/shlink/pull/887) Started tracking the API key used to create short URLs, in order to allow restrictions in future releases.
|
||||
|
||||
### Deprecated
|
||||
* [#883](https://github.com/shlinkio/shlink/issues/883) Deprecated `POST /tags` endpoint and `tag:create` command, as tags are created automatically while creating short URLs.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#837](https://github.com/shlinkio/shlink/issues/837) Drastically improved performance when creating a new shortUrl and providing `findIfExists = true`.
|
||||
* [#878](https://github.com/shlinkio/shlink/issues/878) Added missing `gmp` extension to the official docker image.
|
||||
|
||||
|
||||
## [2.3.0] - 2020-08-09
|
||||
### Added
|
||||
* [#746](https://github.com/shlinkio/shlink/issues/746) Allowed to configure the kind of redirect you want to use for your short URLs. You can either set:
|
||||
|
||||
* `302` redirects: Default behavior. Visitors always hit the server.
|
||||
* `301` redirects: Better for SEO. Visitors hit the server the first time and then cache the redirect.
|
||||
|
||||
When selecting 301 redirects, you can also configure the time redirects are cached, to mitigate deviations in stats.
|
||||
|
||||
* [#734](https://github.com/shlinkio/shlink/issues/734) Added support to redirect to deeplinks and other links with schemas different from `http` and `https`.
|
||||
* [#709](https://github.com/shlinkio/shlink/issues/709) Added multi-architecture builds for the docker image.
|
||||
|
||||
* [#707](https://github.com/shlinkio/shlink/issues/707) Added `--all` flag to `short-urls:list` command, which will print all existing URLs in one go, with no pagination.
|
||||
|
||||
It has one limitation, though. Because of the way the CLI tooling works, all rows in the table must be loaded in memory. If the amount of URLs is too high, the command may fail due to too much memory usage.
|
||||
|
||||
### Changed
|
||||
* [#508](https://github.com/shlinkio/shlink/issues/508) Added mutation checks to database tests.
|
||||
* [#790](https://github.com/shlinkio/shlink/issues/790) Updated to doctrine/migrations v3.
|
||||
* [#798](https://github.com/shlinkio/shlink/issues/798) Updated to guzzlehttp/guzzle v7.
|
||||
* [#822](https://github.com/shlinkio/shlink/issues/822) Updated docker image to use PHP 7.4.9 with Alpine 3.12 and swoole 4.5.2.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* *Nothing*
|
||||
|
||||
|
||||
## [2.2.2] - 2020-06-08
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#769](https://github.com/shlinkio/shlink/issues/769) Fixed custom slugs not allowing valid URL characters, like `.`, `_` or `~`.
|
||||
* [#781](https://github.com/shlinkio/shlink/issues/781) Fixed memory leak when loading visits for a tag which is used for big amounts of short URLs.
|
||||
|
||||
|
||||
## [2.2.1] - 2020-05-11
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#764](https://github.com/shlinkio/shlink/issues/764) Fixed error when trying to match an existing short URL which does not have `validSince` and/or `validUntil`, but you are providing either one of them for the new one.
|
||||
|
||||
|
||||
## [2.2.0] - 2020-05-09
|
||||
### Added
|
||||
* [#712](https://github.com/shlinkio/shlink/issues/712) Added support to integrate Shlink with a [mercure hub](https://mercure.rocks/) server.
|
||||
|
||||
Thanks to that, Shlink will be able to publish events that can be consumed in real time.
|
||||
|
||||
For now, two topics (events) are published, when new visits occur. Both include a payload with the visit and the shortUrl:
|
||||
|
||||
* A visit occurs on any short URL: `https://shlink.io/new-visit`.
|
||||
* A visit occurs on short URLs with a specific short code: `https://shlink.io/new-visit/{shortCode}`.
|
||||
|
||||
The updates are only published when serving Shlink with swoole.
|
||||
|
||||
Also, Shlink exposes a new endpoint `GET /rest/v2/mercure-info`, which returns the public URL of the mercure hub, and a valid JWT that can be used to subscribe to updates.
|
||||
|
||||
* [#673](https://github.com/shlinkio/shlink/issues/673) Added new `[GET /visits]` rest endpoint which returns basic visits stats.
|
||||
* [#674](https://github.com/shlinkio/shlink/issues/674) Added new `[GET /tags/{tag}/visits]` rest endpoint which returns visits by tag.
|
||||
|
||||
It works in the same way as the `[GET /short-urls/{shortCode}/visits]` one, returning the same response payload, and supporting the same query params, but the response is the list of visits in all short URLs which have provided tag.
|
||||
|
||||
* [#672](https://github.com/shlinkio/shlink/issues/672) Enhanced `[GET /tags]` rest endpoint so that it is possible to get basic stats info for every tag.
|
||||
|
||||
Now, if the `withStats=true` query param is provided, the response payload will include a new `stats` property which is a list with the amount of short URLs and visits for every tag.
|
||||
|
||||
Also, the `tag:list` CLI command has been changed and it always behaves like this.
|
||||
|
||||
* [#640](https://github.com/shlinkio/shlink/issues/640) Allowed to optionally disable visitors' IP address anonymization. This will make Shlink no longer be GDPR-compliant, but it's OK if you only plan to share your URLs in countries without this regulation.
|
||||
|
||||
### Changed
|
||||
* [#692](https://github.com/shlinkio/shlink/issues/692) Drastically improved performance when loading visits. Specially noticeable when loading big result sets.
|
||||
* [#657](https://github.com/shlinkio/shlink/issues/657) Updated how DB tests are run in travis by using docker containers which allow all engines to be covered.
|
||||
* [#751](https://github.com/shlinkio/shlink/issues/751) Updated PHP and swoole versions used in docker image, and removed mssql-tools, as they are not needed.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#729](https://github.com/shlinkio/shlink/issues/729) Fixed weird error when fetching multiple visits result sets concurrently using mariadb or mysql.
|
||||
* [#735](https://github.com/shlinkio/shlink/issues/735) Fixed error when cleaning metadata cache during installation when APCu is enabled.
|
||||
* [#677](https://github.com/shlinkio/shlink/issues/677) Fixed `/health` endpoint returning `503` fail responses when the database connection has expired.
|
||||
* [#732](https://github.com/shlinkio/shlink/issues/732) Fixed wrong client IP in access logs when serving app with swoole behind load balancer.
|
||||
|
||||
|
||||
## [2.1.4] - 2020-04-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#742](https://github.com/shlinkio/shlink/issues/742) Allowed a custom GeoLite2 license key to be provided, in order to avoid download limits.
|
||||
|
||||
|
||||
## [2.1.3] - 2020-04-09
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#712](https://github.com/shlinkio/shlink/issues/712) Fixed app set-up not clearing entities metadata cache.
|
||||
* [#711](https://github.com/shlinkio/shlink/issues/711) Fixed `HEAD` requests returning a duplicated `Content-Length` header.
|
||||
* [#716](https://github.com/shlinkio/shlink/issues/716) Fixed Twitter not properly displaying preview for final long URL.
|
||||
* [#717](https://github.com/shlinkio/shlink/issues/717) Fixed DB connection expiring on task workers when using swoole.
|
||||
* [#705](https://github.com/shlinkio/shlink/issues/705) Fixed how the short URL domain is inferred when generating QR codes, making sure the configured domain is respected even if the request is performed using a different one, and only when a custom domain is used, then that one is used instead.
|
||||
|
||||
|
||||
## [2.1.2] - 2020-03-29
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#696](https://github.com/shlinkio/shlink/issues/696) Updated to infection v0.16.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#700](https://github.com/shlinkio/shlink/issues/700) Fixed migration not working with postgres.
|
||||
* [#690](https://github.com/shlinkio/shlink/issues/690) Fixed tags being incorrectly sluggified when filtering short URL lists, making results not be the expected.
|
||||
|
||||
|
||||
## [2.1.1] - 2020-03-28
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#697](https://github.com/shlinkio/shlink/issues/697) Recovered `.htaccess` file that was unintentionally removed in v2.1.0, making Shlink unusable with Apache.
|
||||
|
||||
|
||||
## [2.1.0] - 2020-03-28
|
||||
### Added
|
||||
* [#626](https://github.com/shlinkio/shlink/issues/626) Added support for Microsoft SQL Server.
|
||||
* [#556](https://github.com/shlinkio/shlink/issues/556) Short code lengths can now be customized, both globally and on a per-short URL basis.
|
||||
* [#541](https://github.com/shlinkio/shlink/issues/541) Added a request ID that is returned on `X-Request-Id` header, can be provided from outside and is set in log entries.
|
||||
* [#642](https://github.com/shlinkio/shlink/issues/642) IP geolocation is now performed over the non-anonymized IP address when using swoole.
|
||||
* [#521](https://github.com/shlinkio/shlink/issues/521) The long URL for any existing short URL can now be edited using the `PATCH /short-urls/{shortCode}` endpoint.
|
||||
|
||||
### Changed
|
||||
* [#656](https://github.com/shlinkio/shlink/issues/656) Updated to PHPUnit 9.
|
||||
* [#641](https://github.com/shlinkio/shlink/issues/641) Added two new flags to the `visit:locate` command, `--retry` and `--all`.
|
||||
|
||||
* When `--retry` is provided, it will try to re-locate visits which IP address was originally considered not found, in case it was a temporal issue.
|
||||
* When `--all` is provided together with `--retry`, it will try to re-locate all existing visits. A warning and confirmation are displayed, as this can have side effects.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#665](https://github.com/shlinkio/shlink/issues/665) Fixed `base_url_redirect_to` simplified config option not being properly parsed.
|
||||
* [#663](https://github.com/shlinkio/shlink/issues/663) Fixed Shlink allowing short URLs to be created with an empty custom slug.
|
||||
* [#678](https://github.com/shlinkio/shlink/issues/678) Fixed `db` commands not running in a non-interactive way.
|
||||
|
||||
|
||||
## [2.0.5] - 2020-02-09
|
||||
### Added
|
||||
* [#651](https://github.com/shlinkio/shlink/issues/651) Documented how Shlink behaves when using multiple domains.
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#648](https://github.com/shlinkio/shlink/issues/648) Ensured any user can write in log files, in case shlink is run by several system users.
|
||||
* [#650](https://github.com/shlinkio/shlink/issues/650) Ensured default domain is ignored when trying to create a short URL.
|
||||
|
||||
|
||||
## [2.0.4] - 2020-02-02
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#577](https://github.com/shlinkio/shlink/issues/577) Wrapped params used to customize short URL lists into a DTO with implicit validation.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#620](https://github.com/shlinkio/shlink/issues/620) Ensured "controlled" errors (like validation errors and such) won't be logged with error level, preventing logs to be polluted.
|
||||
* [#637](https://github.com/shlinkio/shlink/issues/637) Fixed several work flows in which short URLs with domain are handled form the API.
|
||||
* [#644](https://github.com/shlinkio/shlink/issues/644) Fixed visits to short URL on non-default domain being linked to the URL on default domain with the same short code.
|
||||
* [#643](https://github.com/shlinkio/shlink/issues/643) Fixed searching on short URL lists not taking into consideration the domain name.
|
||||
|
||||
|
||||
## [2.0.3] - 2020-01-27
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#624](https://github.com/shlinkio/shlink/issues/624) Fixed order in which headers for remote IP detection are inspected.
|
||||
* [#623](https://github.com/shlinkio/shlink/issues/623) Fixed short URLs metadata being impossible to reset.
|
||||
* [#628](https://github.com/shlinkio/shlink/issues/628) Fixed `GET /short-urls/{shortCode}` REST endpoint returning a 404 for short URLs which are not enabled.
|
||||
* [#621](https://github.com/shlinkio/shlink/issues/621) Fixed permission denied error when updating same GeoLite file version more than once.
|
||||
|
||||
|
||||
## [2.0.2] - 2020-01-12
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#614](https://github.com/shlinkio/shlink/issues/614) Fixed `OPTIONS` requests including the `Origin` header not always returning an empty body with status 2xx.
|
||||
* [#615](https://github.com/shlinkio/shlink/issues/615) Fixed query args with no value being lost from the long URL when users are redirected.
|
||||
|
||||
|
||||
## [2.0.1] - 2020-01-10
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#607](https://github.com/shlinkio/shlink/issues/607) Added missing info on UPGRADE.md doc.
|
||||
* [#610](https://github.com/shlinkio/shlink/issues/610) Fixed use of hardcoded quotes on a database migration which makes it fail on postgres.
|
||||
* [#605](https://github.com/shlinkio/shlink/issues/605) Fixed crashes occurring when migrating from old Shlink versions with nullable DB columns that are assigned to non-nullable entity typed props.
|
||||
|
||||
|
||||
## [2.0.0] - 2020-01-08
|
||||
### Added
|
||||
* [#429](https://github.com/shlinkio/shlink/issues/429) Added support for PHP 7.4
|
||||
* [#529](https://github.com/shlinkio/shlink/issues/529) Created an UPGRADING.md file explaining how to upgrade from v1.x to v2.x
|
||||
* [#594](https://github.com/shlinkio/shlink/issues/594) Updated external shlink packages, including installer v4.0, which adds the option to ask for the redis cluster config.
|
||||
|
||||
### Changed
|
||||
* [#592](https://github.com/shlinkio/shlink/issues/592) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.1.0.
|
||||
* [#530](https://github.com/shlinkio/shlink/issues/530) Migrated project from deprecated `zendframework` components to the new `laminas` and `mezzio` ones.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#429](https://github.com/shlinkio/shlink/issues/429) Dropped support for PHP 7.2 and 7.3
|
||||
|
||||
* [#229](https://github.com/shlinkio/shlink/issues/229) Remove everything which was deprecated, including:
|
||||
|
||||
* Preview generation feature completely removed.
|
||||
* Authentication against REST API using JWT is no longer supported.
|
||||
|
||||
See [UPGRADE](UPGRADE.md#from-v1x-to-v2x) doc in order to get details on how to migrate to this version.
|
||||
|
||||
### Fixed
|
||||
* [#600](https://github.com/shlinkio/shlink/issues/600) Fixed health action so that it works with and without version in the path.
|
||||
@@ -1,14 +1,10 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["visitedUrl", "type"],
|
||||
"required": ["type"],
|
||||
"allOf": [{
|
||||
"$ref": "./Visit.json"
|
||||
}],
|
||||
"properties": {
|
||||
"visitedUrl": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["device", "language", "query-param"],
|
||||
"description": "The type of the condition, which will condition the logic used to match it"
|
||||
"enum": ["device", "language", "query-param", "ip-address"],
|
||||
"description": "The type of the condition, which will determine the logic used to match it"
|
||||
},
|
||||
"matchKey": {
|
||||
"type": ["string", "null"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["referer", "date", "userAgent", "visitLocation"],
|
||||
"required": ["referer", "date", "userAgent", "visitLocation", "potentialBot", "visitedUrl"],
|
||||
"properties": {
|
||||
"referer": {
|
||||
"type": "string",
|
||||
@@ -21,6 +21,10 @@
|
||||
"potentialBot": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if Shlink thinks this visit comes potentially from a bot or crawler"
|
||||
},
|
||||
"visitedUrl": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
|
||||
"visitLocation": null,
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
@@ -115,14 +116,16 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null,
|
||||
"potentialBot": true
|
||||
"potentialBot": true,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -103,7 +103,8 @@
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
|
||||
"visitLocation": null,
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
@@ -118,14 +119,16 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null,
|
||||
"potentialBot": true
|
||||
"potentialBot": true,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -103,7 +103,8 @@
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
|
||||
"visitLocation": null,
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
@@ -118,14 +119,16 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null,
|
||||
"potentialBot": true
|
||||
"potentialBot": true,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -94,7 +94,8 @@
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
|
||||
"visitLocation": null,
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
@@ -109,14 +110,16 @@
|
||||
"regionName": "California",
|
||||
"timezone": "America/Los_Angeles"
|
||||
},
|
||||
"potentialBot": false
|
||||
"potentialBot": false,
|
||||
"visitedUrl": "https://s.test"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null,
|
||||
"potentialBot": true
|
||||
"potentialBot": true,
|
||||
"visitedUrl": "https://s.test"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
|
||||
@@ -77,12 +77,12 @@
|
||||
"priority": 3,
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "foo",
|
||||
"matchValue": "bar"
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "hello",
|
||||
"matchValue": "world"
|
||||
}
|
||||
@@ -209,12 +209,12 @@
|
||||
"longUrl": "https://example.com/query-foo-bar-hello-world",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "foo",
|
||||
"matchValue": "bar"
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "hello",
|
||||
"matchValue": "world"
|
||||
}
|
||||
@@ -280,12 +280,12 @@
|
||||
"priority": 3,
|
||||
"conditions": [
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "foo",
|
||||
"matchValue": "bar"
|
||||
},
|
||||
{
|
||||
"type": "query",
|
||||
"type": "query-param",
|
||||
"matchKey": "hello",
|
||||
"matchValue": "world"
|
||||
}
|
||||
|
||||
@@ -9,11 +9,14 @@ return [
|
||||
'cli' => [
|
||||
'commands' => [
|
||||
Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class,
|
||||
Command\ShortUrl\EditShortUrlCommand::NAME => Command\ShortUrl\EditShortUrlCommand::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
|
||||
Command\ShortUrl\DeleteExpiredShortUrlsCommand::NAME =>
|
||||
Command\ShortUrl\DeleteExpiredShortUrlsCommand::class,
|
||||
|
||||
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
|
||||
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
||||
@@ -40,6 +43,10 @@ return [
|
||||
|
||||
Command\RedirectRule\ManageRedirectRulesCommand::NAME =>
|
||||
Command\RedirectRule\ManageRedirectRulesCommand::class,
|
||||
|
||||
Command\Integration\MatomoSendVisitsCommand::NAME => Command\Integration\MatomoSendVisitsCommand::class,
|
||||
|
||||
Command\Config\ReadEnvVarCommand::NAME => Command\Config\ReadEnvVarCommand::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||
use Shlinkio\Shlink\Core\Matomo;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleService;
|
||||
@@ -40,11 +41,13 @@ return [
|
||||
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\EditShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteExpiredShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -70,6 +73,10 @@ return [
|
||||
Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\RedirectRule\ManageRedirectRulesCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Integration\MatomoSendVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\ReadEnvVarCommand::class => InvokableFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -88,6 +95,7 @@ return [
|
||||
ShortUrlStringifier::class,
|
||||
UrlShortenerOptions::class,
|
||||
],
|
||||
Command\ShortUrl\EditShortUrlCommand::class => [ShortUrl\ShortUrlService::class, ShortUrlStringifier::class],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||
ShortUrl\ShortUrlListService::class,
|
||||
@@ -96,6 +104,7 @@ return [
|
||||
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
||||
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
|
||||
Command\ShortUrl\DeleteExpiredShortUrlsCommand::class => [ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
|
||||
Command\Visit\LocateVisitsCommand::class => [
|
||||
@@ -127,6 +136,11 @@ return [
|
||||
RedirectRule\RedirectRuleHandler::class,
|
||||
],
|
||||
|
||||
Command\Integration\MatomoSendVisitsCommand::class => [
|
||||
Matomo\MatomoOptions::class,
|
||||
Matomo\MatomoVisitSender::class,
|
||||
],
|
||||
|
||||
Command\Db\CreateDatabaseCommand::class => [
|
||||
LockFactory::class,
|
||||
Util\ProcessRunner::class,
|
||||
|
||||
@@ -50,11 +50,11 @@ class ListKeysCommand extends Command
|
||||
$enabledOnly = $input->getOption('enabled-only');
|
||||
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
$expiration = $apiKey->expirationDate;
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [sprintf($messagePattern, $apiKey), sprintf($messagePattern, $apiKey->name() ?? '-')];
|
||||
$rowData = [sprintf($messagePattern, $apiKey), sprintf($messagePattern, $apiKey->name ?? '-')];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||
}
|
||||
|
||||
68
module/CLI/src/Command/Config/ReadEnvVarCommand.php
Normal file
68
module/CLI/src/Command/Config/ReadEnvVarCommand.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Closure;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Shlinkio\Shlink\Config\formatEnvVarValue;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
use function sprintf;
|
||||
|
||||
class ReadEnvVarCommand extends Command
|
||||
{
|
||||
public const NAME = 'env-var:read';
|
||||
|
||||
/** @var Closure(string $envVar): mixed */
|
||||
private readonly Closure $loadEnvVar;
|
||||
|
||||
public function __construct(?Closure $loadEnvVar = null)
|
||||
{
|
||||
$this->loadEnvVar = $loadEnvVar ?? static fn (string $envVar) => EnvVars::from($envVar)->loadFromEnv();
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setHidden()
|
||||
->setDescription('Display current value for an env var')
|
||||
->addArgument('envVar', InputArgument::REQUIRED, 'The env var to read');
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$envVar = $input->getArgument('envVar');
|
||||
$validEnvVars = enumValues(EnvVars::class);
|
||||
|
||||
if ($envVar === null) {
|
||||
$envVar = $io->choice('Select the env var to read', $validEnvVars);
|
||||
}
|
||||
|
||||
if (! contains($envVar, $validEnvVars)) {
|
||||
throw new InvalidArgumentException(sprintf('%s is not a valid Shlink environment variable', $envVar));
|
||||
}
|
||||
|
||||
$input->setArgument('envVar', $envVar);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$envVar = $input->getArgument('envVar');
|
||||
$output->writeln(formatEnvVarValue(($this->loadEnvVar)($envVar)));
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
|
||||
public function __construct(
|
||||
LockFactory $locker,
|
||||
private ProcessRunnerInterface $processRunner,
|
||||
private readonly ProcessRunnerInterface $processRunner,
|
||||
PhpExecutableFinder $phpFinder,
|
||||
) {
|
||||
parent::__construct($locker);
|
||||
|
||||
@@ -33,6 +33,9 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand
|
||||
->addArgument('domain', InputArgument::REQUIRED, 'The domain which visits we want to get.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||
{
|
||||
$domain = $input->getArgument('domain');
|
||||
@@ -44,7 +47,7 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand
|
||||
*/
|
||||
protected function mapExtraFields(Visit $visit): array
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
$shortUrl = $visit->shortUrl;
|
||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||
}
|
||||
}
|
||||
|
||||
140
module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php
Normal file
140
module/CLI/src/Command/Integration/MatomoSendVisitsCommand.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Integration;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSenderInterface;
|
||||
use Shlinkio\Shlink\Core\Matomo\VisitSendingProgressTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Throwable;
|
||||
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
use function Shlinkio\Shlink\Core\dateRangeToHumanFriendly;
|
||||
use function sprintf;
|
||||
|
||||
class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTrackerInterface
|
||||
{
|
||||
public const NAME = 'integration:matomo:send-visits';
|
||||
|
||||
private readonly bool $matomoEnabled;
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(MatomoOptions $matomoOptions, private readonly MatomoVisitSenderInterface $visitSender)
|
||||
{
|
||||
$this->matomoEnabled = $matomoOptions->enabled;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$help = <<<HELP
|
||||
This command allows you to send existing visits from this Shlink instance to the configured Matomo server.
|
||||
|
||||
Its intention is to allow you to configure Matomo at some point in time, and still have your whole visits
|
||||
history tracked there.
|
||||
|
||||
This command will unconditionally send to Matomo all visits for a specific date range, so make sure you
|
||||
provide the proper limits to avoid duplicated visits.
|
||||
|
||||
Send all visits created so far:
|
||||
<info>%command.name%</info>
|
||||
|
||||
Send all visits created before 2024:
|
||||
<info>%command.name% --until 2023-12-31</info>
|
||||
|
||||
Send all visits created after a specific day:
|
||||
<info>%command.name% --since 2022-03-27</info>
|
||||
|
||||
Send all visits created during 2022:
|
||||
<info>%command.name% --since 2022-01-01 --until 2022-12-31</info>
|
||||
HELP;
|
||||
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(sprintf(
|
||||
'%sSend existing visits to the configured matomo instance',
|
||||
$this->matomoEnabled ? '' : '[MATOMO INTEGRATION DISABLED] ',
|
||||
))
|
||||
->setHelp($help)
|
||||
->addOption(
|
||||
'since',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Only visits created since this date, inclusively, will be sent to Matomo',
|
||||
)
|
||||
->addOption(
|
||||
'until',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Only visits created until this date, inclusively, will be sent to Matomo',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (! $this->matomoEnabled) {
|
||||
$this->io->warning('Matomo integration is not enabled in this Shlink instance');
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
// TODO Validate provided date formats
|
||||
$since = $input->getOption('since');
|
||||
$until = $input->getOption('until');
|
||||
$dateRange = buildDateRange(
|
||||
startDate: $since !== null ? Chronos::parse($since) : null,
|
||||
endDate: $until !== null ? Chronos::parse($until) : null,
|
||||
);
|
||||
|
||||
if ($input->isInteractive()) {
|
||||
$this->io->warning([
|
||||
'You are about to send visits from this Shlink instance to Matomo',
|
||||
'Resolved date range -> ' . dateRangeToHumanFriendly($dateRange),
|
||||
'Shlink will not check for already sent visits, which could result in some duplications. Make sure '
|
||||
. 'you have verified only visits in the right date range are going to be sent.',
|
||||
]);
|
||||
if (! $this->io->confirm('Continue?', default: false)) {
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->visitSender->sendVisitsInDateRange($dateRange, $this);
|
||||
|
||||
match (true) {
|
||||
$result->hasFailures() && $result->hasSuccesses() => $this->io->warning(
|
||||
sprintf('%s visits sent to Matomo. %s failed.', $result->successfulVisits, $result->failedVisits),
|
||||
),
|
||||
$result->hasFailures() => $this->io->error(
|
||||
sprintf('Failed to send %s visits to Matomo.', $result->failedVisits),
|
||||
),
|
||||
$result->hasSuccesses() => $this->io->success(
|
||||
sprintf('%s visits sent to Matomo.', $result->successfulVisits),
|
||||
),
|
||||
default => $this->io->info('There was no visits matching provided date range.'),
|
||||
};
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
public function success(int $index): void
|
||||
{
|
||||
$this->io->write('.');
|
||||
}
|
||||
|
||||
public function error(int $index, Throwable $e): void
|
||||
{
|
||||
$this->io->write('<error>E</error>');
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,24 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function explode;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
|
||||
use function sprintf;
|
||||
|
||||
class CreateShortUrlCommand extends Command
|
||||
@@ -29,6 +23,7 @@ class CreateShortUrlCommand extends Command
|
||||
public const NAME = 'short-url:create';
|
||||
|
||||
private ?SymfonyStyle $io;
|
||||
private readonly ShortUrlDataInput $shortUrlDataInput;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlShortenerInterface $urlShortener,
|
||||
@@ -36,6 +31,7 @@ class CreateShortUrlCommand extends Command
|
||||
private readonly UrlShortenerOptions $options,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->shortUrlDataInput = new ShortUrlDataInput($this);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -43,26 +39,11 @@ class CreateShortUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Generates a short URL for provided long URL and returns it')
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
'Tags to apply to the new short URL',
|
||||
)
|
||||
->addOption(
|
||||
'valid-since',
|
||||
's',
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.',
|
||||
)
|
||||
->addOption(
|
||||
'valid-until',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.',
|
||||
'The domain to which this short URL will be attached.',
|
||||
)
|
||||
->addOption(
|
||||
'custom-slug',
|
||||
@@ -70,30 +51,6 @@ class CreateShortUrlCommand extends Command
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'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(
|
||||
'max-visits',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.',
|
||||
)
|
||||
->addOption(
|
||||
'find-if-exists',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'This will force existing matching URL to be returned if found, instead of creating a new one.',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The domain to which this short URL will be attached.',
|
||||
)
|
||||
->addOption(
|
||||
'short-code-length',
|
||||
'l',
|
||||
@@ -101,16 +58,16 @@ class CreateShortUrlCommand extends Command
|
||||
'The length for generated short code (it will be ignored if --custom-slug was provided).',
|
||||
)
|
||||
->addOption(
|
||||
'crawlable',
|
||||
'r',
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if this URL will be included as "Allow" in Shlink\'s robots.txt.',
|
||||
'path-prefix',
|
||||
'p',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Prefix to prepend before the generated short code or provided custom slug',
|
||||
)
|
||||
->addOption(
|
||||
'no-forward-query',
|
||||
'w',
|
||||
'find-if-exists',
|
||||
'f',
|
||||
InputOption::VALUE_NONE,
|
||||
'Disables the forwarding of the query string to the long URL, when the new short URL is visited.',
|
||||
'This will force existing matching URL to be returned if found, instead of creating a new one.',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,32 +93,17 @@ class CreateShortUrlCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = $this->getIO($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error('A URL was not provided!');
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
|
||||
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
|
||||
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
|
||||
|
||||
try {
|
||||
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
|
||||
ShortUrlInputFilter::LONG_URL => $longUrl,
|
||||
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
|
||||
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
|
||||
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::DOMAIN => $input->getOption('domain'),
|
||||
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
ShortUrlInputFilter::TAGS => $tags,
|
||||
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
||||
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
||||
], $this->options));
|
||||
$result = $this->urlShortener->shorten($this->shortUrlDataInput->toShortUrlCreation(
|
||||
$input,
|
||||
$this->options,
|
||||
customSlugField: 'custom-slug',
|
||||
shortCodeLengthField: 'short-code-length',
|
||||
pathPrefixField: 'path-prefix',
|
||||
findIfExistsField: 'find-if-exists',
|
||||
domainField: 'domain',
|
||||
));
|
||||
|
||||
$result->onEventDispatchingError(static fn () => $io->isVerbose() && $io->warning(
|
||||
'Short URL properly created, but the real-time updates cannot be notified when generating the '
|
||||
@@ -169,7 +111,7 @@ class CreateShortUrlCommand extends Command
|
||||
));
|
||||
|
||||
$io->writeln([
|
||||
sprintf('Processed long URL: <info>%s</info>', $longUrl),
|
||||
sprintf('Processed long URL: <info>%s</info>', $result->shortUrl->getLongUrl()),
|
||||
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
|
||||
]);
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
@@ -181,6 +123,6 @@ class CreateShortUrlCommand extends Command
|
||||
|
||||
private function getIO(InputInterface $input, OutputInterface $output): SymfonyStyle
|
||||
{
|
||||
return $this->io ?? ($this->io = new SymfonyStyle($input, $output));
|
||||
return $this->io ??= new SymfonyStyle($input, $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DeleteExpiredShortUrlsCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete-expired';
|
||||
|
||||
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(
|
||||
'Deletes all short URLs that are considered expired, because they have a validUntil date in the past',
|
||||
)
|
||||
->addOption(
|
||||
'evaluate-max-visits',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'Also take into consideration short URLs which have reached their max amount of visits.',
|
||||
)
|
||||
->addOption('force', 'f', InputOption::VALUE_NONE, 'Delete short URLs with no confirmation')
|
||||
->addOption(
|
||||
'dry-run',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'Delete short URLs with no confirmation',
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$force = $input->getOption('force') || ! $input->isInteractive();
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
$conditions = new ExpiredShortUrlsConditions(maxVisitsReached: $input->getOption('evaluate-max-visits'));
|
||||
|
||||
if (! $force && ! $dryRun) {
|
||||
$io->warning([
|
||||
'Careful!',
|
||||
'You are about to perform a destructive operation that can result in deleted short URLs and visits.',
|
||||
'This action cannot be undone. Proceed at your own risk',
|
||||
]);
|
||||
if (! $io->confirm('Continue?', default: false)) {
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$result = $this->deleteShortUrlService->countExpiredShortUrls($conditions);
|
||||
$io->success(sprintf('There are %s expired short URLs matching provided conditions', $result));
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
$result = $this->deleteShortUrlService->deleteExpiredShortUrls($conditions);
|
||||
$io->success(sprintf('%s expired short URLs have been deleted', $result));
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
71
module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php
Normal file
71
module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
|
||||
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class EditShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:edit';
|
||||
|
||||
private readonly ShortUrlDataInput $shortUrlDataInput;
|
||||
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||
|
||||
public function __construct(
|
||||
private readonly ShortUrlServiceInterface $shortUrlService,
|
||||
private readonly ShortUrlStringifierInterface $stringifier,
|
||||
) {
|
||||
parent::__construct();
|
||||
|
||||
$this->shortUrlDataInput = new ShortUrlDataInput($this, longUrlAsOption: true);
|
||||
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||
$this,
|
||||
shortCodeDesc: 'The short code to edit',
|
||||
domainDesc: 'The domain to which the short URL is attached.',
|
||||
);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Edit an existing short URL');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||
|
||||
try {
|
||||
$shortUrl = $this->shortUrlService->updateShortUrl(
|
||||
$identifier,
|
||||
$this->shortUrlDataInput->toShortUrlEdition($input),
|
||||
);
|
||||
|
||||
$io->success(sprintf('Short URL "%s" properly edited', $this->stringifier->stringify($shortUrl)));
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (ShortUrlNotFoundException $e) {
|
||||
$io->error(sprintf('Short URL not found for "%s"', $identifier->__toString()));
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,9 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||
{
|
||||
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||
|
||||
@@ -9,13 +9,14 @@ use Shlinkio\Shlink\CLI\Input\StartDateOption;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
|
||||
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -23,16 +24,14 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_pad;
|
||||
use function explode;
|
||||
use function implode;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\map;
|
||||
use function sprintf;
|
||||
|
||||
class ListShortUrlsCommand extends Command
|
||||
{
|
||||
use PagerfantaUtilsTrait;
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
|
||||
private readonly StartDateOption $startDateOption;
|
||||
@@ -40,7 +39,7 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
public function __construct(
|
||||
private readonly ShortUrlListServiceInterface $shortUrlService,
|
||||
private readonly DataTransformerInterface $transformer,
|
||||
private readonly ShortUrlDataTransformerInterface $transformer,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->startDateOption = new StartDateOption($this, 'short URLs');
|
||||
@@ -176,6 +175,10 @@ class ListShortUrlsCommand extends Command
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string> $columnsMap
|
||||
* @return Paginator<ShortUrlWithVisitsSummary>
|
||||
*/
|
||||
private function renderPage(
|
||||
OutputInterface $output,
|
||||
array $columnsMap,
|
||||
@@ -184,15 +187,15 @@ class ListShortUrlsCommand extends Command
|
||||
): Paginator {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$rows = array_map(function (ShortUrl $shortUrl) use ($columnsMap) {
|
||||
$rawShortUrl = $this->transformer->transform($shortUrl);
|
||||
return array_map(fn (callable $call) => $call($rawShortUrl, $shortUrl), $columnsMap);
|
||||
}, [...$shortUrls]);
|
||||
$rows = map([...$shortUrls], function (ShortUrlWithVisitsSummary $shortUrl) use ($columnsMap) {
|
||||
$serializedShortUrl = $this->transformer->transform($shortUrl);
|
||||
return map($columnsMap, fn (callable $call) => $call($serializedShortUrl, $shortUrl->shortUrl));
|
||||
});
|
||||
|
||||
ShlinkTable::default($output)->render(
|
||||
array_keys($columnsMap),
|
||||
$rows,
|
||||
$all ? null : $this->formatCurrentPageMessage($shortUrls, 'Page %s of %s'),
|
||||
$all ? null : PagerfantaUtils::formatCurrentPageMessage($shortUrls, 'Page %s of %s'),
|
||||
);
|
||||
|
||||
return $shortUrls;
|
||||
@@ -209,6 +212,9 @@ class ListShortUrlsCommand extends Command
|
||||
return $dir === null ? $field : sprintf('%s-%s', $field, $dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string>
|
||||
*/
|
||||
private function resolveColumnsMap(InputInterface $input): array
|
||||
{
|
||||
$pickProp = static fn (string $prop): callable => static fn (array $shortUrl) => $shortUrl[$prop];
|
||||
@@ -229,11 +235,11 @@ class ListShortUrlsCommand extends Command
|
||||
}
|
||||
if ($input->getOption('show-api-key')) {
|
||||
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||
$shortUrl->authorApiKey()?->__toString() ?? '';
|
||||
$shortUrl->authorApiKey?->__toString() ?? '';
|
||||
}
|
||||
if ($input->getOption('show-api-key-name')) {
|
||||
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): ?string =>
|
||||
$shortUrl->authorApiKey()?->name();
|
||||
$shortUrl->authorApiKey?->name;
|
||||
}
|
||||
|
||||
return $columnsMap;
|
||||
|
||||
@@ -33,6 +33,9 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
|
||||
->addArgument('tag', InputArgument::REQUIRED, 'The tag which visits we want to get.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||
{
|
||||
$tag = $input->getArgument('tag');
|
||||
@@ -44,7 +47,7 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
|
||||
*/
|
||||
protected function mapExtraFields(Visit $visit): array
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
$shortUrl = $visit->shortUrl;
|
||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Paginator<Visit> $paginator
|
||||
*/
|
||||
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||
{
|
||||
$extraKeys = [];
|
||||
@@ -54,9 +57,12 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
$extraKeys = array_keys($extraFields);
|
||||
|
||||
$rowData = [
|
||||
...$visit->jsonSerialize(),
|
||||
'country' => $visit->getVisitLocation()?->getCountryName() ?? 'Unknown',
|
||||
'city' => $visit->getVisitLocation()?->getCityName() ?? 'Unknown',
|
||||
'referer' => $visit->referer,
|
||||
'date' => $visit->date->toAtomString(),
|
||||
'userAgent' => $visit->userAgent,
|
||||
'potentialBot' => $visit->potentialBot,
|
||||
'country' => $visit->getVisitLocation()?->countryName ?? 'Unknown',
|
||||
'city' => $visit->getVisitLocation()?->cityName ?? 'Unknown',
|
||||
...$extraFields,
|
||||
];
|
||||
|
||||
@@ -71,6 +77,9 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
abstract protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator;
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,9 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
->setDescription('Returns the list of non-orphan visits.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||
{
|
||||
return $this->visitsHelper->nonOrphanVisits(new VisitsParams($dateRange));
|
||||
@@ -40,7 +43,7 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
*/
|
||||
protected function mapExtraFields(Visit $visit): array
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
$shortUrl = $visit->shortUrl;
|
||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Paginator<Visit>
|
||||
*/
|
||||
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||
{
|
||||
$rawType = $input->getOption('type');
|
||||
@@ -42,6 +45,6 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||
*/
|
||||
protected function mapExtraFields(Visit $visit): array
|
||||
{
|
||||
return ['type' => $visit->type()->value];
|
||||
return ['type' => $visit->type->value];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
*/
|
||||
public function geolocateVisit(Visit $visit): Location
|
||||
{
|
||||
$ipAddr = $visit->getRemoteAddr() ?? '?';
|
||||
$ipAddr = $visit->remoteAddr ?? '?';
|
||||
$this->io->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||
|
||||
try {
|
||||
@@ -154,9 +154,9 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
|
||||
public function onVisitLocated(VisitLocation $visitLocation, Visit $visit): void
|
||||
{
|
||||
if (! $visitLocation->isEmpty()) {
|
||||
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->getCountryName()));
|
||||
} elseif ($visit->hasRemoteAddr() && $visit->getRemoteAddr() !== IpAddress::LOCALHOST) {
|
||||
if (! $visitLocation->isEmpty) {
|
||||
$this->io->writeln(sprintf(' [<info>Address located in "%s"</info>]', $visitLocation->countryName));
|
||||
} elseif ($visit->hasRemoteAddr() && $visit->remoteAddr !== IpAddress::LOCALHOST) {
|
||||
$this->io->writeln(' <comment>[Could not locate address]</comment>');
|
||||
}
|
||||
}
|
||||
|
||||
136
module/CLI/src/Input/ShortUrlDataInput.php
Normal file
136
module/CLI/src/Input/ShortUrlDataInput.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
|
||||
use function Shlinkio\Shlink\Core\splitByComma;
|
||||
|
||||
readonly final class ShortUrlDataInput
|
||||
{
|
||||
public function __construct(Command $command, private bool $longUrlAsOption = false)
|
||||
{
|
||||
if ($longUrlAsOption) {
|
||||
$command->addOption('long-url', 'l', InputOption::VALUE_REQUIRED, 'The long URL to set');
|
||||
} else {
|
||||
$command->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to set');
|
||||
}
|
||||
|
||||
$command
|
||||
->addOption(
|
||||
ShortUrlDataOption::TAGS->value,
|
||||
ShortUrlDataOption::TAGS->shortcut(),
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
'Tags to apply to the short URL',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::VALID_SINCE->value,
|
||||
ShortUrlDataOption::VALID_SINCE->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::VALID_UNTIL->value,
|
||||
ShortUrlDataOption::VALID_UNTIL->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::MAX_VISITS->value,
|
||||
ShortUrlDataOption::MAX_VISITS->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::TITLE->value,
|
||||
ShortUrlDataOption::TITLE->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A descriptive title for the short URL.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::CRAWLABLE->value,
|
||||
ShortUrlDataOption::CRAWLABLE->shortcut(),
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if this short URL will be included as "Allow" in Shlink\'s robots.txt.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::NO_FORWARD_QUERY->value,
|
||||
ShortUrlDataOption::NO_FORWARD_QUERY->shortcut(),
|
||||
InputOption::VALUE_NONE,
|
||||
'Disables the forwarding of the query string to the long URL, when the short URL is visited.',
|
||||
);
|
||||
}
|
||||
|
||||
public function toShortUrlEdition(InputInterface $input): ShortUrlEdition
|
||||
{
|
||||
return ShortUrlEdition::fromRawData($this->getCommonData($input));
|
||||
}
|
||||
|
||||
public function toShortUrlCreation(
|
||||
InputInterface $input,
|
||||
UrlShortenerOptions $options,
|
||||
string $customSlugField,
|
||||
string $shortCodeLengthField,
|
||||
string $pathPrefixField,
|
||||
string $findIfExistsField,
|
||||
string $domainField,
|
||||
): ShortUrlCreation {
|
||||
$shortCodeLength = $input->getOption($shortCodeLengthField) ?? $options->defaultShortCodesLength;
|
||||
return ShortUrlCreation::fromRawData([
|
||||
...$this->getCommonData($input),
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => $input->getOption($customSlugField),
|
||||
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
ShortUrlInputFilter::PATH_PREFIX => $input->getOption($pathPrefixField),
|
||||
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption($findIfExistsField),
|
||||
ShortUrlInputFilter::DOMAIN => $input->getOption($domainField),
|
||||
], $options);
|
||||
}
|
||||
|
||||
private function getCommonData(InputInterface $input): array
|
||||
{
|
||||
$longUrl = $this->longUrlAsOption ? $input->getOption('long-url') : $input->getArgument('longUrl');
|
||||
$data = [ShortUrlInputFilter::LONG_URL => $longUrl];
|
||||
|
||||
// Avoid setting arguments that were not explicitly provided.
|
||||
// This is important when editing short URLs and should not make a difference when creating.
|
||||
if (ShortUrlDataOption::VALID_SINCE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::VALID_SINCE] = $input->getOption('valid-since');
|
||||
}
|
||||
if (ShortUrlDataOption::VALID_UNTIL->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::VALID_UNTIL] = $input->getOption('valid-until');
|
||||
}
|
||||
if (ShortUrlDataOption::MAX_VISITS->wasProvided($input)) {
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$data[ShortUrlInputFilter::MAX_VISITS] = $maxVisits !== null ? (int) $maxVisits : null;
|
||||
}
|
||||
if (ShortUrlDataOption::TAGS->wasProvided($input)) {
|
||||
$tags = array_unique(flatten(array_map(splitByComma(...), $input->getOption('tags'))));
|
||||
$data[ShortUrlInputFilter::TAGS] = $tags;
|
||||
}
|
||||
if (ShortUrlDataOption::TITLE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::TITLE] = $input->getOption('title');
|
||||
}
|
||||
if (ShortUrlDataOption::CRAWLABLE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::CRAWLABLE] = $input->getOption('crawlable');
|
||||
}
|
||||
if (ShortUrlDataOption::NO_FORWARD_QUERY->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::FORWARD_QUERY] = !$input->getOption('no-forward-query');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
41
module/CLI/src/Input/ShortUrlDataOption.php
Normal file
41
module/CLI/src/Input/ShortUrlDataOption.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
enum ShortUrlDataOption: string
|
||||
{
|
||||
case TAGS = 'tags';
|
||||
case VALID_SINCE = 'valid-since';
|
||||
case VALID_UNTIL = 'valid-until';
|
||||
case MAX_VISITS = 'max-visits';
|
||||
case TITLE = 'title';
|
||||
case CRAWLABLE = 'crawlable';
|
||||
case NO_FORWARD_QUERY = 'no-forward-query';
|
||||
|
||||
public function shortcut(): ?string
|
||||
{
|
||||
return match ($this) {
|
||||
self::TAGS => 't',
|
||||
self::VALID_SINCE => 's',
|
||||
self::VALID_UNTIL => 'u',
|
||||
self::MAX_VISITS => 'm',
|
||||
self::TITLE => null,
|
||||
self::CRAWLABLE => 'r',
|
||||
self::NO_FORWARD_QUERY => 'w',
|
||||
};
|
||||
}
|
||||
|
||||
public function wasProvided(InputInterface $input): bool
|
||||
{
|
||||
$option = sprintf('--%s', $this->value);
|
||||
$shortcut = $this->shortcut();
|
||||
|
||||
return $input->hasParameterOption($shortcut === null ? $option : [$option, sprintf('-%s', $shortcut)]);
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,9 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
$this->askMandatory('Query param name?', $io),
|
||||
$this->askOptional('Query param value?', $io),
|
||||
),
|
||||
RedirectConditionType::IP_ADDRESS => RedirectCondition::forIpAddress(
|
||||
$this->askMandatory('IP address, CIDR block or wildcard-pattern (1.2.*.*)', $io),
|
||||
),
|
||||
};
|
||||
|
||||
$continue = $io->confirm('Do you want to add another condition?');
|
||||
|
||||
54
module/CLI/test/Command/Config/ReadEnvVarCommandTest.php
Normal file
54
module/CLI/test/Command/Config/ReadEnvVarCommandTest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Config;
|
||||
|
||||
use Monolog\Test\TestCase;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\ReadEnvVarCommand;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ReadEnvVarCommandTest extends TestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private string $envVarValue = 'the_env_var_value';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ReadEnvVarCommand(fn () => $this->envVarValue));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function errorIsThrownIfProvidedEnvVarIsInvalid(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('foo is not a valid Shlink environment variable');
|
||||
|
||||
$this->commandTester->execute(['envVar' => 'foo']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function valueIsPrintedIfProvidedEnvVarIsValid(): void
|
||||
{
|
||||
$this->commandTester->execute(['envVar' => EnvVars::BASE_PATH->value]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringNotContainsString('Select the env var to read', $output);
|
||||
self::assertStringContainsString($this->envVarValue, $output);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function envVarNameIsRequestedIfArgumentIsMissing(): void
|
||||
{
|
||||
$this->commandTester->setInputs([EnvVars::BASE_PATH->value]);
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Select the env var to read', $output);
|
||||
self::assertStringContainsString($this->envVarValue, $output);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Driver;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\DBAL\Schema\AbstractSchemaManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
@@ -31,6 +32,7 @@ class CreateDatabaseCommandTest extends TestCase
|
||||
private MockObject & ProcessRunnerInterface $processHelper;
|
||||
private MockObject & Connection $regularConn;
|
||||
private MockObject & ClassMetadataFactory $metadataFactory;
|
||||
/** @var MockObject&AbstractSchemaManager<SQLitePlatform> */
|
||||
private MockObject & AbstractSchemaManager $schemaManager;
|
||||
private MockObject & Driver $driver;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class GetDomainVisitsCommandTest extends TestCase
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| Referer | Date | User agent | Country | City | Short Url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
|
||||
OUTPUT,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Integration;
|
||||
|
||||
use Exception;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Integration\MatomoSendVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSenderInterface;
|
||||
use Shlinkio\Shlink\Core\Matomo\Model\SendVisitsResult;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
|
||||
class MatomoSendVisitsCommandTest extends TestCase
|
||||
{
|
||||
private MockObject & MatomoVisitSenderInterface $visitSender;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->visitSender = $this->createMock(MatomoVisitSenderInterface::class);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function warningDisplayedIfIntegrationIsNotEnabled(): void
|
||||
{
|
||||
[$output, $exitCode] = $this->executeCommand(matomoEnabled: false);
|
||||
|
||||
self::assertStringContainsString('Matomo integration is not enabled in this Shlink instance', $output);
|
||||
self::assertEquals(ExitCode::EXIT_WARNING, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([true], 'interactive')]
|
||||
#[TestWith([false], 'not interactive')]
|
||||
public function warningIsOnlyDisplayedInInteractiveMode(bool $interactive): void
|
||||
{
|
||||
$this->visitSender->method('sendVisitsInDateRange')->willReturn(new SendVisitsResult());
|
||||
|
||||
[$output] = $this->executeCommand(['y'], ['interactive' => $interactive]);
|
||||
|
||||
if ($interactive) {
|
||||
self::assertStringContainsString('You are about to send visits', $output);
|
||||
} else {
|
||||
self::assertStringNotContainsString('You are about to send visits', $output);
|
||||
}
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([true])]
|
||||
#[TestWith([false])]
|
||||
public function canCancelExecutionInInteractiveMode(bool $interactive): void
|
||||
{
|
||||
$this->visitSender->expects($this->exactly($interactive ? 0 : 1))->method('sendVisitsInDateRange')->willReturn(
|
||||
new SendVisitsResult(),
|
||||
);
|
||||
$this->executeCommand(['n'], ['interactive' => $interactive]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([new SendVisitsResult(), 'There was no visits matching provided date range'])]
|
||||
#[TestWith([new SendVisitsResult(successfulVisits: 10), '10 visits sent to Matomo.'])]
|
||||
#[TestWith([new SendVisitsResult(successfulVisits: 2), '2 visits sent to Matomo.'])]
|
||||
#[TestWith([new SendVisitsResult(failedVisits: 238), 'Failed to send 238 visits to Matomo.'])]
|
||||
#[TestWith([new SendVisitsResult(failedVisits: 18), 'Failed to send 18 visits to Matomo.'])]
|
||||
#[TestWith([new SendVisitsResult(successfulVisits: 2, failedVisits: 35), '2 visits sent to Matomo. 35 failed.'])]
|
||||
#[TestWith([new SendVisitsResult(successfulVisits: 81, failedVisits: 6), '81 visits sent to Matomo. 6 failed.'])]
|
||||
public function expectedResultIsDisplayed(SendVisitsResult $result, string $expectedResultMessage): void
|
||||
{
|
||||
$this->visitSender->expects($this->once())->method('sendVisitsInDateRange')->willReturn($result);
|
||||
[$output, $exitCode] = $this->executeCommand(['y']);
|
||||
|
||||
self::assertStringContainsString($expectedResultMessage, $output);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function printsResultOfSendingVisits(): void
|
||||
{
|
||||
$this->visitSender->method('sendVisitsInDateRange')->willReturnCallback(
|
||||
function (DateRange $_, MatomoSendVisitsCommand $command): SendVisitsResult {
|
||||
// Call it a few times for an easier match of its result in the command putput
|
||||
$command->success(0);
|
||||
$command->success(1);
|
||||
$command->success(2);
|
||||
$command->error(3, new Exception('Error'));
|
||||
$command->success(4);
|
||||
$command->error(5, new Exception('Error'));
|
||||
|
||||
return new SendVisitsResult();
|
||||
},
|
||||
);
|
||||
|
||||
[$output] = $this->executeCommand(['y']);
|
||||
|
||||
self::assertStringContainsString('...E.E', $output);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([[], 'All time'])]
|
||||
#[TestWith([['--since' => '2023-05-01'], 'Since 2023-05-01 00:00:00'])]
|
||||
#[TestWith([['--until' => '2023-05-01'], 'Until 2023-05-01 00:00:00'])]
|
||||
#[TestWith([
|
||||
['--since' => '2023-05-01', '--until' => '2024-02-02 23:59:59'],
|
||||
'Between 2023-05-01 00:00:00 and 2024-02-02 23:59:59',
|
||||
])]
|
||||
public function providedDateAreParsed(array $args, string $expectedMessage): void
|
||||
{
|
||||
[$output] = $this->executeCommand(['n'], args: $args);
|
||||
self::assertStringContainsString('Resolved date range -> ' . $expectedMessage, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{string, int, MatomoSendVisitsCommand}
|
||||
*/
|
||||
private function executeCommand(
|
||||
array $input = [],
|
||||
array $options = [],
|
||||
array $args = [],
|
||||
bool $matomoEnabled = true,
|
||||
): array {
|
||||
$command = new MatomoSendVisitsCommand(new MatomoOptions(enabled: $matomoEnabled), $this->visitSender);
|
||||
$commandTester = CliTestUtils::testerForCommand($command);
|
||||
$commandTester->setInputs($input);
|
||||
$commandTester->execute($args, $options);
|
||||
|
||||
$output = $commandTester->getDisplay();
|
||||
$exitCode = $commandTester->getStatusCode();
|
||||
|
||||
return [$output, $exitCode, $command];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteExpiredShortUrlsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ExpiredShortUrlsConditions;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteExpiredShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DeleteShortUrlServiceInterface $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->service = $this->createMock(DeleteShortUrlServiceInterface::class);
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteExpiredShortUrlsCommand($this->service));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function warningIsDisplayedAndExecutionCanBeCancelled(): void
|
||||
{
|
||||
$this->service->expects($this->never())->method('countExpiredShortUrls');
|
||||
$this->service->expects($this->never())->method('deleteExpiredShortUrls');
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$status = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString('Careful!', $output);
|
||||
self::assertEquals(ExitCode::EXIT_WARNING, $status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([[], [], true])]
|
||||
#[TestWith([['--force' => true], [], false])]
|
||||
#[TestWith([['-f' => true], [], false])]
|
||||
#[TestWith([[], ['interactive' => false], false])]
|
||||
public function deletionIsExecutedByDefault(array $input, array $options, bool $expectsWarning): void
|
||||
{
|
||||
$this->service->expects($this->never())->method('countExpiredShortUrls');
|
||||
$this->service->expects($this->once())->method('deleteExpiredShortUrls')->willReturn(5);
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
$this->commandTester->execute($input, $options);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$status = $this->commandTester->getStatusCode();
|
||||
|
||||
if ($expectsWarning) {
|
||||
self::assertStringContainsString('Careful!', $output);
|
||||
} else {
|
||||
self::assertStringNotContainsString('Careful!', $output);
|
||||
}
|
||||
self::assertStringContainsString('5 expired short URLs have been deleted', $output);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function countIsExecutedDuringDryRun(): void
|
||||
{
|
||||
$this->service->expects($this->once())->method('countExpiredShortUrls')->willReturn(38);
|
||||
$this->service->expects($this->never())->method('deleteExpiredShortUrls');
|
||||
|
||||
$this->commandTester->execute(['--dry-run' => true]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$status = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringNotContainsString('Careful!', $output);
|
||||
self::assertStringContainsString('There are 38 expired short URLs matching provided conditions', $output);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $status);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([[], new ExpiredShortUrlsConditions()])]
|
||||
#[TestWith([['--evaluate-max-visits' => true], new ExpiredShortUrlsConditions(maxVisitsReached: true)])]
|
||||
public function providesExpectedConditionsToService(array $extraInput, ExpiredShortUrlsConditions $conditions): void
|
||||
{
|
||||
$this->service->expects($this->once())->method('countExpiredShortUrls')->with($conditions)->willReturn(4);
|
||||
$this->commandTester->execute(['--dry-run' => true, ...$extraInput]);
|
||||
}
|
||||
}
|
||||
74
module/CLI/test/Command/ShortUrl/EditShortUrlCommandTest.php
Normal file
74
module/CLI/test/Command/ShortUrl/EditShortUrlCommandTest.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\EditShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class EditShortUrlCommandTest extends TestCase
|
||||
{
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlServiceInterface $shortUrlService;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->shortUrlService = $this->createMock(ShortUrlServiceInterface::class);
|
||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
||||
|
||||
$command = new EditShortUrlCommand($this->shortUrlService, $this->stringifier);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function successMessageIsPrintedIfNoErrorOccurs(): void
|
||||
{
|
||||
$this->shortUrlService->expects($this->once())->method('updateShortUrl')->willReturn(
|
||||
ShortUrl::createFake(),
|
||||
);
|
||||
$this->stringifier->expects($this->once())->method('stringify')->willReturn('https://s.test/foo');
|
||||
|
||||
$this->commandTester->execute(['shortCode' => 'foobar']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString('Short URL "https://s.test/foo" properly edited', $output);
|
||||
self::assertEquals(ExitCode::EXIT_SUCCESS, $exitCode);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[TestWith([OutputInterface::VERBOSITY_NORMAL])]
|
||||
#[TestWith([OutputInterface::VERBOSITY_VERBOSE])]
|
||||
#[TestWith([OutputInterface::VERBOSITY_VERY_VERBOSE])]
|
||||
#[TestWith([OutputInterface::VERBOSITY_DEBUG])]
|
||||
public function errorIsPrintedInCaseOfFailure(int $verbosity): void
|
||||
{
|
||||
$e = ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain('foo'));
|
||||
$this->shortUrlService->expects($this->once())->method('updateShortUrl')->willThrowException($e);
|
||||
$this->stringifier->expects($this->never())->method('stringify');
|
||||
|
||||
$this->commandTester->execute(['shortCode' => 'foo'], ['verbosity' => $verbosity]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString('Short URL not found for "foo"', $output);
|
||||
if ($verbosity >= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
self::assertStringContainsString('Exception trace:', $output);
|
||||
} else {
|
||||
self::assertStringNotContainsString('Exception trace:', $output);
|
||||
}
|
||||
self::assertEquals(ExitCode::EXIT_FAILURE, $exitCode);
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
||||
+---------+---------------------------+------------+---------+--------+
|
||||
| Referer | Date | User agent | Country | City |
|
||||
+---------+---------------------------+------------+---------+--------+
|
||||
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid |
|
||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid |
|
||||
+---------+---------------------------+------------+---------+--------+
|
||||
|
||||
OUTPUT,
|
||||
|
||||
@@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
@@ -47,7 +48,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$data[] = ShortUrl::withLongUrl('https://url_' . $i);
|
||||
$data[] = ShortUrlWithVisitsSummary::fromShortUrl(ShortUrl::withLongUrl('https://url_' . $i));
|
||||
}
|
||||
|
||||
$this->shortUrlService->expects($this->exactly(3))->method('listShortUrls')->withAnyParameters()
|
||||
@@ -69,7 +70,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = ShortUrl::withLongUrl('https://url_' . $i);
|
||||
$data[] = ShortUrlWithVisitsSummary::fromShortUrl(ShortUrl::withLongUrl('https://url_' . $i));
|
||||
}
|
||||
|
||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
|
||||
@@ -111,11 +112,13 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(
|
||||
ShortUrlsParams::emptyInstance(),
|
||||
)->willReturn(new Paginator(new ArrayAdapter([
|
||||
ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
'longUrl' => 'https://foo.com',
|
||||
'tags' => ['foo', 'bar', 'baz'],
|
||||
'apiKey' => $apiKey,
|
||||
])),
|
||||
ShortUrlWithVisitsSummary::fromShortUrl(
|
||||
ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
'longUrl' => 'https://foo.com',
|
||||
'tags' => ['foo', 'bar', 'baz'],
|
||||
'apiKey' => $apiKey,
|
||||
])),
|
||||
),
|
||||
])));
|
||||
|
||||
$this->commandTester->setInputs(['y']);
|
||||
|
||||
@@ -57,7 +57,7 @@ class GetTagVisitsCommandTest extends TestCase
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| Referer | Date | User agent | Country | City | Short Url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
|
||||
OUTPUT,
|
||||
|
||||
@@ -56,7 +56,7 @@ class GetNonOrphanVisitsCommandTest extends TestCase
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| Referer | Date | User agent | Country | City | Short Url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||
+---------+---------------------------+------------+---------+--------+---------------+
|
||||
|
||||
OUTPUT,
|
||||
|
||||
@@ -54,7 +54,7 @@ class GetOrphanVisitsCommandTest extends TestCase
|
||||
+---------+---------------------------+------------+---------+--------+----------+
|
||||
| Referer | Date | User agent | Country | City | Type |
|
||||
+---------+---------------------------+------------+---------+--------+----------+
|
||||
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | base_url |
|
||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | base_url |
|
||||
+---------+---------------------------+------------+---------+--------+----------+
|
||||
|
||||
OUTPUT,
|
||||
|
||||
@@ -116,6 +116,7 @@ class RedirectRuleHandlerTest extends TestCase
|
||||
'Language to match?' => 'en-US',
|
||||
'Query param name?' => 'foo',
|
||||
'Query param value?' => 'bar',
|
||||
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
|
||||
default => '',
|
||||
},
|
||||
);
|
||||
@@ -163,6 +164,7 @@ class RedirectRuleHandlerTest extends TestCase
|
||||
[RedirectCondition::forQueryParam('foo', 'bar'), RedirectCondition::forQueryParam('foo', 'bar')],
|
||||
true,
|
||||
];
|
||||
yield 'IP address' => [RedirectConditionType::IP_ADDRESS, [RedirectCondition::forIpAddress('1.2.3.4')]];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
||||
@@ -25,6 +25,7 @@ class CliTestUtils
|
||||
$command = $generator->testDouble(
|
||||
Command::class,
|
||||
mockObject: true,
|
||||
markAsMockObject: true,
|
||||
callOriginalConstructor: false,
|
||||
callOriginalClone: false,
|
||||
cloneArguments: false,
|
||||
|
||||
@@ -10,6 +10,7 @@ use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\EntityRepositoryFactory;
|
||||
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
use Symfony\Component\Lock;
|
||||
@@ -31,6 +32,7 @@ return [
|
||||
Options\TrackingOptions::class => [ValinorConfigFactory::class, 'config.tracking'],
|
||||
Options\QrCodeOptions::class => [ValinorConfigFactory::class, 'config.qr_codes'],
|
||||
Options\RabbitMqOptions::class => [ValinorConfigFactory::class, 'config.rabbitmq'],
|
||||
Options\RobotsOptions::class => [ValinorConfigFactory::class, 'config.robots'],
|
||||
|
||||
RedirectRule\ShortUrlRedirectRuleService::class => ConfigAbstractFactory::class,
|
||||
RedirectRule\ShortUrlRedirectionResolver::class => ConfigAbstractFactory::class,
|
||||
@@ -57,6 +59,10 @@ return [
|
||||
EntityRepositoryFactory::class,
|
||||
ShortUrl\Entity\ShortUrl::class,
|
||||
],
|
||||
ShortUrl\Repository\ExpiredShortUrlsRepository::class => [
|
||||
EntityRepositoryFactory::class,
|
||||
ShortUrl\Entity\ShortUrl::class,
|
||||
],
|
||||
|
||||
Tag\TagService::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -68,8 +74,7 @@ return [
|
||||
Visit\Geolocation\VisitLocator::class => ConfigAbstractFactory::class,
|
||||
Visit\Geolocation\VisitToLocationHelper::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
||||
Visit\Repository\VisitLocationRepository::class => [
|
||||
Visit\Repository\VisitIterationRepository::class => [
|
||||
EntityRepositoryFactory::class,
|
||||
Visit\Entity\Visit::class,
|
||||
],
|
||||
@@ -77,6 +82,8 @@ return [
|
||||
EntityRepositoryFactory::class,
|
||||
Visit\Entity\Visit::class,
|
||||
],
|
||||
Visit\Listener\ShortUrlVisitsCountTracker::class => InvokableFactory::class,
|
||||
Visit\Listener\OrphanVisitsCountTracker::class => InvokableFactory::class,
|
||||
|
||||
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
|
||||
Util\RedirectResponseHelper::class => ConfigAbstractFactory::class,
|
||||
@@ -96,6 +103,7 @@ return [
|
||||
|
||||
Matomo\MatomoOptions::class => [ValinorConfigFactory::class, 'config.matomo'],
|
||||
Matomo\MatomoTrackerBuilder::class => ConfigAbstractFactory::class,
|
||||
Matomo\MatomoVisitSender::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
@@ -105,6 +113,11 @@ return [
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Matomo\MatomoTrackerBuilder::class => [Matomo\MatomoOptions::class],
|
||||
Matomo\MatomoVisitSender::class => [
|
||||
Matomo\MatomoTrackerBuilder::class,
|
||||
ShortUrlStringifier::class,
|
||||
Visit\Repository\VisitIterationRepository::class,
|
||||
],
|
||||
|
||||
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\RequestTracker::class],
|
||||
@@ -138,7 +151,7 @@ return [
|
||||
ShortUrl\Repository\ShortUrlListRepository::class,
|
||||
Options\UrlShortenerOptions::class,
|
||||
],
|
||||
Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitLocationRepository::class],
|
||||
Visit\Geolocation\VisitLocator::class => ['em', Visit\Repository\VisitIterationRepository::class],
|
||||
Visit\Geolocation\VisitToLocationHelper::class => [IpLocationResolverInterface::class],
|
||||
Visit\VisitsStatsHelper::class => ['em'],
|
||||
Tag\TagService::class => ['em'],
|
||||
@@ -146,6 +159,7 @@ return [
|
||||
'em',
|
||||
Options\DeleteShortUrlsOptions::class,
|
||||
ShortUrl\ShortUrlResolver::class,
|
||||
ShortUrl\Repository\ExpiredShortUrlsRepository::class,
|
||||
],
|
||||
ShortUrl\ShortUrlResolver::class => ['em', Options\UrlShortenerOptions::class],
|
||||
ShortUrl\ShortUrlVisitsDeleter::class => [
|
||||
@@ -176,7 +190,7 @@ return [
|
||||
'Logger_Shlink',
|
||||
Options\QrCodeOptions::class,
|
||||
],
|
||||
Action\RobotsAction::class => [Crawling\CrawlingHelper::class],
|
||||
Action\RobotsAction::class => [Crawling\CrawlingHelper::class, Options\RobotsOptions::class],
|
||||
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => [
|
||||
'em',
|
||||
@@ -199,10 +213,7 @@ return [
|
||||
],
|
||||
ShortUrl\Middleware\TrimTrailingSlashMiddleware::class => [Options\UrlShortenerOptions::class],
|
||||
|
||||
EventDispatcher\PublishingUpdatesGenerator::class => [
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
],
|
||||
EventDispatcher\PublishingUpdatesGenerator::class => [ShortUrl\Transformer\ShortUrlDataTransformer::class],
|
||||
|
||||
Importer\ImportedLinksProcessor::class => [
|
||||
'em',
|
||||
|
||||
@@ -67,6 +67,11 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('visitsCounts', Visit\Entity\ShortUrlVisitsCount::class)
|
||||
->mappedBy('shortUrl')
|
||||
->fetchExtraLazy() // TODO Check if this makes sense
|
||||
->build();
|
||||
|
||||
$builder->createManyToMany('tags', Tag\Entity\Tag::class)
|
||||
->setJoinTable(determineTableName('short_urls_in_tags', $emConfig))
|
||||
->addInverseJoinColumn('tag_id', 'id', onDelete: 'CASCADE')
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(determineTableName('orphan_visits_counts', $emConfig))
|
||||
->setCustomRepositoryClass(Visit\Repository\OrphanVisitsCountRepository::class);
|
||||
|
||||
$builder->createField('id', Types::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('potentialBot', Types::BOOLEAN)
|
||||
->columnName('potential_bot')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('count', Types::BIGINT)
|
||||
->columnName('count')
|
||||
->option('unsigned', true)
|
||||
->option('default', 1)
|
||||
->build();
|
||||
|
||||
$builder->createField('slotId', Types::INTEGER)
|
||||
->columnName('slot_id')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->addUniqueConstraint(['potential_bot', 'slot_id'], 'UQ_slot');
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(determineTableName('short_url_visits_counts', $emConfig))
|
||||
->setCustomRepositoryClass(Visit\Repository\ShortUrlVisitsCountRepository::class);
|
||||
|
||||
$builder->createField('id', Types::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createField('potentialBot', Types::BOOLEAN)
|
||||
->columnName('potential_bot')
|
||||
->option('default', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('count', Types::BIGINT)
|
||||
->columnName('count')
|
||||
->option('unsigned', true)
|
||||
->option('default', 1)
|
||||
->build();
|
||||
|
||||
$builder->createField('slotId', Types::INTEGER)
|
||||
->columnName('slot_id')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
|
||||
->addJoinColumn('short_url_id', 'id', onDelete: 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addUniqueConstraint(['short_url_id', 'potential_bot', 'slot_id'], 'UQ_slot_per_short_url');
|
||||
};
|
||||
@@ -12,7 +12,6 @@ use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
||||
use Shlinkio\Shlink\Common\Mercure\MercureOptions;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocator;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelper;
|
||||
use Shlinkio\Shlink\EventDispatcher\Listener\EnabledListenerCheckerInterface;
|
||||
@@ -157,9 +156,8 @@ return (static function (): array {
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => [
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
ShortUrlStringifier::class,
|
||||
Matomo\MatomoOptions::class,
|
||||
Matomo\MatomoTrackerBuilder::class,
|
||||
Matomo\MatomoVisitSender::class,
|
||||
],
|
||||
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
|
||||
@@ -17,7 +17,6 @@ function contains(mixed $value, array $array): bool
|
||||
|
||||
/**
|
||||
* @param array[] $multiArray
|
||||
* @return array
|
||||
*/
|
||||
function flatten(array $multiArray): array
|
||||
{
|
||||
|
||||
@@ -9,11 +9,13 @@ use Cake\Chronos\Chronos;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||
use GuzzleHttp\Psr7\Query;
|
||||
use Hidehalo\Nanoid\Client as NanoidClient;
|
||||
use Jaybizzle\CrawlerDetect\CrawlerDetect;
|
||||
use Laminas\Filter\Word\CamelCaseToSeparator;
|
||||
use Laminas\Filter\Word\CamelCaseToUnderscore;
|
||||
use Laminas\InputFilter\InputFilter;
|
||||
use PUGX\Shortid\Factory as ShortIdFactory;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
@@ -37,15 +39,15 @@ use function ucfirst;
|
||||
|
||||
function generateRandomShortCode(int $length, ShortUrlMode $mode = ShortUrlMode::STRICT): string
|
||||
{
|
||||
static $shortIdFactory;
|
||||
if ($shortIdFactory === null) {
|
||||
$shortIdFactory = new ShortIdFactory();
|
||||
static $nanoIdClient;
|
||||
if ($nanoIdClient === null) {
|
||||
$nanoIdClient = new NanoidClient();
|
||||
}
|
||||
|
||||
$alphabet = $mode === ShortUrlMode::STRICT
|
||||
? '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
: '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
return $shortIdFactory->generate($length, $alphabet)->serialize();
|
||||
return $nanoIdClient->formattedId($alphabet, $length);
|
||||
}
|
||||
|
||||
function parseDateFromQuery(array $query, string $dateName): ?Chronos
|
||||
@@ -61,6 +63,23 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
|
||||
return buildDateRange($startDate, $endDate);
|
||||
}
|
||||
|
||||
function dateRangeToHumanFriendly(?DateRange $dateRange): string
|
||||
{
|
||||
$startDate = $dateRange?->startDate;
|
||||
$endDate = $dateRange?->endDate;
|
||||
|
||||
return match (true) {
|
||||
$startDate !== null && $endDate !== null => sprintf(
|
||||
'Between %s and %s',
|
||||
$startDate->toDateTimeString(),
|
||||
$endDate->toDateTimeString(),
|
||||
),
|
||||
$startDate !== null => sprintf('Since %s', $startDate->toDateTimeString()),
|
||||
$endDate !== null => sprintf('Until %s', $endDate->toDateTimeString()),
|
||||
default => 'All time',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($date is null ? null : Chronos)
|
||||
*/
|
||||
@@ -90,7 +109,6 @@ function normalizeLocale(string $locale): string
|
||||
* minimum quality
|
||||
*
|
||||
* @param non-empty-string $acceptLanguage
|
||||
* @param float<0, 1> $minQuality
|
||||
* @return iterable<string>;
|
||||
*/
|
||||
function acceptLanguageToLocales(string $acceptLanguage, float $minQuality = 0): iterable
|
||||
@@ -123,21 +141,31 @@ function acceptLanguageToLocales(string $acceptLanguage, float $minQuality = 0):
|
||||
*/
|
||||
function splitLocale(string $locale): array
|
||||
{
|
||||
return array_pad(explode('-', $locale), length: 2, value: null);
|
||||
[$lang, $countryCode] = array_pad(explode('-', $locale), length: 2, value: null);
|
||||
return [$lang, $countryCode];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputFilter<mixed> $inputFilter
|
||||
*/
|
||||
function getOptionalIntFromInputFilter(InputFilter $inputFilter, string $fieldName): ?int
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (int) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputFilter<mixed> $inputFilter
|
||||
*/
|
||||
function getOptionalBoolFromInputFilter(InputFilter $inputFilter, string $fieldName): ?bool
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
return $value !== null ? (bool) $value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputFilter<mixed> $inputFilter
|
||||
*/
|
||||
function getNonEmptyOptionalValueFromInputFilter(InputFilter $inputFilter, string $fieldName): mixed
|
||||
{
|
||||
$value = $inputFilter->getValue($fieldName);
|
||||
@@ -243,3 +271,21 @@ function enumToString(string $enum): string
|
||||
{
|
||||
return sprintf('["%s"]', implode('", "', enumValues($enum)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split provided string by comma and return a list of the results.
|
||||
* An empty array is returned if provided value is empty
|
||||
*/
|
||||
function splitByComma(?string $value): array
|
||||
{
|
||||
if ($value === null || trim($value) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(trim(...), explode(',', $value));
|
||||
}
|
||||
|
||||
function ipAddressFromRequest(ServerRequestInterface $request): ?string
|
||||
{
|
||||
return $request->getAttribute(IpAddressMiddlewareFactory::REQUEST_ATTR);
|
||||
}
|
||||
|
||||
65
module/Core/migrations/Version20240306132518.php
Normal file
65
module/Core/migrations/Version20240306132518.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create the new short_url_visits_counts table that will track visit counts per short URL
|
||||
*/
|
||||
final class Version20240306132518 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->skipIf($schema->hasTable('short_url_visits_counts'));
|
||||
|
||||
$table = $schema->createTable('short_url_visits_counts');
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
|
||||
$table->addColumn('short_url_id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
|
||||
$table->addColumn('potential_bot', Types::BOOLEAN, ['default' => false]);
|
||||
|
||||
$table->addColumn('slot_id', Types::INTEGER, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
'default' => 1,
|
||||
]);
|
||||
|
||||
$table->addColumn('count', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
'default' => 1,
|
||||
]);
|
||||
|
||||
$table->addUniqueIndex(['short_url_id', 'potential_bot', 'slot_id'], 'UQ_slot_per_short_url');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->skipIf(! $schema->hasTable('short_url_visits_counts'));
|
||||
$schema->dropTable('short_url_visits_counts');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
58
module/Core/migrations/Version20240318084804.php
Normal file
58
module/Core/migrations/Version20240318084804.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create initial entries in the short_url_visits_counts table for existing visits
|
||||
*/
|
||||
final class Version20240318084804 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder();
|
||||
$result = $qb->select('id')
|
||||
->from('short_urls')
|
||||
->executeQuery();
|
||||
|
||||
while ($shortUrlId = $result->fetchOne()) {
|
||||
$visitsQb = $this->connection->createQueryBuilder();
|
||||
$visitsQb->select('COUNT(id)')
|
||||
->from('visits')
|
||||
->where($visitsQb->expr()->eq('short_url_id', ':short_url_id'))
|
||||
->andWhere($visitsQb->expr()->eq('potential_bot', ':potential_bot'))
|
||||
->setParameter('short_url_id', $shortUrlId);
|
||||
|
||||
$botsCount = $visitsQb->setParameter('potential_bot', '1')->executeQuery()->fetchOne();
|
||||
$nonBotsCount = $visitsQb->setParameter('potential_bot', '0')->executeQuery()->fetchOne();
|
||||
|
||||
if ($botsCount > 0) {
|
||||
$this->insertCount($shortUrlId, $botsCount, potentialBot: true);
|
||||
}
|
||||
if ($nonBotsCount > 0) {
|
||||
$this->insertCount($shortUrlId, $nonBotsCount, potentialBot: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function insertCount(string|int $shortUrlId, string|int $count, bool $potentialBot): void
|
||||
{
|
||||
$this->connection->createQueryBuilder()
|
||||
->insert('short_url_visits_counts')
|
||||
->values([
|
||||
'short_url_id' => ':short_url_id',
|
||||
'count' => ':count',
|
||||
'potential_bot' => ':potential_bot',
|
||||
])
|
||||
->setParameters([
|
||||
'short_url_id' => $shortUrlId,
|
||||
'count' => $count,
|
||||
'potential_bot' => $potentialBot ? '1' : '0',
|
||||
])
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
56
module/Core/migrations/Version20240331111103.php
Normal file
56
module/Core/migrations/Version20240331111103.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Create a new orphan_visits_counts that will work similarly to the short_url_visits_counts
|
||||
*/
|
||||
final class Version20240331111103 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->skipIf($schema->hasTable('orphan_visits_counts'));
|
||||
|
||||
$table = $schema->createTable('orphan_visits_counts');
|
||||
$table->addColumn('id', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->setPrimaryKey(['id']);
|
||||
|
||||
$table->addColumn('potential_bot', Types::BOOLEAN, ['default' => false]);
|
||||
|
||||
$table->addColumn('slot_id', Types::INTEGER, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
'default' => 1,
|
||||
]);
|
||||
|
||||
$table->addColumn('count', Types::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
'default' => 1,
|
||||
]);
|
||||
|
||||
$table->addUniqueIndex(['potential_bot', 'slot_id'], 'UQ_slot');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->skipIf(! $schema->hasTable('orphan_visits_counts'));
|
||||
$schema->dropTable('orphan_visits_counts');
|
||||
}
|
||||
|
||||
public function isTransactional(): bool
|
||||
{
|
||||
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
|
||||
}
|
||||
}
|
||||
45
module/Core/migrations/Version20240331111447.php
Normal file
45
module/Core/migrations/Version20240331111447.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20240331111447 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$visitsQb = $this->connection->createQueryBuilder();
|
||||
$visitsQb->select('COUNT(id)')
|
||||
->from('visits')
|
||||
->where($visitsQb->expr()->isNull('short_url_id'))
|
||||
->andWhere($visitsQb->expr()->eq('potential_bot', ':potential_bot'));
|
||||
|
||||
$botsCount = $visitsQb->setParameter('potential_bot', '1')->executeQuery()->fetchOne();
|
||||
$nonBotsCount = $visitsQb->setParameter('potential_bot', '0')->executeQuery()->fetchOne();
|
||||
|
||||
if ($botsCount > 0) {
|
||||
$this->insertCount($botsCount, potentialBot: true);
|
||||
}
|
||||
if ($nonBotsCount > 0) {
|
||||
$this->insertCount($nonBotsCount, potentialBot: false);
|
||||
}
|
||||
}
|
||||
|
||||
private function insertCount(string|int $count, bool $potentialBot): void
|
||||
{
|
||||
$this->connection->createQueryBuilder()
|
||||
->insert('orphan_visits_counts')
|
||||
->values([
|
||||
'count' => ':count',
|
||||
'potential_bot' => ':potential_bot',
|
||||
])
|
||||
->setParameters([
|
||||
'count' => $count,
|
||||
'potential_bot' => $potentialBot ? '1' : '0',
|
||||
])
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,15 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Crawling\CrawlingHelperInterface;
|
||||
use Shlinkio\Shlink\Core\Options\RobotsOptions;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_EOL;
|
||||
|
||||
class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
||||
readonly class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
||||
{
|
||||
public function __construct(private readonly CrawlingHelperInterface $crawlingHelper)
|
||||
public function __construct(private CrawlingHelperInterface $crawlingHelper, private RobotsOptions $robotsOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,10 +34,20 @@ class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
||||
# For more information about the robots.txt standard, see:
|
||||
# https://www.robotstxt.org/orig.html
|
||||
|
||||
User-agent: *
|
||||
|
||||
ROBOTS;
|
||||
|
||||
$userAgents = $this->robotsOptions->hasUserAgents() ? $this->robotsOptions->userAgents : ['*'];
|
||||
foreach ($userAgents as $userAgent) {
|
||||
yield sprintf('User-agent: %s%s', $userAgent, PHP_EOL);
|
||||
}
|
||||
|
||||
if ($this->robotsOptions->allowAllShortUrls) {
|
||||
// Disallow rest URLs, but allow all short codes
|
||||
yield 'Disallow: /rest/';
|
||||
return;
|
||||
}
|
||||
|
||||
$shortCodes = $this->crawlingHelper->listCrawlableShortCodes();
|
||||
foreach ($shortCodes as $shortCode) {
|
||||
yield sprintf('Allow: /%s%s', $shortCode, PHP_EOL);
|
||||
|
||||
@@ -4,12 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Config;
|
||||
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
use function date_default_timezone_get;
|
||||
use function file_get_contents;
|
||||
use function is_file;
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Config\parseEnvVar;
|
||||
use function sprintf;
|
||||
|
||||
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_ERROR_CORRECTION;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ROUND_BLOCK_SIZE;
|
||||
use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
|
||||
use const Shlinkio\Shlink\DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||
use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
|
||||
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
|
||||
|
||||
enum EnvVars: string
|
||||
{
|
||||
case DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
|
||||
@@ -69,12 +84,73 @@ enum EnvVars: string
|
||||
case DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
|
||||
case AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
|
||||
case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
|
||||
case TIMEZONE = 'TIMEZONE';
|
||||
case MULTI_SEGMENT_SLUGS_ENABLED = 'MULTI_SEGMENT_SLUGS_ENABLED';
|
||||
case ROBOTS_ALLOW_ALL_SHORT_URLS = 'ROBOTS_ALLOW_ALL_SHORT_URLS';
|
||||
case ROBOTS_USER_AGENTS = 'ROBOTS_USER_AGENTS';
|
||||
case TIMEZONE = 'TIMEZONE';
|
||||
case MEMORY_LIMIT = 'MEMORY_LIMIT';
|
||||
case INITIAL_API_KEY = 'INITIAL_API_KEY';
|
||||
case SKIP_INITIAL_GEOLITE_DOWNLOAD = 'SKIP_INITIAL_GEOLITE_DOWNLOAD';
|
||||
|
||||
public function loadFromEnv(mixed $default = null): mixed
|
||||
public function loadFromEnv(): mixed
|
||||
{
|
||||
return env($this->value) ?? $this->loadFromFileEnv() ?? $default;
|
||||
return env($this->value) ?? $this->loadFromFileEnv() ?? $this->defaultValue();
|
||||
}
|
||||
|
||||
private function defaultValue(): string|int|bool|null
|
||||
{
|
||||
return match ($this) {
|
||||
self::MEMORY_LIMIT => '512M',
|
||||
self::TIMEZONE => date_default_timezone_get(),
|
||||
|
||||
self::DEFAULT_SHORT_CODES_LENGTH => DEFAULT_SHORT_CODES_LENGTH,
|
||||
self::SHORT_URL_MODE => ShortUrlMode::STRICT->value,
|
||||
self::IS_HTTPS_ENABLED, self::AUTO_RESOLVE_TITLES => true,
|
||||
self::REDIRECT_APPEND_EXTRA_PATH,
|
||||
self::MULTI_SEGMENT_SLUGS_ENABLED,
|
||||
self::SHORT_URL_TRAILING_SLASH => false,
|
||||
self::DEFAULT_DOMAIN, self::BASE_PATH => '',
|
||||
self::CACHE_NAMESPACE => 'Shlink',
|
||||
|
||||
self::REDIS_PUB_SUB_ENABLED,
|
||||
self::MATOMO_ENABLED,
|
||||
self::ROBOTS_ALLOW_ALL_SHORT_URLS => false,
|
||||
|
||||
self::DB_NAME => 'shlink',
|
||||
self::DB_HOST => self::DB_UNIX_SOCKET->loadFromEnv(),
|
||||
self::DB_DRIVER => 'sqlite',
|
||||
self::DB_PORT => match (self::DB_DRIVER->loadFromEnv()) {
|
||||
'postgres' => '5432',
|
||||
'mssql' => '1433',
|
||||
default => '3306',
|
||||
},
|
||||
|
||||
self::MERCURE_INTERNAL_HUB_URL => self::MERCURE_PUBLIC_HUB_URL->loadFromEnv(),
|
||||
|
||||
self::DEFAULT_QR_CODE_SIZE, => DEFAULT_QR_CODE_SIZE,
|
||||
self::DEFAULT_QR_CODE_MARGIN, => DEFAULT_QR_CODE_MARGIN,
|
||||
self::DEFAULT_QR_CODE_FORMAT, => DEFAULT_QR_CODE_FORMAT,
|
||||
self::DEFAULT_QR_CODE_ERROR_CORRECTION, => DEFAULT_QR_CODE_ERROR_CORRECTION,
|
||||
self::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE, => DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
|
||||
self::QR_CODE_FOR_DISABLED_SHORT_URLS, => DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
|
||||
self::DEFAULT_QR_CODE_COLOR, => DEFAULT_QR_CODE_COLOR,
|
||||
self::DEFAULT_QR_CODE_BG_COLOR, => DEFAULT_QR_CODE_BG_COLOR,
|
||||
|
||||
self::RABBITMQ_ENABLED, self::RABBITMQ_USE_SSL => false,
|
||||
self::RABBITMQ_PORT => 5672,
|
||||
self::RABBITMQ_VHOST => '/',
|
||||
|
||||
self::REDIRECT_STATUS_CODE => DEFAULT_REDIRECT_STATUS_CODE->value,
|
||||
self::REDIRECT_CACHE_LIFETIME => DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||
|
||||
self::ANONYMIZE_REMOTE_ADDR, self::TRACK_ORPHAN_VISITS => true,
|
||||
self::DISABLE_TRACKING,
|
||||
self::DISABLE_IP_TRACKING,
|
||||
self::DISABLE_REFERRER_TRACKING,
|
||||
self::DISABLE_UA_TRACKING => false,
|
||||
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Config\PostProcessor;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use Mezzio\Router\Route;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectStatus;
|
||||
|
||||
@@ -40,9 +38,7 @@ class ShortUrlMethodsProcessor
|
||||
$redirectStatus = RedirectStatus::tryFrom(
|
||||
$config['redirects']['redirect_status_code'] ?? 0,
|
||||
) ?? DEFAULT_REDIRECT_STATUS_CODE;
|
||||
$redirectRoute['allowed_methods'] = $redirectStatus->isLegacyStatus()
|
||||
? [RequestMethodInterface::METHOD_GET]
|
||||
: Route::HTTP_METHOD_ANY;
|
||||
$redirectRoute['allowed_methods'] = $redirectStatus->allowedHttpMethods();
|
||||
|
||||
$config['routes'] = [...$rest, $redirectRoute];
|
||||
return $config;
|
||||
|
||||
@@ -11,12 +11,12 @@ use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
|
||||
|
||||
class Domain extends AbstractEntity implements JsonSerializable, NotFoundRedirectConfigInterface
|
||||
{
|
||||
private ?string $baseUrlRedirect = null;
|
||||
private ?string $regular404Redirect = null;
|
||||
private ?string $invalidShortUrlRedirect = null;
|
||||
|
||||
private function __construct(public readonly string $authority)
|
||||
{
|
||||
private function __construct(
|
||||
public readonly string $authority,
|
||||
private ?string $baseUrlRedirect = null,
|
||||
private ?string $regular404Redirect = null,
|
||||
private ?string $invalidShortUrlRedirect = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function withAuthority(string $authority): self
|
||||
|
||||
@@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToApiKey;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
/** @extends EntitySpecificationRepository<Domain> */
|
||||
class DomainRepository extends EntitySpecificationRepository implements DomainRepositoryInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryInterfa
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
/** @extends ObjectRepository<Domain> */
|
||||
interface DomainRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ use Laminas\InputFilter\InputFilter;
|
||||
use Shlinkio\Shlink\Common\Validation\HostAndPortValidator;
|
||||
use Shlinkio\Shlink\Common\Validation\InputFactory;
|
||||
|
||||
/** @extends InputFilter<mixed> */
|
||||
class DomainRedirectsInputFilter extends InputFilter
|
||||
{
|
||||
public const DOMAIN = 'domain';
|
||||
|
||||
20
module/Core/src/EventDispatcher/Helper/RequestIdProvider.php
Normal file
20
module/Core/src/EventDispatcher/Helper/RequestIdProvider.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Helper;
|
||||
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\EventDispatcher\Util\RequestIdProviderInterface;
|
||||
|
||||
readonly class RequestIdProvider implements RequestIdProviderInterface
|
||||
{
|
||||
public function __construct(private RequestIdMiddleware $requestIdMiddleware)
|
||||
{
|
||||
}
|
||||
|
||||
public function currentRequestId(): string
|
||||
{
|
||||
return $this->requestIdMiddleware->currentRequestId();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user