Merge pull request #1434 from acelaya-forks/feature/drop-php-8.0

Feature/drop php 8.0
This commit is contained in:
Alejandro Celaya 2022-04-23 19:26:02 +02:00 committed by GitHub
commit 24b06c24dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
128 changed files with 596 additions and 950 deletions

View file

@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0']
php-version: ['8.1']
command: ['cs', 'stan', 'swagger:validate']
steps:
- name: Checkout code
@ -32,7 +32,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0', '8.1']
php-version: ['8.1']
test-group: ['unit', 'api']
steps:
- name: Checkout code
@ -51,7 +51,7 @@ jobs:
- run: composer install --no-interaction --prefer-dist
- run: composer test:${{ matrix.test-group }}:ci
- uses: actions/upload-artifact@v2
if: ${{ matrix.php-version == '8.0' }}
if: ${{ matrix.php-version == '8.1' }}
with:
name: coverage-${{ matrix.test-group }}
path: |
@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0', '8.1']
php-version: ['8.1']
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
env:
LC_ALL: C
@ -91,7 +91,7 @@ jobs:
run: composer test:db:${{ matrix.platform }}
- name: Upload code coverage
uses: actions/upload-artifact@v2
if: ${{ matrix.php-version == '8.0' && matrix.platform == 'sqlite:ci' }}
if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }}
with:
name: coverage-db
path: |
@ -105,7 +105,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0', '8.1']
php-version: ['8.1']
test-group: ['unit', 'db', 'api']
steps:
- name: Checkout code
@ -136,7 +136,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0']
php-version: ['8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2
@ -152,8 +152,8 @@ jobs:
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
- run: wget https://phar.phpunit.de/phpcov-8.2.0.phar
- run: php phpcov-8.2.0.phar merge build --clover build/clover.xml
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
- run: php phpcov-8.2.1.phar merge build --clover build/clover.xml
- name: Publish coverage
uses: codecov/codecov-action@v1
with:

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0', '8.1']
php-version: ['8.1']
swoole: ['yes', 'no']
steps:
- name: Checkout code
@ -53,8 +53,8 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: [ '8.0', '8.1' ]
swoole: [ 'yes', 'no' ]
php-version: ['8.1']
swoole: ['yes', 'no']
steps:
- uses: geekyeggo/delete-artifact@v1
with:

View file

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['8.0']
php-version: ['8.1']
steps:
- name: Checkout code
uses: actions/checkout@v2

View file

@ -4,6 +4,23 @@ 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]
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* [#1280](https://github.com/shlinkio/shlink/issues/1280) Dropped support for PHP 8.0
### Fixed
* *Nothing*
## [3.1.0] - 2022-04-23
### Added
* [#1294](https://github.com/shlinkio/shlink/issues/1294) Allowed to provide a specific domain when importing URLs from YOURLS.

View file

@ -35,7 +35,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 or 8.1
* PHP 8.1
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
* apcu extension is recommended if you don't plan to use openswoole.
* xml extension is required if you want to generate QR codes in svg format.

View file

@ -12,7 +12,7 @@
}
],
"require": {
"php": "^8.0",
"php": "^8.1",
"ext-json": "*",
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",

View file

@ -7,7 +7,7 @@ namespace Shlinkio\Shlink;
use Shlinkio\Shlink\Core\Config\EnvVars;
return (static function (): array {
$threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD()->loadFromEnv();
$threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD->loadFromEnv();
return [

View file

@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
use function Functional\contains;
return (static function (): array {
$driver = EnvVars::DB_DRIVER()->loadFromEnv();
$driver = EnvVars::DB_DRIVER->loadFromEnv();
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
$resolveDriver = static fn () => match ($driver) {
@ -35,12 +35,12 @@ return (static function (): array {
],
default => [
'driver' => $resolveDriver(),
'dbname' => EnvVars::DB_NAME()->loadFromEnv('shlink'),
'user' => EnvVars::DB_USER()->loadFromEnv(),
'password' => EnvVars::DB_PASSWORD()->loadFromEnv(),
'host' => EnvVars::DB_HOST()->loadFromEnv(EnvVars::DB_UNIX_SOCKET()->loadFromEnv()),
'port' => EnvVars::DB_PORT()->loadFromEnv($resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET()->loadFromEnv() : null,
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
'user' => EnvVars::DB_USER->loadFromEnv(),
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
'charset' => $resolveCharset(),
],
};

View file

@ -9,7 +9,7 @@ return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => __DIR__ . '/../../data',
'license_key' => EnvVars::GEOLITE_LICENSE_KEY()->loadFromEnv(),
'license_key' => EnvVars::GEOLITE_LICENSE_KEY->loadFromEnv(),
],
];

View file

@ -24,7 +24,7 @@ return [
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
],
'aliases' => [
'lock_store' => EnvVars::REDIS_SERVERS()->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
'redis_lock_store' => Lock\Store\RedisStore::class,
'local_lock_store' => Lock\Store\FlockStore::class,

View file

@ -9,14 +9,14 @@ use Symfony\Component\Mercure\Hub;
use Symfony\Component\Mercure\HubInterface;
return (static function (): array {
$publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL()->loadFromEnv();
$publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL->loadFromEnv();
return [
'mercure' => [
'public_hub_url' => $publicUrl,
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL()->loadFromEnv($publicUrl),
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET()->loadFromEnv(),
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv($publicUrl),
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(),
'jwt_issuer' => 'Shlink',
],

View file

@ -13,13 +13,13 @@ use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
return [
'qr_codes' => [
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE()->loadFromEnv(DEFAULT_QR_CODE_SIZE),
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN()->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT()->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION()->loadFromEnv(
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(DEFAULT_QR_CODE_SIZE),
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(
DEFAULT_QR_CODE_ERROR_CORRECTION,
),
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()->loadFromEnv(
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
),
],

View file

@ -10,12 +10,12 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
return [
'rabbitmq' => [
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED()->loadFromEnv(false),
'host' => EnvVars::RABBITMQ_HOST()->loadFromEnv(),
'port' => (int) EnvVars::RABBITMQ_PORT()->loadFromEnv('5672'),
'user' => EnvVars::RABBITMQ_USER()->loadFromEnv(),
'password' => EnvVars::RABBITMQ_PASSWORD()->loadFromEnv(),
'vhost' => EnvVars::RABBITMQ_VHOST()->loadFromEnv('/'),
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
],
'dependencies' => [

View file

@ -10,14 +10,14 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
return [
'not_found_redirects' => [
'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT()->loadFromEnv(),
'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT()->loadFromEnv(),
'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT()->loadFromEnv(),
'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT->loadFromEnv(),
'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT->loadFromEnv(),
'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT->loadFromEnv(),
],
'redirects' => [
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE()->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE),
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME()->loadFromEnv(
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE),
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(
DEFAULT_REDIRECT_CACHE_LIFETIME,
),
],

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return (static function (): array {
$redisServers = EnvVars::REDIS_SERVERS()->loadFromEnv();
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
return match ($redisServers) {
null => [],
@ -13,7 +13,7 @@ return (static function (): array {
'cache' => [
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE()->loadFromEnv(),
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
],
],
],

View file

@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
return [
'router' => [
'base_path' => EnvVars::BASE_PATH()->loadFromEnv(''),
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true,

View file

@ -7,7 +7,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
return (static function (): array {
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16);
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16);
return [
@ -17,11 +17,11 @@ return (static function (): array {
'swoole-http-server' => [
'host' => '0.0.0.0',
'port' => (int) EnvVars::PORT()->loadFromEnv(8080),
'port' => (int) EnvVars::PORT->loadFromEnv(8080),
'process-name' => 'shlink',
'options' => [
'worker_num' => (int) EnvVars::WEB_WORKER_NUM()->loadFromEnv(16),
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
],
],

View file

@ -9,28 +9,28 @@ return [
'tracking' => [
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
// This applies only if IP address tracking is enabled
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR()->loadFromEnv(true),
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS()->loadFromEnv(true),
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM()->loadFromEnv(),
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
// If true, visits will not be tracked at all
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING()->loadFromEnv(false),
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING()->loadFromEnv(false),
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
// If true, the referrer will not be tracked
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING()->loadFromEnv(false),
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
// If true, the user agent will not be tracked
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING()->loadFromEnv(false),
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM()->loadFromEnv(),
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(),
],
];

View file

@ -9,7 +9,7 @@ use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
return (static function (): array {
$shortCodesLength = max(
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH()->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
MIN_SHORT_CODES_LENGTH,
);
@ -17,12 +17,12 @@ return (static function (): array {
'url_shortener' => [
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http',
'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''),
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv(true)) ? 'https' : 'http',
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
],
'default_short_codes_length' => $shortCodesLength,
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES()->loadFromEnv(false),
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH()->loadFromEnv(false),
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
],
];

View file

@ -6,14 +6,14 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
return (static function (): array {
$webhooks = EnvVars::VISITS_WEBHOOKS()->loadFromEnv();
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
return [
'visits_webhooks' => [
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
'notify_orphan_visits_to_webhooks' =>
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()->loadFromEnv(false),
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
],
];

View file

@ -13,7 +13,7 @@ chdir(dirname(__DIR__));
require 'vendor/autoload.php';
// This is one of the first files loaded. Configure the timezone here
date_default_timezone_set(EnvVars::TIMEZONE()->loadFromEnv(date_default_timezone_get()));
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
// It needs to be placed here as individual config files will not be loaded once config is cached

View file

@ -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/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}

View file

@ -8,8 +8,8 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
final class Version20210207100807 extends AbstractMigration
{
@ -27,7 +27,7 @@ final class Version20210207100807 extends AbstractMigration
]);
$visits->addColumn('type', Types::STRING, [
'length' => 255,
'default' => Visit::TYPE_VALID_SHORT_URL,
'default' => VisitType::VALID_SHORT_URL->value,
]);
}

View file

@ -73,13 +73,16 @@ class GenerateKeyCommand extends Command
$authorOnly,
'a',
InputOption::VALUE_NONE,
sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS),
sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS->value),
)
->addOption(
$domainOnly,
'd',
InputOption::VALUE_REQUIRED,
sprintf('Adds the "%s" role to the new API key, with the domain provided.', Role::DOMAIN_SPECIFIC),
sprintf(
'Adds the "%s" role to the new API key, with the domain provided.',
Role::DOMAIN_SPECIFIC->value,
),
)
->setHelp($help);
}
@ -99,7 +102,7 @@ class GenerateKeyCommand extends Command
if (! $apiKey->isAdmin()) {
ShlinkTable::default($io)->render(
['Role name', 'Role metadata'],
$apiKey->mapRoles(fn (string $name, array $meta) => [$name, arrayToString($meta, 0)]),
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
null,
'Roles',
);

View file

@ -60,10 +60,10 @@ class ListKeysCommand extends Command
}
$rowData[] = $expiration?->toAtomString() ?? '-';
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (string $roleName, array $meta) =>
fn (Role $role, array $meta) =>
empty($meta)
? Role::toFriendlyName($roleName)
: sprintf('%s: %s', Role::toFriendlyName($roleName), Role::domainAuthorityFromMeta($meta)),
? Role::toFriendlyName($role)
: sprintf('%s: %s', Role::toFriendlyName($role), Role::domainAuthorityFromMeta($meta)),
));
return $rowData;

View file

@ -53,7 +53,7 @@ class DomainRedirectsCommand extends Command
/** @var string[] $availableDomains */
$availableDomains = invoke(
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault()),
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
'toString',
);
if (empty($availableDomains)) {

View file

@ -48,12 +48,12 @@ class ListDomainsCommand extends Command
$table->render(
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
map($domains, function (DomainItem $domain) use ($showRedirects) {
$commonValues = [$domain->toString(), $domain->isDefault() ? 'Yes' : 'No'];
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
return $showRedirects
? [
...$commonValues,
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig()),
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
]
: $commonValues;
}),

View file

@ -81,6 +81,6 @@ class DeleteShortUrlCommand extends Command
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
{
$this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold);
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode()));
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode));
}
}

View file

@ -13,6 +13,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -120,9 +121,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$page = (int) $input->getOption('page');
$searchTerm = $input->getOption('search-term');
$tags = $input->getOption('tags');
$tagsMode = $input->getOption('including-all-tags') === true
? ShortUrlsParams::TAGS_MODE_ALL
: ShortUrlsParams::TAGS_MODE_ANY;
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
$tags = ! empty($tags) ? explode(',', $tags) : [];
$all = $input->getOption('all');
$startDate = $this->getStartDateOption($input, $output);

View file

@ -46,7 +46,7 @@ class ListTagsCommand extends Command
return map(
$tags,
static fn (TagInfo $tagInfo) => [$tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsCount],
);
}
}

View file

@ -22,11 +22,11 @@ abstract class AbstractLockedCommand extends Command
final protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$lockConfig = $this->getLockConfig();
$lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking());
$lock = $this->locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
if (! $lock->acquire($lockConfig->isBlocking())) {
if (! $lock->acquire($lockConfig->isBlocking)) {
$output->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName()),
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
);
return ExitCodes::EXIT_WARNING;
}

View file

@ -9,9 +9,9 @@ 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,
public readonly string $lockName,
public readonly bool $isBlocking,
public readonly float $ttl = self::DEFAULT_TTL,
) {
}
@ -24,19 +24,4 @@ final class LockedCommandConfig
{
return new self($lockName, false);
}
public function lockName(): string
{
return $this->lockName;
}
public function isBlocking(): bool
{
return $this->isBlocking;
}
public function ttl(): float
{
return $this->ttl;
}
}

View file

@ -16,7 +16,7 @@ class InvalidRoleConfigException extends InvalidArgumentException implements Exc
return new self(sprintf(
'You cannot create an API key with the "%s" role attached to the default domain. '
. 'The role is currently limited to non-default domains.',
Role::DOMAIN_SPECIFIC,
Role::DOMAIN_SPECIFIC->value,
));
}
}

View file

@ -15,7 +15,7 @@ final class ShlinkTable
private const DEFAULT_STYLE_NAME = 'default';
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
private function __construct(private Table $baseTable, private bool $withRowSeparators)
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
{
}

View file

@ -36,10 +36,11 @@ class DeleteShortUrlCommandTest extends TestCase
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->will(
function (): void {
},
);
$deleteByShortCode = $this->service->deleteByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->will(function (): void {
});
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@ -55,7 +56,7 @@ class DeleteShortUrlCommandTest extends TestCase
public function invalidShortCodePrintsMessage(): void
{
$shortCode = 'abc123';
$identifier = new ShortUrlIdentifier($shortCode);
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow(
Exception\ShortUrlNotFoundException::fromNotFound($identifier),
);
@ -77,7 +78,7 @@ class DeleteShortUrlCommandTest extends TestCase
string $expectedMessage,
): void {
$shortCode = 'abc123';
$identifier = new ShortUrlIdentifier($shortCode);
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will(
function (array $args) use ($shortCode): void {
$ignoreThreshold = array_pop($args);
@ -114,12 +115,13 @@ class DeleteShortUrlCommandTest extends TestCase
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->willThrow(
Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
),
);
$deleteByShortCode = $this->service->deleteByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->willThrow(Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
));
$this->commandTester->setInputs(['no']);
$this->commandTester->execute(['shortCode' => $shortCode]);

View file

@ -44,7 +44,7 @@ class GetVisitsCommandTest extends TestCase
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::emptyInstance()),
)
->willReturn(new Paginator(new ArrayAdapter([])))
@ -60,7 +60,7 @@ class GetVisitsCommandTest extends TestCase
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::withStartAndEndDate(Chronos::parse($startDate), Chronos::parse($endDate))),
)
->willReturn(new Paginator(new ArrayAdapter([])))
@ -79,7 +79,7 @@ class GetVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$startDate = 'foo';
$info = $this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::emptyInstance()),
)->willReturn(new Paginator(new ArrayAdapter([])));
@ -100,7 +100,10 @@ class GetVisitsCommandTest extends TestCase
public function outputIsProperlyGenerated(): void
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn(
$this->visitsHelper->visitsForShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
Argument::any(),
)->willReturn(
new Paginator(new ArrayAdapter([
Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate(
VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')),

View file

@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -205,23 +206,23 @@ class ListShortUrlsCommandTest extends TestCase
public function provideArgs(): iterable
{
yield [[], 1, null, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [['--page' => $page = 3], $page, null, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [['--including-all-tags' => true], 1, null, [], ShortUrlsParams::TAGS_MODE_ALL];
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], ShortUrlsParams::TAGS_MODE_ANY];
yield [[], 1, null, [], TagsMode::ANY->value];
yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value];
yield [['--including-all-tags' => true], 1, null, [], TagsMode::ALL->value];
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], TagsMode::ANY->value];
yield [
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
$page,
$searchTerm,
explode(',', $tags),
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY->value,
];
yield [
['--start-date' => $startDate = '2019-01-01'],
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY->value,
$startDate,
];
yield [
@ -229,7 +230,7 @@ class ListShortUrlsCommandTest extends TestCase
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY->value,
null,
$endDate,
];
@ -238,7 +239,7 @@ class ListShortUrlsCommandTest extends TestCase
1,
null,
[],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY->value,
$startDate,
$endDate,
];
@ -276,7 +277,7 @@ class ListShortUrlsCommandTest extends TestCase
'page' => 1,
'searchTerm' => null,
'tags' => [],
'tagsMode' => ShortUrlsParams::TAGS_MODE_ANY,
'tagsMode' => TagsMode::ANY->value,
'startDate' => null,
'endDate' => null,
'orderBy' => null,

View file

@ -37,8 +37,9 @@ class ResolveUrlCommandTest extends TestCase
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl)
->shouldBeCalledOnce();
$this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode))->willReturn(
$shortUrl,
)->shouldBeCalledOnce();
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@ -48,8 +49,8 @@ class ResolveUrlCommandTest extends TestCase
/** @test */
public function incorrectShortCodeOutputsErrorMessage(): void
{
$identifier = new ShortUrlIdentifier('abc123');
$shortCode = $identifier->shortCode();
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
$shortCode = $identifier->shortCode;
$this->urlResolver->resolveShortUrl($identifier)
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))

