mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-05 14:53:12 +08:00
Compare commits
8 Commits
v2.8.0-alp
...
v2.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fda231ad | ||
|
|
e4d4686717 | ||
|
|
ca6c6a1b6e | ||
|
|
51c7d0ed3e | ||
|
|
b3af493758 | ||
|
|
7b9ebbbb5f | ||
|
|
ea735fc0a0 | ||
|
|
06227e97d0 |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: composer test:unit:ci
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.0' }}
|
||||
if: ${{ matrix.php-version == '7.4' }}
|
||||
with:
|
||||
name: coverage-unit
|
||||
path: |
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: composer test:db:sqlite:ci
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.0' }}
|
||||
if: ${{ matrix.php-version == '7.4' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -140,7 +140,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
@@ -201,7 +201,7 @@ jobs:
|
||||
- run: composer install --no-interaction --prefer-dist
|
||||
- run: bin/test/run-api-tests.sh
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: ${{ matrix.php-version == '8.0' }}
|
||||
if: ${{ matrix.php-version == '7.4' }}
|
||||
with:
|
||||
name: coverage-api
|
||||
path: |
|
||||
@@ -216,7 +216,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
test-group: ['unit', 'db']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
4
.github/workflows/publish-release.yml
vendored
4
.github/workflows/publish-release.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.0']
|
||||
php-version: ['7.4', '8.0']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: [ '8.0' ]
|
||||
php-version: [ '7.4', '8.0' ]
|
||||
swoole: [ 'yes', 'no' ]
|
||||
steps:
|
||||
- uses: geekyeggo/delete-artifact@v1
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -4,18 +4,9 @@ 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).
|
||||
|
||||
## [Unreleased]
|
||||
## [2.7.3] - 2021-08-02
|
||||
### 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.
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
@@ -24,11 +15,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* [#1046](https://github.com/shlinkio/shlink/issues/1046) Dropped support for PHP 7.4.
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1135](https://github.com/shlinkio/shlink/issues/1135) Fixed error when importing short URLs with no visits from another Shlink instance.
|
||||
* [#1136](https://github.com/shlinkio/shlink/issues/1136) Fixed error when fetching tag/short-url/orphan visits for a page lower than 1.
|
||||
|
||||
|
||||
## [2.7.2] - 2021-07-30
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#1128](https://github.com/shlinkio/shlink/issues/1128) Increased memory limit reserved for the docker image, preventing it from crashing on GeoLite db download.
|
||||
|
||||
|
||||
## [2.7.1] - 2021-05-30
|
||||
### Added
|
||||
|
||||
@@ -121,7 +121,7 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
||||
For example, `test:db:postgres`.
|
||||
|
||||
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
|
||||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer infect:test` ti run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer ci` to run all previous commands together. This command is run during the project's continuous integration.
|
||||
* Run `./indocker composer ci:parallel` to do the same as in previous case, but parallelizing non-conflicting tasks as much as possible.
|
||||
|
||||
|
||||
@@ -78,13 +78,4 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||
|
||||
# Change the ownership of /etc/shlink/data to be writable, then change the user to non-root
|
||||
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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
* PHP 8.0
|
||||
* PHP 7.4 or 8.0
|
||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp.
|
||||
* 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.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.0",
|
||||
"php": "^7.4 || ^8.0",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"endroid/qr-code": "^4.0",
|
||||
"geoip2/geoip2": "^2.9",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"guzzlehttp/psr7": "^1.7",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.2",
|
||||
"laminas/laminas-config": "^3.3",
|
||||
@@ -50,8 +51,8 @@
|
||||
"shlinkio/shlink-common": "^3.7",
|
||||
"shlinkio/shlink-config": "^1.0",
|
||||
"shlinkio/shlink-event-dispatcher": "^2.1",
|
||||
"shlinkio/shlink-importer": "^2.3",
|
||||
"shlinkio/shlink-installer": "dev-develop#fa6a4ca as 6.1",
|
||||
"shlinkio/shlink-importer": "^2.3.1",
|
||||
"shlinkio/shlink-installer": "^6.0",
|
||||
"shlinkio/shlink-ip-geolocation": "^2.0",
|
||||
"symfony/console": "^5.1",
|
||||
"symfony/filesystem": "^5.1",
|
||||
|
||||
@@ -42,7 +42,6 @@ return [
|
||||
Option\UrlShortener\RedirectStatusCodeConfigOption::class,
|
||||
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||
|
||||
@@ -68,7 +68,6 @@ return [
|
||||
// This middleware is in front of tracking actions explicitly. Putting here for orphan visits tracking
|
||||
IpAddress::class,
|
||||
Core\ErrorHandler\NotFoundTypeResolverMiddleware::class,
|
||||
Core\ShortUrl\Middleware\ExtraPathRedirectMiddleware::class,
|
||||
Core\ErrorHandler\NotFoundTrackerMiddleware::class,
|
||||
Core\ErrorHandler\NotFoundRedirectHandler::class,
|
||||
Core\ErrorHandler\NotFoundTemplateHandler::class,
|
||||
|
||||
@@ -16,12 +16,9 @@ return [
|
||||
'validate_url' => false, // Deprecated
|
||||
'visits_webhooks' => [],
|
||||
'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_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||
'auto_resolve_titles' => false,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -11,7 +11,7 @@ server {
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ final class Version20180913205455 extends AbstractMigration
|
||||
|
||||
try {
|
||||
return (string) IpAddress::fromString($addr)->getAnonymizedCopy();
|
||||
} catch (InvalidArgumentException) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
memory_limit=256M
|
||||
|
||||
@@ -111,10 +111,9 @@ return [
|
||||
'validate_url' => (bool) env('VALIDATE_URLS', false),
|
||||
'visits_webhooks' => $helper->getVisitsWebhooks(),
|
||||
'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_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
|
||||
'append_extra_path' => (bool) env('REDIRECT_APPEND_EXTRA_PATH', false),
|
||||
'auto_resolve_titles' => (bool) env('AUTO_RESOLVE_TITLES', false),
|
||||
],
|
||||
|
||||
'tracking' => [
|
||||
|
||||
@@ -21,15 +21,6 @@ if [ ! -z "${GEOLITE_LICENSE_KEY}" ]; then
|
||||
php bin/cli visit:download-db -n -q
|
||||
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
|
||||
# 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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"URL Shortener"
|
||||
],
|
||||
"summary": "Short URL QR code",
|
||||
"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.",
|
||||
"description": "Generates a QR code image pointing to a short URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
@@ -35,8 +35,10 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["png", "svg"],
|
||||
"default": "png"
|
||||
"enum": [
|
||||
"png",
|
||||
"svg"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -49,17 +51,6 @@
|
||||
"minimum": 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": {
|
||||
|
||||
@@ -10,8 +10,11 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
class RoleResolver implements RoleResolverInterface
|
||||
{
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
private DomainServiceInterface $domainService;
|
||||
|
||||
public function __construct(DomainServiceInterface $domainService)
|
||||
{
|
||||
$this->domainService = $domainService;
|
||||
}
|
||||
|
||||
public function determineRoles(InputInterface $input): array
|
||||
|
||||
@@ -19,9 +19,12 @@ class DisableKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -23,11 +23,14 @@ class GenerateKeyCommand extends BaseCommand
|
||||
{
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
public function __construct(
|
||||
private ApiKeyServiceInterface $apiKeyService,
|
||||
private RoleResolverInterface $roleResolver
|
||||
) {
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
private RoleResolverInterface $roleResolver;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, RoleResolverInterface $roleResolver)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->roleResolver = $roleResolver;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -27,9 +27,12 @@ class ListKeysCommand extends BaseCommand
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||
private ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -58,7 +61,7 @@ class ListKeysCommand extends BaseCommand
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||
}
|
||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
||||
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||
fn (string $roleName, array $meta) =>
|
||||
empty($meta)
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class BaseCommand extends Command
|
||||
?string $shortcut = null,
|
||||
?int $mode = null,
|
||||
string $description = '',
|
||||
$default = null,
|
||||
$default = null
|
||||
): self {
|
||||
$this->addOption($name, $shortcut, $mode, $description, $default);
|
||||
|
||||
|
||||
@@ -13,14 +13,16 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
||||
|
||||
abstract class AbstractDatabaseCommand extends AbstractLockedCommand
|
||||
{
|
||||
private ProcessRunnerInterface $processRunner;
|
||||
private string $phpBinary;
|
||||
|
||||
public function __construct(
|
||||
LockFactory $locker,
|
||||
private ProcessRunnerInterface $processRunner,
|
||||
ProcessRunnerInterface $processRunner,
|
||||
PhpExecutableFinder $phpFinder
|
||||
) {
|
||||
parent::__construct($locker);
|
||||
$this->processRunner = $processRunner;
|
||||
$this->phpBinary = $phpFinder->find(false) ?: 'php';
|
||||
}
|
||||
|
||||
|
||||
@@ -21,14 +21,19 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
|
||||
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
|
||||
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
|
||||
|
||||
private Connection $regularConn;
|
||||
private Connection $noDbNameConn;
|
||||
|
||||
public function __construct(
|
||||
LockFactory $locker,
|
||||
ProcessRunnerInterface $processRunner,
|
||||
PhpExecutableFinder $phpFinder,
|
||||
private Connection $regularConn,
|
||||
private Connection $noDbNameConn
|
||||
Connection $conn,
|
||||
Connection $noDbNameConn
|
||||
) {
|
||||
parent::__construct($locker, $processRunner, $phpFinder);
|
||||
$this->regularConn = $conn;
|
||||
$this->noDbNameConn = $noDbNameConn;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -18,9 +18,12 @@ class ListDomainsCommand extends Command
|
||||
{
|
||||
public const NAME = 'domain:list';
|
||||
|
||||
public function __construct(private DomainServiceInterface $domainService)
|
||||
private DomainServiceInterface $domainService;
|
||||
|
||||
public function __construct(DomainServiceInterface $domainService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->domainService = $domainService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -21,9 +21,12 @@ class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:delete';
|
||||
|
||||
public function __construct(private DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
private DeleteShortUrlServiceInterface $deleteShortUrlService;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -30,12 +30,19 @@ class GenerateShortUrlCommand extends BaseCommand
|
||||
{
|
||||
public const NAME = 'short-url:generate';
|
||||
|
||||
private UrlShortenerInterface $urlShortener;
|
||||
private ShortUrlStringifierInterface $stringifier;
|
||||
private int $defaultShortCodeLength;
|
||||
|
||||
public function __construct(
|
||||
private UrlShortenerInterface $urlShortener,
|
||||
private ShortUrlStringifierInterface $stringifier,
|
||||
private int $defaultShortCodeLength
|
||||
UrlShortenerInterface $urlShortener,
|
||||
ShortUrlStringifierInterface $stringifier,
|
||||
int $defaultShortCodeLength
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->stringifier = $stringifier;
|
||||
$this->defaultShortCodeLength = $defaultShortCodeLength;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -27,8 +27,11 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
|
||||
{
|
||||
public const NAME = 'short-url:visits';
|
||||
|
||||
public function __construct(private VisitsStatsHelperInterface $visitsHelper)
|
||||
private VisitsStatsHelperInterface $visitsHelper;
|
||||
|
||||
public function __construct(VisitsStatsHelperInterface $visitsHelper)
|
||||
{
|
||||
$this->visitsHelper = $visitsHelper;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
|
||||
public const NAME = 'short-url:list';
|
||||
|
||||
public function __construct(
|
||||
private ShortUrlServiceInterface $shortUrlService,
|
||||
private DataTransformerInterface $transformer
|
||||
) {
|
||||
private ShortUrlServiceInterface $shortUrlService;
|
||||
private DataTransformerInterface $transformer;
|
||||
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, DataTransformerInterface $transformer)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->transformer = $transformer;
|
||||
}
|
||||
|
||||
protected function doConfigure(): void
|
||||
@@ -126,8 +129,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
|
||||
ShortUrlsParamsInputFilter::TAGS => $tags,
|
||||
ShortUrlsOrdering::ORDER_BY => $orderBy,
|
||||
ShortUrlsParamsInputFilter::START_DATE => $startDate?->toAtomString(),
|
||||
ShortUrlsParamsInputFilter::END_DATE => $endDate?->toAtomString(),
|
||||
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
|
||||
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
|
||||
];
|
||||
|
||||
if ($all) {
|
||||
@@ -155,7 +158,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
OutputInterface $output,
|
||||
array $columnsMap,
|
||||
ShortUrlsParams $params,
|
||||
bool $all,
|
||||
bool $all
|
||||
): Paginator {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
@@ -200,11 +203,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
||||
}
|
||||
if ($input->getOption('show-api-key')) {
|
||||
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||
(string) $shortUrl->authorApiKey();
|
||||
(string) $shortUrl->authorApiKey();
|
||||
}
|
||||
if ($input->getOption('show-api-key-name')) {
|
||||
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): ?string =>
|
||||
$shortUrl->authorApiKey()?->name();
|
||||
$columnsMap['API Key Name'] = static function (array $_, ShortUrl $shortUrl): ?string {
|
||||
$apiKey = $shortUrl->authorApiKey();
|
||||
|
||||
return $apiKey !== null ? $apiKey->name() : null;
|
||||
};
|
||||
}
|
||||
|
||||
return $columnsMap;
|
||||
|
||||
@@ -21,9 +21,12 @@ class ResolveUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:parse';
|
||||
|
||||
public function __construct(private ShortUrlResolverInterface $urlResolver)
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
|
||||
public function __construct(ShortUrlResolverInterface $urlResolver)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlResolver = $urlResolver;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -17,9 +17,12 @@ class CreateTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -16,9 +16,12 @@ class DeleteTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:delete';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -18,9 +18,12 @@ class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -19,9 +19,12 @@ class RenameTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:rename';
|
||||
|
||||
public function __construct(private TagServiceInterface $tagService)
|
||||
private TagServiceInterface $tagService;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
|
||||
@@ -14,9 +14,12 @@ use function sprintf;
|
||||
|
||||
abstract class AbstractLockedCommand extends Command
|
||||
{
|
||||
public function __construct(private LockFactory $locker)
|
||||
private LockFactory $locker;
|
||||
|
||||
public function __construct(LockFactory $locker)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||
|
||||
@@ -63,7 +63,7 @@ abstract class AbstractWithDateRangeCommand extends BaseCommand
|
||||
));
|
||||
|
||||
if ($output->isVeryVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $output);
|
||||
$this->getApplication()->renderThrowable($e, $output);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -8,11 +8,15 @@ final class LockedCommandConfig
|
||||
{
|
||||
public const DEFAULT_TTL = 600.0; // 10 minutes
|
||||
|
||||
private function __construct(
|
||||
private string $lockName,
|
||||
private bool $isBlocking,
|
||||
private float $ttl = self::DEFAULT_TTL
|
||||
) {
|
||||
private string $lockName;
|
||||
private bool $isBlocking;
|
||||
private float $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
|
||||
|
||||
@@ -19,11 +19,13 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:download-db';
|
||||
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private ?ProgressBar $progressBar = null;
|
||||
|
||||
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater)
|
||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -69,7 +71,7 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
$this->getApplication()->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCodes::EXIT_WARNING : ExitCodes::EXIT_FAILURE;
|
||||
|
||||
@@ -30,14 +30,19 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
{
|
||||
public const NAME = 'visit:locate';
|
||||
|
||||
private VisitLocatorInterface $visitLocator;
|
||||
private IpLocationResolverInterface $ipLocationResolver;
|
||||
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private VisitLocatorInterface $visitLocator,
|
||||
private IpLocationResolverInterface $ipLocationResolver,
|
||||
VisitLocatorInterface $visitLocator,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
LockFactory $locker
|
||||
) {
|
||||
parent::__construct($locker);
|
||||
$this->visitLocator = $visitLocator;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -119,7 +124,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
} catch (Throwable $e) {
|
||||
$this->io->error($e->getMessage());
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||
$this->getApplication()->renderThrowable($e, $this->io);
|
||||
}
|
||||
|
||||
return ExitCodes::EXIT_FAILURE;
|
||||
@@ -151,7 +156,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
||||
} catch (WrongIpException $e) {
|
||||
$this->io->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||
if ($this->io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $this->io);
|
||||
$this->getApplication()->renderThrowable($e, $this->io);
|
||||
}
|
||||
|
||||
throw IpCannotBeLocatedException::forError($e);
|
||||
|
||||
@@ -19,12 +19,21 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
{
|
||||
private const LOCK_NAME = 'geolocation-db-update';
|
||||
|
||||
private DbUpdaterInterface $dbUpdater;
|
||||
private Reader $geoLiteDbReader;
|
||||
private LockFactory $locker;
|
||||
private TrackingOptions $trackingOptions;
|
||||
|
||||
public function __construct(
|
||||
private DbUpdaterInterface $dbUpdater,
|
||||
private Reader $geoLiteDbReader,
|
||||
private LockFactory $locker,
|
||||
private TrackingOptions $trackingOptions
|
||||
DbUpdaterInterface $dbUpdater,
|
||||
Reader $geoLiteDbReader,
|
||||
LockFactory $locker,
|
||||
TrackingOptions $trackingOptions
|
||||
) {
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||
$this->locker = $locker;
|
||||
$this->trackingOptions = $trackingOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,10 +18,12 @@ use function str_replace;
|
||||
|
||||
class ProcessRunner implements ProcessRunnerInterface
|
||||
{
|
||||
private ProcessHelper $helper;
|
||||
private Closure $createProcess;
|
||||
|
||||
public function __construct(private ProcessHelper $helper, ?callable $createProcess = null)
|
||||
public function __construct(ProcessHelper $helper, ?callable $createProcess = null)
|
||||
{
|
||||
$this->helper = $helper;
|
||||
$this->createProcess = $createProcess !== null
|
||||
? Closure::fromCallable($createProcess)
|
||||
: static fn (array $cmd) => new Process($cmd, null, null, null, LockedCommandConfig::DEFAULT_TTL);
|
||||
|
||||
@@ -12,8 +12,11 @@ final class ShlinkTable
|
||||
private const DEFAULT_STYLE_NAME = 'default';
|
||||
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
|
||||
|
||||
public function __construct(private Table $baseTable)
|
||||
private ?Table $baseTable;
|
||||
|
||||
public function __construct(Table $baseTable)
|
||||
{
|
||||
$this->baseTable = $baseTable;
|
||||
}
|
||||
|
||||
public static function fromOutput(OutputInterface $output): self
|
||||
|
||||
@@ -33,7 +33,7 @@ class RoleResolverTest extends TestCase
|
||||
public function properRolesAreResolvedBasedOnInput(
|
||||
InputInterface $input,
|
||||
array $expectedRoles,
|
||||
int $expectedDomainCalls,
|
||||
int $expectedDomainCalls
|
||||
): void {
|
||||
$getDomain = $this->domainService->getOrCreate('example.com')->willReturn(
|
||||
(new Domain('example.com'))->setId('1'),
|
||||
|
||||
@@ -74,7 +74,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
||||
public function deleteIsRetriedWhenThresholdIsReachedAndQuestionIsAccepted(
|
||||
array $retryAnswer,
|
||||
int $expectedDeleteCalls,
|
||||
string $expectedMessage,
|
||||
string $expectedMessage
|
||||
): void {
|
||||
$shortCode = 'abc123';
|
||||
$identifier = new ShortUrlIdentifier($shortCode);
|
||||
|
||||
@@ -110,7 +110,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
array $input,
|
||||
array $expectedContents,
|
||||
array $notExpectedContents,
|
||||
ApiKey $apiKey,
|
||||
ApiKey $apiKey
|
||||
): void {
|
||||
$this->shortUrlService->listShortUrls(ShortUrlsParams::emptyInstance())
|
||||
->willReturn(new Paginator(new ArrayAdapter([
|
||||
@@ -185,7 +185,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
?string $startDate = null,
|
||||
?string $endDate = null,
|
||||
?string $endDate = null
|
||||
): void {
|
||||
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
|
||||
'page' => $page,
|
||||
|
||||
@@ -36,7 +36,7 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
||||
public function showsProperMessageWhenGeoLiteUpdateFails(
|
||||
bool $olderDbExists,
|
||||
string $expectedMessage,
|
||||
int $expectedExitCode,
|
||||
int $expectedExitCode
|
||||
): void {
|
||||
$checkDbUpdate = $this->dbUpdater->checkDbUpdate(Argument::cetera())->will(
|
||||
function (array $args) use ($olderDbExists): void {
|
||||
|
||||
@@ -73,7 +73,7 @@ class LocateVisitsCommandTest extends TestCase
|
||||
int $expectedEmptyCalls,
|
||||
int $expectedAllCalls,
|
||||
bool $expectWarningPrint,
|
||||
array $args,
|
||||
array $args
|
||||
): void {
|
||||
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('', '', '1.2.3.4', ''));
|
||||
$location = VisitLocation::fromGeolocation(Location::emptyInstance());
|
||||
|
||||
@@ -37,7 +37,6 @@ return [
|
||||
Domain\DomainService::class => ConfigAbstractFactory::class,
|
||||
|
||||
Visit\VisitsTracker::class => ConfigAbstractFactory::class,
|
||||
Visit\RequestTracker::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitLocator::class => ConfigAbstractFactory::class,
|
||||
Visit\VisitsStatsHelper::class => ConfigAbstractFactory::class,
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class => InvokableFactory::class,
|
||||
@@ -54,9 +53,7 @@ return [
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlStringifier::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
||||
|
||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -72,7 +69,7 @@ return [
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\RequestTracker::class],
|
||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\VisitsTracker::class],
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||
NotFoundRedirectOptions::class,
|
||||
Util\RedirectResponseHelper::class,
|
||||
@@ -95,7 +92,6 @@ return [
|
||||
EventDispatcherInterface::class,
|
||||
Options\TrackingOptions::class,
|
||||
],
|
||||
Visit\RequestTracker::class => [Visit\VisitsTracker::class, Options\TrackingOptions::class],
|
||||
Service\ShortUrlService::class => [
|
||||
'em',
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
@@ -120,11 +116,17 @@ return [
|
||||
|
||||
Action\RedirectAction::class => [
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
Visit\RequestTracker::class,
|
||||
ShortUrl\Helper\ShortUrlRedirectionBuilder::class,
|
||||
Visit\VisitsTracker::class,
|
||||
Options\TrackingOptions::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 => [
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
ShortUrl\Helper\ShortUrlStringifier::class,
|
||||
@@ -135,15 +137,7 @@ return [
|
||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class => ['em'],
|
||||
ShortUrl\Helper\ShortUrlStringifier::class => ['config.url_shortener.domain', 'config.router.base_path'],
|
||||
ShortUrl\Helper\ShortUrlTitleResolutionHelper::class => [Util\UrlValidator::class],
|
||||
ShortUrl\Helper\ShortUrlRedirectionBuilder::class => [Options\TrackingOptions::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 => [
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
|
||||
@@ -51,12 +51,20 @@ function parseDateRangeFromQuery(array $query, string $startDateName, string $en
|
||||
$startDate = parseDateFromQuery($query, $startDateName);
|
||||
$endDate = parseDateFromQuery($query, $endDateName);
|
||||
|
||||
return match (true) {
|
||||
$startDate === null && $endDate === null => DateRange::emptyInstance(),
|
||||
$startDate !== null && $endDate !== null => DateRange::withStartAndEndDate($startDate, $endDate),
|
||||
$startDate !== null => DateRange::withStartDate($startDate),
|
||||
default => DateRange::withEndDate($endDate),
|
||||
};
|
||||
// TODO Use match expression when migrating to PHP8
|
||||
if ($startDate === null && $endDate === null) {
|
||||
return DateRange::emptyInstance();
|
||||
}
|
||||
|
||||
if ($startDate !== null && $endDate !== null) {
|
||||
return DateRange::withStartAndEndDate($startDate, $endDate);
|
||||
}
|
||||
|
||||
if ($startDate !== null) {
|
||||
return DateRange::withStartDate($startDate);
|
||||
}
|
||||
|
||||
return DateRange::withEndDate($endDate);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,46 +5,91 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Action;
|
||||
|
||||
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 as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
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\Visit\RequestTrackerInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_merge;
|
||||
|
||||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
private VisitsTrackerInterface $visitTracker;
|
||||
private TrackingOptions $trackingOptions;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
private ShortUrlResolverInterface $urlResolver,
|
||||
private RequestTrackerInterface $requestTracker,
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
VisitsTrackerInterface $visitTracker,
|
||||
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
|
||||
{
|
||||
$identifier = ShortUrlIdentifier::fromRedirectRequest($request);
|
||||
$query = $request->getQueryParams();
|
||||
$disableTrackParam = $this->trackingOptions->getDisableTrackParam();
|
||||
|
||||
try {
|
||||
$shortUrl = $this->urlResolver->resolveEnabledShortUrl($identifier);
|
||||
$this->requestTracker->trackIfApplicable($shortUrl, $request);
|
||||
|
||||
return $this->createSuccessResp($shortUrl, $request);
|
||||
} catch (ShortUrlNotFoundException) {
|
||||
if ($this->shouldTrackRequest($request, $query, $disableTrackParam)) {
|
||||
$this->visitTracker->track($shortUrl, Visitor::fromRequest($request));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function createSuccessResp(
|
||||
ShortUrl $shortUrl,
|
||||
ServerRequestInterface $request,
|
||||
): ResponseInterface;
|
||||
|
||||
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||
private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string
|
||||
{
|
||||
return $handler->handle($request);
|
||||
$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,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface;
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<?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,18 +8,17 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Common\Response\PixelResponse;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
class PixelAction extends AbstractTrackingAction
|
||||
{
|
||||
protected function createSuccessResp(ShortUrl $shortUrl, ServerRequestInterface $request): ResponseInterface
|
||||
protected function createSuccessResp(string $longUrl): ResponseInterface
|
||||
{
|
||||
return new PixelResponse();
|
||||
}
|
||||
|
||||
protected function createErrorResp(
|
||||
ServerRequestInterface $request,
|
||||
RequestHandlerInterface $handler,
|
||||
RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
return new PixelResponse();
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Action;
|
||||
|
||||
use Endroid\QrCode\Builder\Builder;
|
||||
use Endroid\QrCode\Writer\SvgWriter;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||
use Shlinkio\Shlink\Core\Action\Model\QrCodeParams;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
@@ -19,11 +20,22 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
|
||||
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(
|
||||
private ShortUrlResolverInterface $urlResolver,
|
||||
private ShortUrlStringifierInterface $stringifier,
|
||||
private LoggerInterface $logger
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
ShortUrlStringifierInterface $stringifier,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
$this->urlResolver = $urlResolver;
|
||||
$this->logger = $logger ?? new NullLogger();
|
||||
$this->stringifier = $stringifier;
|
||||
}
|
||||
|
||||
public function process(Request $request, RequestHandlerInterface $handler): Response
|
||||
@@ -37,14 +49,43 @@ class QrCodeAction implements MiddlewareInterface
|
||||
return $handler->handle($request);
|
||||
}
|
||||
|
||||
$params = QrCodeParams::fromRequest($request);
|
||||
$qrCodeBuilder = Builder::create()
|
||||
$query = $request->getQueryParams();
|
||||
$qrCode = Builder::create()
|
||||
->data($this->stringifier->stringify($shortUrl))
|
||||
->size($params->size())
|
||||
->margin($params->margin())
|
||||
->writer($params->writer())
|
||||
->errorCorrectionLevel($params->errorCorrectionLevel());
|
||||
->size($this->resolveSize($request, $query))
|
||||
->margin($this->resolveMargin($query));
|
||||
|
||||
return new QrCodeResponse($qrCodeBuilder->build());
|
||||
$format = $query['format'] ?? 'png';
|
||||
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,26 +7,35 @@ namespace Shlinkio\Shlink\Core\Action;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
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 Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
||||
|
||||
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
||||
{
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
RequestTrackerInterface $requestTracker,
|
||||
private ShortUrlRedirectionBuilderInterface $redirectionBuilder,
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
VisitsTrackerInterface $visitTracker,
|
||||
Options\TrackingOptions $trackingOptions,
|
||||
RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($urlResolver, $requestTracker);
|
||||
parent::__construct($urlResolver, $visitTracker, $trackingOptions, $logger);
|
||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||
}
|
||||
|
||||
protected function createSuccessResp(ShortUrl $shortUrl, ServerRequestInterface $request): Response
|
||||
protected function createSuccessResp(string $longUrl): Response
|
||||
{
|
||||
$longUrl = $this->redirectionBuilder->buildShortUrlRedirect($shortUrl, $request->getQueryParams());
|
||||
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
||||
}
|
||||
|
||||
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||
{
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,11 @@ use const PHP_EOL;
|
||||
|
||||
class RobotsAction implements RequestHandlerInterface, StatusCodeInterface
|
||||
{
|
||||
public function __construct(private CrawlingHelperInterface $crawlingHelper)
|
||||
private CrawlingHelperInterface $crawlingHelper;
|
||||
|
||||
public function __construct(CrawlingHelperInterface $crawlingHelper)
|
||||
{
|
||||
$this->crawlingHelper = $crawlingHelper;
|
||||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
|
||||
@@ -10,8 +10,11 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
|
||||
class CrawlingHelper implements CrawlingHelperInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function listCrawlableShortCodes(): iterable
|
||||
|
||||
@@ -16,8 +16,13 @@ use function Functional\map;
|
||||
|
||||
class DomainService implements DomainServiceInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em, private string $defaultDomain)
|
||||
private EntityManagerInterface $em;
|
||||
private string $defaultDomain;
|
||||
|
||||
public function __construct(EntityManagerInterface $em, string $defaultDomain)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->defaultDomain = $defaultDomain;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +35,7 @@ class DomainService implements DomainServiceInterface
|
||||
$domains = $repo->findDomainsWithout($this->defaultDomain, $apiKey);
|
||||
$mappedDomains = map($domains, fn (Domain $domain) => new DomainItem($domain->getAuthority(), false));
|
||||
|
||||
if ($apiKey?->hasRole(Role::DOMAIN_SPECIFIC)) {
|
||||
if ($apiKey !== null && $apiKey->hasRole(Role::DOMAIN_SPECIFIC)) {
|
||||
return $mappedDomains;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,13 @@ use JsonSerializable;
|
||||
|
||||
final class DomainItem implements JsonSerializable
|
||||
{
|
||||
public function __construct(private string $domain, private bool $isDefault)
|
||||
private string $domain;
|
||||
private bool $isDefault;
|
||||
|
||||
public function __construct(string $domain, bool $isDefault)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
$this->isDefault = $isDefault;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
|
||||
@@ -9,8 +9,11 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
|
||||
class Domain extends AbstractEntity implements JsonSerializable
|
||||
{
|
||||
public function __construct(private string $authority)
|
||||
private string $authority;
|
||||
|
||||
public function __construct(string $authority)
|
||||
{
|
||||
$this->authority = $authority;
|
||||
}
|
||||
|
||||
public function getAuthority(): string
|
||||
|
||||
@@ -60,7 +60,7 @@ class ShortUrl extends AbstractEntity
|
||||
|
||||
public static function fromMeta(
|
||||
ShortUrlMeta $meta,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
): self {
|
||||
$instance = new self();
|
||||
$relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
|
||||
@@ -87,7 +87,7 @@ class ShortUrl extends AbstractEntity
|
||||
public static function fromImport(
|
||||
ImportedShlinkUrl $url,
|
||||
bool $importShortCode,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
): self {
|
||||
$meta = [
|
||||
ShortUrlInputFilter::VALIDATE_URL => false,
|
||||
@@ -209,7 +209,7 @@ class ShortUrl extends AbstractEntity
|
||||
|
||||
public function update(
|
||||
ShortUrlEdit $shortUrlEdit,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null,
|
||||
?ShortUrlRelationResolverInterface $relationResolver = null
|
||||
): void {
|
||||
if ($shortUrlEdit->validSinceWasProvided()) {
|
||||
$this->validSince = $shortUrlEdit->validSince();
|
||||
|
||||
@@ -10,10 +10,12 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
|
||||
class Tag extends AbstractEntity implements JsonSerializable
|
||||
{
|
||||
private string $name;
|
||||
private Collections\Collection $shortUrls;
|
||||
|
||||
public function __construct(private string $name)
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->shortUrls = new Collections\ArrayCollection();
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
||||
|
||||
try {
|
||||
return (string) IpAddress::fromString($address)->getAnonymizedCopy();
|
||||
} catch (InvalidArgumentException) {
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,24 +13,31 @@ use function rtrim;
|
||||
|
||||
class NotFoundType
|
||||
{
|
||||
private function __construct(private string $type)
|
||||
private string $type;
|
||||
|
||||
private function __construct(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
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 */
|
||||
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
||||
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
|
||||
if ($routeResult->isFailure()) {
|
||||
return new self(Visit::TYPE_REGULAR_404);
|
||||
}
|
||||
|
||||
$type = match (true) {
|
||||
$isBaseUrl => Visit::TYPE_BASE_URL,
|
||||
$routeResult->isFailure() => Visit::TYPE_REGULAR_404,
|
||||
$routeResult->getMatchedRouteName() === RedirectAction::class => Visit::TYPE_INVALID_SHORT_URL,
|
||||
default => self::class,
|
||||
};
|
||||
if ($routeResult->getMatchedRouteName() === RedirectAction::class) {
|
||||
return new self(Visit::TYPE_INVALID_SHORT_URL);
|
||||
}
|
||||
|
||||
return new self($type);
|
||||
return new self(self::class);
|
||||
}
|
||||
|
||||
public function isBaseUrl(): bool
|
||||
|
||||
@@ -14,10 +14,15 @@ use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||
|
||||
class NotFoundRedirectHandler implements MiddlewareInterface
|
||||
{
|
||||
private Options\NotFoundRedirectOptions $redirectOptions;
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||
|
||||
public function __construct(
|
||||
private Options\NotFoundRedirectOptions $redirectOptions,
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper
|
||||
Options\NotFoundRedirectOptions $redirectOptions,
|
||||
RedirectResponseHelperInterface $redirectResponseHelper
|
||||
) {
|
||||
$this->redirectOptions = $redirectOptions;
|
||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
|
||||
@@ -20,7 +20,6 @@ class NotFoundTemplateHandler implements RequestHandlerInterface
|
||||
private const TEMPLATES_BASE_DIR = __DIR__ . '/../../templates';
|
||||
public const NOT_FOUND_TEMPLATE = '404.html';
|
||||
public const INVALID_SHORT_CODE_TEMPLATE = 'invalid-short-code.html';
|
||||
|
||||
private Closure $readFile;
|
||||
|
||||
public function __construct(?callable $readFile = null)
|
||||
|
||||
@@ -8,17 +8,33 @@ use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\RequestTrackerInterface;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsTrackerInterface;
|
||||
|
||||
class NotFoundTrackerMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(private RequestTrackerInterface $requestTracker)
|
||||
private VisitsTrackerInterface $visitsTracker;
|
||||
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$this->requestTracker->trackNotFoundIfApplicable($request);
|
||||
/** @var NotFoundType $notFoundType */
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,11 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
||||
|
||||
class NotFoundTypeResolverMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function __construct(private string $shlinkBasePath)
|
||||
private string $shlinkBasePath;
|
||||
|
||||
public function __construct(string $shlinkBasePath)
|
||||
{
|
||||
$this->shlinkBasePath = $shlinkBasePath;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
|
||||
@@ -8,11 +8,13 @@ use Shlinkio\Shlink\Common\Doctrine\ReopeningEntityManagerInterface;
|
||||
|
||||
class CloseDbConnectionEventListener
|
||||
{
|
||||
private ReopeningEntityManagerInterface $em;
|
||||
/** @var callable */
|
||||
private $wrapped;
|
||||
|
||||
public function __construct(private ReopeningEntityManagerInterface $em, callable $wrapped)
|
||||
public function __construct(ReopeningEntityManagerInterface $em, callable $wrapped)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->wrapped = $wrapped;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class CloseDbConnectionEventListenerDelegator
|
||||
public function __invoke(
|
||||
ContainerInterface $container,
|
||||
string $name,
|
||||
callable $callback,
|
||||
callable $callback
|
||||
): CloseDbConnectionEventListener {
|
||||
/** @var callable $wrapped */
|
||||
$wrapped = $callback();
|
||||
|
||||
@@ -8,8 +8,11 @@ use JsonSerializable;
|
||||
|
||||
abstract class AbstractVisitEvent implements JsonSerializable
|
||||
{
|
||||
public function __construct(protected string $visitId)
|
||||
protected string $visitId;
|
||||
|
||||
public function __construct(string $visitId)
|
||||
{
|
||||
$this->visitId = $visitId;
|
||||
}
|
||||
|
||||
public function visitId(): string
|
||||
|
||||
@@ -6,9 +6,12 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
||||
|
||||
final class UrlVisited extends AbstractVisitEvent
|
||||
{
|
||||
public function __construct(string $visitId, private ?string $originalIpAddress = null)
|
||||
private ?string $originalIpAddress;
|
||||
|
||||
public function __construct(string $visitId, ?string $originalIpAddress = null)
|
||||
{
|
||||
parent::__construct($visitId);
|
||||
$this->originalIpAddress = $originalIpAddress;
|
||||
}
|
||||
|
||||
public function originalIpAddress(): ?string
|
||||
|
||||
@@ -19,13 +19,24 @@ use Throwable;
|
||||
|
||||
class LocateVisit
|
||||
{
|
||||
private IpLocationResolverInterface $ipLocationResolver;
|
||||
private EntityManagerInterface $em;
|
||||
private LoggerInterface $logger;
|
||||
private DbUpdaterInterface $dbUpdater;
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(
|
||||
private IpLocationResolverInterface $ipLocationResolver,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
private DbUpdaterInterface $dbUpdater,
|
||||
private EventDispatcherInterface $eventDispatcher
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
DbUpdaterInterface $dbUpdater,
|
||||
EventDispatcherInterface $eventDispatcher
|
||||
) {
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public function __invoke(UrlVisited $shortUrlVisited): void
|
||||
|
||||
@@ -17,12 +17,21 @@ use function Functional\each;
|
||||
|
||||
class NotifyVisitToMercure
|
||||
{
|
||||
private HubInterface $hub;
|
||||
private MercureUpdatesGeneratorInterface $updatesGenerator;
|
||||
private EntityManagerInterface $em;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
private HubInterface $hub,
|
||||
private MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger
|
||||
HubInterface $hub,
|
||||
MercureUpdatesGeneratorInterface $updatesGenerator,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->hub = $hub;
|
||||
$this->em = $em;
|
||||
$this->logger = $logger;
|
||||
$this->updatesGenerator = $updatesGenerator;
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $shortUrlLocated): void
|
||||
|
||||
@@ -24,15 +24,28 @@ use function Functional\partial_left;
|
||||
|
||||
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(
|
||||
private ClientInterface $httpClient,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger,
|
||||
/** @var string[] */
|
||||
private array $webhooks,
|
||||
private DataTransformerInterface $transformer,
|
||||
private AppOptions $appOptions
|
||||
ClientInterface $httpClient,
|
||||
EntityManagerInterface $em,
|
||||
LoggerInterface $logger,
|
||||
array $webhooks,
|
||||
DataTransformerInterface $transformer,
|
||||
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
|
||||
|
||||
@@ -12,8 +12,13 @@ use function sprintf;
|
||||
|
||||
class UpdateGeoLiteDb
|
||||
{
|
||||
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater, private LoggerInterface $logger)
|
||||
private GeolocationDbUpdaterInterface $dbUpdater;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(GeolocationDbUpdaterInterface $dbUpdater, LoggerInterface $logger)
|
||||
{
|
||||
$this->dbUpdater = $dbUpdater;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
public function __invoke(): void
|
||||
|
||||
@@ -20,14 +20,22 @@ use function sprintf;
|
||||
|
||||
class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
private ShortCodeHelperInterface $shortCodeHelper;
|
||||
private DoctrineBatchHelperInterface $batchHelper;
|
||||
private ShortUrlRepositoryInterface $shortUrlRepo;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private ShortUrlRelationResolverInterface $relationResolver,
|
||||
private ShortCodeHelperInterface $shortCodeHelper,
|
||||
private DoctrineBatchHelperInterface $batchHelper
|
||||
EntityManagerInterface $em,
|
||||
ShortUrlRelationResolverInterface $relationResolver,
|
||||
ShortCodeHelperInterface $shortCodeHelper,
|
||||
DoctrineBatchHelperInterface $batchHelper
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->relationResolver = $relationResolver;
|
||||
$this->shortCodeHelper = $shortCodeHelper;
|
||||
$this->batchHelper = $batchHelper;
|
||||
$this->shortUrlRepo = $this->em->getRepository(ShortUrl::class); // @phpstan-ignore-line
|
||||
}
|
||||
|
||||
@@ -56,7 +64,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||
|
||||
try {
|
||||
$shortUrlImporting = $this->resolveShortUrl($importedUrl, $importShortCodes, $skipOnShortCodeConflict);
|
||||
} catch (NonUniqueSlugException) {
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$io->text(sprintf('%s: <fg=red>Error</>', $longUrl));
|
||||
continue;
|
||||
}
|
||||
@@ -69,7 +77,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||
private function resolveShortUrl(
|
||||
ImportedShlinkUrl $importedUrl,
|
||||
bool $importShortCodes,
|
||||
callable $skipOnShortCodeConflict,
|
||||
callable $skipOnShortCodeConflict
|
||||
): ShortUrlImporting {
|
||||
$alreadyImportedShortUrl = $this->shortUrlRepo->findOneByImportedUrl($importedUrl);
|
||||
if ($alreadyImportedShortUrl !== null) {
|
||||
@@ -88,7 +96,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
||||
private function handleShortCodeUniqueness(
|
||||
ShortUrl $shortUrl,
|
||||
bool $importShortCodes,
|
||||
callable $skipOnShortCodeConflict,
|
||||
callable $skipOnShortCodeConflict
|
||||
): bool {
|
||||
if ($this->shortCodeHelper->ensureShortCodeUniqueness($shortUrl, $importShortCodes)) {
|
||||
return true;
|
||||
|
||||
@@ -14,8 +14,13 @@ use function sprintf;
|
||||
|
||||
final class ShortUrlImporting
|
||||
{
|
||||
private function __construct(private ShortUrl $shortUrl, private bool $isNew)
|
||||
private ShortUrl $shortUrl;
|
||||
private bool $isNew;
|
||||
|
||||
private function __construct(ShortUrl $shortUrl, bool $isNew)
|
||||
{
|
||||
$this->shortUrl = $shortUrl;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
public static function fromExistingShortUrl(ShortUrl $shortUrl): self
|
||||
@@ -38,7 +43,10 @@ final class ShortUrlImporting
|
||||
$importedVisits = 0;
|
||||
foreach ($visits as $importedVisit) {
|
||||
// Skip visits which are older than the most recent already imported visit's date
|
||||
if ($mostRecentImportedDate?->gte(Chronos::instance($importedVisit->date()))) {
|
||||
if (
|
||||
$mostRecentImportedDate !== null
|
||||
&& $mostRecentImportedDate->gte(Chronos::instance($importedVisit->date()))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,10 +18,15 @@ final class MercureUpdatesGenerator implements MercureUpdatesGeneratorInterface
|
||||
private const NEW_VISIT_TOPIC = 'https://shlink.io/new-visit';
|
||||
private const NEW_ORPHAN_VISIT_TOPIC = 'https://shlink.io/new-orphan-visit';
|
||||
|
||||
private DataTransformerInterface $shortUrlTransformer;
|
||||
private DataTransformerInterface $orphanVisitTransformer;
|
||||
|
||||
public function __construct(
|
||||
private DataTransformerInterface $shortUrlTransformer,
|
||||
private DataTransformerInterface $orphanVisitTransformer
|
||||
DataTransformerInterface $shortUrlTransformer,
|
||||
DataTransformerInterface $orphanVisitTransformer
|
||||
) {
|
||||
$this->shortUrlTransformer = $shortUrlTransformer;
|
||||
$this->orphanVisitTransformer = $orphanVisitTransformer;
|
||||
}
|
||||
|
||||
public function newVisitUpdate(Visit $visit): Update
|
||||
|
||||
@@ -10,8 +10,13 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
final class ShortUrlIdentifier
|
||||
{
|
||||
public function __construct(private string $shortCode, private ?string $domain = null)
|
||||
private string $shortCode;
|
||||
private ?string $domain;
|
||||
|
||||
public function __construct(string $shortCode, ?string $domain = null)
|
||||
{
|
||||
$this->shortCode = $shortCode;
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
public static function fromApiRequest(ServerRequestInterface $request): self
|
||||
@@ -41,7 +46,7 @@ final class ShortUrlIdentifier
|
||||
public static function fromShortUrl(ShortUrl $shortUrl): self
|
||||
{
|
||||
$domain = $shortUrl->getDomain();
|
||||
$domainAuthority = $domain?->getAuthority();
|
||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
||||
|
||||
return new self($shortUrl->getShortCode(), $domainAuthority);
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ final class ShortUrlsParams
|
||||
public const DEFAULT_ITEMS_PER_PAGE = 10;
|
||||
|
||||
private int $page;
|
||||
private ?int $itemsPerPage = null;
|
||||
private ?string $searchTerm;
|
||||
private array $tags;
|
||||
private ShortUrlsOrdering $orderBy;
|
||||
private ?DateRange $dateRange;
|
||||
private ?int $itemsPerPage = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
|
||||
@@ -14,16 +14,25 @@ final class VisitsParams
|
||||
private const ALL_ITEMS = -1;
|
||||
|
||||
private ?DateRange $dateRange;
|
||||
private int $page;
|
||||
private int $itemsPerPage;
|
||||
private bool $excludeBots;
|
||||
|
||||
public function __construct(
|
||||
?DateRange $dateRange = null,
|
||||
private int $page = self::FIRST_PAGE,
|
||||
int $page = self::FIRST_PAGE,
|
||||
?int $itemsPerPage = null,
|
||||
private bool $excludeBots = false
|
||||
bool $excludeBots = false
|
||||
) {
|
||||
$this->dateRange = $dateRange ?? new DateRange();
|
||||
$this->page = $this->determinePage($page);
|
||||
$this->itemsPerPage = $this->determineItemsPerPage($itemsPerPage);
|
||||
$this->excludeBots = $excludeBots;
|
||||
}
|
||||
|
||||
private function determinePage(int $page): int
|
||||
{
|
||||
return $page > 0 ? $page : self::FIRST_PAGE;
|
||||
}
|
||||
|
||||
private function determineItemsPerPage(?int $itemsPerPage): int
|
||||
@@ -39,7 +48,7 @@ final class VisitsParams
|
||||
{
|
||||
return new self(
|
||||
parseDateRangeFromQuery($query, 'startDate', 'endDate'),
|
||||
(int) ($query['page'] ?? 1),
|
||||
(int) ($query['page'] ?? self::FIRST_PAGE),
|
||||
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
|
||||
isset($query['excludeBots']),
|
||||
);
|
||||
|
||||
@@ -19,7 +19,6 @@ class UrlShortenerOptions extends AbstractOptions
|
||||
private int $redirectStatusCode = DEFAULT_REDIRECT_STATUS_CODE;
|
||||
private int $redirectCacheLifetime = DEFAULT_REDIRECT_CACHE_LIFETIME;
|
||||
private bool $autoResolveTitles = false;
|
||||
private bool $appendExtraPath = false;
|
||||
|
||||
public function isUrlValidationEnabled(): bool
|
||||
{
|
||||
@@ -68,16 +67,6 @@ class UrlShortenerOptions extends AbstractOptions
|
||||
$this->autoResolveTitles = $autoResolveTitles;
|
||||
}
|
||||
|
||||
public function appendExtraPath(): bool
|
||||
{
|
||||
return $this->appendExtraPath;
|
||||
}
|
||||
|
||||
protected function setAppendExtraPath(bool $appendExtraPath): void
|
||||
{
|
||||
$this->appendExtraPath = $appendExtraPath;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
protected function setAnonymizeRemoteAddr(bool $anonymizeRemoteAddr): void
|
||||
{
|
||||
|
||||
@@ -11,8 +11,13 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
||||
|
||||
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||
{
|
||||
public function __construct(private VisitRepositoryInterface $repo, private VisitsParams $params)
|
||||
private VisitRepositoryInterface $repo;
|
||||
private VisitsParams $params;
|
||||
|
||||
public function __construct(VisitRepositoryInterface $repo, VisitsParams $params)
|
||||
{
|
||||
$this->repo = $repo;
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
protected function doCount(): int
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||
|
||||
use Happyr\DoctrineSpecification\Specification\Specification;
|
||||
use Pagerfanta\Adapter\AdapterInterface;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
|
||||
@@ -11,11 +12,15 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ShortUrlRepositoryInterface $repository,
|
||||
private ShortUrlsParams $params,
|
||||
private ?ApiKey $apiKey
|
||||
) {
|
||||
private ShortUrlRepositoryInterface $repository;
|
||||
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
|
||||
@@ -27,7 +32,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
$this->params->tags(),
|
||||
$this->params->orderBy(),
|
||||
$this->params->dateRange(),
|
||||
$this->apiKey?->spec(),
|
||||
$this->resolveSpec(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +42,12 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
|
||||
$this->params->searchTerm(),
|
||||
$this->params->tags(),
|
||||
$this->params->dateRange(),
|
||||
$this->apiKey?->spec(),
|
||||
$this->resolveSpec(),
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveSpec(): ?Specification
|
||||
{
|
||||
return $this->apiKey !== null ? $this->apiKey->spec() : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Paginator\Adapter;
|
||||
|
||||
use Happyr\DoctrineSpecification\Specification\Specification;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepositoryInterface;
|
||||
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
|
||||
@@ -12,12 +13,21 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||
{
|
||||
private VisitRepositoryInterface $visitRepository;
|
||||
private string $tag;
|
||||
private VisitsParams $params;
|
||||
private ?ApiKey $apiKey;
|
||||
|
||||
public function __construct(
|
||||
private VisitRepositoryInterface $visitRepository,
|
||||
private string $tag,
|
||||
private VisitsParams $params,
|
||||
private ?ApiKey $apiKey
|
||||
VisitRepositoryInterface $visitRepository,
|
||||
string $tag,
|
||||
VisitsParams $params,
|
||||
?ApiKey $apiKey
|
||||
) {
|
||||
$this->visitRepository = $visitRepository;
|
||||
$this->params = $params;
|
||||
$this->tag = $tag;
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
|
||||
public function getSlice($offset, $length): array // phpcs:ignore
|
||||
@@ -27,7 +37,7 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
||||
new VisitsListFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey?->spec(true),
|
||||
$this->resolveSpec(),
|
||||
$length,
|
||||
$offset,
|
||||
),
|
||||
@@ -41,8 +51,13 @@ class VisitsForTagPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
|
||||
new VisitsCountFiltering(
|
||||
$this->params->getDateRange(),
|
||||
$this->params->excludeBots(),
|
||||
$this->apiKey?->spec(true),
|
||||
$this->resolveSpec(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function resolveSpec(): ?Specification
|
||||
{
|
||||
return $this->apiKey !== null ? $this->apiKey->spec(true) : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,21 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
|
||||
|
||||
class VisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
|
||||
{
|
||||
private VisitRepositoryInterface $visitRepository;
|
||||
private ShortUrlIdentifier $identifier;
|
||||
private VisitsParams $params;
|
||||
private ?Specification $spec;
|
||||
|
||||
public function __construct(
|
||||
private VisitRepositoryInterface $visitRepository,
|
||||
private ShortUrlIdentifier $identifier,
|
||||
private VisitsParams $params,
|
||||
private ?Specification $spec
|
||||
VisitRepositoryInterface $visitRepository,
|
||||
ShortUrlIdentifier $identifier,
|
||||
VisitsParams $params,
|
||||
?Specification $spec
|
||||
) {
|
||||
$this->visitRepository = $visitRepository;
|
||||
$this->params = $params;
|
||||
$this->identifier = $identifier;
|
||||
$this->spec = $spec;
|
||||
}
|
||||
|
||||
public function getSlice($offset, $length): array // phpcs:ignore
|
||||
|
||||
@@ -35,7 +35,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
array $tags = [],
|
||||
?ShortUrlsOrdering $orderBy = null,
|
||||
?DateRange $dateRange = null,
|
||||
?Specification $spec = null,
|
||||
?Specification $spec = null
|
||||
): array {
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
||||
$qb->select('DISTINCT s')
|
||||
@@ -43,7 +43,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
->setFirstResult($offset);
|
||||
|
||||
// In case the ordering has been specified, the query could be more complex. Process it
|
||||
if ($orderBy?->hasOrderField()) {
|
||||
if ($orderBy !== null && $orderBy->hasOrderField()) {
|
||||
return $this->processOrderByForList($qb, $orderBy);
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
?DateRange $dateRange = null,
|
||||
?Specification $spec = null,
|
||||
?Specification $spec = null
|
||||
): int {
|
||||
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec);
|
||||
$qb->select('COUNT(DISTINCT s)');
|
||||
@@ -97,17 +97,17 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
|
||||
?string $searchTerm,
|
||||
array $tags,
|
||||
?DateRange $dateRange,
|
||||
?Specification $spec,
|
||||
?Specification $spec
|
||||
): QueryBuilder {
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->from(ShortUrl::class, 's')
|
||||
->where('1=1');
|
||||
|
||||
if ($dateRange?->getStartDate() !== null) {
|
||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->gte('s.dateCreated', ':startDate'));
|
||||
$qb->setParameter('startDate', $dateRange->getStartDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
||||
}
|
||||
if ($dateRange?->getEndDate() !== null) {
|
||||
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->lte('s.dateCreated', ':endDate'));
|
||||
$qb->setParameter('endDate', $dateRange->getEndDate(), ChronosDateTimeType::CHRONOS_DATETIME);
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificat
|
||||
array $tags = [],
|
||||
?ShortUrlsOrdering $orderBy = null,
|
||||
?DateRange $dateRange = null,
|
||||
?Specification $spec = null,
|
||||
?Specification $spec = null
|
||||
): array;
|
||||
|
||||
public function countList(
|
||||
?string $searchTerm = null,
|
||||
array $tags = [],
|
||||
?DateRange $dateRange = null,
|
||||
?Specification $spec = null,
|
||||
?Specification $spec = null
|
||||
): int;
|
||||
|
||||
public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl;
|
||||
|
||||
@@ -71,14 +71,14 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
||||
$iterator = $qb->getQuery()->toIterable();
|
||||
$resultsFound = false;
|
||||
|
||||
/** @var Visit $visit */
|
||||
foreach ($iterator as $key => $visit) {
|
||||
$resultsFound = true;
|
||||
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
|
||||
/** @var Visit|null $visit */
|
||||
$lastId = $visit?->getId() ?? $lastId;
|
||||
$lastId = isset($visit) ? $visit->getId() : $lastId;
|
||||
} while ($resultsFound);
|
||||
}
|
||||
|
||||
@@ -101,12 +101,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
|
||||
|
||||
private function createVisitsByShortCodeQueryBuilder(
|
||||
ShortUrlIdentifier $identifier,
|
||||
VisitsCountFiltering $filtering,
|
||||
VisitsCountFiltering $filtering
|
||||
): QueryBuilder {
|
||||
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
|
||||
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
|
||||
$shortUrl = $shortUrlRepo->findOne($identifier, $filtering->spec());
|
||||
$shortUrlId = $shortUrl?->getId() ?? '-1';
|
||||
$shortUrlId = $shortUrl !== null ? $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
|
||||
// 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
|
||||
{
|
||||
if ($dateRange?->getStartDate() !== null) {
|
||||
if ($dateRange !== null && $dateRange->getStartDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->gte('v.date', '\'' . $dateRange->getStartDate()->toDateTimeString() . '\''));
|
||||
}
|
||||
if ($dateRange?->getEndDate() !== null) {
|
||||
if ($dateRange !== null && $dateRange->getEndDate() !== null) {
|
||||
$qb->andWhere($qb->expr()->lte('v.date', '\'' . $dateRange->getEndDate()->toDateTimeString() . '\''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private DeleteShortUrlsOptions $deleteShortUrlsOptions;
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
|
||||
public function __construct(
|
||||
private EntityManagerInterface $em,
|
||||
private DeleteShortUrlsOptions $deleteShortUrlsOptions,
|
||||
private ShortUrlResolverInterface $urlResolver
|
||||
EntityManagerInterface $em,
|
||||
DeleteShortUrlsOptions $deleteShortUrlsOptions,
|
||||
ShortUrlResolverInterface $urlResolver
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->deleteShortUrlsOptions = $deleteShortUrlsOptions;
|
||||
$this->urlResolver = $urlResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +34,7 @@ class DeleteShortUrlService implements DeleteShortUrlServiceInterface
|
||||
public function deleteByShortCode(
|
||||
ShortUrlIdentifier $identifier,
|
||||
bool $ignoreThreshold = false,
|
||||
?ApiKey $apiKey = null,
|
||||
?ApiKey $apiKey = null
|
||||
): void {
|
||||
$shortUrl = $this->urlResolver->resolveShortUrl($identifier, $apiKey);
|
||||
if (! $ignoreThreshold && $this->isThresholdReached($shortUrl)) {
|
||||
|
||||
@@ -17,6 +17,6 @@ interface DeleteShortUrlServiceInterface
|
||||
public function deleteByShortCode(
|
||||
ShortUrlIdentifier $identifier,
|
||||
bool $ignoreThreshold = false,
|
||||
?ApiKey $apiKey = null,
|
||||
?ApiKey $apiKey = null
|
||||
): void;
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||
|
||||
class ShortCodeHelper implements ShortCodeHelperInterface // TODO Rename to ShortCodeUniquenessHelper
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
public function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool
|
||||
|
||||
@@ -13,8 +13,11 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class ShortUrlResolver implements ShortUrlResolverInterface
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em)
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,7 +27,7 @@ class ShortUrlResolver implements ShortUrlResolverInterface
|
||||
{
|
||||
/** @var ShortUrlRepository $shortUrlRepo */
|
||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||
$shortUrl = $shortUrlRepo->findOne($identifier, $apiKey?->spec());
|
||||
$shortUrl = $shortUrlRepo->findOne($identifier, $apiKey !== null ? $apiKey->spec() : null);
|
||||
if ($shortUrl === null) {
|
||||
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
||||
}
|
||||
@@ -40,7 +43,7 @@ class ShortUrlResolver implements ShortUrlResolverInterface
|
||||
/** @var ShortUrlRepository $shortUrlRepo */
|
||||
$shortUrlRepo = $this->em->getRepository(ShortUrl::class);
|
||||
$shortUrl = $shortUrlRepo->findOneWithDomainFallback($identifier->shortCode(), $identifier->domain());
|
||||
if (! $shortUrl?->isEnabled()) {
|
||||
if ($shortUrl === null || ! $shortUrl->isEnabled()) {
|
||||
throw ShortUrlNotFoundException::fromNotFound($identifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,21 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
class ShortUrlService implements ShortUrlServiceInterface
|
||||
{
|
||||
private ORM\EntityManagerInterface $em;
|
||||
private ShortUrlResolverInterface $urlResolver;
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
|
||||
public function __construct(
|
||||
private ORM\EntityManagerInterface $em,
|
||||
private ShortUrlResolverInterface $urlResolver,
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
private ShortUrlRelationResolverInterface $relationResolver
|
||||
ORM\EntityManagerInterface $em,
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
ShortUrlRelationResolverInterface $relationResolver
|
||||
) {
|
||||
$this->em = $em;
|
||||
$this->urlResolver = $urlResolver;
|
||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
||||
$this->relationResolver = $relationResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +59,7 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||
public function updateShortUrl(
|
||||
ShortUrlIdentifier $identifier,
|
||||
ShortUrlEdit $shortUrlEdit,
|
||||
?ApiKey $apiKey = null,
|
||||
?ApiKey $apiKey = null
|
||||
): ShortUrl {
|
||||
if ($shortUrlEdit->longUrlWasProvided()) {
|
||||
/** @var ShortUrlEdit $shortUrlEdit */
|
||||
|
||||
@@ -27,6 +27,6 @@ interface ShortUrlServiceInterface
|
||||
public function updateShortUrl(
|
||||
ShortUrlIdentifier $identifier,
|
||||
ShortUrlEdit $shortUrlEdit,
|
||||
?ApiKey $apiKey = null,
|
||||
?ApiKey $apiKey = null
|
||||
): ShortUrl;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,21 @@ use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
|
||||
class UrlShortener implements UrlShortenerInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper;
|
||||
private ShortUrlRelationResolverInterface $relationResolver;
|
||||
private ShortCodeHelperInterface $shortCodeHelper;
|
||||
|
||||
public function __construct(
|
||||
private ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
private EntityManagerInterface $em,
|
||||
private ShortUrlRelationResolverInterface $relationResolver,
|
||||
private ShortCodeHelperInterface $shortCodeHelper
|
||||
ShortUrlTitleResolutionHelperInterface $titleResolutionHelper,
|
||||
EntityManagerInterface $em,
|
||||
ShortUrlRelationResolverInterface $relationResolver,
|
||||
ShortCodeHelperInterface $shortCodeHelper
|
||||
) {
|
||||
$this->titleResolutionHelper = $titleResolutionHelper;
|
||||
$this->em = $em;
|
||||
$this->relationResolver = $relationResolver;
|
||||
$this->shortCodeHelper = $shortCodeHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +78,7 @@ class UrlShortener implements UrlShortenerInterface
|
||||
|
||||
if (! $couldBeMadeUnique) {
|
||||
$domain = $shortUrlToBeCreated->getDomain();
|
||||
$domainAuthority = $domain?->getAuthority();
|
||||
$domainAuthority = $domain !== null ? $domain->getAuthority() : null;
|
||||
|
||||
throw NonUniqueSlugException::fromSlug($shortUrlToBeCreated->getShortCode(), $domainAuthority);
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?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,8 +11,13 @@ use function sprintf;
|
||||
|
||||
class ShortUrlStringifier implements ShortUrlStringifierInterface
|
||||
{
|
||||
public function __construct(private array $domainConfig, private string $basePath = '')
|
||||
private array $domainConfig;
|
||||
private string $basePath;
|
||||
|
||||
public function __construct(array $domainConfig, string $basePath = '')
|
||||
{
|
||||
$this->domainConfig = $domainConfig;
|
||||
$this->basePath = $basePath;
|
||||
}
|
||||
|
||||
public function stringify(ShortUrl $shortUrl): string
|
||||
|
||||
@@ -8,8 +8,11 @@ use Shlinkio\Shlink\Core\Util\UrlValidatorInterface;
|
||||
|
||||
class ShortUrlTitleResolutionHelper implements ShortUrlTitleResolutionHelperInterface
|
||||
{
|
||||
public function __construct(private UrlValidatorInterface $urlValidator)
|
||||
private UrlValidatorInterface $urlValidator;
|
||||
|
||||
public function __construct(UrlValidatorInterface $urlValidator)
|
||||
{
|
||||
$this->urlValidator = $urlValidator;
|
||||
}
|
||||
|
||||
public function processTitleAndValidateUrl(TitleResolutionModelInterface $data): TitleResolutionModelInterface
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
<?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)];
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user