From 13555366e3f16a06afee16921b4c4edaf9322598 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 18:54:40 +0100 Subject: [PATCH 1/6] Short code lengths can now be customized --- config/autoload/url-shortener.global.php | 3 +++ module/Core/functions/functions.php | 4 ++- module/Core/src/Entity/ShortUrl.php | 6 +++-- module/Core/src/Model/ShortUrlMeta.php | 21 +++++++++++++-- .../Validation/ShortUrlMetaInputFilter.php | 17 +++++++++--- module/Core/test/Entity/ShortUrlTest.php | 26 +++++++++++++++++++ 6 files changed, 68 insertions(+), 9 deletions(-) diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php index 5cf4f86f..165e0258 100644 --- a/config/autoload/url-shortener.global.php +++ b/config/autoload/url-shortener.global.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; + return [ 'url_shortener' => [ @@ -11,6 +13,7 @@ return [ ], 'validate_url' => false, 'visits_webhooks' => [], + 'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH, ], ]; diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 7ab5ebbb..61d0be1e 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -10,7 +10,9 @@ use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; -function generateRandomShortCode(int $length = 5): string +const DEFAULT_SHORT_CODES_LENGTH = 5; + +function generateRandomShortCode(int $length): string { static $shortIdFactory; if ($shortIdFactory === null) { diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index 98d6a146..4af8844b 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -34,6 +34,7 @@ class ShortUrl extends AbstractEntity private ?int $maxVisits = null; private ?Domain $domain; private bool $customSlugWasProvided; + private int $shortCodeLength; public function __construct( string $longUrl, @@ -50,7 +51,8 @@ class ShortUrl extends AbstractEntity $this->validUntil = $meta->getValidUntil(); $this->maxVisits = $meta->getMaxVisits(); $this->customSlugWasProvided = $meta->hasCustomSlug(); - $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode(); + $this->shortCodeLength = $meta->getShortCodeLength(); + $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($this->shortCodeLength); $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain()); } @@ -119,7 +121,7 @@ class ShortUrl extends AbstractEntity throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted(); } - $this->shortCode = generateRandomShortCode(); + $this->shortCode = generateRandomShortCode($this->shortCodeLength); return $this; } diff --git a/module/Core/src/Model/ShortUrlMeta.php b/module/Core/src/Model/ShortUrlMeta.php index 27c8e624..3bba5c98 100644 --- a/module/Core/src/Model/ShortUrlMeta.php +++ b/module/Core/src/Model/ShortUrlMeta.php @@ -11,6 +11,8 @@ use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter; use function array_key_exists; use function Shlinkio\Shlink\Core\parseDateField; +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; + final class ShortUrlMeta { private bool $validSincePropWasProvided = false; @@ -22,6 +24,7 @@ final class ShortUrlMeta private ?int $maxVisits = null; private ?bool $findIfExists = null; private ?string $domain = null; + private int $shortCodeLength = 5; // Force named constructors private function __construct() @@ -58,11 +61,20 @@ final class ShortUrlMeta $this->validUntil = parseDateField($inputFilter->getValue(ShortUrlMetaInputFilter::VALID_UNTIL)); $this->validUntilPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::VALID_UNTIL, $data); $this->customSlug = $inputFilter->getValue(ShortUrlMetaInputFilter::CUSTOM_SLUG); - $maxVisits = $inputFilter->getValue(ShortUrlMetaInputFilter::MAX_VISITS); - $this->maxVisits = $maxVisits !== null ? (int) $maxVisits : null; + $this->maxVisits = $this->getOptionalIntFromInputFilter($inputFilter, ShortUrlMetaInputFilter::MAX_VISITS); $this->maxVisitsPropWasProvided = array_key_exists(ShortUrlMetaInputFilter::MAX_VISITS, $data); $this->findIfExists = $inputFilter->getValue(ShortUrlMetaInputFilter::FIND_IF_EXISTS); $this->domain = $inputFilter->getValue(ShortUrlMetaInputFilter::DOMAIN); + $this->shortCodeLength = $this->getOptionalIntFromInputFilter( + $inputFilter, + ShortUrlMetaInputFilter::SHORT_CODE_LENGTH, + ) ?? DEFAULT_SHORT_CODES_LENGTH; + } + + private function getOptionalIntFromInputFilter(ShortUrlMetaInputFilter $inputFilter, string $fieldName): ?int + { + $value = $inputFilter->getValue($fieldName); + return $value !== null ? (int) $value : null; } public function getValidSince(): ?Chronos @@ -119,4 +131,9 @@ final class ShortUrlMeta { return $this->domain; } + + public function getShortCodeLength(): int + { + return $this->shortCodeLength; + } } diff --git a/module/Core/src/Validation/ShortUrlMetaInputFilter.php b/module/Core/src/Validation/ShortUrlMetaInputFilter.php index 187ec66f..0663a760 100644 --- a/module/Core/src/Validation/ShortUrlMetaInputFilter.php +++ b/module/Core/src/Validation/ShortUrlMetaInputFilter.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Validation; use DateTime; +use Laminas\InputFilter\Input; use Laminas\InputFilter\InputFilter; use Laminas\Validator; use Shlinkio\Shlink\Common\Validation; @@ -19,6 +20,7 @@ class ShortUrlMetaInputFilter extends InputFilter public const MAX_VISITS = 'maxVisits'; public const FIND_IF_EXISTS = 'findIfExists'; public const DOMAIN = 'domain'; + public const SHORT_CODE_LENGTH = 'shortCodeLength'; public function __construct(array $data) { @@ -40,10 +42,8 @@ class ShortUrlMetaInputFilter extends InputFilter $customSlug->getFilterChain()->attach(new Validation\SluggerFilter()); $this->add($customSlug); - $maxVisits = $this->createInput(self::MAX_VISITS, false); - $maxVisits->getValidatorChain()->attach(new Validator\Digits()) - ->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true])); - $this->add($maxVisits); + $this->add($this->createPositiveNumberInput(self::MAX_VISITS)); + $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH)); $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); @@ -51,4 +51,13 @@ class ShortUrlMetaInputFilter extends InputFilter $domain->getValidatorChain()->attach(new Validation\HostAndPortValidator()); $this->add($domain); } + + private function createPositiveNumberInput(string $name): Input + { + $input = $this->createInput($name, false); + $input->getValidatorChain()->attach(new Validator\Digits()) + ->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true])); + + return $input; + } } diff --git a/module/Core/test/Entity/ShortUrlTest.php b/module/Core/test/Entity/ShortUrlTest.php index 9aba83fa..21c869aa 100644 --- a/module/Core/test/Entity/ShortUrlTest.php +++ b/module/Core/test/Entity/ShortUrlTest.php @@ -8,6 +8,13 @@ use PHPUnit\Framework\TestCase; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; +use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter; + +use function Functional\map; +use function range; +use function strlen; + +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; class ShortUrlTest extends TestCase { @@ -48,4 +55,23 @@ class ShortUrlTest extends TestCase $this->assertNotEquals($firstShortCode, $secondShortCode); } + + /** + * @test + * @dataProvider provideLengths + */ + public function shortCodesHaveExpectedLength(?int $length, int $expectedLength): void + { + $shortUrl = new ShortUrl('', ShortUrlMeta::fromRawData( + [ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $length], + )); + + $this->assertEquals($expectedLength, strlen($shortUrl->getShortCode())); + } + + public function provideLengths(): iterable + { + yield [null, DEFAULT_SHORT_CODES_LENGTH]; + yield from map(range(1, 10), fn (int $value) => [$value, $value]); + } } From 9372d1739aacf801698ae88cc8056a8e6176aee1 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 18:57:24 +0100 Subject: [PATCH 2/6] Enforced short URLs length to be 4 at least --- module/Core/src/Validation/ShortUrlMetaInputFilter.php | 6 +++--- module/Core/test/Entity/ShortUrlTest.php | 2 +- module/Core/test/Model/ShortUrlMetaTest.php | 3 +++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/module/Core/src/Validation/ShortUrlMetaInputFilter.php b/module/Core/src/Validation/ShortUrlMetaInputFilter.php index 0663a760..9e8b1c7a 100644 --- a/module/Core/src/Validation/ShortUrlMetaInputFilter.php +++ b/module/Core/src/Validation/ShortUrlMetaInputFilter.php @@ -43,7 +43,7 @@ class ShortUrlMetaInputFilter extends InputFilter $this->add($customSlug); $this->add($this->createPositiveNumberInput(self::MAX_VISITS)); - $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH)); + $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH, 4)); $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); @@ -52,11 +52,11 @@ class ShortUrlMetaInputFilter extends InputFilter $this->add($domain); } - private function createPositiveNumberInput(string $name): Input + private function createPositiveNumberInput(string $name, int $min = 1): Input { $input = $this->createInput($name, false); $input->getValidatorChain()->attach(new Validator\Digits()) - ->attach(new Validator\GreaterThan(['min' => 1, 'inclusive' => true])); + ->attach(new Validator\GreaterThan(['min' => $min, 'inclusive' => true])); return $input; } diff --git a/module/Core/test/Entity/ShortUrlTest.php b/module/Core/test/Entity/ShortUrlTest.php index 21c869aa..e410dedb 100644 --- a/module/Core/test/Entity/ShortUrlTest.php +++ b/module/Core/test/Entity/ShortUrlTest.php @@ -72,6 +72,6 @@ class ShortUrlTest extends TestCase public function provideLengths(): iterable { yield [null, DEFAULT_SHORT_CODES_LENGTH]; - yield from map(range(1, 10), fn (int $value) => [$value, $value]); + yield from map(range(4, 10), fn (int $value) => [$value, $value]); } } diff --git a/module/Core/test/Model/ShortUrlMetaTest.php b/module/Core/test/Model/ShortUrlMetaTest.php index 13c5ae14..fed2c662 100644 --- a/module/Core/test/Model/ShortUrlMetaTest.php +++ b/module/Core/test/Model/ShortUrlMetaTest.php @@ -44,6 +44,9 @@ class ShortUrlMetaTest extends TestCase ShortUrlMetaInputFilter::VALID_UNTIL => 500, ShortUrlMetaInputFilter::DOMAIN => 4, ]]; + yield [[ + ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => 3, + ]]; } /** @test */ From 343ee04acbb17ffc38c94953ef0799a9eb360186 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 19:21:34 +0100 Subject: [PATCH 3/6] Created middleware which injects default short code length from config when a value was not explicitly provided --- docs/swagger/paths/v1_short-urls.json | 4 ++ module/Rest/config/dependencies.config.php | 4 ++ module/Rest/config/routes.config.php | 6 ++- .../DefaultShortCodesLengthMiddleware.php | 31 +++++++++++ .../DefaultShortCodesLengthMiddlewareTest.php | 54 +++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 module/Rest/src/Middleware/ShortUrl/DefaultShortCodesLengthMiddleware.php create mode 100644 module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json index be274ab6..ee8a6060 100644 --- a/docs/swagger/paths/v1_short-urls.json +++ b/docs/swagger/paths/v1_short-urls.json @@ -243,6 +243,10 @@ "domain": { "description": "The domain to which the short URL will be attached", "type": "string" + }, + "shortCodeLength": { + "description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided", + "type": "number" } } } diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index dc4c0e3b..b24ec1ee 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -38,6 +38,7 @@ return [ Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class, Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class => ConfigAbstractFactory::class, + Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => ConfigAbstractFactory::class, ], ], @@ -75,6 +76,9 @@ return [ Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class], Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class => ['config.url_shortener.domain.hostname'], + Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class => [ + 'config.url_shortener.default_short_codes_length', + ], ], ]; diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 61abb1b7..b104d81b 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -13,7 +13,11 @@ return [ Action\HealthAction::getRouteDef(), // Short codes - Action\ShortUrl\CreateShortUrlAction::getRouteDef([$contentNegotiationMiddleware, $dropDomainMiddleware]), + Action\ShortUrl\CreateShortUrlAction::getRouteDef([ + $contentNegotiationMiddleware, + $dropDomainMiddleware, + Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class, + ]), Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([$contentNegotiationMiddleware]), Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]), Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]), diff --git a/module/Rest/src/Middleware/ShortUrl/DefaultShortCodesLengthMiddleware.php b/module/Rest/src/Middleware/ShortUrl/DefaultShortCodesLengthMiddleware.php new file mode 100644 index 00000000..bcad748e --- /dev/null +++ b/module/Rest/src/Middleware/ShortUrl/DefaultShortCodesLengthMiddleware.php @@ -0,0 +1,31 @@ +defaultShortCodesLength = $defaultShortCodesLength; + } + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $body = $request->getParsedBody(); + if (! isset($body[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH])) { + $body[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH] = $this->defaultShortCodesLength; + } + + return $handler->handle($request->withParsedBody($body)); + } +} diff --git a/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php b/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php new file mode 100644 index 00000000..38d875d9 --- /dev/null +++ b/module/Rest/test/Middleware/ShortUrl/DefaultShortCodesLengthMiddlewareTest.php @@ -0,0 +1,54 @@ +handler = $this->prophesize(RequestHandlerInterface::class); + $this->middleware = new DefaultShortCodesLengthMiddleware(8); + } + + /** + * @test + * @dataProvider provideBodies + */ + public function defaultValueIsInjectedInBodyWhenNotProvided(array $body, int $expectedLength): void + { + $request = ServerRequestFactory::fromGlobals()->withParsedBody($body); + $handle = $this->handler->handle(Argument::that(function (ServerRequestInterface $req) use ($expectedLength) { + $parsedBody = $req->getParsedBody(); + Assert::assertArrayHasKey(ShortUrlMetaInputFilter::SHORT_CODE_LENGTH, $parsedBody); + Assert::assertEquals($expectedLength, $parsedBody[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH]); + + return $req; + }))->willReturn(new Response()); + + $this->middleware->process($request, $this->handler->reveal()); + + $handle->shouldHaveBeenCalledOnce(); + } + + public function provideBodies(): iterable + { + yield 'value provided' => [[ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => 6], 6]; + yield 'value not provided' => [[], 8]; + } +} From 51e130c7a0d7330c783a0ea4911b9b46156bf3d3 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 19:34:01 +0100 Subject: [PATCH 4/6] Added env var that can be used to define default short codes length on docker image --- docker/README.md | 3 +++ docker/config/shlink_in_docker.local.php | 10 ++++++++++ module/Core/functions/functions.php | 1 + module/Core/src/Config/SimplifiedConfigParser.php | 1 + module/Core/src/Validation/ShortUrlMetaInputFilter.php | 4 +++- module/Core/test/Config/SimplifiedConfigParserTest.php | 2 ++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/docker/README.md b/docker/README.md index 76c47a09..699c1c75 100644 --- a/docker/README.md +++ b/docker/README.md @@ -113,6 +113,7 @@ This is the complete list of supported env vars: * `WEB_WORKER_NUM`: The amount of concurrent http requests this shlink instance will be able to server. Defaults to 16. * `TASK_WORKER_NUM`: The amount of concurrent background tasks this shlink instance will be able to execute. Defaults to 16. * `VISITS_WEBHOOKS`: A comma-separated list of URLs that will receive a `POST` request when a short URL receives a visit. +* `DEFAULT_SHORT_CODES_LENGTH`: The length you want generated short codes to have. It defaults to 5 and has to be at least 4, so any value smaller than that will fall back to 4. * `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel). This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately. @@ -146,6 +147,7 @@ docker run \ -e WEB_WORKER_NUM=64 \ -e TASK_WORKER_NUM=32 \ -e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \ + -e DEFAULT_SHORT_CODES_LENGTH=6 \ shlinkio/shlink:stable ``` @@ -170,6 +172,7 @@ The whole configuration should have this format, but it can be split into multip "base_path": "/my-campaign", "web_worker_num": 64, "task_worker_num": 32, + "default_short_codes_length": 6, "redis_servers": [ "tcp://172.20.0.1:6379", "tcp://172.20.0.2:6379" diff --git a/docker/config/shlink_in_docker.local.php b/docker/config/shlink_in_docker.local.php index 9e10d419..6cf86434 100644 --- a/docker/config/shlink_in_docker.local.php +++ b/docker/config/shlink_in_docker.local.php @@ -11,6 +11,9 @@ use function explode; use function Functional\contains; use function Shlinkio\Shlink\Common\env; +use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH; +use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH; + $helper = new class { private const DB_DRIVERS_MAP = [ 'mysql' => 'pdo_mysql', @@ -70,6 +73,12 @@ $helper = new class { $redisServers = env('REDIS_SERVERS'); return $redisServers === null ? null : ['servers' => $redisServers]; } + + public function getDefaultShortCodesLength(): int + { + $value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH); + return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value; + } }; return [ @@ -96,6 +105,7 @@ return [ ], 'validate_url' => (bool) env('VALIDATE_URLS', false), 'visits_webhooks' => $helper->getVisitsWebhooks(), + 'default_short_codes_length' => $helper->getDefaultShortCodesLength(), ], 'not_found_redirects' => $helper->getNotFoundRedirectsConfig(), diff --git a/module/Core/functions/functions.php b/module/Core/functions/functions.php index 61d0be1e..87399208 100644 --- a/module/Core/functions/functions.php +++ b/module/Core/functions/functions.php @@ -11,6 +11,7 @@ use PUGX\Shortid\Factory as ShortIdFactory; use function sprintf; const DEFAULT_SHORT_CODES_LENGTH = 5; +const MIN_SHORT_CODES_LENGTH = 4; function generateRandomShortCode(int $length): string { diff --git a/module/Core/src/Config/SimplifiedConfigParser.php b/module/Core/src/Config/SimplifiedConfigParser.php index fa7a4acb..a03ccc3e 100644 --- a/module/Core/src/Config/SimplifiedConfigParser.php +++ b/module/Core/src/Config/SimplifiedConfigParser.php @@ -32,6 +32,7 @@ class SimplifiedConfigParser 'web_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'worker_num'], 'task_worker_num' => ['mezzio-swoole', 'swoole-http-server', 'options', 'task_worker_num'], 'visits_webhooks' => ['url_shortener', 'visits_webhooks'], + 'default_short_codes_length' => ['url_shortener', 'default_short_codes_length'], ]; private const SIMPLIFIED_CONFIG_SIDE_EFFECTS = [ 'delete_short_url_threshold' => [ diff --git a/module/Core/src/Validation/ShortUrlMetaInputFilter.php b/module/Core/src/Validation/ShortUrlMetaInputFilter.php index 9e8b1c7a..a71b4cc2 100644 --- a/module/Core/src/Validation/ShortUrlMetaInputFilter.php +++ b/module/Core/src/Validation/ShortUrlMetaInputFilter.php @@ -10,6 +10,8 @@ use Laminas\InputFilter\InputFilter; use Laminas\Validator; use Shlinkio\Shlink\Common\Validation; +use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH; + class ShortUrlMetaInputFilter extends InputFilter { use Validation\InputFactoryTrait; @@ -43,7 +45,7 @@ class ShortUrlMetaInputFilter extends InputFilter $this->add($customSlug); $this->add($this->createPositiveNumberInput(self::MAX_VISITS)); - $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH, 4)); + $this->add($this->createPositiveNumberInput(self::SHORT_CODE_LENGTH, MIN_SHORT_CODES_LENGTH)); $this->add($this->createBooleanInput(self::FIND_IF_EXISTS, false)); diff --git a/module/Core/test/Config/SimplifiedConfigParserTest.php b/module/Core/test/Config/SimplifiedConfigParserTest.php index 1d4f3b8d..7a304ad5 100644 --- a/module/Core/test/Config/SimplifiedConfigParserTest.php +++ b/module/Core/test/Config/SimplifiedConfigParserTest.php @@ -57,6 +57,7 @@ class SimplifiedConfigParserTest extends TestCase 'http://my-api.com/api/v2.3/notify', 'https://third-party.io/foo', ], + 'default_short_codes_length' => 8, ]; $expected = [ 'app_options' => [ @@ -84,6 +85,7 @@ class SimplifiedConfigParserTest extends TestCase 'http://my-api.com/api/v2.3/notify', 'https://third-party.io/foo', ], + 'default_short_codes_length' => 8, ], 'delete_short_urls' => [ From 33a404f051835998a50cfcfbba7393ccd70efe1e Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 20:34:48 +0100 Subject: [PATCH 5/6] Updated CLI command to create short URLs so that it respects configs for short code length --- composer.json | 2 +- config/autoload/installer.global.php | 1 + module/CLI/config/dependencies.config.php | 6 +++++- .../src/Command/ShortUrl/GenerateShortUrlCommand.php | 12 +++++++++++- .../Command/ShortUrl/GenerateShortUrlCommandTest.php | 2 +- 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index cdda9028..52681a0e 100644 --- a/composer.json +++ b/composer.json @@ -49,7 +49,7 @@ "pugx/shortid-php": "^0.5", "shlinkio/shlink-common": "^2.7.0", "shlinkio/shlink-event-dispatcher": "^1.3", - "shlinkio/shlink-installer": "^4.1.0", + "shlinkio/shlink-installer": "^4.2.0", "shlinkio/shlink-ip-geolocation": "^1.3.1", "symfony/console": "^5.0", "symfony/filesystem": "^5.0", diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php index 296c0635..c40d75d1 100644 --- a/config/autoload/installer.global.php +++ b/config/autoload/installer.global.php @@ -30,6 +30,7 @@ return [ Option\TaskWorkerNumConfigOption::class, Option\WebWorkerNumConfigOption::class, Option\RedisServersConfigOption::class, + Option\ShortCodeLengthOption::class, ], 'installation_commands' => [ diff --git a/module/CLI/config/dependencies.config.php b/module/CLI/config/dependencies.config.php index 1f94f5a6..1cc67fd9 100644 --- a/module/CLI/config/dependencies.config.php +++ b/module/CLI/config/dependencies.config.php @@ -54,7 +54,11 @@ return [ ConfigAbstractFactory::class => [ GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, 'Shlinkio\Shlink\LocalLockFactory'], - Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'], + Command\ShortUrl\GenerateShortUrlCommand::class => [ + Service\UrlShortener::class, + 'config.url_shortener.domain', + 'config.url_shortener.default_short_codes_length', + ], Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class], Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'], Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class], diff --git a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php index 28d192b1..7369f1f6 100644 --- a/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php +++ b/module/CLI/src/Command/ShortUrl/GenerateShortUrlCommand.php @@ -30,12 +30,14 @@ class GenerateShortUrlCommand extends Command private UrlShortenerInterface $urlShortener; private array $domainConfig; + private int $defaultShortCodeLength; - public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig) + public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig, int $defaultShortCodeLength) { parent::__construct(); $this->urlShortener = $urlShortener; $this->domainConfig = $domainConfig; + $this->defaultShortCodeLength = $defaultShortCodeLength; } protected function configure(): void @@ -87,6 +89,12 @@ class GenerateShortUrlCommand extends Command 'd', InputOption::VALUE_REQUIRED, 'The domain to which this short URL will be attached.', + ) + ->addOption( + 'shortCodeLength', + 'l', + InputOption::VALUE_REQUIRED, + 'The length for generated short code (it will be ignored if --customSlug was provided).', ); } @@ -117,6 +125,7 @@ class GenerateShortUrlCommand extends Command $tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags')))); $customSlug = $input->getOption('customSlug'); $maxVisits = $input->getOption('maxVisits'); + $shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength; try { $shortUrl = $this->urlShortener->urlToShortCode( @@ -129,6 +138,7 @@ class GenerateShortUrlCommand extends Command ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null, ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'), ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'), + ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength, ]), ); diff --git a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php index df1019b1..bcf00acb 100644 --- a/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php +++ b/module/CLI/test/Command/ShortUrl/GenerateShortUrlCommandTest.php @@ -31,7 +31,7 @@ class GenerateShortUrlCommandTest extends TestCase public function setUp(): void { $this->urlShortener = $this->prophesize(UrlShortener::class); - $command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG); + $command = new GenerateShortUrlCommand($this->urlShortener->reveal(), self::DOMAIN_CONFIG, 5); $app = new Application(); $app->add($command); $this->commandTester = new CommandTester($command); From 1f3e0d1f735b464cb4fd7f4e5390bf0cca6d5446 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 18 Feb 2020 20:35:41 +0100 Subject: [PATCH 6/6] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index edf73367..139a5877 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this #### Added * [#626](https://github.com/shlinkio/shlink/issues/626) Added support for Microsoft SQL Server. +* [#556](https://github.com/shlinkio/shlink/issues/556) Short code lengths can now be customized, both globally and on a per-short URL basis. #### Changed