View file

@ -20,7 +20,7 @@ class InvalidRoleConfigExceptionTest extends TestCase
self::assertEquals(sprintf(
'You cannot create an API key with the "%s" role attached to the default domain. '
. 'The role is currently limited to non-default domains.',
Role::DOMAIN_SPECIFIC,
Role::DOMAIN_SPECIFIC->value,
), $e->getMessage());
}
}

View file

@ -6,9 +6,11 @@ namespace Shlinkio\Shlink\Core;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata);
@ -61,10 +63,13 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->nullable()
->build();
$builder->createField('type', Types::STRING)
->columnName('type')
->length(255)
->build();
(new FieldBuilder($builder, [
'fieldName' => 'type',
'type' => Types::STRING,
'enumType' => VisitType::class,
]))->columnName('type')
->length(255)
->build();
$builder->createField('potentialBot', Types::BOOLEAN)
->columnName('potential_bot')

View file

@ -29,11 +29,11 @@ final class QrCodeParams
private const SUPPORTED_FORMATS = ['png', 'svg'];
private function __construct(
private int $size,
private int $margin,
private WriterInterface $writer,
private ErrorCorrectionLevelInterface $errorCorrectionLevel,
private RoundBlockSizeModeInterface $roundBlockSizeMode,
public readonly int $size,
public readonly int $margin,
public readonly WriterInterface $writer,
public readonly ErrorCorrectionLevelInterface $errorCorrectionLevel,
public readonly RoundBlockSizeModeInterface $roundBlockSizeMode,
) {
}
@ -105,29 +105,4 @@ final class QrCodeParams
{
return strtolower(trim($param));
}
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;
}
public function roundBlockSizeMode(): RoundBlockSizeModeInterface
{
return $this->roundBlockSizeMode;
}
}

