Simplified how the custom slugs are processed, allowing more characters in the process

This commit is contained in:
Alejandro Celaya 2022-01-09 21:02:23 +01:00
parent d2fef20239
commit e47c90c645
5 changed files with 18 additions and 33 deletions

View File

@ -17,7 +17,6 @@
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^2.3",
"cocur/slugify": "^4.0",
"doctrine/migrations": "^3.3",
"doctrine/orm": "^2.10",
"endroid/qr-code": "^4.4",

View File

@ -12,7 +12,6 @@ const MIN_SHORT_CODES_LENGTH = 4;
const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
const CUSTOM_SLUGS_REGEXP = '/[^\pL\pN._~]/u'; // Any unicode letter or number, plus ".", "_" and "~" chars
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside an html title tag
const DEFAULT_QR_CODE_SIZE = 300;
const DEFAULT_QR_CODE_MARGIN = 0;

View File

@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Util;
use Cocur\Slugify\SlugifyInterface;
use Symfony\Component\String\AbstractUnicodeString;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\String\UnicodeString;
class CocurSymfonySluggerBridge implements SluggerInterface
{
public function __construct(private SlugifyInterface $slugger)
{
}
public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString
{
return new UnicodeString($this->slugger->slugify($string, $separator));
}
}

View File

@ -4,19 +4,18 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Validation;
use Cocur\Slugify\Slugify;
use DateTime;
use Laminas\Filter;
use Laminas\InputFilter\Input;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator;
use Shlinkio\Shlink\Common\Validation;
use Shlinkio\Shlink\Core\Util\CocurSymfonySluggerBridge;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function is_string;
use function str_replace;
use function substr;
use const Shlinkio\Shlink\CUSTOM_SLUGS_REGEXP;
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
class ShortUrlInputFilter extends InputFilter
@ -77,11 +76,9 @@ class ShortUrlInputFilter extends InputFilter
// 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
$customSlug = $this->createInput(self::CUSTOM_SLUG, false)->setContinueIfEmpty(true);
$customSlug->getFilterChain()->attach(new Validation\SluggerFilter(new CocurSymfonySluggerBridge(new Slugify([
'regexp' => CUSTOM_SLUGS_REGEXP,
'lowercase' => false, // We want to keep it case-sensitive
'rulesets' => ['default'],
]))));
$customSlug->getFilterChain()->attach(new Filter\Callback(
static fn (mixed $value) => is_string($value) ? str_replace([' ', '/'], ['-', ''], $value) : $value,
));
$customSlug->getValidatorChain()->attach(new Validator\NotEmpty([
Validator\NotEmpty::STRING,
Validator\NotEmpty::SPACE,

View File

@ -30,34 +30,43 @@ class ShortUrlMetaTest extends TestCase
public function provideInvalidData(): iterable
{
yield [[]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::VALID_SINCE => '',
ShortUrlInputFilter::VALID_UNTIL => '',
ShortUrlInputFilter::CUSTOM_SLUG => 'foobar',
ShortUrlInputFilter::MAX_VISITS => 'invalid',
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::VALID_SINCE => '2017',
ShortUrlInputFilter::MAX_VISITS => 5,
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::VALID_SINCE => new stdClass(),
ShortUrlInputFilter::VALID_UNTIL => 'foo',
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::VALID_UNTIL => 500,
ShortUrlInputFilter::DOMAIN => 4,
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::SHORT_CODE_LENGTH => 3,
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::CUSTOM_SLUG => '/',
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::CUSTOM_SLUG => '',
]];
yield [[
ShortUrlInputFilter::LONG_URL => 'foo',
ShortUrlInputFilter::CUSTOM_SLUG => ' ',
]];
yield [[
@ -92,12 +101,15 @@ class ShortUrlMetaTest extends TestCase
public function provideCustomSlugs(): iterable
{
yield ['🔥', '🔥'];
yield ['🦣 🍅', '🦣-🍅'];
yield ['foobar', 'foobar'];
yield ['foo bar', 'foo-bar'];
yield ['foo bar baz', 'foo-bar-baz'];
yield ['foo bar-baz', 'foo-bar-baz'];
yield ['wp-admin.php', 'wp-admin.php'];
yield ['UPPER_lower', 'UPPER_lower'];
yield ['more~url_special.chars', 'more~url_special.chars'];
yield ['äéñ', 'äen'];
yield ['구글', '구글'];
yield ['グーグル', 'グーグル'];
yield ['谷歌', '谷歌'];