mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Compare commits
33 Commits
v2.7.1
...
v2.8.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8efda2ef56 | ||
|
|
f86cda6730 | ||
|
|
43f59a19fb | ||
|
|
eabaa94e06 | ||
|
|
20575a2b0f | ||
|
|
0096a778ac | ||
|
|
050f83e3bb | ||
|
|
32f7b4fbf6 | ||
|
|
265e8cdeaf | ||
|
|
fe5460e0c5 | ||
|
|
d4cad337fc | ||
|
|
0af6ecbd34 | ||
|
|
6466045363 | ||
|
|
67c7e503d9 | ||
|
|
01e06f0503 | ||
|
|
d6e155d874 | ||
|
|
5a2350bac1 | ||
|
|
2b97f9ac9e | ||
|
|
090b215179 | ||
|
|
32f483c333 | ||
|
|
655652f94f | ||
|
|
53b84c147c | ||
|
|
d8b4827601 | ||
|
|
5737acf759 | ||
|
|
58262e8604 | ||
|
|
b9e5eaf689 | ||
|
|
6d78cd59e9 | ||
|
|
bfdece1c23 | ||
|
|
a68f450d36 | ||
|
|
d1df225e47 | ||
|
|
9c6ba4bc61 | ||
|
|
c01121d61a | ||
|
|
e0f0bb5523 |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist
|
||||||
- run: composer test:unit:ci
|
- run: composer test:unit:ci
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.php-version == '7.4' }}
|
if: ${{ matrix.php-version == '8.0' }}
|
||||||
with:
|
with:
|
||||||
name: coverage-unit
|
name: coverage-unit
|
||||||
path: |
|
path: |
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist
|
||||||
- run: composer test:db:sqlite:ci
|
- run: composer test:db:sqlite:ci
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.php-version == '7.4' }}
|
if: ${{ matrix.php-version == '8.0' }}
|
||||||
with:
|
with:
|
||||||
name: coverage-db
|
name: coverage-db
|
||||||
path: |
|
path: |
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -140,7 +140,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -160,7 +160,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -184,7 +184,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -201,7 +201,7 @@ jobs:
|
|||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist
|
||||||
- run: bin/test/run-api-tests.sh
|
- run: bin/test/run-api-tests.sh
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.php-version == '7.4' }}
|
if: ${{ matrix.php-version == '8.0' }}
|
||||||
with:
|
with:
|
||||||
name: coverage-api
|
name: coverage-api
|
||||||
path: |
|
path: |
|
||||||
@@ -216,7 +216,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
test-group: ['unit', 'db']
|
test-group: ['unit', 'db']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
5
.github/workflows/publish-release.yml
vendored
5
.github/workflows/publish-release.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['7.4', '8.0']
|
php-version: ['8.0']
|
||||||
swoole: ['yes', 'no']
|
swoole: ['yes', 'no']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -43,7 +43,6 @@ jobs:
|
|||||||
uses: docker://antonyurchenko/git-release:latest
|
uses: docker://antonyurchenko/git-release:latest
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
ALLOW_TAG_PREFIX: "true"
|
|
||||||
ALLOW_EMPTY_CHANGELOG: "true"
|
ALLOW_EMPTY_CHANGELOG: "true"
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
@@ -54,7 +53,7 @@ jobs:
|
|||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: [ '7.4', '8.0' ]
|
php-version: [ '8.0' ]
|
||||||
swoole: [ 'yes', 'no' ]
|
swoole: [ 'yes', 'no' ]
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v1
|
||||||
|
|||||||
26
CHANGELOG.md
26
CHANGELOG.md
@@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* [#1046](https://github.com/shlinkio/shlink/issues/1046) Dropped support for PHP 7.4.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
|
||||||
## [2.7.1] - 2021-05-30
|
## [2.7.1] - 2021-05-30
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
|||||||
For example, `test:db:postgres`.
|
For example, `test:db:postgres`.
|
||||||
|
|
||||||
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
||||||
* Run `./indocker composer infect:test` ti run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||||
* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration.
|
* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration.
|
||||||
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
||||||
|
|
||||||
|
|||||||
@@ -78,4 +78,13 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
|||||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||||
|
|
||||||
|
# Change the ownership of /etc/shlink/data to be writable, then change the user to non-root
|
||||||
|
RUN chown 1001 /etc/shlink/data
|
||||||
|
RUN chown 1001 /etc/shlink/data/locks
|
||||||
|
RUN chown 1001 /etc/shlink/data/proxies
|
||||||
|
RUN chown 1001 /etc/shlink/data/cache
|
||||||
|
RUN chown 1001 /etc/shlink/data/log
|
||||||
|
|
||||||
|
USER 1001
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ The idea is that you can just generate a container using the image and provide t
|
|||||||
|
|
||||||
First, make sure the host where you are going to run shlink fulfills these requirements:
|
First, make sure the host where you are going to run shlink fulfills these requirements:
|
||||||
|
|
||||||
* PHP 7.4 or 8.0
|
* PHP 8.0
|
||||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp.
|
* The next PHP extensions: json, curl, pdo, intl, gd and gmp.
|
||||||
* apcu extension is recommended if you don't plan to use swoole.
|
* apcu extension is recommended if you don't plan to use swoole.
|
||||||
* xml extension is required if you want to generate QR codes in svg format.
|
* xml extension is required if you want to generate QR codes in svg format.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.4 || ^8.0",
|
"php": "^8.0",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"akrabat/ip-address-middleware": "^2.0",
|
"akrabat/ip-address-middleware": "^2.0",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"shlinkio/shlink-config": "^1.0",
|
"shlinkio/shlink-config": "^1.0",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.1",
|
"shlinkio/shlink-event-dispatcher": "^2.1",
|
||||||
"shlinkio/shlink-importer": "^2.3",
|
"shlinkio/shlink-importer": "^2.3",
|
||||||
"shlinkio/shlink-installer": "^6.0",
|
"shlinkio/shlink-installer": "dev-develop#fa6a4ca as 6.1",
|
||||||
"shlinkio/shlink-ip-geolocation": "^2.0",
|
"shlinkio/shlink-ip-geolocation": "^2.0",
|
||||||
"symfony/console": "^5.1",
|
"symfony/console": "^5.1",
|
||||||
"symfony/filesystem": "^5.1",
|
"symfony/filesystem": "^5.1",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ return [
|
|||||||
Option\UrlShortener\RedirectStatusCodeConfigOption::class,
|
Option\UrlShortener\RedirectStatusCodeConfigOption::class,
|
||||||
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
||||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||||
|
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ return [
|
|||||||
// This middleware is in front of tracking actions explicitly. Putting here for orphan visits tracking
|
// This middleware is in front of tracking actions explicitly. Putting here for orphan visits tracking
|
||||||
IpAddress::class,
|
IpAddress::class,
|
||||||
Core\ErrorHandler\NotFoundTypeResolverMiddleware::class,
|
Core\ErrorHandler\NotFoundTypeResolverMiddleware::class,
|
||||||
|
Core\ShortUrl\Middleware\ExtraPathRedirectMiddleware::class,
|
||||||
Core\ErrorHandler\NotFoundTrackerMiddleware::class,
|
Core\ErrorHandler\NotFoundTrackerMiddleware::class,
|
||||||
Core\ErrorHandler\NotFoundRedirectHandler::class,
|
Core\ErrorHandler\NotFoundRedirectHandler::class,
|
||||||
Core\ErrorHandler\NotFoundTemplateHandler::class,
|
Core\ErrorHandler\NotFoundTemplateHandler::class,
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ return [
|
|||||||
'validate_url' => false, // Deprecated
|
'validate_url' => false, // Deprecated
|
||||||
'visits_webhooks' => [],
|
'visits_webhooks' => [],
|
||||||
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
|
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
|
||||||
|
'auto_resolve_titles' => false,
|
||||||
|
'append_extra_path' => false,
|
||||||
|
|
||||||
|
// TODO Move these two options to their own config namespace. Maybe "redirects".
|
||||||
'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE,
|
'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE,
|
||||||
'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
|
'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||||
'auto_resolve_titles' => false,
|
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ server {
|
|||||||
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
include fastcgi.conf;
|
include fastcgi.conf;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ final class Version20180913205455 extends AbstractMigration
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return (string) IpAddress::fromString($addr)->getAnonymizedCopy();
|
return (string) IpAddress::fromString($addr)->getAnonymizedCopy();
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,10 @@ return [
|
|||||||
'validate_url' => (bool) env('VALIDATE_URLS', false),
|
'validate_url' => (bool) env('VALIDATE_URLS', false),
|
||||||
'visits_webhooks' => $helper->getVisitsWebhooks(),
|
'visits_webhooks' => $helper->getVisitsWebhooks(),
|
||||||
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
|
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
|
||||||
|
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
|
||||||
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
|
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
|
||||||
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
|
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
|
||||||
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
|
'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
|
||||||
],
|
],
|
||||||
|
|
||||||
'tracking' => [
|
'tracking' => [
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
|||||||
php bin/cli visit:download-db -n -q
|
php bin/cli visit:download-db -n -q
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Periodicaly run visit:locate every hour
|
||||||
|
# https://shlink.io/documentation/long-running-tasks/#locate-visits
|
||||||
|
# set env var "ENABLE_PERIODIC_VISIT_LOCATE=true" to enable
|
||||||
|
if [ $ENABLE_PERIODIC_VISIT_LOCATE ]; then
|
||||||
|
echo "Starting periodic visite locate..."
|
||||||
|
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
|
||||||
|
/usr/sbin/crond &
|
||||||
|
fi
|
||||||
|
|
||||||
# When restarting the container, swoole might think it is already in execution
|
# When restarting the container, swoole might think it is already in execution
|
||||||
# This forces the app to be started every second until the exit code is 0
|
# This forces the app to be started every second until the exit code is 0
|
||||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"URL Shortener"
|
"URL Shortener"
|
||||||
],
|
],
|
||||||
"summary": "Short URL QR code",
|
"summary": "Short URL QR code",
|
||||||
"description": "Generates a QR code image pointing to a short URL",
|
"description": "Generates a QR code image pointing to a short URL.<br />Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
@@ -35,10 +35,8 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": ["png", "svg"],
|
||||||
"png",
|
"default": "png"
|
||||||
"svg"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -51,6 +49,17 @@
|
|||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"default": 0
|
"default": 0
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "errorCorrection",
|
||||||
|
"in": "query",
|
||||||
|
"description": "The error correction level to apply to the the QR code: **[L]**ow, **[M]**edium, **[Q]**uartile or **[H]**igh. See [docs](https://www.qrcode.com/en/about/error_correction.html).",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["L", "M", "Q", "H"],
|
||||||
|
"default": "L"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
|
|
||||||
class RoleResolver implements RoleResolverInterface
|
class RoleResolver implements RoleResolverInterface
|
||||||
{
|
{
|
||||||
private DomainServiceInterface $domainService;
|
public function __construct(private DomainServiceInterface $domainService)
|
||||||
|
|
||||||
public function __construct(DomainServiceInterface $domainService)
|
|
||||||
{
|
{
|
||||||
$this->domainService = $domainService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function determineRoles(InputInterface $input): array
|
public function determineRoles(InputInterface $input): array
|
||||||
|
|||||||
@@ -19,12 +19,9 @@ class DisableKeyCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'api-key:disable';
|
public const NAME = 'api-key:disable';
|
||||||
|
|
||||||
private ApiKeyServiceInterface $apiKeyService;
|
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||||
|
|
||||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->apiKeyService = $apiKeyService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -23,14 +23,11 @@ class GenerateKeyCommand extends BaseCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'api-key:generate';
|
public const NAME = 'api-key:generate';
|
||||||
|
|
||||||
private ApiKeyServiceInterface $apiKeyService;
|
public function __construct(
|
||||||
private RoleResolverInterface $roleResolver;
|
private ApiKeyServiceInterface $apiKeyService,
|
||||||
|
private RoleResolverInterface $roleResolver
|
||||||
public function __construct(ApiKeyServiceInterface $apiKeyService, RoleResolverInterface $roleResolver)
|
) {
|
||||||
{
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->apiKeyService = $apiKeyService;
|
|
||||||
$this->roleResolver = $roleResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -27,12 +27,9 @@ class ListKeysCommand extends BaseCommand
|
|||||||
|
|
||||||
public const NAME = 'api-key:list';
|
public const NAME = 'api-key:list';
|
||||||
|
|
||||||
private ApiKeyServiceInterface $apiKeyService;
|
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||||
|
|
||||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->apiKeyService = $apiKeyService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -61,7 +58,7 @@ class ListKeysCommand extends BaseCommand
|
|||||||
if (! $enabledOnly) {
|
if (! $enabledOnly) {
|
||||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||||
}
|
}
|
||||||
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||||
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||||
fn (string $roleName, array $meta) =>
|
fn (string $roleName, array $meta) =>
|
||||||
empty($meta)
|
empty($meta)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ abstract class BaseCommand extends Command
|
|||||||
?string $shortcut = null,
|
?string $shortcut = null,
|
||||||
?int $mode = null,
|
?int $mode = null,
|
||||||
string $description = '',
|
string $description = '',
|
||||||
$default = null
|
$default = null,
|
||||||
): self {
|
): self {
|
||||||
$this->addOption($name, $shortcut, $mode, $description, $default);
|
$this->addOption($name, $shortcut, $mode, $description, $default);
|
||||||
|
|
||||||
|
|||||||
@@ -13,16 +13,14 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
|||||||
|
|
||||||
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||||
{
|
{
|
||||||
private ProcessRunnerInterface $processRunner;
|
|
||||||
private string $phpBinary;
|
private string $phpBinary;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
LockFactory $locker,
|
LockFactory $locker,
|
||||||
ProcessRunnerInterface $processRunner,
|
private ProcessRunnerInterface $processRunner,
|
||||||
PhpExecutableFinder $phpFinder
|
PhpExecutableFinder $phpFinder
|
||||||
) {
|
) {
|
||||||
parent::__construct($locker);
|
parent::__construct($locker);
|
||||||
$this->processRunner = $processRunner;
|
|
||||||
$this->phpBinary = $phpFinder->find(false) ?: 'php';
|
$this->phpBinary = $phpFinder->find(false) ?: 'php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,19 +21,14 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
|||||||
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||||
|
|
||||||
private Connection $regularConn;
|
|
||||||
private Connection $noDbNameConn;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
LockFactory $locker,
|
LockFactory $locker,
|
||||||
ProcessRunnerInterface $processRunner,
|
ProcessRunnerInterface $processRunner,
|
||||||
PhpExecutableFinder $phpFinder,
|
PhpExecutableFinder $phpFinder,
|
||||||
Connection $conn,
|
private Connection $regularConn,
|
||||||
Connection $noDbNameConn
|
private Connection $noDbNameConn
|
||||||
) {
|
) {
|
||||||
parent::__construct($locker, $processRunner, $phpFinder);
|
parent::__construct($locker, $processRunner, $phpFinder);
|
||||||
$this->regularConn = $conn;
|
|
||||||
$this->noDbNameConn = $noDbNameConn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -18,12 +18,9 @@ class ListDomainsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'domain:list';
|
public const NAME = 'domain:list';
|
||||||
|
|
||||||
private DomainServiceInterface $domainService;
|
public function __construct(private DomainServiceInterface $domainService)
|
||||||
|
|
||||||
public function __construct(DomainServiceInterface $domainService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->domainService = $domainService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ class DeleteShortUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:delete';
|
public const NAME = 'short-url:delete';
|
||||||
|
|
||||||
private DeleteShortUrlServiceInterface $deleteShortUrlService;
|
public function __construct(private DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||||
|
|
||||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -30,19 +30,12 @@ class GenerateShortUrlCommand extends BaseCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:generate';
|
public const NAME = 'short-url:generate';
|
||||||
|
|
||||||
private UrlShortenerInterface $urlShortener;
|
|
||||||
private ShortUrlStringifierInterface $stringifier;
|
|
||||||
private int $defaultShortCodeLength;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UrlShortenerInterface $urlShortener,
|
private UrlShortenerInterface $urlShortener,
|
||||||
ShortUrlStringifierInterface $stringifier,
|
private ShortUrlStringifierInterface $stringifier,
|
||||||
int $defaultShortCodeLength
|
private int $defaultShortCodeLength
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->urlShortener = $urlShortener;
|
|
||||||
$this->stringifier = $stringifier;
|
|
||||||
$this->defaultShortCodeLength = $defaultShortCodeLength;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -27,11 +27,8 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:visits';
|
public const NAME = 'short-url:visits';
|
||||||
|
|
||||||
private VisitsStatsHelperInterface $visitsHelper;
|
public function __construct(private VisitsStatsHelperInterface $visitsHelper)
|
||||||
|
|
||||||
public function __construct(VisitsStatsHelperInterface $visitsHelper)
|
|
||||||
{
|
{
|
||||||
$this->visitsHelper = $visitsHelper;
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,11 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
|
|
||||||
public const NAME = 'short-url:list';
|
public const NAME = 'short-url:list';
|
||||||
|
|
||||||
private ShortUrlServiceInterface $shortUrlService;
|
public function __construct(
|
||||||
private DataTransformerInterface $transformer;
|
private ShortUrlServiceInterface $shortUrlService,
|
||||||
|
private DataTransformerInterface $transformer
|
||||||
public function __construct(ShortUrlServiceInterface $shortUrlService, DataTransformerInterface $transformer)
|
) {
|
||||||
{
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->shortUrlService = $shortUrlService;
|
|
||||||
$this->transformer = $transformer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doConfigure(): void
|
protected function doConfigure(): void
|
||||||
@@ -129,8 +126,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
|
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
|
||||||
ShortUrlsParamsInputFilter::TAGS => $tags,
|
ShortUrlsParamsInputFilter::TAGS => $tags,
|
||||||
ShortUrlsOrdering::ORDER_BY => $orderBy,
|
ShortUrlsOrdering::ORDER_BY => $orderBy,
|
||||||
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
|
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
|
||||||
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
|
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($all) {
|
if ($all) {
|
||||||
@@ -158,7 +155,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
OutputInterface $output,
|
OutputInterface $output,
|
||||||
array $columnsMap,
|
array $columnsMap,
|
||||||
ShortUrlsParams $params,
|
ShortUrlsParams $params,
|
||||||
bool $all
|
bool $all,
|
||||||
): Paginator {
|
): Paginator {
|
||||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||||
|
|
||||||
@@ -203,14 +200,11 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
}
|
}
|
||||||
if ($input->getOption('show-api-key')) {
|
if ($input->getOption('show-api-key')) {
|
||||||
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||||
(string) $shortUrl->authorApiKey();
|
(string) $shortUrl->authorApiKey();
|
||||||
}
|
}
|
||||||
if ($input->getOption('show-api-key-name')) {
|
if ($input->getOption('show-api-key-name')) {
|
||||||
$columnsMap['API Key Name'] = static function (array $_, ShortUrl $shortUrl): ?string {
|
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): ?string =>
|
||||||
$apiKey = $shortUrl->authorApiKey();
|
$shortUrl->authorApiKey()?->name();
|
||||||
|
|
||||||
return $apiKey !== null ? $apiKey->name() : null;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $columnsMap;
|
return $columnsMap;
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ class ResolveUrlCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'short-url:parse';
|
public const NAME = 'short-url:parse';
|
||||||
|
|
||||||
private ShortUrlResolverInterface $urlResolver;
|
public function __construct(private ShortUrlResolverInterface $urlResolver)
|
||||||
|
|
||||||
public function __construct(ShortUrlResolverInterface $urlResolver)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->urlResolver = $urlResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -17,12 +17,9 @@ class CreateTagCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'tag:create';
|
public const NAME = 'tag:create';
|
||||||
|
|
||||||
private TagServiceInterface $tagService;
|
public function __construct(private TagServiceInterface $tagService)
|
||||||
|
|
||||||
public function __construct(TagServiceInterface $tagService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->tagService = $tagService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ class DeleteTagsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'tag:delete';
|
public const NAME = 'tag:delete';
|
||||||
|
|
||||||
private TagServiceInterface $tagService;
|
public function __construct(private TagServiceInterface $tagService)
|
||||||
|
|
||||||
public function __construct(TagServiceInterface $tagService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->tagService = $tagService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -18,12 +18,9 @@ class ListTagsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'tag:list';
|
public const NAME = 'tag:list';
|
||||||
|
|
||||||
private TagServiceInterface $tagService;
|
public function __construct(private TagServiceInterface $tagService)
|
||||||
|
|
||||||
public function __construct(TagServiceInterface $tagService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->tagService = $tagService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -19,12 +19,9 @@ class RenameTagCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'tag:rename';
|
public const NAME = 'tag:rename';
|
||||||
|
|
||||||
private TagServiceInterface $tagService;
|
public function __construct(private TagServiceInterface $tagService)
|
||||||
|
|
||||||
public function __construct(TagServiceInterface $tagService)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->tagService = $tagService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
|
|||||||
@@ -14,12 +14,9 @@ use function sprintf;
|
|||||||
|
|
||||||
abstract class AbstractLockedCommand extends Command
|
abstract class AbstractLockedCommand extends Command
|
||||||
{
|
{
|
||||||
private LockFactory $locker;
|
public function __construct(private LockFactory $locker)
|
||||||
|
|
||||||
public function __construct(LockFactory $locker)
|
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->locker = $locker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ abstract class AbstractWithDateRangeCommand extends BaseCommand
|
|||||||
));
|
));
|
||||||
|
|
||||||
if ($output->isVeryVerbose()) {
|
if ($output->isVeryVerbose()) {
|
||||||
$this->getApplication()->renderThrowable($e, $output);
|
$this->getApplication()?->renderThrowable($e, $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -8,15 +8,11 @@ final class LockedCommandConfig
|
|||||||
{
|
{
|
||||||
public const DEFAULT_TTL = 600.0; // 10 minutes
|
public const DEFAULT_TTL = 600.0; // 10 minutes
|
||||||
|
|
||||||
private string $lockName;
|
private function __construct(
|
||||||
private bool $isBlocking;
|
private string $lockName,
|
||||||
private float $ttl;
|
private bool $isBlocking,
|
||||||
|
private float $ttl = self::DEFAULT_TTL
|
||||||
private function __construct(string $lockName, bool $isBlocking, float $ttl = self::DEFAULT_TTL)
|
) {
|
||||||
{
|
|
||||||
$this->lockName = $lockName;
|
|
||||||
$this->isBlocking = $isBlocking;
|
|
||||||
$this->ttl = $ttl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function blocking(string $lockName): self
|
public static function blocking(string $lockName): self
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ class DownloadGeoLiteDbCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'visit:download-db';
|
public const NAME = 'visit:download-db';
|
||||||
|
|
||||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
|
||||||
private ?ProgressBar $progressBar = null;
|
private ?ProgressBar $progressBar = null;
|
||||||
|
|
||||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater)
|
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
$this->dbUpdater = $dbUpdater;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -71,7 +69,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($io->isVerbose()) {
|
if ($io->isVerbose()) {
|
||||||
$this->getApplication()->renderThrowable($e, $io);
|
$this->getApplication()?->renderThrowable($e, $io);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
||||||
|
|||||||
@@ -30,19 +30,14 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
{
|
{
|
||||||
public const NAME = 'visit:locate';
|
public const NAME = 'visit:locate';
|
||||||
|
|
||||||
private VisitLocatorInterface $visitLocator;
|
|
||||||
private IpLocationResolverInterface $ipLocationResolver;
|
|
||||||
|
|
||||||
private SymfonyStyle $io;
|
private SymfonyStyle $io;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
VisitLocatorInterface $visitLocator,
|
private VisitLocatorInterface $visitLocator,
|
||||||
IpLocationResolverInterface $ipLocationResolver,
|
private IpLocationResolverInterface $ipLocationResolver,
|
||||||
LockFactory $locker
|
LockFactory $locker
|
||||||
) {
|
) {
|
||||||
parent::__construct($locker);
|
parent::__construct($locker);
|
||||||
$this->visitLocator = $visitLocator;
|
|
||||||
$this->ipLocationResolver = $ipLocationResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@@ -124,7 +119,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->io->error($e->getMessage());
|
$this->io->error($e->getMessage());
|
||||||
if ($this->io->isVerbose()) {
|
if ($this->io->isVerbose()) {
|
||||||
$this->getApplication()->renderThrowable($e, $this->io);
|
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCodes::EXIT_FAILURE;
|
||||||
@@ -156,7 +151,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
} catch (WrongIpException $e) {
|
} catch (WrongIpException $e) {
|
||||||
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||||
if ($this->io->isVerbose()) {
|
if ($this->io->isVerbose()) {
|
||||||
$this->getApplication()->renderThrowable($e, $this->io);
|
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw IpCannotBeLocatedException::forError($e);
|
throw IpCannotBeLocatedException::forError($e);
|
||||||
|
|||||||
@@ -19,21 +19,12 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||||||
{
|
{
|
||||||
private const LOCK_NAME = 'geolocation-db-update';
|
private const LOCK_NAME = 'geolocation-db-update';
|
||||||
|
|
||||||
private DbUpdaterInterface $dbUpdater;
|
|
||||||
private Reader $geoLiteDbReader;
|
|
||||||
private LockFactory $locker;
|
|
||||||
private TrackingOptions $trackingOptions;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
DbUpdaterInterface $dbUpdater,
|
private DbUpdaterInterface $dbUpdater,
|
||||||
Reader $geoLiteDbReader,
|
private Reader $geoLiteDbReader,
|
||||||
LockFactory $locker,
|
private LockFactory $locker,
|
||||||
TrackingOptions $trackingOptions
|
private TrackingOptions $trackingOptions
|
||||||
) {
|
) {
|
||||||
$this->dbUpdater = $dbUpdater;
|
|
||||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
|
||||||
$this->locker = $locker;
|
|
||||||
$this->trackingOptions = $trackingOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,12 +18,10 @@ use function str_replace;
|
|||||||
|
|
||||||
class ProcessRunner implements ProcessRunnerInterface
|
class ProcessRunner implements ProcessRunnerInterface
|
||||||
{
|
{
|
||||||
private ProcessHelper $helper;
|
|
||||||
private Closure $createProcess;
|
private Closure $createProcess;
|
||||||
|
|
||||||
public function __construct(ProcessHelper $helper, ?callable $createProcess = null)
|
public function __construct(private ProcessHelper $helper, ?callable $createProcess = null)
|
||||||
{
|
{
|
||||||
$this->helper = $helper;
|
|
||||||
$this->createProcess = $createProcess !== null
|
$this->createProcess = $createProcess !== null
|
||||||
? Closure::fromCallable($createProcess)
|
? Closure::fromCallable($createProcess)
|
||||||
: static fn (array $cmd) => new Process($cmd, null, null, null, LockedCommandConfig::DEFAULT_TTL);
|
: static fn (array $cmd) => new Process($cmd, null, null, null, LockedCommandConfig::DEFAULT_TTL);
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ final class ShlinkTable
|
|||||||
private const DEFAULT_STYLE_NAME = 'default';
|
private const DEFAULT_STYLE_NAME = 'default';
|
||||||
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
||||||
|
|
||||||
private ?Table $baseTable;
|
public function __construct(private Table $baseTable)
|
||||||
|
|
||||||
public function __construct(Table $baseTable)
|
|
||||||
{
|
{
|
||||||
$this->baseTable = $baseTable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromOutput(OutputInterface $output): self
|
public static function fromOutput(OutputInterface $output): self
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class RoleResolverTest extends TestCase
|
|||||||
public function properRolesAreResolvedBasedOnInput(
|
public function properRolesAreResolvedBasedOnInput(
|
||||||
InputInterface $input,
|
InputInterface $input,
|
||||||
array $expectedRoles,
|
array $expectedRoles,
|
||||||
int $expectedDomainCalls
|
int $expectedDomainCalls,
|
||||||
): void {
|
): void {
|
||||||
$getDomain = $this->domainService->getOrCreate('example.com')->willReturn(
|
$getDomain = $this->domainService->getOrCreate('example.com')->willReturn(
|
||||||
(new Domain('example.com'))->setId('1'),
|
(new Domain('example.com'))->setId('1'),
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
||||||
array $retryAnswer,
|
array $retryAnswer,
|
||||||
int $expectedDeleteCalls,
|
int $expectedDeleteCalls,
|
||||||
string $expectedMessage
|
string $expectedMessage,
|
||||||
): void {
|
): void {
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$identifier = new ShortUrlIdentifier($shortCode);
|
$identifier = new ShortUrlIdentifier($shortCode);
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
array $input,
|
array $input,
|
||||||
array $expectedContents,
|
array $expectedContents,
|
||||||
array $notExpectedContents,
|
array $notExpectedContents,
|
||||||
ApiKey $apiKey
|
ApiKey $apiKey,
|
||||||
): void {
|
): void {
|
||||||
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())
|
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())
|
||||||
->willReturn(new Paginator(new ArrayAdapter([
|
->willReturn(new Paginator(new ArrayAdapter([
|
||||||
@@ -185,7 +185,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
?string $searchTerm,
|
?string $searchTerm,
|
||||||
array $tags,
|
array $tags,
|
||||||
?string $startDate = null,
|
?string $startDate = null,
|
||||||
?string $endDate = null
|
?string $endDate = null,
|
||||||
): void {
|
): void {
|
||||||
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
|
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||||||
public function showsProperMessageWhenGeoLiteUpdateFails(
|
public function showsProperMessageWhenGeoLiteUpdateFails(
|
||||||
bool $olderDbExists,
|
bool $olderDbExists,
|
||||||
string $expectedMessage,
|
string $expectedMessage,
|
||||||
int $expectedExitCode
|
int $expectedExitCode,
|
||||||
): void {
|
): void {
|
||||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||||
function (array $args) use ($olderDbExists): void {
|
function (array $args) use ($olderDbExists): void {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class LocateVisitsCommandTest extends TestCase
|
|||||||
int $expectedEmptyCalls,
|
int $expectedEmptyCalls,
|
||||||
int $expectedAllCalls,
|
int $expectedAllCalls,
|
||||||
bool $expectWarningPrint,
|
bool $expectWarningPrint,
|
||||||
array $args
|
array $args,
|
||||||
): void {
|
): void {
|
||||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', ''));
|
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', ''));
|
||||||
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
|
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ return [
|
|||||||
Domain\DomainService::class => ConfigAbstractFactory::class,
|
Domain\DomainService::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
||||||
|
Visit\RequestTracker::class => ConfigAbstractFactory::class,
|
||||||
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
||||||
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
||||||
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
||||||
@@ -53,7 +54,9 @@ return [
|
|||||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
|
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
|
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
|
||||||
|
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||||
|
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
@@ -69,7 +72,7 @@ return [
|
|||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
||||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\VisitsTracker::class],
|
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\RequestTracker::class],
|
||||||
ErrorHandler\NotFoundRedirectHandler::class => [
|
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||||
NotFoundRedirectOptions::class,
|
NotFoundRedirectOptions::class,
|
||||||
Util\RedirectResponseHelper::class,
|
Util\RedirectResponseHelper::class,
|
||||||
@@ -92,6 +95,7 @@ return [
|
|||||||
EventDispatcherInterface::class,
|
EventDispatcherInterface::class,
|
||||||
Options\TrackingOptions::class,
|
Options\TrackingOptions::class,
|
||||||
],
|
],
|
||||||
|
Visit\RequestTracker::class => [Visit\VisitsTracker::class, Options\TrackingOptions::class],
|
||||||
Service\ShortUrlService::class => [
|
Service\ShortUrlService::class => [
|
||||||
'em',
|
'em',
|
||||||
Service\ShortUrl\ShortUrlResolver::class,
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
@@ -116,17 +120,11 @@ return [
|
|||||||
|
|
||||||
Action\RedirectAction::class => [
|
Action\RedirectAction::class => [
|
||||||
Service\ShortUrl\ShortUrlResolver::class,
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
Visit\VisitsTracker::class,
|
Visit\RequestTracker::class,
|
||||||
Options\TrackingOptions::class,
|
ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
|
||||||
Util\RedirectResponseHelper::class,
|
Util\RedirectResponseHelper::class,
|
||||||
'Logger_Shlink',
|
|
||||||
],
|
|
||||||
Action\PixelAction::class => [
|
|
||||||
Service\ShortUrl\ShortUrlResolver::class,
|
|
||||||
Visit\VisitsTracker::class,
|
|
||||||
Options\TrackingOptions::class,
|
|
||||||
'Logger_Shlink',
|
|
||||||
],
|
],
|
||||||
|
Action\PixelAction::class => [Service\ShortUrl\ShortUrlResolver::class, Visit\RequestTracker::class],
|
||||||
Action\QrCodeAction::class => [
|
Action\QrCodeAction::class => [
|
||||||
Service\ShortUrl\ShortUrlResolver::class,
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
ShortUrl\Helper\ShortUrlStringifier::class,
|
ShortUrl\Helper\ShortUrlStringifier::class,
|
||||||
@@ -137,7 +135,15 @@ return [
|
|||||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
||||||
ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'],
|
ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'],
|
||||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class],
|
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class],
|
||||||
|
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::class],
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
|
ShortUrl\Transformer\ShortUrlDataTransformer::class => [ShortUrl\Helper\ShortUrlStringifier::class],
|
||||||
|
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => [
|
||||||
|
Service\ShortUrl\ShortUrlResolver::class,
|
||||||
|
Visit\RequestTracker::class,
|
||||||
|
ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
|
||||||
|
Util\RedirectResponseHelper::class,
|
||||||
|
Options\UrlShortenerOptions::class,
|
||||||
|
],
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => [
|
Mercure\MercureUpdatesGenerator::class => [
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
|
|||||||
@@ -51,20 +51,12 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
|
|||||||
$startDate = parseDateFromQuery($query, $startDateName);
|
$startDate = parseDateFromQuery($query, $startDateName);
|
||||||
$endDate = parseDateFromQuery($query, $endDateName);
|
$endDate = parseDateFromQuery($query, $endDateName);
|
||||||
|
|
||||||
// TODO Use match expression when migrating to PHP8
|
return match (true) {
|
||||||
if ($startDate === null && $endDate === null) {
|
$startDate === null && $endDate === null => DateRange::emptyInstance(),
|
||||||
return DateRange::emptyInstance();
|
$startDate !== null && $endDate !== null => DateRange::withStartAndEndDate($startDate, $endDate),
|
||||||
}
|
$startDate !== null => DateRange::withStartDate($startDate),
|
||||||
|
default => DateRange::withEndDate($endDate),
|
||||||
if ($startDate !== null && $endDate !== null) {
|
};
|
||||||
return DateRange::withStartAndEndDate($startDate, $endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($startDate !== null) {
|
|
||||||
return DateRange::withStartDate($startDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateRange::withEndDate($endDate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,91 +5,46 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Action;
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
use Fig\Http\Message\RequestMethodInterface;
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
use GuzzleHttp\Psr7\Query;
|
|
||||||
use League\Uri\Uri;
|
|
||||||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
||||||
|
|
||||||
use function array_key_exists;
|
|
||||||
use function array_merge;
|
|
||||||
|
|
||||||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||||
{
|
{
|
||||||
private ShortUrlResolverInterface $urlResolver;
|
|
||||||
private VisitsTrackerInterface $visitTracker;
|
|
||||||
private TrackingOptions $trackingOptions;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShortUrlResolverInterface $urlResolver,
|
private ShortUrlResolverInterface $urlResolver,
|
||||||
VisitsTrackerInterface $visitTracker,
|
private RequestTrackerInterface $requestTracker,
|
||||||
TrackingOptions $trackingOptions,
|
|
||||||
?LoggerInterface $logger = null
|
|
||||||
) {
|
) {
|
||||||
$this->urlResolver = $urlResolver;
|
|
||||||
$this->visitTracker = $visitTracker;
|
|
||||||
$this->trackingOptions = $trackingOptions;
|
|
||||||
$this->logger = $logger ?? new NullLogger();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
$identifier = ShortUrlIdentifier::fromRedirectRequest($request);
|
$identifier = ShortUrlIdentifier::fromRedirectRequest($request);
|
||||||
$query = $request->getQueryParams();
|
|
||||||
$disableTrackParam = $this->trackingOptions->getDisableTrackParam();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
|
$shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
|
||||||
|
$this->requestTracker->trackIfApplicable($shortUrl, $request);
|
||||||
|
|
||||||
if ($this->shouldTrackRequest($request, $query, $disableTrackParam)) {
|
return $this->createSuccessResp($shortUrl, $request);
|
||||||
$this->visitTracker->track($shortUrl, Visitor::fromRequest($request));
|
} catch (ShortUrlNotFoundException) {
|
||||||
}
|
|
||||||
|
|
||||||
return $this->createSuccessResp($this->buildUrlToRedirectTo($shortUrl, $query, $disableTrackParam));
|
|
||||||
} catch (ShortUrlNotFoundException $e) {
|
|
||||||
$this->logger->warning('An error occurred while tracking short code. {e}', ['e' => $e]);
|
|
||||||
return $this->createErrorResp($request, $handler);
|
return $this->createErrorResp($request, $handler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string
|
abstract protected function createSuccessResp(
|
||||||
{
|
ShortUrl $shortUrl,
|
||||||
$uri = Uri::createFromString($shortUrl->getLongUrl());
|
|
||||||
$hardcodedQuery = Query::parse($uri->getQuery() ?? '');
|
|
||||||
if ($disableTrackParam !== null) {
|
|
||||||
unset($currentQuery[$disableTrackParam]);
|
|
||||||
}
|
|
||||||
$mergedQuery = array_merge($hardcodedQuery, $currentQuery);
|
|
||||||
|
|
||||||
return (string) (empty($mergedQuery) ? $uri : $uri->withQuery(Query::build($mergedQuery)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
|
|
||||||
{
|
|
||||||
$forwardedMethod = $request->getAttribute(ImplicitHeadMiddleware::FORWARDED_HTTP_METHOD_ATTRIBUTE);
|
|
||||||
if ($forwardedMethod === self::METHOD_HEAD) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $disableTrackParam === null || ! array_key_exists($disableTrackParam, $query);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected function createSuccessResp(string $longUrl): ResponseInterface;
|
|
||||||
|
|
||||||
abstract protected function createErrorResp(
|
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
RequestHandlerInterface $handler
|
|
||||||
): ResponseInterface;
|
): ResponseInterface;
|
||||||
|
|
||||||
|
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||||
|
{
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
112
module/Core/src/Action/Model/QrCodeParams.php
Normal file
112
module/Core/src/Action/Model/QrCodeParams.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\Action\Model;
|
||||||
|
|
||||||
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
|
||||||
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
|
||||||
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
|
||||||
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
|
||||||
|
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
|
||||||
|
use Endroid\QrCode\Writer\PngWriter;
|
||||||
|
use Endroid\QrCode\Writer\SvgWriter;
|
||||||
|
use Endroid\QrCode\Writer\WriterInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
|
||||||
|
use function strtolower;
|
||||||
|
use function trim;
|
||||||
|
|
||||||
|
final class QrCodeParams
|
||||||
|
{
|
||||||
|
private const DEFAULT_SIZE = 300;
|
||||||
|
private const MIN_SIZE = 50;
|
||||||
|
private const MAX_SIZE = 1000;
|
||||||
|
|
||||||
|
private function __construct(
|
||||||
|
private int $size,
|
||||||
|
private int $margin,
|
||||||
|
private WriterInterface $writer,
|
||||||
|
private ErrorCorrectionLevelInterface $errorCorrectionLevel
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function fromRequest(ServerRequestInterface $request): self
|
||||||
|
{
|
||||||
|
$query = $request->getQueryParams();
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
self::resolveSize($request, $query),
|
||||||
|
self::resolveMargin($query),
|
||||||
|
self::resolveWriter($query),
|
||||||
|
self::resolveErrorCorrection($query),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function resolveSize(Request $request, array $query): int
|
||||||
|
{
|
||||||
|
// FIXME Size attribute is deprecated. After v3.0.0, always use the query param instead
|
||||||
|
$size = (int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE);
|
||||||
|
if ($size < self::MIN_SIZE) {
|
||||||
|
return self::MIN_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $size > self::MAX_SIZE ? self::MAX_SIZE : $size;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function resolveMargin(array $query): int
|
||||||
|
{
|
||||||
|
$margin = $query['margin'] ?? null;
|
||||||
|
if ($margin === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intMargin = (int) $margin;
|
||||||
|
if ($margin !== (string) $intMargin) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $intMargin < 0 ? 0 : $intMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function resolveWriter(array $query): WriterInterface
|
||||||
|
{
|
||||||
|
$format = strtolower(trim($query['format'] ?? 'png'));
|
||||||
|
return match ($format) {
|
||||||
|
'svg' => new SvgWriter(),
|
||||||
|
default => new PngWriter(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function resolveErrorCorrection(array $query): ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
$errorCorrectionLevel = strtolower(trim($query['errorCorrection'] ?? 'l'));
|
||||||
|
return match ($errorCorrectionLevel) {
|
||||||
|
'h' => new ErrorCorrectionLevelHigh(),
|
||||||
|
'q' => new ErrorCorrectionLevelQuartile(),
|
||||||
|
'm' => new ErrorCorrectionLevelMedium(),
|
||||||
|
default => new ErrorCorrectionLevelLow(), // 'l'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function size(): int
|
||||||
|
{
|
||||||
|
return $this->size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function margin(): int
|
||||||
|
{
|
||||||
|
return $this->margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function writer(): WriterInterface
|
||||||
|
{
|
||||||
|
return $this->writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function errorCorrectionLevel(): ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
return $this->errorCorrectionLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,17 +8,18 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
|
||||||
class PixelAction extends AbstractTrackingAction
|
class PixelAction extends AbstractTrackingAction
|
||||||
{
|
{
|
||||||
protected function createSuccessResp(string $longUrl): ResponseInterface
|
protected function createSuccessResp(ShortUrl $shortUrl, ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
return new PixelResponse();
|
return new PixelResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createErrorResp(
|
protected function createErrorResp(
|
||||||
ServerRequestInterface $request,
|
ServerRequestInterface $request,
|
||||||
RequestHandlerInterface $handler
|
RequestHandlerInterface $handler,
|
||||||
): ResponseInterface {
|
): ResponseInterface {
|
||||||
return new PixelResponse();
|
return new PixelResponse();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core\Action;
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
use Endroid\QrCode\Builder\Builder;
|
use Endroid\QrCode\Builder\Builder;
|
||||||
use Endroid\QrCode\Writer\SvgWriter;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||||
|
use Shlinkio\Shlink\Core\Action\Model\QrCodeParams;
|
||||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
@@ -20,22 +19,11 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|||||||
|
|
||||||
class QrCodeAction implements MiddlewareInterface
|
class QrCodeAction implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private const DEFAULT_SIZE = 300;
|
|
||||||
private const MIN_SIZE = 50;
|
|
||||||
private const MAX_SIZE = 1000;
|
|
||||||
|
|
||||||
private ShortUrlResolverInterface $urlResolver;
|
|
||||||
private ShortUrlStringifierInterface $stringifier;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShortUrlResolverInterface $urlResolver,
|
private ShortUrlResolverInterface $urlResolver,
|
||||||
ShortUrlStringifierInterface $stringifier,
|
private ShortUrlStringifierInterface $stringifier,
|
||||||
?LoggerInterface $logger = null
|
private LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
$this->urlResolver = $urlResolver;
|
|
||||||
$this->logger = $logger ?? new NullLogger();
|
|
||||||
$this->stringifier = $stringifier;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||||
@@ -49,43 +37,14 @@ class QrCodeAction implements MiddlewareInterface
|
|||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$query = $request->getQueryParams();
|
$params = QrCodeParams::fromRequest($request);
|
||||||
$qrCode = Builder::create()
|
$qrCodeBuilder = Builder::create()
|
||||||
->data($this->stringifier->stringify($shortUrl))
|
->data($this->stringifier->stringify($shortUrl))
|
||||||
->size($this->resolveSize($request, $query))
|
->size($params->size())
|
||||||
->margin($this->resolveMargin($query));
|
->margin($params->margin())
|
||||||
|
->writer($params->writer())
|
||||||
|
->errorCorrectionLevel($params->errorCorrectionLevel());
|
||||||
|
|
||||||
$format = $query['format'] ?? 'png';
|
return new QrCodeResponse($qrCodeBuilder->build());
|
||||||
if ($format === 'svg') {
|
|
||||||
$qrCode->writer(new SvgWriter());
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QrCodeResponse($qrCode->build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveSize(Request $request, array $query): int
|
|
||||||
{
|
|
||||||
// Size attribute is deprecated. After v3.0.0, always use the query param instead
|
|
||||||
$size = (int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE);
|
|
||||||
if ($size < self::MIN_SIZE) {
|
|
||||||
return self::MIN_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $size > self::MAX_SIZE ? self::MAX_SIZE : $size;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveMargin(array $query): int
|
|
||||||
{
|
|
||||||
if (! isset($query['margin'])) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$margin = $query['margin'];
|
|
||||||
$intMargin = (int) $margin;
|
|
||||||
if ($margin !== (string) $intMargin) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $intMargin < 0 ? 0 : $intMargin;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,35 +7,26 @@ namespace Shlinkio\Shlink\Core\Action;
|
|||||||
use Fig\Http\Message\StatusCodeInterface;
|
use Fig\Http\Message\StatusCodeInterface;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Psr\Log\LoggerInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Options;
|
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlRedirectionBuilderInterface;
|
||||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
||||||
|
|
||||||
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
||||||
{
|
{
|
||||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShortUrlResolverInterface $urlResolver,
|
ShortUrlResolverInterface $urlResolver,
|
||||||
VisitsTrackerInterface $visitTracker,
|
RequestTrackerInterface $requestTracker,
|
||||||
Options\TrackingOptions $trackingOptions,
|
private ShortUrlRedirectionBuilderInterface $redirectionBuilder,
|
||||||
RedirectResponseHelperInterface $redirectResponseHelper,
|
private RedirectResponseHelperInterface $redirectResponseHelper,
|
||||||
?LoggerInterface $logger = null
|
|
||||||
) {
|
) {
|
||||||
parent::__construct($urlResolver, $visitTracker, $trackingOptions, $logger);
|
parent::__construct($urlResolver, $requestTracker);
|
||||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createSuccessResp(string $longUrl): Response
|
protected function createSuccessResp(ShortUrl $shortUrl, ServerRequestInterface $request): Response
|
||||||
{
|
{
|
||||||
|
$longUrl = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $request->getQueryParams());
|
||||||
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
|
||||||
{
|
|
||||||
return $handler->handle($request);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ use const PHP_EOL;
|
|||||||
|
|
||||||
class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
||||||
{
|
{
|
||||||
private CrawlingHelperInterface $crawlingHelper;
|
public function __construct(private CrawlingHelperInterface $crawlingHelper)
|
||||||
|
|
||||||
public function __construct(CrawlingHelperInterface $crawlingHelper)
|
|
||||||
{
|
{
|
||||||
$this->crawlingHelper = $crawlingHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
|
|||||||
@@ -10,11 +10,8 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
|||||||
|
|
||||||
class CrawlingHelper implements CrawlingHelperInterface
|
class CrawlingHelper implements CrawlingHelperInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
public function __construct(private EntityManagerInterface $em)
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listCrawlableShortCodes(): iterable
|
public function listCrawlableShortCodes(): iterable
|
||||||
|
|||||||
@@ -16,13 +16,8 @@ use function Functional\map;
|
|||||||
|
|
||||||
class DomainService implements DomainServiceInterface
|
class DomainService implements DomainServiceInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
public function __construct(private EntityManagerInterface $em, private string $defaultDomain)
|
||||||
private string $defaultDomain;
|
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em, string $defaultDomain)
|
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
$this->defaultDomain = $defaultDomain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,7 +30,7 @@ class DomainService implements DomainServiceInterface
|
|||||||
$domains = $repo->findDomainsWithout($this->defaultDomain, $apiKey);
|
$domains = $repo->findDomainsWithout($this->defaultDomain, $apiKey);
|
||||||
$mappedDomains = map($domains, fn (Domain $domain) => new DomainItem($domain->getAuthority(), false));
|
$mappedDomains = map($domains, fn (Domain $domain) => new DomainItem($domain->getAuthority(), false));
|
||||||
|
|
||||||
if ($apiKey !== null && $apiKey->hasRole(Role::DOMAIN_SPECIFIC)) {
|
if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) {
|
||||||
return $mappedDomains;
|
return $mappedDomains;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,8 @@ use JsonSerializable;
|
|||||||
|
|
||||||
final class DomainItem implements JsonSerializable
|
final class DomainItem implements JsonSerializable
|
||||||
{
|
{
|
||||||
private string $domain;
|
public function __construct(private string $domain, private bool $isDefault)
|
||||||
private bool $isDefault;
|
|
||||||
|
|
||||||
public function __construct(string $domain, bool $isDefault)
|
|
||||||
{
|
{
|
||||||
$this->domain = $domain;
|
|
||||||
$this->isDefault = $isDefault;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
|||||||
|
|
||||||
class Domain extends AbstractEntity implements JsonSerializable
|
class Domain extends AbstractEntity implements JsonSerializable
|
||||||
{
|
{
|
||||||
private string $authority;
|
public function __construct(private string $authority)
|
||||||
|
|
||||||
public function __construct(string $authority)
|
|
||||||
{
|
{
|
||||||
$this->authority = $authority;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAuthority(): string
|
public function getAuthority(): string
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
|
|
||||||
public static function fromMeta(
|
public static function fromMeta(
|
||||||
ShortUrlMeta $meta,
|
ShortUrlMeta $meta,
|
||||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||||
): self {
|
): self {
|
||||||
$instance = new self();
|
$instance = new self();
|
||||||
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
||||||
@@ -87,7 +87,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
public static function fromImport(
|
public static function fromImport(
|
||||||
ImportedShlinkUrl $url,
|
ImportedShlinkUrl $url,
|
||||||
bool $importShortCode,
|
bool $importShortCode,
|
||||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||||
): self {
|
): self {
|
||||||
$meta = [
|
$meta = [
|
||||||
ShortUrlInputFilter::VALIDATE_URL => false,
|
ShortUrlInputFilter::VALIDATE_URL => false,
|
||||||
@@ -209,7 +209,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
|
|
||||||
public function update(
|
public function update(
|
||||||
ShortUrlEdit $shortUrlEdit,
|
ShortUrlEdit $shortUrlEdit,
|
||||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||||
): void {
|
): void {
|
||||||
if ($shortUrlEdit->validSinceWasProvided()) {
|
if ($shortUrlEdit->validSinceWasProvided()) {
|
||||||
$this->validSince = $shortUrlEdit->validSince();
|
$this->validSince = $shortUrlEdit->validSince();
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
|||||||
|
|
||||||
class Tag extends AbstractEntity implements JsonSerializable
|
class Tag extends AbstractEntity implements JsonSerializable
|
||||||
{
|
{
|
||||||
private string $name;
|
|
||||||
private Collections\Collection $shortUrls;
|
private Collections\Collection $shortUrls;
|
||||||
|
|
||||||
public function __construct(string $name)
|
public function __construct(private string $name)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
|
||||||
$this->shortUrls = new Collections\ArrayCollection();
|
$this->shortUrls = new Collections\ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return (string) IpAddress::fromString($address)->getAnonymizedCopy();
|
return (string) IpAddress::fromString($address)->getAnonymizedCopy();
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,31 +13,24 @@ use function rtrim;
|
|||||||
|
|
||||||
class NotFoundType
|
class NotFoundType
|
||||||
{
|
{
|
||||||
private string $type;
|
private function __construct(private string $type)
|
||||||
|
|
||||||
private function __construct(string $type)
|
|
||||||
{
|
{
|
||||||
$this->type = $type;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromRequest(ServerRequestInterface $request, string $basePath): self
|
public static function fromRequest(ServerRequestInterface $request, string $basePath): self
|
||||||
{
|
{
|
||||||
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
|
|
||||||
if ($isBaseUrl) {
|
|
||||||
return new self(Visit::TYPE_BASE_URL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var RouteResult $routeResult */
|
/** @var RouteResult $routeResult */
|
||||||
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
||||||
if ($routeResult->isFailure()) {
|
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
|
||||||
return new self(Visit::TYPE_REGULAR_404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($routeResult->getMatchedRouteName() === RedirectAction::class) {
|
$type = match (true) {
|
||||||
return new self(Visit::TYPE_INVALID_SHORT_URL);
|
$isBaseUrl => Visit::TYPE_BASE_URL,
|
||||||
}
|
$routeResult->isFailure() => Visit::TYPE_REGULAR_404,
|
||||||
|
$routeResult->getMatchedRouteName() === RedirectAction::class => Visit::TYPE_INVALID_SHORT_URL,
|
||||||
|
default => self::class,
|
||||||
|
};
|
||||||
|
|
||||||
return new self(self::class);
|
return new self($type);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isBaseUrl(): bool
|
public function isBaseUrl(): bool
|
||||||
|
|||||||
@@ -14,15 +14,10 @@ use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
|||||||
|
|
||||||
class NotFoundRedirectHandler implements MiddlewareInterface
|
class NotFoundRedirectHandler implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private Options\NotFoundRedirectOptions $redirectOptions;
|
|
||||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
Options\NotFoundRedirectOptions $redirectOptions,
|
private Options\NotFoundRedirectOptions $redirectOptions,
|
||||||
RedirectResponseHelperInterface $redirectResponseHelper
|
private RedirectResponseHelperInterface $redirectResponseHelper
|
||||||
) {
|
) {
|
||||||
$this->redirectOptions = $redirectOptions;
|
|
||||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class NotFoundTemplateHandler implements RequestHandlerInterface
|
|||||||
private const TEMPLATES_BASE_DIR = __DIR__ . '/../../templates';
|
private const TEMPLATES_BASE_DIR = __DIR__ . '/../../templates';
|
||||||
public const NOT_FOUND_TEMPLATE = '404.html';
|
public const NOT_FOUND_TEMPLATE = '404.html';
|
||||||
public const INVALID_SHORT_CODE_TEMPLATE = 'invalid-short-code.html';
|
public const INVALID_SHORT_CODE_TEMPLATE = 'invalid-short-code.html';
|
||||||
|
|
||||||
private Closure $readFile;
|
private Closure $readFile;
|
||||||
|
|
||||||
public function __construct(?callable $readFile = null)
|
public function __construct(?callable $readFile = null)
|
||||||
|
|||||||
@@ -8,33 +8,17 @@ use Psr\Http\Message\ResponseInterface;
|
|||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
|
||||||
|
|
||||||
class NotFoundTrackerMiddleware implements MiddlewareInterface
|
class NotFoundTrackerMiddleware implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private VisitsTrackerInterface $visitsTracker;
|
public function __construct(private RequestTrackerInterface $requestTracker)
|
||||||
|
|
||||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
|
||||||
{
|
{
|
||||||
$this->visitsTracker = $visitsTracker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
{
|
{
|
||||||
/** @var NotFoundType $notFoundType */
|
$this->requestTracker->trackNotFoundIfApplicable($request);
|
||||||
$notFoundType = $request->getAttribute(NotFoundType::class);
|
|
||||||
$visitor = Visitor::fromRequest($request);
|
|
||||||
|
|
||||||
if ($notFoundType->isBaseUrl()) {
|
|
||||||
$this->visitsTracker->trackBaseUrlVisit($visitor);
|
|
||||||
} elseif ($notFoundType->isRegularNotFound()) {
|
|
||||||
$this->visitsTracker->trackRegularNotFoundVisit($visitor);
|
|
||||||
} elseif ($notFoundType->isInvalidShortUrl()) {
|
|
||||||
$this->visitsTracker->trackInvalidShortUrlVisit($visitor);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $handler->handle($request);
|
return $handler->handle($request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,8 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
|||||||
|
|
||||||
class NotFoundTypeResolverMiddleware implements MiddlewareInterface
|
class NotFoundTypeResolverMiddleware implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
private string $shlinkBasePath;
|
public function __construct(private string $shlinkBasePath)
|
||||||
|
|
||||||
public function __construct(string $shlinkBasePath)
|
|
||||||
{
|
{
|
||||||
$this->shlinkBasePath = $shlinkBasePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
|||||||
@@ -8,13 +8,11 @@ use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManagerInterface;
|
|||||||
|
|
||||||
class CloseDbConnectionEventListener
|
class CloseDbConnectionEventListener
|
||||||
{
|
{
|
||||||
private ReopeningEntityManagerInterface $em;
|
|
||||||
/** @var callable */
|
/** @var callable */
|
||||||
private $wrapped;
|
private $wrapped;
|
||||||
|
|
||||||
public function __construct(ReopeningEntityManagerInterface $em, callable $wrapped)
|
public function __construct(private ReopeningEntityManagerInterface $em, callable $wrapped)
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
$this->wrapped = $wrapped;
|
$this->wrapped = $wrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class CloseDbConnectionEventListenerDelegator
|
|||||||
public function __invoke(
|
public function __invoke(
|
||||||
ContainerInterface $container,
|
ContainerInterface $container,
|
||||||
string $name,
|
string $name,
|
||||||
callable $callback
|
callable $callback,
|
||||||
): CloseDbConnectionEventListener {
|
): CloseDbConnectionEventListener {
|
||||||
/** @var callable $wrapped */
|
/** @var callable $wrapped */
|
||||||
$wrapped = $callback();
|
$wrapped = $callback();
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ use JsonSerializable;
|
|||||||
|
|
||||||
abstract class AbstractVisitEvent implements JsonSerializable
|
abstract class AbstractVisitEvent implements JsonSerializable
|
||||||
{
|
{
|
||||||
protected string $visitId;
|
public function __construct(protected string $visitId)
|
||||||
|
|
||||||
public function __construct(string $visitId)
|
|
||||||
{
|
{
|
||||||
$this->visitId = $visitId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function visitId(): string
|
public function visitId(): string
|
||||||
|
|||||||
@@ -6,12 +6,9 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
|||||||
|
|
||||||
final class UrlVisited extends AbstractVisitEvent
|
final class UrlVisited extends AbstractVisitEvent
|
||||||
{
|
{
|
||||||
private ?string $originalIpAddress;
|
public function __construct(string $visitId, private ?string $originalIpAddress = null)
|
||||||
|
|
||||||
public function __construct(string $visitId, ?string $originalIpAddress = null)
|
|
||||||
{
|
{
|
||||||
parent::__construct($visitId);
|
parent::__construct($visitId);
|
||||||
$this->originalIpAddress = $originalIpAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function originalIpAddress(): ?string
|
public function originalIpAddress(): ?string
|
||||||
|
|||||||
@@ -19,24 +19,13 @@ use Throwable;
|
|||||||
|
|
||||||
class LocateVisit
|
class LocateVisit
|
||||||
{
|
{
|
||||||
private IpLocationResolverInterface $ipLocationResolver;
|
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
private DbUpdaterInterface $dbUpdater;
|
|
||||||
private EventDispatcherInterface $eventDispatcher;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IpLocationResolverInterface $ipLocationResolver,
|
private IpLocationResolverInterface $ipLocationResolver,
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
DbUpdaterInterface $dbUpdater,
|
private DbUpdaterInterface $dbUpdater,
|
||||||
EventDispatcherInterface $eventDispatcher
|
private EventDispatcherInterface $eventDispatcher
|
||||||
) {
|
) {
|
||||||
$this->ipLocationResolver = $ipLocationResolver;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->dbUpdater = $dbUpdater;
|
|
||||||
$this->eventDispatcher = $eventDispatcher;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(UrlVisited $shortUrlVisited): void
|
public function __invoke(UrlVisited $shortUrlVisited): void
|
||||||
|
|||||||
@@ -17,21 +17,12 @@ use function Functional\each;
|
|||||||
|
|
||||||
class NotifyVisitToMercure
|
class NotifyVisitToMercure
|
||||||
{
|
{
|
||||||
private HubInterface $hub;
|
|
||||||
private MercureUpdatesGeneratorInterface $updatesGenerator;
|
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
HubInterface $hub,
|
private HubInterface $hub,
|
||||||
MercureUpdatesGeneratorInterface $updatesGenerator,
|
private MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
LoggerInterface $logger
|
private LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
$this->hub = $hub;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->updatesGenerator = $updatesGenerator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||||
|
|||||||
@@ -24,28 +24,15 @@ use function Functional\partial_left;
|
|||||||
|
|
||||||
class NotifyVisitToWebHooks
|
class NotifyVisitToWebHooks
|
||||||
{
|
{
|
||||||
private ClientInterface $httpClient;
|
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private LoggerInterface $logger;
|
|
||||||
/** @var string[] */
|
|
||||||
private array $webhooks;
|
|
||||||
private DataTransformerInterface $transformer;
|
|
||||||
private AppOptions $appOptions;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ClientInterface $httpClient,
|
private ClientInterface $httpClient,
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
LoggerInterface $logger,
|
private LoggerInterface $logger,
|
||||||
array $webhooks,
|
/** @var string[] */
|
||||||
DataTransformerInterface $transformer,
|
private array $webhooks,
|
||||||
AppOptions $appOptions
|
private DataTransformerInterface $transformer,
|
||||||
|
private AppOptions $appOptions
|
||||||
) {
|
) {
|
||||||
$this->httpClient = $httpClient;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->logger = $logger;
|
|
||||||
$this->webhooks = $webhooks;
|
|
||||||
$this->transformer = $transformer;
|
|
||||||
$this->appOptions = $appOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||||
|
|||||||
@@ -12,13 +12,8 @@ use function sprintf;
|
|||||||
|
|
||||||
class UpdateGeoLiteDb
|
class UpdateGeoLiteDb
|
||||||
{
|
{
|
||||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater, private LoggerInterface $logger)
|
||||||
private LoggerInterface $logger;
|
|
||||||
|
|
||||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater, LoggerInterface $logger)
|
|
||||||
{
|
{
|
||||||
$this->dbUpdater = $dbUpdater;
|
|
||||||
$this->logger = $logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __invoke(): void
|
public function __invoke(): void
|
||||||
|
|||||||
@@ -20,22 +20,14 @@ use function sprintf;
|
|||||||
|
|
||||||
class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private ShortUrlRelationResolverInterface $relationResolver;
|
|
||||||
private ShortCodeHelperInterface $shortCodeHelper;
|
|
||||||
private DoctrineBatchHelperInterface $batchHelper;
|
|
||||||
private ShortUrlRepositoryInterface $shortUrlRepo;
|
private ShortUrlRepositoryInterface $shortUrlRepo;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
ShortUrlRelationResolverInterface $relationResolver,
|
private ShortUrlRelationResolverInterface $relationResolver,
|
||||||
ShortCodeHelperInterface $shortCodeHelper,
|
private ShortCodeHelperInterface $shortCodeHelper,
|
||||||
DoctrineBatchHelperInterface $batchHelper
|
private DoctrineBatchHelperInterface $batchHelper
|
||||||
) {
|
) {
|
||||||
$this->em = $em;
|
|
||||||
$this->relationResolver = $relationResolver;
|
|
||||||
$this->shortCodeHelper = $shortCodeHelper;
|
|
||||||
$this->batchHelper = $batchHelper;
|
|
||||||
$this->shortUrlRepo = $this->em->getRepository(ShortUrl::class); // @phpstan-ignore-line
|
$this->shortUrlRepo = $this->em->getRepository(ShortUrl::class); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +56,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
$shortUrlImporting = $this->resolveShortUrl($importedUrl, $importShortCodes, $skipOnShortCodeConflict);
|
$shortUrlImporting = $this->resolveShortUrl($importedUrl, $importShortCodes, $skipOnShortCodeConflict);
|
||||||
} catch (NonUniqueSlugException $e) {
|
} catch (NonUniqueSlugException) {
|
||||||
$io->text(sprintf('%s: <fg=red>Error</>', $longUrl));
|
$io->text(sprintf('%s: <fg=red>Error</>', $longUrl));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -77,7 +69,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||||||
private function resolveShortUrl(
|
private function resolveShortUrl(
|
||||||
ImportedShlinkUrl $importedUrl,
|
ImportedShlinkUrl $importedUrl,
|
||||||
bool $importShortCodes,
|
bool $importShortCodes,
|
||||||
callable $skipOnShortCodeConflict
|
callable $skipOnShortCodeConflict,
|
||||||
): ShortUrlImporting {
|
): ShortUrlImporting {
|
||||||
$alreadyImportedShortUrl = $this->shortUrlRepo->findOneByImportedUrl($importedUrl);
|
$alreadyImportedShortUrl = $this->shortUrlRepo->findOneByImportedUrl($importedUrl);
|
||||||
if ($alreadyImportedShortUrl !== null) {
|
if ($alreadyImportedShortUrl !== null) {
|
||||||
@@ -96,7 +88,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||||||
private function handleShortCodeUniqueness(
|
private function handleShortCodeUniqueness(
|
||||||
ShortUrl $shortUrl,
|
ShortUrl $shortUrl,
|
||||||
bool $importShortCodes,
|
bool $importShortCodes,
|
||||||
callable $skipOnShortCodeConflict
|
callable $skipOnShortCodeConflict,
|
||||||
): bool {
|
): bool {
|
||||||
if ($this->shortCodeHelper->ensureShortCodeUniqueness($shortUrl, $importShortCodes)) {
|
if ($this->shortCodeHelper->ensureShortCodeUniqueness($shortUrl, $importShortCodes)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -14,13 +14,8 @@ use function sprintf;
|
|||||||
|
|
||||||
final class ShortUrlImporting
|
final class ShortUrlImporting
|
||||||
{
|
{
|
||||||
private ShortUrl $shortUrl;
|
private function __construct(private ShortUrl $shortUrl, private bool $isNew)
|
||||||
private bool $isNew;
|
|
||||||
|
|
||||||
private function __construct(ShortUrl $shortUrl, bool $isNew)
|
|
||||||
{
|
{
|
||||||
$this->shortUrl = $shortUrl;
|
|
||||||
$this->isNew = $isNew;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromExistingShortUrl(ShortUrl $shortUrl): self
|
public static function fromExistingShortUrl(ShortUrl $shortUrl): self
|
||||||
@@ -43,10 +38,7 @@ final class ShortUrlImporting
|
|||||||
$importedVisits = 0;
|
$importedVisits = 0;
|
||||||
foreach ($visits as $importedVisit) {
|
foreach ($visits as $importedVisit) {
|
||||||
// Skip visits which are older than the most recent already imported visit's date
|
// Skip visits which are older than the most recent already imported visit's date
|
||||||
if (
|
if ($mostRecentImportedDate?->gte(Chronos::instance($importedVisit->date()))) {
|
||||||
$mostRecentImportedDate !== null
|
|
||||||
&& $mostRecentImportedDate->gte(Chronos::instance($importedVisit->date()))
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,10 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
|
|||||||
private const NEW_VISIT_TOPIC = 'https://shlink.io/new-visit';
|
private const NEW_VISIT_TOPIC = 'https://shlink.io/new-visit';
|
||||||
private const NEW_ORPHAN_VISIT_TOPIC = 'https://shlink.io/new-orphan-visit';
|
private const NEW_ORPHAN_VISIT_TOPIC = 'https://shlink.io/new-orphan-visit';
|
||||||
|
|
||||||
private DataTransformerInterface $shortUrlTransformer;
|
|
||||||
private DataTransformerInterface $orphanVisitTransformer;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
DataTransformerInterface $shortUrlTransformer,
|
private DataTransformerInterface $shortUrlTransformer,
|
||||||
DataTransformerInterface $orphanVisitTransformer
|
private DataTransformerInterface $orphanVisitTransformer
|
||||||
) {
|
) {
|
||||||
$this->shortUrlTransformer = $shortUrlTransformer;
|
|
||||||
$this->orphanVisitTransformer = $orphanVisitTransformer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function newVisitUpdate(Visit $visit): Update
|
public function newVisitUpdate(Visit $visit): Update
|
||||||
|
|||||||
@@ -10,13 +10,8 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
|
|
||||||
final class ShortUrlIdentifier
|
final class ShortUrlIdentifier
|
||||||
{
|
{
|
||||||
private string $shortCode;
|
public function __construct(private string $shortCode, private ?string $domain = null)
|
||||||
private ?string $domain;
|
|
||||||
|
|
||||||
public function __construct(string $shortCode, ?string $domain = null)
|
|
||||||
{
|
{
|
||||||
$this->shortCode = $shortCode;
|
|
||||||
$this->domain = $domain;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromApiRequest(ServerRequestInterface $request): self
|
public static function fromApiRequest(ServerRequestInterface $request): self
|
||||||
@@ -46,7 +41,7 @@ final class ShortUrlIdentifier
|
|||||||
public static function fromShortUrl(ShortUrl $shortUrl): self
|
public static function fromShortUrl(ShortUrl $shortUrl): self
|
||||||
{
|
{
|
||||||
$domain = $shortUrl->getDomain();
|
$domain = $shortUrl->getDomain();
|
||||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
$domainAuthority = $domain?->getAuthority();
|
||||||
|
|
||||||
return new self($shortUrl->getShortCode(), $domainAuthority);
|
return new self($shortUrl->getShortCode(), $domainAuthority);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,20 +14,16 @@ final class VisitsParams
|
|||||||
private const ALL_ITEMS = -1;
|
private const ALL_ITEMS = -1;
|
||||||
|
|
||||||
private ?DateRange $dateRange;
|
private ?DateRange $dateRange;
|
||||||
private int $page;
|
|
||||||
private int $itemsPerPage;
|
private int $itemsPerPage;
|
||||||
private bool $excludeBots;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
?DateRange $dateRange = null,
|
?DateRange $dateRange = null,
|
||||||
int $page = self::FIRST_PAGE,
|
private int $page = self::FIRST_PAGE,
|
||||||
?int $itemsPerPage = null,
|
?int $itemsPerPage = null,
|
||||||
bool $excludeBots = false
|
private bool $excludeBots = false
|
||||||
) {
|
) {
|
||||||
$this->dateRange = $dateRange ?? new DateRange();
|
$this->dateRange = $dateRange ?? new DateRange();
|
||||||
$this->page = $page;
|
|
||||||
$this->itemsPerPage = $this->determineItemsPerPage($itemsPerPage);
|
$this->itemsPerPage = $this->determineItemsPerPage($itemsPerPage);
|
||||||
$this->excludeBots = $excludeBots;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function determineItemsPerPage(?int $itemsPerPage): int
|
private function determineItemsPerPage(?int $itemsPerPage): int
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class UrlShortenerOptions extends AbstractOptions
|
|||||||
private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE;
|
private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE;
|
||||||
private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME;
|
private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||||
private bool $autoResolveTitles = false;
|
private bool $autoResolveTitles = false;
|
||||||
|
private bool $appendExtraPath = false;
|
||||||
|
|
||||||
public function isUrlValidationEnabled(): bool
|
public function isUrlValidationEnabled(): bool
|
||||||
{
|
{
|
||||||
@@ -67,6 +68,16 @@ class UrlShortenerOptions extends AbstractOptions
|
|||||||
$this->autoResolveTitles = $autoResolveTitles;
|
$this->autoResolveTitles = $autoResolveTitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function appendExtraPath(): bool
|
||||||
|
{
|
||||||
|
return $this->appendExtraPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setAppendExtraPath(bool $appendExtraPath): void
|
||||||
|
{
|
||||||
|
$this->appendExtraPath = $appendExtraPath;
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void
|
protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,13 +11,8 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
|||||||
|
|
||||||
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||||
{
|
{
|
||||||
private VisitRepositoryInterface $repo;
|
public function __construct(private VisitRepositoryInterface $repo, private VisitsParams $params)
|
||||||
private VisitsParams $params;
|
|
||||||
|
|
||||||
public function __construct(VisitRepositoryInterface $repo, VisitsParams $params)
|
|
||||||
{
|
{
|
||||||
$this->repo = $repo;
|
|
||||||
$this->params = $params;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function doCount(): int
|
protected function doCount(): int
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||||
|
|
||||||
use Happyr\DoctrineSpecification\Specification\Specification;
|
|
||||||
use Pagerfanta\Adapter\AdapterInterface;
|
use Pagerfanta\Adapter\AdapterInterface;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||||
@@ -12,15 +11,11 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class ShortUrlRepositoryAdapter implements AdapterInterface
|
class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||||
{
|
{
|
||||||
private ShortUrlRepositoryInterface $repository;
|
public function __construct(
|
||||||
private ShortUrlsParams $params;
|
private ShortUrlRepositoryInterface $repository,
|
||||||
private ?ApiKey $apiKey;
|
private ShortUrlsParams $params,
|
||||||
|
private ?ApiKey $apiKey
|
||||||
public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params, ?ApiKey $apiKey)
|
) {
|
||||||
{
|
|
||||||
$this->repository = $repository;
|
|
||||||
$this->params = $params;
|
|
||||||
$this->apiKey = $apiKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSlice($offset, $length): array // phpcs:ignore
|
public function getSlice($offset, $length): array // phpcs:ignore
|
||||||
@@ -32,7 +27,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
|||||||
$this->params->tags(),
|
$this->params->tags(),
|
||||||
$this->params->orderBy(),
|
$this->params->orderBy(),
|
||||||
$this->params->dateRange(),
|
$this->params->dateRange(),
|
||||||
$this->resolveSpec(),
|
$this->apiKey?->spec(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,12 +37,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
|||||||
$this->params->searchTerm(),
|
$this->params->searchTerm(),
|
||||||
$this->params->tags(),
|
$this->params->tags(),
|
||||||
$this->params->dateRange(),
|
$this->params->dateRange(),
|
||||||
$this->resolveSpec(),
|
$this->apiKey?->spec(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSpec(): ?Specification
|
|
||||||
{
|
|
||||||
return $this->apiKey !== null ? $this->apiKey->spec() : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||||
|
|
||||||
use Happyr\DoctrineSpecification\Specification\Specification;
|
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
|
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
|
||||||
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
|
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
|
||||||
@@ -13,21 +12,12 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||||
{
|
{
|
||||||
private VisitRepositoryInterface $visitRepository;
|
|
||||||
private string $tag;
|
|
||||||
private VisitsParams $params;
|
|
||||||
private ?ApiKey $apiKey;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
VisitRepositoryInterface $visitRepository,
|
private VisitRepositoryInterface $visitRepository,
|
||||||
string $tag,
|
private string $tag,
|
||||||
VisitsParams $params,
|
private VisitsParams $params,
|
||||||
?ApiKey $apiKey
|
private ?ApiKey $apiKey
|
||||||
) {
|
) {
|
||||||
$this->visitRepository = $visitRepository;
|
|
||||||
$this->params = $params;
|
|
||||||
$this->tag = $tag;
|
|
||||||
$this->apiKey = $apiKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSlice($offset, $length): array // phpcs:ignore
|
public function getSlice($offset, $length): array // phpcs:ignore
|
||||||
@@ -37,7 +27,7 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
|||||||
new VisitsListFiltering(
|
new VisitsListFiltering(
|
||||||
$this->params->getDateRange(),
|
$this->params->getDateRange(),
|
||||||
$this->params->excludeBots(),
|
$this->params->excludeBots(),
|
||||||
$this->resolveSpec(),
|
$this->apiKey?->spec(true),
|
||||||
$length,
|
$length,
|
||||||
$offset,
|
$offset,
|
||||||
),
|
),
|
||||||
@@ -51,13 +41,8 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
|||||||
new VisitsCountFiltering(
|
new VisitsCountFiltering(
|
||||||
$this->params->getDateRange(),
|
$this->params->getDateRange(),
|
||||||
$this->params->excludeBots(),
|
$this->params->excludeBots(),
|
||||||
$this->resolveSpec(),
|
$this->apiKey?->spec(true),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSpec(): ?Specification
|
|
||||||
{
|
|
||||||
return $this->apiKey !== null ? $this->apiKey->spec(true) : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,21 +13,12 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
|||||||
|
|
||||||
class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||||
{
|
{
|
||||||
private VisitRepositoryInterface $visitRepository;
|
|
||||||
private ShortUrlIdentifier $identifier;
|
|
||||||
private VisitsParams $params;
|
|
||||||
private ?Specification $spec;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
VisitRepositoryInterface $visitRepository,
|
private VisitRepositoryInterface $visitRepository,
|
||||||
ShortUrlIdentifier $identifier,
|
private ShortUrlIdentifier $identifier,
|
||||||
VisitsParams $params,
|
private VisitsParams $params,
|
||||||
?Specification $spec
|
private ?Specification $spec
|
||||||
) {
|
) {
|
||||||
$this->visitRepository = $visitRepository;
|
|
||||||
$this->params = $params;
|
|
||||||
$this->identifier = $identifier;
|
|
||||||
$this->spec = $spec;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSlice($offset, $length): array // phpcs:ignore
|
public function getSlice($offset, $length): array // phpcs:ignore
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||||||
array $tags = [],
|
array $tags = [],
|
||||||
?ShortUrlsOrdering $orderBy = null,
|
?ShortUrlsOrdering $orderBy = null,
|
||||||
?DateRange $dateRange = null,
|
?DateRange $dateRange = null,
|
||||||
?Specification $spec = null
|
?Specification $spec = null,
|
||||||
): array {
|
): array {
|
||||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
||||||
$qb->select('DISTINCT s')
|
$qb->select('DISTINCT s')
|
||||||
@@ -43,7 +43,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||||||
->setFirstResult($offset);
|
->setFirstResult($offset);
|
||||||
|
|
||||||
// In case the ordering has been specified, the query could be more complex. Process it
|
// In case the ordering has been specified, the query could be more complex. Process it
|
||||||
if ($orderBy !== null && $orderBy->hasOrderField()) {
|
if ($orderBy?->hasOrderField()) {
|
||||||
return $this->processOrderByForList($qb, $orderBy);
|
return $this->processOrderByForList($qb, $orderBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||||||
?string $searchTerm = null,
|
?string $searchTerm = null,
|
||||||
array $tags = [],
|
array $tags = [],
|
||||||
?DateRange $dateRange = null,
|
?DateRange $dateRange = null,
|
||||||
?Specification $spec = null
|
?Specification $spec = null,
|
||||||
): int {
|
): int {
|
||||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
||||||
$qb->select('COUNT(DISTINCT s)');
|
$qb->select('COUNT(DISTINCT s)');
|
||||||
@@ -97,17 +97,17 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
|||||||
?string $searchTerm,
|
?string $searchTerm,
|
||||||
array $tags,
|
array $tags,
|
||||||
?DateRange $dateRange,
|
?DateRange $dateRange,
|
||||||
?Specification $spec
|
?Specification $spec,
|
||||||
): QueryBuilder {
|
): QueryBuilder {
|
||||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||||
$qb->from(ShortUrl::class, 's')
|
$qb->from(ShortUrl::class, 's')
|
||||||
->where('1=1');
|
->where('1=1');
|
||||||
|
|
||||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
if ($dateRange?->getStartDate() !== null) {
|
||||||
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
||||||
$qb->setParameter('startDate', $dateRange->getStartDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
$qb->setParameter('startDate', $dateRange->getStartDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
||||||
}
|
}
|
||||||
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
|
if ($dateRange?->getEndDate() !== null) {
|
||||||
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
||||||
$qb->setParameter('endDate', $dateRange->getEndDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
$qb->setParameter('endDate', $dateRange->getEndDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
|
|||||||
array $tags = [],
|
array $tags = [],
|
||||||
?ShortUrlsOrdering $orderBy = null,
|
?ShortUrlsOrdering $orderBy = null,
|
||||||
?DateRange $dateRange = null,
|
?DateRange $dateRange = null,
|
||||||
?Specification $spec = null
|
?Specification $spec = null,
|
||||||
): array;
|
): array;
|
||||||
|
|
||||||
public function countList(
|
public function countList(
|
||||||
?string $searchTerm = null,
|
?string $searchTerm = null,
|
||||||
array $tags = [],
|
array $tags = [],
|
||||||
?DateRange $dateRange = null,
|
?DateRange $dateRange = null,
|
||||||
?Specification $spec = null
|
?Specification $spec = null,
|
||||||
): int;
|
): int;
|
||||||
|
|
||||||
public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl;
|
public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl;
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
|||||||
$iterator = $qb->getQuery()->toIterable();
|
$iterator = $qb->getQuery()->toIterable();
|
||||||
$resultsFound = false;
|
$resultsFound = false;
|
||||||
|
|
||||||
/** @var Visit $visit */
|
|
||||||
foreach ($iterator as $key => $visit) {
|
foreach ($iterator as $key => $visit) {
|
||||||
$resultsFound = true;
|
$resultsFound = true;
|
||||||
yield $key => $visit;
|
yield $key => $visit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// As the query is ordered by ID, we can take the last one every time in order to exclude the whole list
|
// As the query is ordered by ID, we can take the last one every time in order to exclude the whole list
|
||||||
$lastId = isset($visit) ? $visit->getId() : $lastId;
|
/** @var Visit|null $visit */
|
||||||
|
$lastId = $visit?->getId() ?? $lastId;
|
||||||
} while ($resultsFound);
|
} while ($resultsFound);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,12 +101,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
|||||||
|
|
||||||
private function createVisitsByShortCodeQueryBuilder(
|
private function createVisitsByShortCodeQueryBuilder(
|
||||||
ShortUrlIdentifier $identifier,
|
ShortUrlIdentifier $identifier,
|
||||||
VisitsCountFiltering $filtering
|
VisitsCountFiltering $filtering,
|
||||||
): QueryBuilder {
|
): QueryBuilder {
|
||||||
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
|
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
|
||||||
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
|
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
|
||||||
$shortUrl = $shortUrlRepo->findOne($identifier, $filtering->spec());
|
$shortUrl = $shortUrlRepo->findOne($identifier, $filtering->spec());
|
||||||
$shortUrlId = $shortUrl !== null ? $shortUrl->getId() : -1;
|
$shortUrlId = $shortUrl?->getId() ?? '-1';
|
||||||
|
|
||||||
// Parameters in this query need to be part of the query itself, as we need to use it a sub-query later
|
// Parameters in this query need to be part of the query itself, as we need to use it a sub-query later
|
||||||
// Since they are not strictly provided by the caller, it's reasonably safe
|
// Since they are not strictly provided by the caller, it's reasonably safe
|
||||||
@@ -187,10 +187,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
|||||||
|
|
||||||
private function applyDatesInline(QueryBuilder $qb, ?DateRange $dateRange): void
|
private function applyDatesInline(QueryBuilder $qb, ?DateRange $dateRange): void
|
||||||
{
|
{
|
||||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
if ($dateRange?->getStartDate() !== null) {
|
||||||
$qb->andWhere($qb->expr()->gte('v.date', '\'' . $dateRange->getStartDate()->toDateTimeString() . '\''));
|
$qb->andWhere($qb->expr()->gte('v.date', '\'' . $dateRange->getStartDate()->toDateTimeString() . '\''));
|
||||||
}
|
}
|
||||||
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
|
if ($dateRange?->getEndDate() !== null) {
|
||||||
$qb->andWhere($qb->expr()->lte('v.date', '\'' . $dateRange->getEndDate()->toDateTimeString() . '\''));
|
$qb->andWhere($qb->expr()->lte('v.date', '\'' . $dateRange->getEndDate()->toDateTimeString() . '\''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,11 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private DeleteShortUrlsOptions $deleteShortUrlsOptions;
|
|
||||||
private ShortUrlResolverInterface $urlResolver;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
DeleteShortUrlsOptions $deleteShortUrlsOptions,
|
private DeleteShortUrlsOptions $deleteShortUrlsOptions,
|
||||||
ShortUrlResolverInterface $urlResolver
|
private ShortUrlResolverInterface $urlResolver
|
||||||
) {
|
) {
|
||||||
$this->em = $em;
|
|
||||||
$this->deleteShortUrlsOptions = $deleteShortUrlsOptions;
|
|
||||||
$this->urlResolver = $urlResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,7 +27,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
|||||||
public function deleteByShortCode(
|
public function deleteByShortCode(
|
||||||
ShortUrlIdentifier $identifier,
|
ShortUrlIdentifier $identifier,
|
||||||
bool $ignoreThreshold = false,
|
bool $ignoreThreshold = false,
|
||||||
?ApiKey $apiKey = null
|
?ApiKey $apiKey = null,
|
||||||
): void {
|
): void {
|
||||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey);
|
$shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey);
|
||||||
if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) {
|
if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) {
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ interface DeleteShortUrlServiceInterface
|
|||||||
public function deleteByShortCode(
|
public function deleteByShortCode(
|
||||||
ShortUrlIdentifier $identifier,
|
ShortUrlIdentifier $identifier,
|
||||||
bool $ignoreThreshold = false,
|
bool $ignoreThreshold = false,
|
||||||
?ApiKey $apiKey = null
|
?ApiKey $apiKey = null,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,8 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
|||||||
|
|
||||||
class ShortCodeHelper implements ShortCodeHelperInterface // TODO Rename to ShortCodeUniquenessHelper
|
class ShortCodeHelper implements ShortCodeHelperInterface // TODO Rename to ShortCodeUniquenessHelper
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
public function __construct(private EntityManagerInterface $em)
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool
|
public function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool
|
||||||
|
|||||||
@@ -13,11 +13,8 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class ShortUrlResolver implements ShortUrlResolverInterface
|
class ShortUrlResolver implements ShortUrlResolverInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
public function __construct(private EntityManagerInterface $em)
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +24,7 @@ class ShortUrlResolver implements ShortUrlResolverInterface
|
|||||||
{
|
{
|
||||||
/** @var ShortUrlRepository $shortUrlRepo */
|
/** @var ShortUrlRepository $shortUrlRepo */
|
||||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||||
$shortUrl = $shortUrlRepo->findOne($identifier, $apiKey !== null ? $apiKey->spec() : null);
|
$shortUrl = $shortUrlRepo->findOne($identifier, $apiKey?->spec());
|
||||||
if ($shortUrl === null) {
|
if ($shortUrl === null) {
|
||||||
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
||||||
}
|
}
|
||||||
@@ -43,7 +40,7 @@ class ShortUrlResolver implements ShortUrlResolverInterface
|
|||||||
/** @var ShortUrlRepository $shortUrlRepo */
|
/** @var ShortUrlRepository $shortUrlRepo */
|
||||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||||
$shortUrl = $shortUrlRepo->findOneWithDomainFallback($identifier->shortCode(), $identifier->domain());
|
$shortUrl = $shortUrlRepo->findOneWithDomainFallback($identifier->shortCode(), $identifier->domain());
|
||||||
if ($shortUrl === null || ! $shortUrl->isEnabled()) {
|
if (! $shortUrl?->isEnabled()) {
|
||||||
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,21 +21,12 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class ShortUrlService implements ShortUrlServiceInterface
|
class ShortUrlService implements ShortUrlServiceInterface
|
||||||
{
|
{
|
||||||
private ORM\EntityManagerInterface $em;
|
|
||||||
private ShortUrlResolverInterface $urlResolver;
|
|
||||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
|
||||||
private ShortUrlRelationResolverInterface $relationResolver;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ORM\EntityManagerInterface $em,
|
private ORM\EntityManagerInterface $em,
|
||||||
ShortUrlResolverInterface $urlResolver,
|
private ShortUrlResolverInterface $urlResolver,
|
||||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||||
ShortUrlRelationResolverInterface $relationResolver
|
private ShortUrlRelationResolverInterface $relationResolver
|
||||||
) {
|
) {
|
||||||
$this->em = $em;
|
|
||||||
$this->urlResolver = $urlResolver;
|
|
||||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
|
||||||
$this->relationResolver = $relationResolver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,7 +50,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
|||||||
public function updateShortUrl(
|
public function updateShortUrl(
|
||||||
ShortUrlIdentifier $identifier,
|
ShortUrlIdentifier $identifier,
|
||||||
ShortUrlEdit $shortUrlEdit,
|
ShortUrlEdit $shortUrlEdit,
|
||||||
?ApiKey $apiKey = null
|
?ApiKey $apiKey = null,
|
||||||
): ShortUrl {
|
): ShortUrl {
|
||||||
if ($shortUrlEdit->longUrlWasProvided()) {
|
if ($shortUrlEdit->longUrlWasProvided()) {
|
||||||
/** @var ShortUrlEdit $shortUrlEdit */
|
/** @var ShortUrlEdit $shortUrlEdit */
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ interface ShortUrlServiceInterface
|
|||||||
public function updateShortUrl(
|
public function updateShortUrl(
|
||||||
ShortUrlIdentifier $identifier,
|
ShortUrlIdentifier $identifier,
|
||||||
ShortUrlEdit $shortUrlEdit,
|
ShortUrlEdit $shortUrlEdit,
|
||||||
?ApiKey $apiKey = null
|
?ApiKey $apiKey = null,
|
||||||
): ShortUrl;
|
): ShortUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,21 +16,12 @@ use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
|||||||
|
|
||||||
class UrlShortener implements UrlShortenerInterface
|
class UrlShortener implements UrlShortenerInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
|
||||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
|
||||||
private ShortUrlRelationResolverInterface $relationResolver;
|
|
||||||
private ShortCodeHelperInterface $shortCodeHelper;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||||
EntityManagerInterface $em,
|
private EntityManagerInterface $em,
|
||||||
ShortUrlRelationResolverInterface $relationResolver,
|
private ShortUrlRelationResolverInterface $relationResolver,
|
||||||
ShortCodeHelperInterface $shortCodeHelper
|
private ShortCodeHelperInterface $shortCodeHelper
|
||||||
) {
|
) {
|
||||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
|
||||||
$this->em = $em;
|
|
||||||
$this->relationResolver = $relationResolver;
|
|
||||||
$this->shortCodeHelper = $shortCodeHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +69,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
|
|
||||||
if (! $couldBeMadeUnique) {
|
if (! $couldBeMadeUnique) {
|
||||||
$domain = $shortUrlToBeCreated->getDomain();
|
$domain = $shortUrlToBeCreated->getDomain();
|
||||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
$domainAuthority = $domain?->getAuthority();
|
||||||
|
|
||||||
throw NonUniqueSlugException::fromSlug($shortUrlToBeCreated->getShortCode(), $domainAuthority);
|
throw NonUniqueSlugException::fromSlug($shortUrlToBeCreated->getShortCode(), $domainAuthority);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||||
|
|
||||||
|
use GuzzleHttp\Psr7\Query;
|
||||||
|
use League\Uri\Uri;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
|
|
||||||
|
use function array_merge;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class ShortUrlRedirectionBuilder implements ShortUrlRedirectionBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private TrackingOptions $trackingOptions)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildShortUrlRedirect(ShortUrl $shortUrl, array $currentQuery, ?string $extraPath = null): string
|
||||||
|
{
|
||||||
|
$uri = Uri::createFromString($shortUrl->getLongUrl());
|
||||||
|
|
||||||
|
return $uri
|
||||||
|
->withQuery($this->resolveQuery($uri, $currentQuery))
|
||||||
|
->withPath($this->resolvePath($uri, $extraPath))
|
||||||
|
->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveQuery(Uri $uri, array $currentQuery): ?string
|
||||||
|
{
|
||||||
|
$hardcodedQuery = Query::parse($uri->getQuery() ?? '');
|
||||||
|
|
||||||
|
$disableTrackParam = $this->trackingOptions->getDisableTrackParam();
|
||||||
|
if ($disableTrackParam !== null) {
|
||||||
|
unset($currentQuery[$disableTrackParam]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$mergedQuery = array_merge($hardcodedQuery, $currentQuery);
|
||||||
|
|
||||||
|
return empty($mergedQuery) ? null : Query::build($mergedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolvePath(Uri $uri, ?string $extraPath): string
|
||||||
|
{
|
||||||
|
$hardcodedPath = $uri->getPath();
|
||||||
|
return $extraPath === null ? $hardcodedPath : sprintf('%s%s', $hardcodedPath, $extraPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Helper;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
|
||||||
|
interface ShortUrlRedirectionBuilderInterface
|
||||||
|
{
|
||||||
|
public function buildShortUrlRedirect(ShortUrl $shortUrl, array $currentQuery, ?string $extraPath = null): string;
|
||||||
|
}
|
||||||
@@ -11,13 +11,8 @@ use function sprintf;
|
|||||||
|
|
||||||
class ShortUrlStringifier implements ShortUrlStringifierInterface
|
class ShortUrlStringifier implements ShortUrlStringifierInterface
|
||||||
{
|
{
|
||||||
private array $domainConfig;
|
public function __construct(private array $domainConfig, private string $basePath = '')
|
||||||
private string $basePath;
|
|
||||||
|
|
||||||
public function __construct(array $domainConfig, string $basePath = '')
|
|
||||||
{
|
{
|
||||||
$this->domainConfig = $domainConfig;
|
|
||||||
$this->basePath = $basePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stringify(ShortUrl $shortUrl): string
|
public function stringify(ShortUrl $shortUrl): string
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
|||||||
|
|
||||||
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
||||||
{
|
{
|
||||||
private UrlValidatorInterface $urlValidator;
|
public function __construct(private UrlValidatorInterface $urlValidator)
|
||||||
|
|
||||||
public function __construct(UrlValidatorInterface $urlValidator)
|
|
||||||
{
|
{
|
||||||
$this->urlValidator = $urlValidator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\ShortUrl\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlRedirectionBuilderInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
||||||
|
|
||||||
|
use function array_pad;
|
||||||
|
use function explode;
|
||||||
|
use function sprintf;
|
||||||
|
use function trim;
|
||||||
|
|
||||||
|
class ExtraPathRedirectMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private ShortUrlResolverInterface $resolver,
|
||||||
|
private RequestTrackerInterface $requestTracker,
|
||||||
|
private ShortUrlRedirectionBuilderInterface $redirectionBuilder,
|
||||||
|
private RedirectResponseHelperInterface $redirectResponseHelper,
|
||||||
|
private UrlShortenerOptions $urlShortenerOptions,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||||
|
{
|
||||||
|
/** @var NotFoundType|null $notFoundType */
|
||||||
|
$notFoundType = $request->getAttribute(NotFoundType::class);
|
||||||
|
|
||||||
|
// We'll apply this logic only if actively opted in and current URL is potentially /{shortCode}/[...]
|
||||||
|
if (! $notFoundType?->isRegularNotFound() || ! $this->urlShortenerOptions->appendExtraPath()) {
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$uri = $request->getUri();
|
||||||
|
$query = $request->getQueryParams();
|
||||||
|
[$potentialShortCode, $extraPath] = $this->resolvePotentialShortCodeAndExtraPath($uri);
|
||||||
|
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($potentialShortCode, $uri->getAuthority());
|
||||||
|
|
||||||
|
try {
|
||||||
|
$shortUrl = $this->resolver->resolveEnabledShortUrl($identifier);
|
||||||
|
$this->requestTracker->trackIfApplicable($shortUrl, $request);
|
||||||
|
|
||||||
|
$longUrl = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $query, $extraPath);
|
||||||
|
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
||||||
|
} catch (ShortUrlNotFoundException) {
|
||||||
|
return $handler->handle($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0: string, 1: string|null}
|
||||||
|
*/
|
||||||
|
private function resolvePotentialShortCodeAndExtraPath(UriInterface $uri): array
|
||||||
|
{
|
||||||
|
$pathParts = explode('/', trim($uri->getPath(), '/'), 2);
|
||||||
|
[$potentialShortCode, $extraPath] = array_pad($pathParts, 2, null);
|
||||||
|
|
||||||
|
return [$potentialShortCode, $extraPath === null ? null : sprintf('/%s', $extraPath)];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,16 +16,13 @@ use function Functional\unique;
|
|||||||
|
|
||||||
class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
class PersistenceShortUrlRelationResolver implements ShortUrlRelationResolverInterface
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $em;
|
|
||||||
|
|
||||||
/** @var array<string, Domain> */
|
/** @var array<string, Domain> */
|
||||||
private array $memoizedNewDomains = [];
|
private array $memoizedNewDomains = [];
|
||||||
/** @var array<string, Tag> */
|
/** @var array<string, Tag> */
|
||||||
private array $memoizedNewTags = [];
|
private array $memoizedNewTags = [];
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $em)
|
public function __construct(private EntityManagerInterface $em)
|
||||||
{
|
{
|
||||||
$this->em = $em;
|
|
||||||
$this->em->getEventManager()->addEventListener(Events::postFlush, $this);
|
$this->em->getEventManager()->addEventListener(Events::postFlush, $this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,8 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
|||||||
|
|
||||||
class BelongsToApiKey extends BaseSpecification
|
class BelongsToApiKey extends BaseSpecification
|
||||||
{
|
{
|
||||||
private ApiKey $apiKey;
|
public function __construct(private ApiKey $apiKey, private ?string $dqlAlias = null)
|
||||||
private ?string $dqlAlias;
|
|
||||||
|
|
||||||
public function __construct(ApiKey $apiKey, ?string $dqlAlias = null)
|
|
||||||
{
|
{
|
||||||
$this->apiKey = $apiKey;
|
|
||||||
$this->dqlAlias = $dqlAlias;
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user