View file

@ -42,11 +42,11 @@ class QrCodeAction implements MiddlewareInterface
$params = QrCodeParams::fromRequest($request, $this->defaultOptions);
$qrCodeBuilder = Builder::create()
->data($this->stringifier->stringify($shortUrl))
->size($params->size())
->margin($params->margin())
->writer($params->writer())
->errorCorrectionLevel($params->errorCorrectionLevel())
->roundBlockSizeMode($params->roundBlockSizeMode());
->size($params->size)
->margin($params->margin)
->writer($params->writer)
->errorCorrectionLevel($params->errorCorrectionLevel)
->roundBlockSizeMode($params->roundBlockSizeMode);
return new QrCodeResponse($qrCodeBuilder->build());
}

View file

@ -2,155 +2,70 @@
declare(strict_types=1);
// phpcs:disable
// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474
namespace Shlinkio\Shlink\Core\Config;
use ReflectionClass;
use ReflectionClassConstant;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use function array_values;
use function Functional\contains;
use function Shlinkio\Shlink\Config\env;
// TODO Convert to enum after dropping PHP 8.0 support
/**
* @method static EnvVars DELETE_SHORT_URL_THRESHOLD()
* @method static EnvVars DB_DRIVER()
* @method static EnvVars DB_NAME()
* @method static EnvVars DB_USER()
* @method static EnvVars DB_PASSWORD()
* @method static EnvVars DB_HOST()
* @method static EnvVars DB_UNIX_SOCKET()
* @method static EnvVars DB_PORT()
* @method static EnvVars GEOLITE_LICENSE_KEY()
* @method static EnvVars REDIS_SERVERS()
* @method static EnvVars REDIS_SENTINEL_SERVICE()
* @method static EnvVars MERCURE_PUBLIC_HUB_URL()
* @method static EnvVars MERCURE_INTERNAL_HUB_URL()
* @method static EnvVars MERCURE_JWT_SECRET()
* @method static EnvVars DEFAULT_QR_CODE_SIZE()
* @method static EnvVars DEFAULT_QR_CODE_MARGIN()
* @method static EnvVars DEFAULT_QR_CODE_FORMAT()
* @method static EnvVars DEFAULT_QR_CODE_ERROR_CORRECTION()
* @method static EnvVars DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()
* @method static EnvVars RABBITMQ_ENABLED()
* @method static EnvVars RABBITMQ_HOST()
* @method static EnvVars RABBITMQ_PORT()
* @method static EnvVars RABBITMQ_USER()
* @method static EnvVars RABBITMQ_PASSWORD()
* @method static EnvVars RABBITMQ_VHOST()
* @method static EnvVars DEFAULT_INVALID_SHORT_URL_REDIRECT()
* @method static EnvVars DEFAULT_REGULAR_404_REDIRECT()
* @method static EnvVars DEFAULT_BASE_URL_REDIRECT()
* @method static EnvVars REDIRECT_STATUS_CODE()
* @method static EnvVars REDIRECT_CACHE_LIFETIME()
* @method static EnvVars BASE_PATH()
* @method static EnvVars PORT()
* @method static EnvVars TASK_WORKER_NUM()
* @method static EnvVars WEB_WORKER_NUM()
* @method static EnvVars ANONYMIZE_REMOTE_ADDR()
* @method static EnvVars TRACK_ORPHAN_VISITS()
* @method static EnvVars DISABLE_TRACK_PARAM()
* @method static EnvVars DISABLE_TRACKING()
* @method static EnvVars DISABLE_IP_TRACKING()
* @method static EnvVars DISABLE_REFERRER_TRACKING()
* @method static EnvVars DISABLE_UA_TRACKING()
* @method static EnvVars DISABLE_TRACKING_FROM()
* @method static EnvVars DEFAULT_SHORT_CODES_LENGTH()
* @method static EnvVars IS_HTTPS_ENABLED()
* @method static EnvVars DEFAULT_DOMAIN()
* @method static EnvVars AUTO_RESOLVE_TITLES()
* @method static EnvVars REDIRECT_APPEND_EXTRA_PATH()
* @method static EnvVars TIMEZONE()
* @method static EnvVars VISITS_WEBHOOKS()
* @method static EnvVars NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()
*/
final class EnvVars
enum EnvVars: string
{
public const DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
public const DB_DRIVER = 'DB_DRIVER';
public const DB_NAME = 'DB_NAME';
public const DB_USER = 'DB_USER';
public const DB_PASSWORD = 'DB_PASSWORD';
public const DB_HOST = 'DB_HOST';
public const DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
public const DB_PORT = 'DB_PORT';
public const GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
public const REDIS_SERVERS = 'REDIS_SERVERS';
public const REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
public const MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
public const MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
public const MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
public const DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
public const DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
public const DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
public const DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
public const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
public const RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
public const RABBITMQ_HOST = 'RABBITMQ_HOST';
public const RABBITMQ_PORT = 'RABBITMQ_PORT';
public const RABBITMQ_USER = 'RABBITMQ_USER';
public const RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
public const RABBITMQ_VHOST = 'RABBITMQ_VHOST';
public const DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
public const DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
public const DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
public const REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
public const REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
public const BASE_PATH = 'BASE_PATH';
public const PORT = 'PORT';
public const TASK_WORKER_NUM = 'TASK_WORKER_NUM';
public const WEB_WORKER_NUM = 'WEB_WORKER_NUM';
public const ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
public const TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
public const DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
public const DISABLE_TRACKING = 'DISABLE_TRACKING';
public const DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING';
public const DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING';
public const DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING';
public const DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM';
public const DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH';
public const IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED';
public const DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
public const AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
public const REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
public const TIMEZONE = 'TIMEZONE';
case DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
case DB_DRIVER = 'DB_DRIVER';
case DB_NAME = 'DB_NAME';
case DB_USER = 'DB_USER';
case DB_PASSWORD = 'DB_PASSWORD';
case DB_HOST = 'DB_HOST';
case DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
case DB_PORT = 'DB_PORT';
case GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
case REDIS_SERVERS = 'REDIS_SERVERS';
case REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
case MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
case MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
case MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
case RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
case RABBITMQ_HOST = 'RABBITMQ_HOST';
case RABBITMQ_PORT = 'RABBITMQ_PORT';
case RABBITMQ_USER = 'RABBITMQ_USER';
case RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
case RABBITMQ_VHOST = 'RABBITMQ_VHOST';
case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
case REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
case REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
case BASE_PATH = 'BASE_PATH';
case PORT = 'PORT';
case TASK_WORKER_NUM = 'TASK_WORKER_NUM';
case WEB_WORKER_NUM = 'WEB_WORKER_NUM';
case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
case DISABLE_TRACKING = 'DISABLE_TRACKING';
case DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING';
case DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING';
case DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING';
case DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM';
case DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH';
case IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED';
case DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
case AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
case TIMEZONE = 'TIMEZONE';
/** @deprecated */
public const VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
case VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
/** @deprecated */
public const NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
/**
* @return string[]
*/
public static function cases(): array
{
static $constants;
if ($constants !== null) {
return $constants;
}
$ref = new ReflectionClass(self::class);
return $constants = array_values($ref->getConstants(ReflectionClassConstant::IS_PUBLIC));
}
private function __construct(private string $envVar)
{
}
public static function __callStatic(string $name, array $arguments): self
{
if (! contains(self::cases(), $name)) {
throw new InvalidArgumentException('Invalid env var: "' . $name . '"');
}
return new self($name);
}
case NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
public function loadFromEnv(mixed $default = null): mixed
{
return env($this->envVar, $default);
return env($this->value, $default);
}
public function existsInEnv(): bool

View file

@ -13,7 +13,9 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
use function Functional\compose;
use function Functional\id;
use function str_replace;
use function urlencode;
class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
{
@ -71,10 +73,10 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
$replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier),
);
$replacePlaceholdersInPath = compose(
$replacePlaceholders('\Functional\id'),
static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), // Fix duplicated bars
$replacePlaceholders(id(...)),
static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path),
);
$replacePlaceholdersInQuery = $replacePlaceholders('\urlencode');
$replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...));
return $redirectUri
->withPath($replacePlaceholdersInPath($redirectUri->getPath()))

