Add support for short URL mode in installer, and handle loosely mode in custom slugs

This commit is contained in:
Alejandro Celaya
2023-01-28 10:06:11 +01:00
parent 2f83e90c8b
commit fdaf5fb2f3
13 changed files with 76 additions and 30 deletions

View File

@@ -21,4 +21,9 @@ final class UrlShortenerOptions
public readonly ShortUrlMode $mode = ShortUrlMode::STRICT,
) {
}
public function isLooselyMode(): bool
{
return $this->mode === ShortUrlMode::LOOSELY;
}
}

View File

@@ -63,6 +63,9 @@ class ShortUrl extends AbstractEntity
{
}
/**
* @internal
*/
public static function createFake(): self
{
return self::withLongUrl('foo');
@@ -70,6 +73,7 @@ class ShortUrl extends AbstractEntity
/**
* @param non-empty-string $longUrl
* @internal
*/
public static function withLongUrl(string $longUrl): self
{

View File

@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -48,9 +49,10 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
/**
* @throws ValidationException
*/
public static function fromRawData(array $data, ShortUrlMode $mode = ShortUrlMode::STRICT): self
public static function fromRawData(array $data, ?UrlShortenerOptions $options = null): self
{
$inputFilter = ShortUrlInputFilter::withRequiredLongUrl($data);
$options = $options ?? new UrlShortenerOptions();
$inputFilter = ShortUrlInputFilter::withRequiredLongUrl($data, $options);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
@@ -61,7 +63,7 @@ final class ShortUrlCreation implements TitleResolutionModelInterface
return new self(
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
shortUrlMode: $mode,
shortUrlMode: $options->mode,
deviceLongUrls: $deviceLongUrls,
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
use Laminas\Filter\FilterInterface;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use function is_string;
use function str_replace;
use function strtolower;
use function trim;
class CustomSlugFilter implements FilterInterface
{
public function __construct(private readonly UrlShortenerOptions $options)
{
}
public function filter(mixed $value): mixed
{
if (! is_string($value)) {
return $value;
}
$value = $this->options->isLooselyMode() ? strtolower($value) : $value;
if ($this->options->multiSegmentSlugsEnabled) {
return trim(str_replace(' ', '-', $value), '/');
}
return str_replace([' ', '/'], '-', $value);
}
}

View File

@@ -9,16 +9,17 @@ use Laminas\Filter;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function is_string;
use function str_replace;
use function substr;
use function trim;
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
/**
* @todo Pass forCreation/forEdition, instead of withRequiredLongUrl/withNonRequiredLongUrl.
* Make it also dynamically add the relevant fields
*/
class ShortUrlInputFilter extends InputFilter
{
use Validation\InputFactoryTrait;
@@ -40,23 +41,23 @@ class ShortUrlInputFilter extends InputFilter
public const CRAWLABLE = 'crawlable';
public const FORWARD_QUERY = 'forwardQuery';
private function __construct(array $data, bool $requireLongUrl)
private function __construct(array $data, bool $requireLongUrl, UrlShortenerOptions $options)
{
$this->initialize($requireLongUrl, $data[EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value] ?? false);
$this->initialize($requireLongUrl, $options);
$this->setData($data);
}
public static function withRequiredLongUrl(array $data): self
public static function withRequiredLongUrl(array $data, UrlShortenerOptions $options): self
{
return new self($data, true);
return new self($data, true, $options);
}
public static function withNonRequiredLongUrl(array $data): self
{
return new self($data, false);
return new self($data, false, new UrlShortenerOptions());
}
private function initialize(bool $requireLongUrl, bool $multiSegmentEnabled): void
private function initialize(bool $requireLongUrl, UrlShortenerOptions $options): void
{
$longUrlNotEmptyCommonOptions = [
Validator\NotEmpty::OBJECT,
@@ -93,10 +94,7 @@ class ShortUrlInputFilter extends InputFilter
// The only way to enforce the NotEmpty validator to be evaluated when the key is present with an empty value
// is by using the deprecated setContinueIfEmpty
$customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true);
$customSlug->getFilterChain()->attach(new Filter\Callback(match ($multiSegmentEnabled) {
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->getFilterChain()->attach(new CustomSlugFilter($options));
$customSlug->getValidatorChain()->attach(new Validator\NotEmpty([
Validator\NotEmpty::STRING,
Validator\NotEmpty::SPACE,

View File

@@ -36,7 +36,7 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
->setParameter('shortCode', $isStrict ? $identifier->shortCode : strtolower($identifier->shortCode))
->andWhere($qb->expr()->orX(
$qb->expr()->isNull('s.domain'),
$qb->expr()->eq('d.authority', ':domain')
$qb->expr()->eq('d.authority', ':domain'),
))
->setParameter('domain', $identifier->domain);