Ensure filtering of custom-slug is different depending on the multi-sement lugsfeature flag

This commit is contained in:
Alejandro Celaya
2022-08-05 08:38:05 +02:00
parent 6834e72c4a
commit fc0d99be41
13 changed files with 82 additions and 24 deletions

View File

@@ -11,6 +11,7 @@ use Laminas\ServiceManager\Factory\InvokableFactory;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory; use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
use Shlinkio\Shlink\Core\Domain\DomainService; use Shlinkio\Shlink\Core\Domain\DomainService;
use Shlinkio\Shlink\Core\Options\TrackingOptions; use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
@@ -81,8 +82,7 @@ return [
Command\ShortUrl\CreateShortUrlCommand::class => [ Command\ShortUrl\CreateShortUrlCommand::class => [
Service\UrlShortener::class, Service\UrlShortener::class,
ShortUrlStringifier::class, ShortUrlStringifier::class,
'config.url_shortener.default_short_codes_length', UrlShortenerOptions::class,
'config.url_shortener.domain.hostname',
], ],
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class], Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
Command\ShortUrl\ListShortUrlsCommand::class => [ Command\ShortUrl\ListShortUrlsCommand::class => [

View File

@@ -5,9 +5,11 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
@@ -19,6 +21,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use function array_map; use function array_map;
use function explode;
use function Functional\curry; use function Functional\curry;
use function Functional\flatten; use function Functional\flatten;
use function Functional\unique; use function Functional\unique;
@@ -29,14 +32,15 @@ class CreateShortUrlCommand extends Command
public const NAME = 'short-url:create'; public const NAME = 'short-url:create';
private ?SymfonyStyle $io; private ?SymfonyStyle $io;
private string $defaultDomain;
public function __construct( public function __construct(
private UrlShortenerInterface $urlShortener, private readonly UrlShortenerInterface $urlShortener,
private ShortUrlStringifierInterface $stringifier, private readonly ShortUrlStringifierInterface $stringifier,
private int $defaultShortCodeLength, private readonly UrlShortenerOptions $options,
private string $defaultDomain,
) { ) {
parent::__construct(); parent::__construct();
$this->defaultDomain = $this->options->domain()['hostname'] ?? '';
} }
protected function configure(): void protected function configure(): void
@@ -150,11 +154,11 @@ class CreateShortUrlCommand extends Command
return ExitCodes::EXIT_FAILURE; return ExitCodes::EXIT_FAILURE;
} }
$explodeWithComma = curry('explode')(','); $explodeWithComma = curry(explode(...))(',');
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags')))); $tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$customSlug = $input->getOption('custom-slug'); $customSlug = $input->getOption('custom-slug');
$maxVisits = $input->getOption('max-visits'); $maxVisits = $input->getOption('max-visits');
$shortCodeLength = $input->getOption('short-code-length') ?? $this->defaultShortCodeLength; $shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength();
$doValidateUrl = $input->getOption('validate-url'); $doValidateUrl = $input->getOption('validate-url');
try { try {
@@ -171,6 +175,7 @@ class CreateShortUrlCommand extends Command
ShortUrlInputFilter::TAGS => $tags, ShortUrlInputFilter::TAGS => $tags,
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'), ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'), ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled(),
])); ]));
$io->writeln([ $io->writeln([

View File

@@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface; use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait; use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
@@ -38,8 +39,7 @@ class CreateShortUrlCommandTest extends TestCase
$command = new CreateShortUrlCommand( $command = new CreateShortUrlCommand(
$this->urlShortener->reveal(), $this->urlShortener->reveal(),
$this->stringifier->reveal(), $this->stringifier->reveal(),
5, new UrlShortenerOptions(['defaultShortCodesLength' => 5, 'domain' => ['hostname' => self::DEFAULT_DOMAIN]]),
self::DEFAULT_DOMAIN,
); );
$this->commandTester = $this->testerForCommand($command); $this->commandTester = $this->testerForCommand($command);
} }

View File

@@ -14,6 +14,7 @@ use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter; use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
use function Shlinkio\Shlink\Core\normalizeDate; use function Shlinkio\Shlink\Core\normalizeDate;
// TODO Rename to ShortUrlEdition
final class ShortUrlEdit implements TitleResolutionModelInterface final class ShortUrlEdit implements TitleResolutionModelInterface
{ {
private bool $longUrlPropWasProvided = false; private bool $longUrlPropWasProvided = false;

View File

@@ -16,6 +16,7 @@ use function Shlinkio\Shlink\Core\normalizeDate;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
// TODO Rename to ShortUrlCreation
final class ShortUrlMeta implements TitleResolutionModelInterface final class ShortUrlMeta implements TitleResolutionModelInterface
{ {
private string $longUrl; private string $longUrl;

View File

@@ -6,14 +6,40 @@ namespace Shlinkio\Shlink\Core\Options;
use Laminas\Stdlib\AbstractOptions; use Laminas\Stdlib\AbstractOptions;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
class UrlShortenerOptions extends AbstractOptions class UrlShortenerOptions extends AbstractOptions
{ {
protected $__strictMode__ = false; // phpcs:ignore protected $__strictMode__ = false; // phpcs:ignore
private array $domain = [];
private int $defaultShortCodesLength = DEFAULT_SHORT_CODES_LENGTH;
private bool $autoResolveTitles = false; private bool $autoResolveTitles = false;
private bool $appendExtraPath = false; private bool $appendExtraPath = false;
private bool $multiSegmentSlugsEnabled = false; private bool $multiSegmentSlugsEnabled = false;
public function domain(): array
{
return $this->domain;
}
protected function setDomain(array $domain): self
{
$this->domain = $domain;
return $this;
}
public function defaultShortCodesLength(): int
{
return $this->defaultShortCodesLength;
}
protected function setDefaultShortCodesLength(int $defaultShortCodesLength): self
{
$this->defaultShortCodesLength = $defaultShortCodesLength;
return $this;
}
public function autoResolveTitles(): bool public function autoResolveTitles(): bool
{ {
return $this->autoResolveTitles; return $this->autoResolveTitles;

View File

@@ -10,12 +10,13 @@ use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter; use Laminas\InputFilter\InputFilter;
use Laminas\Validator; use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation; use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function is_string; use function is_string;
use function ltrim;
use function str_replace; use function str_replace;
use function substr; use function substr;
use function trim;
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH; use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
@@ -40,7 +41,7 @@ class ShortUrlInputFilter extends InputFilter
private function __construct(array $data, bool $requireLongUrl) private function __construct(array $data, bool $requireLongUrl)
{ {
$this->initialize($requireLongUrl); $this->initialize($requireLongUrl, $data[EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value] ?? false);
$this->setData($data); $this->setData($data);
} }
@@ -54,7 +55,7 @@ class ShortUrlInputFilter extends InputFilter
return new self($data, false); return new self($data, false);
} }
private function initialize(bool $requireLongUrl): void private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
{ {
$longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl); $longUrlInput = $this->createInput(self::LONG_URL, $requireLongUrl);
$longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([ $longUrlInput->getValidatorChain()->attach(new Validator\NotEmpty([
@@ -77,9 +78,10 @@ class ShortUrlInputFilter extends InputFilter
// FIXME The only way to enforce the NotEmpty validator to be evaluated when the value is provided but it's // FIXME The only way to enforce the NotEmpty validator to be evaluated when the value is provided but it's
// empty, is by using the deprecated setContinueIfEmpty // empty, is by using the deprecated setContinueIfEmpty
$customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true); $customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true);
$customSlug->getFilterChain()->attach(new Filter\Callback( $customSlug->getFilterChain()->attach(new Filter\Callback(match ($multiSegmentEnabled) {
static fn (mixed $value) => is_string($value) ? ltrim(str_replace(' ', '-', $value), '/') : $value, true => static fn (mixed $v) => is_string($v) ? trim(str_replace(' ', '-', $v), '/') : $v,
)); false => static fn (mixed $v) => is_string($v) ? str_replace([' ', '/'], '-', $v) : $v,
}));
$customSlug->getValidatorChain()->attach(new Validator\NotEmpty([ $customSlug->getValidatorChain()->attach(new Validator\NotEmpty([
Validator\NotEmpty::STRING, Validator\NotEmpty::STRING,
Validator\NotEmpty::SPACE, Validator\NotEmpty::SPACE,

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\Core\Model;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
@@ -74,12 +75,16 @@ class ShortUrlMetaTest extends TestCase
* @test * @test
* @dataProvider provideCustomSlugs * @dataProvider provideCustomSlugs
*/ */
public function properlyCreatedInstanceReturnsValues(string $customSlug, string $expectedSlug): void public function properlyCreatedInstanceReturnsValues(
{ string $customSlug,
string $expectedSlug,
bool $multiSegmentEnabled = false,
): void {
$meta = ShortUrlMeta::fromRawData([ $meta = ShortUrlMeta::fromRawData([
'validSince' => Chronos::parse('2015-01-01')->toAtomString(), 'validSince' => Chronos::parse('2015-01-01')->toAtomString(),
'customSlug' => $customSlug, 'customSlug' => $customSlug,
'longUrl' => '', 'longUrl' => '',
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $multiSegmentEnabled,
]); ]);
self::assertTrue($meta->hasValidSince()); self::assertTrue($meta->hasValidSince());
@@ -103,8 +108,10 @@ class ShortUrlMetaTest extends TestCase
yield ['foo bar', 'foo-bar']; yield ['foo bar', 'foo-bar'];
yield ['foo bar baz', 'foo-bar-baz']; yield ['foo bar baz', 'foo-bar-baz'];
yield ['foo bar-baz', 'foo-bar-baz']; yield ['foo bar-baz', 'foo-bar-baz'];
yield ['foo/bar/baz', 'foo/bar/baz']; yield ['foo/bar/baz', 'foo/bar/baz', true];
yield ['/foo/bar/baz', 'foo/bar/baz']; yield ['/foo/bar/baz', 'foo/bar/baz', true];
yield ['foo/bar/baz', 'foo-bar-baz'];
yield ['/foo/bar/baz', '-foo-bar-baz'];
yield ['wp-admin.php', 'wp-admin.php']; yield ['wp-admin.php', 'wp-admin.php'];
yield ['UPPER_lower', 'UPPER_lower']; yield ['UPPER_lower', 'UPPER_lower'];
yield ['more~url_special.chars', 'more~url_special.chars']; yield ['more~url_special.chars', 'more~url_special.chars'];

View File

@@ -61,10 +61,15 @@ return [
Action\HealthAction::class => ['em', Options\AppOptions::class], Action\HealthAction::class => ['em', Options\AppOptions::class],
Action\MercureInfoAction::class => [LcobucciJwtProvider::class, 'config.mercure'], Action\MercureInfoAction::class => [LcobucciJwtProvider::class, 'config.mercure'],
Action\ShortUrl\CreateShortUrlAction::class => [Service\UrlShortener::class, ShortUrlDataTransformer::class], Action\ShortUrl\CreateShortUrlAction::class => [
Service\UrlShortener::class,
ShortUrlDataTransformer::class,
Options\UrlShortenerOptions::class,
],
Action\ShortUrl\SingleStepCreateShortUrlAction::class => [ Action\ShortUrl\SingleStepCreateShortUrlAction::class => [
Service\UrlShortener::class, Service\UrlShortener::class,
ShortUrlDataTransformer::class, ShortUrlDataTransformer::class,
Options\UrlShortenerOptions::class,
], ],
Action\ShortUrl\EditShortUrlAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class], Action\ShortUrl\EditShortUrlAction::class => [Service\ShortUrlService::class, ShortUrlDataTransformer::class],
Action\ShortUrl\DeleteShortUrlAction::class => [Service\ShortUrl\DeleteShortUrlService::class], Action\ShortUrl\DeleteShortUrlAction::class => [Service\ShortUrl\DeleteShortUrlService::class],

View File

@@ -10,14 +10,16 @@ use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\AbstractRestAction; use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
abstract class AbstractCreateShortUrlAction extends AbstractRestAction abstract class AbstractCreateShortUrlAction extends AbstractRestAction
{ {
public function __construct( public function __construct(
private UrlShortenerInterface $urlShortener, private readonly UrlShortenerInterface $urlShortener,
private DataTransformerInterface $transformer, private readonly DataTransformerInterface $transformer,
protected readonly UrlShortenerOptions $urlShortenerOptions,
) { ) {
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action\ShortUrl; namespace Shlinkio\Shlink\Rest\Action\ShortUrl;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter; use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
@@ -22,6 +23,7 @@ class CreateShortUrlAction extends AbstractCreateShortUrlAction
{ {
$payload = (array) $request->getParsedBody(); $payload = (array) $request->getParsedBody();
$payload[ShortUrlInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request); $payload[ShortUrlInputFilter::API_KEY] = AuthenticationMiddleware::apiKeyFromRequest($request);
$payload[EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value] = $this->urlShortenerOptions->multiSegmentSlugsEnabled();
return ShortUrlMeta::fromRawData($payload); return ShortUrlMeta::fromRawData($payload);
} }

View File

@@ -16,6 +16,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ValidationException; use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortener; use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\CreateShortUrlAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -34,7 +35,11 @@ class CreateShortUrlActionTest extends TestCase
$this->transformer = $this->prophesize(DataTransformerInterface::class); $this->transformer = $this->prophesize(DataTransformerInterface::class);
$this->transformer->transform(Argument::type(ShortUrl::class))->willReturn([]); $this->transformer->transform(Argument::type(ShortUrl::class))->willReturn([]);
$this->action = new CreateShortUrlAction($this->urlShortener->reveal(), $this->transformer->reveal()); $this->action = new CreateShortUrlAction(
$this->urlShortener->reveal(),
$this->transformer->reveal(),
new UrlShortenerOptions(),
);
} }
/** @test */ /** @test */

View File

@@ -12,6 +12,7 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface; use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface; use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction; use Shlinkio\Shlink\Rest\Action\ShortUrl\SingleStepCreateShortUrlAction;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -33,6 +34,7 @@ class SingleStepCreateShortUrlActionTest extends TestCase
$this->action = new SingleStepCreateShortUrlAction( $this->action = new SingleStepCreateShortUrlAction(
$this->urlShortener->reveal(), $this->urlShortener->reveal(),
$this->transformer->reveal(), $this->transformer->reveal(),
new UrlShortenerOptions(),
); );
} }