View file

@ -9,9 +9,9 @@ use JsonSerializable;
final class NotFoundRedirects implements JsonSerializable
{
private function __construct(
private ?string $baseUrlRedirect,
private ?string $regular404Redirect,
private ?string $invalidShortUrlRedirect,
public readonly ?string $baseUrlRedirect,
public readonly ?string $regular404Redirect,
public readonly ?string $invalidShortUrlRedirect,
) {
}
@ -33,21 +33,6 @@ final class NotFoundRedirects implements JsonSerializable
return new self($config->baseUrlRedirect(), $config->regular404Redirect(), $config->invalidShortUrlRedirect());
}
public function baseUrlRedirect(): ?string
{
return $this->baseUrlRedirect;
}
public function regular404Redirect(): ?string
{
return $this->regular404Redirect;
}
public function invalidShortUrlRedirect(): ?string
{
return $this->invalidShortUrlRedirect;
}
public function jsonSerialize(): array
{
return [

View file

@ -12,9 +12,9 @@ use Shlinkio\Shlink\Core\Entity\Domain;
final class DomainItem implements JsonSerializable
{
private function __construct(
private string $authority,
private NotFoundRedirectConfigInterface $notFoundRedirectConfig,
private bool $isDefault,
private readonly string $authority,
public readonly NotFoundRedirectConfigInterface $notFoundRedirectConfig,
public readonly bool $isDefault,
) {
}
@ -23,9 +23,9 @@ final class DomainItem implements JsonSerializable
return new self($domain->getAuthority(), $domain, false);
}
public static function forDefaultDomain(string $authority, NotFoundRedirectConfigInterface $config): self
public static function forDefaultDomain(string $defaultDomain, NotFoundRedirectConfigInterface $config): self
{
return new self($authority, $config, true);
return new self($defaultDomain, $config, true);
}
public function jsonSerialize(): array
@ -41,14 +41,4 @@ final class DomainItem implements JsonSerializable
{
return $this->authority;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function notFoundRedirectConfig(): NotFoundRedirectConfigInterface
{
return $this->notFoundRedirectConfig;
}
}

View file

@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Core\Domain\Repository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Happyr\DoctrineSpecification\Spec;
use Shlinkio\Shlink\Core\Domain\Spec\IsDomain;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
@ -77,10 +76,9 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
// FIXME The $apiKey->spec() method cannot be used here, as it returns a single spec which assumes the
// ShortUrl is the root entity. Here, the Domain is the root entity.
// Think on a way to centralize the conditional behavior and make $apiKey->spec() more flexible.
yield from $apiKey?->mapRoles(fn (string $roleName, array $meta) => match ($roleName) {
yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) {
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
default => [null, Spec::andX()],
}) ?? [];
}
}

View file

@ -66,8 +66,8 @@ class Domain extends AbstractEntity implements JsonSerializable, NotFoundRedirec
public function configureNotFoundRedirects(NotFoundRedirects $redirects): void
{
$this->baseUrlRedirect = $redirects->baseUrlRedirect();
$this->regular404Redirect = $redirects->regular404Redirect();
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect();
$this->baseUrlRedirect = $redirects->baseUrlRedirect;
$this->regular404Redirect = $redirects->regular404Redirect;
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect;
}
}

View file

@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@ -174,7 +175,7 @@ class ShortUrl extends AbstractEntity
{
/** @var Selectable $visits */
$visits = $this->visits;
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', Visit::TYPE_IMPORTED))
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED))
->orderBy(['id' => 'DESC'])
->setMaxResults(1);

View file

