diff --git a/composer.json b/composer.json
index 2522f158..f12e41fb 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/config/constants.php b/config/constants.php
index 8171cd66..978964c5 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -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>/i'; // Matches the value inside an html title tag
const DEFAULT_QR_CODE_SIZE = 300;
const DEFAULT_QR_CODE_MARGIN = 0;
diff --git a/module/Core/src/Util/CocurSymfonySluggerBridge.php b/module/Core/src/Util/CocurSymfonySluggerBridge.php
deleted file mode 100644
index da60836e..00000000
--- a/module/Core/src/Util/CocurSymfonySluggerBridge.php
+++ /dev/null
@@ -1,22 +0,0 @@
-slugger->slugify($string, $separator));
- }
-}
diff --git a/module/Core/src/Validation/ShortUrlInputFilter.php b/module/Core/src/Validation/ShortUrlInputFilter.php
index 2497f85d..96896e94 100644
--- a/module/Core/src/Validation/ShortUrlInputFilter.php
+++ b/module/Core/src/Validation/ShortUrlInputFilter.php
@@ -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,
diff --git a/module/Core/test/Model/ShortUrlMetaTest.php b/module/Core/test/Model/ShortUrlMetaTest.php
index 9a5eac72..ec2d314e 100644
--- a/module/Core/test/Model/ShortUrlMetaTest.php
+++ b/module/Core/test/Model/ShortUrlMetaTest.php
@@ -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 ['谷歌', '谷歌'];