@ -11,29 +11,24 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
use function Shlinkio\Shlink\Core\isCrawler;
class Visit extends AbstractEntity implements JsonSerializable
{
public const TYPE_VALID_SHORT_URL = 'valid_short_url';
public const TYPE_IMPORTED = 'imported';
public const TYPE_INVALID_SHORT_URL = 'invalid_short_url';
public const TYPE_BASE_URL = 'base_url';
public const TYPE_REGULAR_404 = 'regular_404';
private string $referer;
private Chronos $date;
private ?string $remoteAddr = null;
private ?string $visitedUrl = null;
private string $userAgent;
private string $type;
private VisitType $type;
private ?ShortUrl $shortUrl;
private ?VisitLocation $visitLocation = null;
private bool $potentialBot;
private function __construct(?ShortUrl $shortUrl, string $type)
private function __construct(?ShortUrl $shortUrl, VisitType $type)
{
$this->shortUrl = $shortUrl;
$this->date = Chronos::now();
@ -42,7 +37,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public static function forValidShortUrl(ShortUrl $shortUrl, Visitor $visitor, bool $anonymize = true): self
{
$instance = new self($shortUrl, self::TYPE_VALID_SHORT_URL);
$instance = new self($shortUrl, VisitType::VALID_SHORT_URL);
$instance->hydrateFromVisitor($visitor, $anonymize);
return $instance;
@ -50,7 +45,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public static function fromImport(ShortUrl $shortUrl, ImportedShlinkVisit $importedVisit): self
{
$instance = new self($shortUrl, self::TYPE_IMPORTED);
$instance = new self($shortUrl, VisitType::IMPORTED);
$instance->userAgent = $importedVisit->userAgent();
$instance->potentialBot = isCrawler($instance->userAgent);
$instance->referer = $importedVisit->referer();
@ -64,7 +59,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public static function forBasePath(Visitor $visitor, bool $anonymize = true): self
{
$instance = new self(null, self::TYPE_BASE_URL);
$instance = new self(null, VisitType::BASE_URL);
$instance->hydrateFromVisitor($visitor, $anonymize);
return $instance;
@ -72,7 +67,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public static function forInvalidShortUrl(Visitor $visitor, bool $anonymize = true): self
{
$instance = new self(null, self::TYPE_INVALID_SHORT_URL);
$instance = new self(null, VisitType::INVALID_SHORT_URL);
$instance->hydrateFromVisitor($visitor, $anonymize);
return $instance;
@ -80,7 +75,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public static function forRegularNotFound(Visitor $visitor, bool $anonymize = true): self
{
$instance = new self(null, self::TYPE_REGULAR_404);
$instance = new self(null, VisitType::REGULAR_404);
$instance->hydrateFromVisitor($visitor, $anonymize);
return $instance;
@ -88,10 +83,10 @@ class Visit extends AbstractEntity implements JsonSerializable
private function hydrateFromVisitor(Visitor $visitor, bool $anonymize = true): void
{
$this->userAgent = $visitor->getUserAgent();
$this->referer = $visitor->getReferer();
$this->remoteAddr = $this->processAddress($anonymize, $visitor->getRemoteAddress());
$this->visitedUrl = $visitor->getVisitedUrl();
$this->userAgent = $visitor->userAgent;
$this->referer = $visitor->referer;
$this->remoteAddr = $this->processAddress($anonymize, $visitor->remoteAddress);
$this->visitedUrl = $visitor->visitedUrl;
$this->potentialBot = $visitor->isPotentialBot();
}
@ -150,7 +145,7 @@ class Visit extends AbstractEntity implements JsonSerializable
return $this->visitedUrl;
}
public function type(): string
public function type(): VisitType
{
return $this->type;
}
@ -159,11 +154,19 @@ class Visit extends AbstractEntity implements JsonSerializable
* Needed only for ArrayCollections to be able to apply criteria filtering
* @internal
*/
public function getType(): string
public function getType(): VisitType
{
return $this->type();
}
/**
* @internal
*/
public function getDate(): Chronos
{
return $this->date;
}
public function jsonSerialize(): array
{
return [
@ -174,12 +177,4 @@ class Visit extends AbstractEntity implements JsonSerializable
'potentialBot' => $this->potentialBot,
];
}
/**
* @internal
*/
public function getDate(): Chronos
{
return $this->date;
}
}

View file

@ -7,13 +7,13 @@ namespace Shlinkio\Shlink\Core\ErrorHandler\Model;
use Mezzio\Router\RouteResult;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Action\RedirectAction;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use function rtrim;
class NotFoundType
{
private function __construct(private string $type)
private function __construct(private readonly VisitType $type)
{
}
@ -24,10 +24,10 @@ class NotFoundType
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
$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,
$isBaseUrl => VisitType::BASE_URL,
$routeResult->isFailure() => VisitType::REGULAR_404,
$routeResult->getMatchedRouteName() === RedirectAction::class => VisitType::INVALID_SHORT_URL,
default => VisitType::VALID_SHORT_URL,
};
return new self($type);
@ -35,16 +35,16 @@ class NotFoundType
public function isBaseUrl(): bool
{
return $this->type === Visit::TYPE_BASE_URL;
return $this->type === VisitType::BASE_URL;
}
public function isRegularNotFound(): bool
{
return $this->type === Visit::TYPE_REGULAR_404;
return $this->type === VisitType::REGULAR_404;
}
public function isInvalidShortUrl(): bool
{
return $this->type === Visit::TYPE_INVALID_SHORT_URL;
return $this->type === VisitType::INVALID_SHORT_URL;
}
}

View file

@ -8,15 +8,10 @@ use JsonSerializable;
abstract class AbstractVisitEvent implements JsonSerializable
{
public function __construct(protected string $visitId)
public function __construct(public readonly string $visitId)
{
}
public function visitId(): string
{
return $this->visitId;
}
public function jsonSerialize(): array
{
return ['visitId' => $this->visitId];

View file

@ -6,13 +6,8 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
final class UrlVisited extends AbstractVisitEvent
{
public function __construct(string $visitId, private ?string $originalIpAddress = null)
public function __construct(string $visitId, public readonly ?string $originalIpAddress = null)
{
parent::__construct($visitId);
}
public function originalIpAddress(): ?string
{
return $this->originalIpAddress;
}
}

View file

@ -30,7 +30,7 @@ class LocateVisit
public function __invoke(UrlVisited $shortUrlVisited): void
{
$visitId = $shortUrlVisited->visitId();
$visitId = $shortUrlVisited->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);
@ -41,7 +41,7 @@ class LocateVisit
return;
}
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit);
$this->eventDispatcher->dispatch(new VisitLocated($visitId));
}

View file

@ -27,7 +27,7 @@ class NotifyVisitToMercure
public function __invoke(VisitLocated $shortUrlLocated): void
{
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);

View file

@ -37,7 +37,7 @@ class NotifyVisitToRabbitMq
return;
}
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
$visit = $this->em->find(Visit::class, $visitId);
if ($visit === null) {

View file

@ -40,7 +40,7 @@ class NotifyVisitToWebHooks
return;
}
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);

View file

@ -20,8 +20,8 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE
public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self
{
$shortCode = $identifier->shortCode();
$domain = $identifier->domain();
$shortCode = $identifier->shortCode;
$domain = $identifier->domain;
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf(
'Impossible to delete short URL with short code "%s"%s, since it has more than "%s" visits.',

View file

@ -20,8 +20,8 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail
public static function fromNotFound(ShortUrlIdentifier $identifier): self
{
$shortCode = $identifier->shortCode();
$domain = $identifier->domain();
$shortCode = $identifier->shortCode;
$domain = $identifier->domain;
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix));

View file

@ -14,7 +14,7 @@ use function sprintf;
final class ShortUrlImporting
{
private function __construct(private ShortUrl $shortUrl, private bool $isNew)
private function __construct(private readonly ShortUrl $shortUrl, private readonly bool $isNew)
{
}

View file

@ -10,8 +10,8 @@ abstract class AbstractInfinitePaginableListParams
{
private const FIRST_PAGE = 1;
private int $page;
private int $itemsPerPage;
public readonly int $page;
public readonly int $itemsPerPage;
protected function __construct(?int $page, ?int $itemsPerPage)
{
@ -28,14 +28,4 @@ abstract class AbstractInfinitePaginableListParams
{
return $itemsPerPage === null || $itemsPerPage < 0 ? Paginator::ALL_ITEMS : $itemsPerPage;
}
public function getPage(): int
{
return $this->page;
}
public function getItemsPerPage(): int
{
return $this->itemsPerPage;
}
}

View file

@ -8,7 +8,7 @@ final class Ordering
{
private const DEFAULT_DIR = 'ASC';
private function __construct(private ?string $field, private string $dir)
private function __construct(public readonly ?string $field, public readonly string $direction)
{
}
@ -26,16 +26,6 @@ final class Ordering
return self::fromTuple([null, null]);
}
public function orderField(): ?string
{
return $this->field;
}
public function orderDirection(): string
{
return $this->dir;
}
public function hasOrderField(): bool
{
return $this->field !== null;

View file

@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface;
final class ShortUrlIdentifier
{
public function __construct(private string $shortCode, private ?string $domain = null)
private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
{
}
@ -54,14 +54,4 @@ final class ShortUrlIdentifier
{
return new self($shortCode, $domain);
}
public function shortCode(): string
{
return $this->shortCode;
}
public function domain(): ?string
{
return $this->domain;
}
}

View file

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Model;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
use function Shlinkio\Shlink\Common\buildDateRange;
@ -15,15 +16,12 @@ final class ShortUrlsParams
{
public const ORDERABLE_FIELDS = ['longUrl', 'shortCode', 'dateCreated', 'title', 'visits'];
public const DEFAULT_ITEMS_PER_PAGE = 10;
public const TAGS_MODE_ANY = 'any';
public const TAGS_MODE_ALL = 'all';
private int $page;
private int $itemsPerPage;
private ?string $searchTerm;
private array $tags;
/** @var self::TAGS_MODE_ANY|self::TAGS_MODE_ALL */
private string $tagsMode = self::TAGS_MODE_ANY;
private TagsMode $tagsMode = TagsMode::ANY;
private Ordering $orderBy;
private ?DateRange $dateRange;
@ -68,7 +66,16 @@ final class ShortUrlsParams
$this->itemsPerPage = (int) (
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
);
$this->tagsMode = $inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE) ?? self::TAGS_MODE_ANY;
$this->tagsMode = $this->resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE));
}
private function resolveTagsMode(?string $rawTagsMode): TagsMode
{
if ($rawTagsMode === null) {
return TagsMode::ANY;
}
return TagsMode::tryFrom($rawTagsMode) ?? TagsMode::ANY;
}
public function page(): int
@ -101,10 +108,7 @@ final class ShortUrlsParams
return $this->dateRange;
}
/**
* @return self::TAGS_MODE_ANY|self::TAGS_MODE_ALL
*/
public function tagsMode(): string
public function tagsMode(): TagsMode
{
return $this->tagsMode;
}

View file

@ -18,10 +18,10 @@ final class Visitor
public const REMOTE_ADDRESS_MAX_LENGTH = 256;
public const VISITED_URL_MAX_LENGTH = 2048;
private string $userAgent;
private string $referer;
private string $visitedUrl;
private ?string $remoteAddress;
public readonly string $userAgent;
public readonly string $referer;
public readonly string $visitedUrl;
public readonly ?string $remoteAddress;
private bool $potentialBot;
public function __construct(string $userAgent, string $referer, ?string $remoteAddress, string $visitedUrl)
@ -61,26 +61,6 @@ final class Visitor
return new self('cf-facebook', '', null, '');
}
public function getUserAgent(): string
{
return $this->userAgent;
}
public function getReferer(): string
{
return $this->referer;
}
public function getRemoteAddress(): ?string
{
return $this->remoteAddress;
}
public function getVisitedUrl(): string
{
return $this->visitedUrl;
}
public function isPotentialBot(): bool
{
return $this->potentialBot;

View file

@ -10,13 +10,13 @@ use function Shlinkio\Shlink\Core\parseDateRangeFromQuery;
final class VisitsParams extends AbstractInfinitePaginableListParams
{
private DateRange $dateRange;
public readonly DateRange $dateRange;
public function __construct(
?DateRange $dateRange = null,
?int $page = null,
?int $itemsPerPage = null,
private bool $excludeBots = false,
public readonly bool $excludeBots = false,
) {
parent::__construct($page, $itemsPerPage);
$this->dateRange = $dateRange ?? DateRange::emptyInstance();
@ -31,14 +31,4 @@ final class VisitsParams extends AbstractInfinitePaginableListParams
isset($query['excludeBots']),
);
}
public function getDateRange(): DateRange
{
return $this->dateRange;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
}

View file

@ -15,7 +15,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
@ -47,8 +47,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
private function processOrderByForList(QueryBuilder $qb, Ordering $orderBy): array
{
$fieldName = $orderBy->orderField();
$order = $orderBy->orderDirection();
$fieldName = $orderBy->field;
$order = $orderBy->direction;
if ($fieldName === 'visits') {
// FIXME This query is inefficient.
@ -116,8 +116,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
// Filter by tags if provided
if (! empty($tags)) {
$tagsMode = $filtering->tagsMode() ?? ShortUrlsParams::TAGS_MODE_ANY;
$tagsMode === ShortUrlsParams::TAGS_MODE_ANY
$tagsMode = $filtering->tagsMode() ?? TagsMode::ANY;
$tagsMode === TagsMode::ANY
? $qb->join('s.tags', 't')->andWhere($qb->expr()->in('t.name', $tags))
: $this->joinAllTags($qb, $tags);
}
@ -146,8 +146,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
$query = $this->getEntityManager()->createQuery($dql);
$query->setMaxResults(1)
->setParameters([
'shortCode' => $identifier->shortCode(),
'domain' => $identifier->domain(),
'shortCode' => $identifier->shortCode,
'domain' => $identifier->domain,
]);
// Since we ordered by domain, we will have first the URL matching provided domain, followed by the one
@ -198,10 +198,10 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
$qb->from(ShortUrl::class, 's')
->where($qb->expr()->isNotNull('s.shortCode'))
->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
->setParameter('slug', $identifier->shortCode())
->setParameter('slug', $identifier->shortCode)
->setMaxResults(1);
$this->whereDomainIs($qb, $identifier->domain());
$this->whereDomainIs($qb, $identifier->domain);
$this->applySpecification($qb, $spec, 's');

View file

@ -41,8 +41,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
*/
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
{
$orderField = $filtering?->orderBy()?->orderField();
$orderDir = $filtering?->orderBy()?->orderDirection();
$orderField = $filtering?->orderBy?->field;
$orderDir = $filtering?->orderBy?->direction;
$orderMainQuery = contains(['shortUrlsCount', 'visitsCount'], $orderField);
$conn = $this->getEntityManager()->getConnection();
@ -51,16 +51,16 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
if (! $orderMainQuery) {
$subQb->orderBy('t.name', $orderDir ?? 'ASC')
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset() ?? 0);
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset ?? 0);
}
$searchTerm = $filtering?->searchTerm();
$searchTerm = $filtering?->searchTerm;
if ($searchTerm !== null) {
$subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%')));
}
$apiKey = $filtering?->apiKey();
$apiKey = $filtering?->apiKey;
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't');
// A native query builder needs to be used here, because DQL and ORM query builders do not support
@ -81,14 +81,13 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
->groupBy('t.id_0', 't.name_1');
// Apply API key role conditions to the native query too, as they will affect the amounts on the aggregates
$apiKey?->mapRoles(static fn (string $roleName, array $meta) => match ($roleName) {
$apiKey?->mapRoles(static fn (Role $role, array $meta) => match ($role) {
Role::DOMAIN_SPECIFIC => $nativeQb->andWhere(
$nativeQb->expr()->eq('s.domain_id', $conn->quote(Role::domainIdFromMeta($meta))),
),
Role::AUTHORED_SHORT_URLS => $nativeQb->andWhere(
$nativeQb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
),
default => $nativeQb,
});
if ($orderMainQuery) {
@ -97,8 +96,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$orderField === 'shortUrlsCount' ? 'short_urls_count' : 'visits_count',
$orderDir ?? 'ASC',
)
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset() ?? 0);
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset ?? 0);
}
// Add ordering by tag name, as a fallback in case of same amount, or as default ordering

View file

@ -86,7 +86,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByShortCodeQueryBuilder($identifier, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsCountFiltering $filtering): int
@ -103,7 +103,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
): QueryBuilder {
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
$shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey()?->spec())?->getId() ?? '-1';
$shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey?->spec())?->getId() ?? '-1';
// Parameters in this query need to be part of the query itself, as we need to use it as sub-query later
// Since they are not provided by the caller, it's reasonably safe
@ -111,12 +111,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb->from(Visit::class, 'v')
->where($qb->expr()->eq('v.shortUrl', $shortUrlId));
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
// Apply date range filtering
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applyDatesInline($qb, $filtering->dateRange);
return $qb;
}
@ -124,7 +124,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByTag(string $tag, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByTagQueryBuilder($tag, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int
@ -144,12 +144,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
->join('s.tags', 't')
->where($qb->expr()->eq('t.name', $this->getEntityManager()->getConnection()->quote($tag)));
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
$this->applyDatesInline($qb, $filtering->dateRange);
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v');
return $qb;
}
@ -160,7 +160,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int
@ -185,12 +185,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
->where($qb->expr()->eq('d.authority', $this->getEntityManager()->getConnection()->quote($domain)));
}
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
$this->applyDatesInline($qb, $filtering->dateRange);
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v');
return $qb;
}
@ -199,7 +199,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
{
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countOrphanVisits(VisitsCountFiltering $filtering): int
@ -215,9 +215,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec());
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int
@ -232,11 +232,11 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v');
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applyDatesInline($qb, $filtering->dateRange);
return $qb;
}

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
// phpcs:disable
// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
enum TagsMode: string
{
case ANY = 'any';
case ALL = 'all';
}

View file

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Persistence;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlsCountFiltering
@ -13,7 +14,7 @@ class ShortUrlsCountFiltering
public function __construct(
private ?string $searchTerm = null,
private array $tags = [],
private ?string $tagsMode = null,
private ?TagsMode $tagsMode = null,
private ?DateRange $dateRange = null,
private ?ApiKey $apiKey = null,
) {
@ -34,7 +35,7 @@ class ShortUrlsCountFiltering
return $this->tags;
}
public function tagsMode(): ?string
public function tagsMode(): ?TagsMode
{
return $this->tagsMode;
}

View file

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Persistence;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ShortUrlsListFiltering extends ShortUrlsCountFiltering
@ -17,7 +18,7 @@ class ShortUrlsListFiltering extends ShortUrlsCountFiltering
private Ordering $orderBy,
?string $searchTerm = null,
array $tags = [],
?string $tagsMode = null,
?TagsMode $tagsMode = null,
?DateRange $dateRange = null,
?ApiKey $apiKey = null,
) {

View file

@ -8,23 +8,11 @@ use JsonSerializable;
final class TagInfo implements JsonSerializable
{
public function __construct(private string $tag, private int $shortUrlsCount, private int $visitsCount)
{
}
public function tag(): string
{
return $this->tag;
}
public function shortUrlsCount(): int
{
return $this->shortUrlsCount;
}
public function visitsCount(): int
{
return $this->visitsCount;
public function __construct(
public readonly string $tag,
public readonly int $shortUrlsCount,
public readonly int $visitsCount,
) {
}
public function jsonSerialize(): array

View file

@ -10,7 +10,7 @@ use function sprintf;
final class TagRenaming
{
private function __construct(private string $oldName, private string $newName)
private function __construct(public readonly string $oldName, public readonly string $newName)
{
}
@ -31,16 +31,6 @@ final class TagRenaming
return self::fromNames($payload['oldName'], $payload['newName']);
}
public function oldName(): string
{
return $this->oldName;
}
public function newName(): string
{
return $this->newName;
}
public function nameChanged(): bool
{
return $this->oldName !== $this->newName;

View file

@ -10,41 +10,16 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class TagsListFiltering
{
public function __construct(
private ?int $limit = null,
private ?int $offset = null,
private ?string $searchTerm = null,
private ?Ordering $orderBy = null,
private ?ApiKey $apiKey = null,
public readonly ?int $limit = null,
public readonly ?int $offset = null,
public readonly ?string $searchTerm = null,
public readonly ?Ordering $orderBy = null,
public readonly ?ApiKey $apiKey = null,
) {
}
public static function fromRangeAndParams(int $limit, int $offset, TagsParams $params, ?ApiKey $apiKey): self
{
return new self($limit, $offset, $params->searchTerm(), $params->orderBy(), $apiKey);
}
public function limit(): ?int
{
return $this->limit;
}
public function offset(): ?int
{
return $this->offset;
}
public function searchTerm(): ?string
{
return $this->searchTerm;
}
public function orderBy(): ?Ordering
{
return $this->orderBy;
}
public function apiKey(): ?ApiKey
{
return $this->apiKey;
return new self($limit, $offset, $params->searchTerm, $params->orderBy, $apiKey);
}
}

View file

@ -12,9 +12,9 @@ use function Shlinkio\Shlink\Common\parseOrderBy;
final class TagsParams extends AbstractInfinitePaginableListParams
{
private function __construct(
private ?string $searchTerm,
private Ordering $orderBy,
private bool $withStats,
public readonly ?string $searchTerm,
public readonly Ordering $orderBy,
public readonly bool $withStats,
?int $page,
?int $itemsPerPage,
) {
@ -31,19 +31,4 @@ final class TagsParams extends AbstractInfinitePaginableListParams
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
);
}
public function searchTerm(): ?string
{
return $this->searchTerm;
}
public function orderBy(): Ordering
{
return $this->orderBy;
}
public function withStats(): bool
{
return $this->withStats;
}
}

View file

@ -30,7 +30,7 @@ abstract class AbstractTagsPaginatorAdapter implements AdapterInterface
new WithApiKeySpecsEnsuringJoin($this->apiKey),
];
$searchTerm = $this->params->searchTerm();
$searchTerm = $this->params->searchTerm;
if ($searchTerm !== null) {
$conditions[] = Spec::like('name', $searchTerm);
}

View file

@ -15,13 +15,13 @@ class TagsPaginatorAdapter extends AbstractTagsPaginatorAdapter
new WithApiKeySpecsEnsuringJoin($this->apiKey),
Spec::orderBy(
'name', // Ordering by other fields makes no sense here
$this->params->orderBy()->orderDirection(),
$this->params->orderBy->direction,
),
Spec::limit($length),
Spec::offset($offset),
];
$searchTerm = $this->params->searchTerm();
$searchTerm = $this->params->searchTerm;
if ($searchTerm !== null) {
$conditions[] = Spec::like('name', $searchTerm);
}

View file

@ -49,8 +49,8 @@ class TagService implements TagServiceInterface
private function createPaginator(AdapterInterface $adapter, TagsParams $params): Paginator
{
return (new Paginator($adapter))
->setMaxPerPage($params->getItemsPerPage())
->setCurrentPage($params->getPage());
->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
}
/**
@ -83,17 +83,17 @@ class TagService implements TagServiceInterface
$repo = $this->em->getRepository(Tag::class);
/** @var Tag|null $tag */
$tag = $repo->findOneBy(['name' => $renaming->oldName()]);
$tag = $repo->findOneBy(['name' => $renaming->oldName]);
if ($tag === null) {
throw TagNotFoundException::fromTag($renaming->oldName());
throw TagNotFoundException::fromTag($renaming->oldName);
}
$newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName()]) > 0;
$newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName]) > 0;
if ($newNameExists) {
throw TagConflictException::forExistingTag($renaming);
}
$tag->rename($renaming->newName());
$tag->rename($renaming->newName);
$this->em->flush();
return $tag;

View file

@ -9,6 +9,7 @@ use Laminas\Validator\InArray;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
class ShortUrlsParamsInputFilter extends InputFilter
{
@ -43,7 +44,7 @@ class ShortUrlsParamsInputFilter extends InputFilter
$tagsMode = $this->createInput(self::TAGS_MODE, false);
$tagsMode->getValidatorChain()->attach(new InArray([
'haystack' => [ShortUrlsParams::TAGS_MODE_ALL, ShortUrlsParams::TAGS_MODE_ANY],
'haystack' => [TagsMode::ALL->value, TagsMode::ANY->value],
'strict' => InArray::COMPARE_STRICT,
]));
$this->add($tagsMode);

View file

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
// phpcs:disable
// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474
namespace Shlinkio\Shlink\Core\Visit\Model;
enum VisitType: string
{
case VALID_SHORT_URL = 'valid_short_url';
case IMPORTED = 'imported';
case INVALID_SHORT_URL = 'invalid_short_url';
case BASE_URL = 'base_url';
case REGULAR_404 = 'regular_404';
}

View file

@ -26,8 +26,8 @@ class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->visitRepository->countVisitsByDomain(
$this->domain,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);
@ -38,8 +38,8 @@ class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->visitRepository->findVisitsByDomain(
$this->domain,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,

View file

@ -23,8 +23,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda
protected function doCount(): int
{
return $this->repo->countNonOrphanVisits(new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
));
}
@ -32,8 +32,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findNonOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,

View file

@ -19,16 +19,16 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
protected function doCount(): int
{
return $this->repo->countOrphanVisits(new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
));
}
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
null,
$length,
$offset,

View file

@ -27,8 +27,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap
return $this->visitRepository->findVisitsByShortCode(
$this->identifier,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,
@ -41,8 +41,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap
return $this->visitRepository->countVisitsByShortCode(
$this->identifier,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);

View file

@ -26,8 +26,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
return $this->visitRepository->findVisitsByTag(
$this->tag,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,
@ -40,8 +40,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
return $this->visitRepository->countVisitsByTag(
$this->tag,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);

View file

@ -10,9 +10,9 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsCountFiltering
{
public function __construct(
private ?DateRange $dateRange = null,
private bool $excludeBots = false,
private ?ApiKey $apiKey = null,
public readonly ?DateRange $dateRange = null,
public readonly bool $excludeBots = false,
public readonly ?ApiKey $apiKey = null,
) {
}
@ -20,19 +20,4 @@ class VisitsCountFiltering
{
return new self(null, false, $apiKey);
}
public function dateRange(): ?DateRange
{
return $this->dateRange;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
public function apiKey(): ?ApiKey
{
return $this->apiKey;
}
}

View file

@ -13,19 +13,9 @@ final class VisitsListFiltering extends VisitsCountFiltering
?DateRange $dateRange = null,
bool $excludeBots = false,
?ApiKey $apiKey = null,
private ?int $limit = null,
private ?int $offset = null,
public readonly ?int $limit = null,
public readonly ?int $offset = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey);
}
public function limit(): ?int
{
return $this->limit;
}
public function offset(): ?int
{
return $this->offset;
}
}

View file

@ -22,14 +22,14 @@ class CountOfNonOrphanVisits extends BaseSpecification
{
$conditions = [
Spec::isNotNull('shortUrl'),
new InDateRange($this->filtering->dateRange()),
new InDateRange($this->filtering->dateRange),
];
if ($this->filtering->excludeBots()) {
if ($this->filtering->excludeBots) {
$conditions[] = Spec::eq('potentialBot', false);
}
$apiKey = $this->filtering->apiKey();
$apiKey = $this->filtering->apiKey;
if ($apiKey !== null) {
$conditions[] = new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl');
}

View file

@ -21,10 +21,10 @@ class CountOfOrphanVisits extends BaseSpecification
{
$conditions = [
Spec::isNull('shortUrl'),
new InDateRange($this->filtering->dateRange()),
new InDateRange($this->filtering->dateRange),
];
if ($this->filtering->excludeBots()) {
if ($this->filtering->excludeBots) {
$conditions[] = Spec::eq('potentialBot', false);
}

View file

@ -17,7 +17,7 @@ class OrphanVisitDataTransformer implements DataTransformerInterface
{
$serializedVisit = $visit->jsonSerialize();
$serializedVisit['visitedUrl'] = $visit->visitedUrl();
$serializedVisit['type'] = $visit->type();
$serializedVisit['type'] = $visit->type()->value;
return $serializedVisit;
}

View file

@ -129,8 +129,8 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator
{
$paginator = new Paginator($adapter);
$paginator->setMaxPerPage($params->getItemsPerPage())
->setCurrentPage($params->getPage());
$paginator->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
return $paginator;
}

View file

@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface
$this->em->persist($visit);
$this->em->flush();
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->getRemoteAddress()));
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress));
}
}

View file

@ -14,9 +14,9 @@ use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\Ordering;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsCountFiltering;
use Shlinkio\Shlink\Core\ShortUrl\Persistence\ShortUrlsListFiltering;
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
@ -227,7 +227,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['foo', 'bar'],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY,
)));
self::assertCount(1, $this->repo->findList(new ShortUrlsListFiltering(
null,
@ -235,15 +235,11 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['foo', 'bar'],
ShortUrlsParams::TAGS_MODE_ALL,
TagsMode::ALL,
)));
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'])));
self::assertEquals(5, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ANY),
));
self::assertEquals(1, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['foo', 'bar'], ShortUrlsParams::TAGS_MODE_ALL),
));
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ANY)));
self::assertEquals(1, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar'], TagsMode::ALL)));
self::assertCount(4, $this->repo->findList(
new ShortUrlsListFiltering(null, null, Ordering::emptyInstance(), null, ['bar', 'baz']),
@ -254,7 +250,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY,
)));
self::assertCount(2, $this->repo->findList(new ShortUrlsListFiltering(
null,
@ -262,14 +258,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ALL,
TagsMode::ALL,
)));
self::assertEquals(4, $this->repo->countList(new ShortUrlsCountFiltering(null, ['bar', 'baz'])));
self::assertEquals(4, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY),
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ANY),
));
self::assertEquals(2, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL),
new ShortUrlsCountFiltering(null, ['bar', 'baz'], TagsMode::ALL),
));
self::assertCount(5, $this->repo->findList(
@ -281,7 +277,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['foo', 'bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ANY,
TagsMode::ANY,
)));
self::assertCount(0, $this->repo->findList(new ShortUrlsListFiltering(
null,
@ -289,14 +285,14 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
Ordering::emptyInstance(),
null,
['foo', 'bar', 'baz'],
ShortUrlsParams::TAGS_MODE_ALL,
TagsMode::ALL,
)));
self::assertEquals(5, $this->repo->countList(new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'])));
self::assertEquals(5, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ANY),
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ANY),
));
self::assertEquals(0, $this->repo->countList(
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], ShortUrlsParams::TAGS_MODE_ALL),
new ShortUrlsCountFiltering(null, ['foo', 'bar', 'baz'], TagsMode::ALL),
));
}

View file

@ -64,7 +64,7 @@ class TagRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist(new Tag($name));
}
$apiKey = $filtering?->apiKey();
$apiKey = $filtering?->apiKey;
if ($apiKey !== null) {
$this->getEntityManager()->persist($apiKey);
}
@ -101,9 +101,9 @@ class TagRepositoryTest extends DatabaseTestCase
self::assertCount(count($expectedList), $result);
foreach ($expectedList as $index => [$tag, $shortUrlsCount, $visitsCount]) {
self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount());
self::assertEquals($visitsCount, $result[$index]->visitsCount());
self::assertEquals($tag, $result[$index]->tag());
self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount);
self::assertEquals($visitsCount, $result[$index]->visitsCount);
self::assertEquals($tag, $result[$index]->tag);
}
}

View file

@ -37,9 +37,10 @@ class PixelActionTest extends TestCase
public function imageIsReturned(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn(
ShortUrl::withLongUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce();
$this->urlResolver->resolveEnabledShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''),
)->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar'))
->shouldBeCalledOnce();
$this->requestTracker->trackIfApplicable(Argument::cetera())->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);

View file

@ -59,7 +59,7 @@ class QrCodeActionTest extends TestCase
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -74,7 +74,7 @@ class QrCodeActionTest extends TestCase
public function aCorrectRequestReturnsTheQrCodeResponse(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willReturn(ShortUrl::createEmpty())
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -100,7 +100,7 @@ class QrCodeActionTest extends TestCase
): void {
$this->options->setFromArray(['format' => $defaultFormat]);
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -134,7 +134,7 @@ class QrCodeActionTest extends TestCase
): void {
$this->options->setFromArray($defaults);
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -214,7 +214,7 @@ class QrCodeActionTest extends TestCase
->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize])
->withAttribute('shortCode', $code);
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::withLongUrl('https://shlink.io'),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);

View file

@ -54,7 +54,7 @@ class RedirectActionTest extends TestCase
$shortCode = 'abc123';
$shortUrl = ShortUrl::withLongUrl(self::LONG_URL);
$shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl(
new ShortUrlIdentifier($shortCode, ''),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''),
)->willReturn($shortUrl);
$track = $this->requestTracker->trackIfApplicable(Argument::cetera())->will(function (): void {
});
@ -74,7 +74,7 @@ class RedirectActionTest extends TestCase
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$this->requestTracker->trackIfApplicable(Argument::cetera())->shouldNotBeCalled();

View file

@ -6,7 +6,6 @@ namespace ShlinkioTest\Shlink\Core\Config;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use function putenv;
@ -14,92 +13,14 @@ class EnvVarsTest extends TestCase
{
protected function setUp(): void
{
putenv(EnvVars::BASE_PATH . '=the_base_path');
putenv(EnvVars::DB_NAME . '=shlink');
putenv(EnvVars::BASE_PATH->value . '=the_base_path');
putenv(EnvVars::DB_NAME->value . '=shlink');
}
protected function tearDown(): void
{
putenv(EnvVars::BASE_PATH . '=');
putenv(EnvVars::DB_NAME . '=');
}
/** @test */
public function casesReturnsTheSameListEveryTime(): void
{
$list = EnvVars::cases();
self::assertSame($list, EnvVars::cases());
self::assertSame([
EnvVars::DELETE_SHORT_URL_THRESHOLD,
EnvVars::DB_DRIVER,
EnvVars::DB_NAME,
EnvVars::DB_USER,
EnvVars::DB_PASSWORD,
EnvVars::DB_HOST,
EnvVars::DB_UNIX_SOCKET,
EnvVars::DB_PORT,
EnvVars::GEOLITE_LICENSE_KEY,
EnvVars::REDIS_SERVERS,
EnvVars::REDIS_SENTINEL_SERVICE,
EnvVars::MERCURE_PUBLIC_HUB_URL,
EnvVars::MERCURE_INTERNAL_HUB_URL,
EnvVars::MERCURE_JWT_SECRET,
EnvVars::DEFAULT_QR_CODE_SIZE,
EnvVars::DEFAULT_QR_CODE_MARGIN,
EnvVars::DEFAULT_QR_CODE_FORMAT,
EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION,
EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
EnvVars::RABBITMQ_ENABLED,
EnvVars::RABBITMQ_HOST,
EnvVars::RABBITMQ_PORT,
EnvVars::RABBITMQ_USER,
EnvVars::RABBITMQ_PASSWORD,
EnvVars::RABBITMQ_VHOST,
EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT,
EnvVars::DEFAULT_REGULAR_404_REDIRECT,
EnvVars::DEFAULT_BASE_URL_REDIRECT,
EnvVars::REDIRECT_STATUS_CODE,
EnvVars::REDIRECT_CACHE_LIFETIME,
EnvVars::BASE_PATH,
EnvVars::PORT,
EnvVars::TASK_WORKER_NUM,
EnvVars::WEB_WORKER_NUM,
EnvVars::ANONYMIZE_REMOTE_ADDR,
EnvVars::TRACK_ORPHAN_VISITS,
EnvVars::DISABLE_TRACK_PARAM,
EnvVars::DISABLE_TRACKING,
EnvVars::DISABLE_IP_TRACKING,
EnvVars::DISABLE_REFERRER_TRACKING,
EnvVars::DISABLE_UA_TRACKING,
EnvVars::DISABLE_TRACKING_FROM,
EnvVars::DEFAULT_SHORT_CODES_LENGTH,
EnvVars::IS_HTTPS_ENABLED,
EnvVars::DEFAULT_DOMAIN,
EnvVars::AUTO_RESOLVE_TITLES,
EnvVars::REDIRECT_APPEND_EXTRA_PATH,
EnvVars::TIMEZONE,
EnvVars::VISITS_WEBHOOKS,
EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS,
], $list);
}
/**
* @test
* @dataProvider provideInvalidEnvVars
*/
public function exceptionIsThrownWhenTryingToLoadInvalidEnvVar(string $envVar): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid env var: "' . $envVar . '"');
EnvVars::{$envVar}();
}
public function provideInvalidEnvVars(): iterable
{
yield 'foo' => ['foo'];
yield 'bar' => ['bar'];
yield 'invalid' => ['invalid'];
putenv(EnvVars::BASE_PATH->value . '=');
putenv(EnvVars::DB_NAME->value . '=');
}
/**
@ -113,10 +34,10 @@ class EnvVarsTest extends TestCase
public function provideExistingEnvVars(): iterable
{
yield 'DB_NAME' => [EnvVars::DB_NAME(), true];
yield 'BASE_PATH' => [EnvVars::BASE_PATH(), true];
yield 'DB_DRIVER' => [EnvVars::DB_DRIVER(), false];
yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT(), false];
yield 'DB_NAME' => [EnvVars::DB_NAME, true];
yield 'BASE_PATH' => [EnvVars::BASE_PATH, true];
yield 'DB_DRIVER' => [EnvVars::DB_DRIVER, false];
yield 'DEFAULT_REGULAR_404_REDIRECT' => [EnvVars::DEFAULT_REGULAR_404_REDIRECT, false];
}
/**
@ -130,11 +51,11 @@ class EnvVarsTest extends TestCase
public function provideEnvVarsValues(): iterable
{
yield 'DB_NAME without default' => [EnvVars::DB_NAME(), 'shlink', null];
yield 'DB_NAME with default' => [EnvVars::DB_NAME(), 'shlink', 'foobar'];
yield 'BASE_PATH without default' => [EnvVars::BASE_PATH(), 'the_base_path', null];
yield 'BASE_PATH with default' => [EnvVars::BASE_PATH(), 'the_base_path', 'foobar'];
yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER(), null, null];
yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER(), 'foobar', 'foobar'];
yield 'DB_NAME without default' => [EnvVars::DB_NAME, 'shlink', null];
yield 'DB_NAME with default' => [EnvVars::DB_NAME, 'shlink', 'foobar'];
yield 'BASE_PATH without default' => [EnvVars::BASE_PATH, 'the_base_path', null];
yield 'BASE_PATH with default' => [EnvVars::BASE_PATH, 'the_base_path', 'foobar'];
yield 'DB_DRIVER without default' => [EnvVars::DB_DRIVER, null, null];
yield 'DB_DRIVER with default' => [EnvVars::DB_DRIVER, 'foobar', 'foobar'];
}
}

View file

@ -17,6 +17,7 @@ use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
use Shlinkio\Shlink\Core\EventDispatcher\NotifyVisitToMercure;
use Shlinkio\Shlink\Core\Mercure\MercureUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use Symfony\Component\Mercure\HubInterface;
use Symfony\Component\Mercure\Update;
@ -160,8 +161,8 @@ class NotifyVisitToMercureTest extends TestCase
{
$visitor = Visitor::emptyInstance();
yield Visit::TYPE_REGULAR_404 => [Visit::forRegularNotFound($visitor)];
yield Visit::TYPE_INVALID_SHORT_URL => [Visit::forInvalidShortUrl($visitor)];
yield Visit::TYPE_BASE_URL => [Visit::forBasePath($visitor)];
yield VisitType::REGULAR_404->value => [Visit::forRegularNotFound($visitor)];
yield VisitType::INVALID_SHORT_URL->value => [Visit::forInvalidShortUrl($visitor)];
yield VisitType::BASE_URL->value => [Visit::forBasePath($visitor)];
}
}

Some files were not shown because too many files have changed in this diff Show more