From ac5e827b8171fdcf0bbac479f6047d751d8883ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 3 Jan 2025 23:22:10 +0100 Subject: [PATCH] Update regex to restrict words containing digit only at the beginning or the end of the ID --- src/Sqids.php | 67 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/src/Sqids.php b/src/Sqids.php index 8d100b8..ecce186 100644 --- a/src/Sqids.php +++ b/src/Sqids.php @@ -25,6 +25,8 @@ class Sqids implements SqidsInterface { + private const MIN_LENGTH_LIMIT = 255; + final public const DEFAULT_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; final public const DEFAULT_MIN_LENGTH = 0; final public const DEFAULT_BLOCKLIST = [ @@ -237,7 +239,7 @@ class Sqids implements SqidsInterface protected MathInterface $math; - protected ?string $blocklistRegex = null; + protected ?string $blocklistRegex; /** @throws InvalidArgumentException */ public function __construct( @@ -263,25 +265,12 @@ public function __construct( throw new InvalidArgumentException('Alphabet must contain unique characters'); } - $minLengthLimit = 255; - if ($minLength < 0 || $minLength > $minLengthLimit) { - throw new InvalidArgumentException( - 'Minimum length has to be between 0 and ' . $minLengthLimit, - ); - } - - $filteredBlocklist = []; - foreach ($blocklist as $word) { - if (strlen((string) $word) >= 3) { - $filteredBlocklist[] = strtr(preg_quote((string) $word, '/'), self::LEET); - } - } - if ($filteredBlocklist) { - $this->blocklistRegex = '/(' . implode('|', $filteredBlocklist) . ')/i'; + if ($minLength < 0 || $minLength > self::MIN_LENGTH_LIMIT) { + throw new InvalidArgumentException('Minimum length has to be between 0 and ' . self::MIN_LENGTH_LIMIT); } + $this->blocklistRegex = $this->buildBlocklistRegex(); $this->alphabet = $this->shuffle($alphabet); - $this->blocklist = $filteredBlocklist; } /** @@ -471,4 +460,48 @@ protected function getMathExtension(): MathInterface throw new RuntimeException('Missing math extension for Sqids, install either bcmath or gmp.'); } + + protected function buildBlocklistRegex(): ?string + { + $wordsMatchingExactly = []; + $wordsMatchingBeginningOrEnd = []; + $wordMatchingAnywhere = []; + + foreach ($this->blocklist as $word) { + $word = (string) $word; + if (strlen($word) <= 3) { + $wordsMatchingExactly[] = preg_quote($word, '/'); + } else { + $word = preg_quote($word, '/'); + $leet = strtr($word, self::LEET); + if (!preg_match('/\d/', $word)) { + $wordMatchingAnywhere[] = $word; + } elseif ($leet === $word) { + $wordsMatchingBeginningOrEnd[] = $word; + } + + if ($leet !== $word) { + $wordsMatchingBeginningOrEnd[] = $leet; + } + } + } + + $regexParts = []; + if ($wordsMatchingExactly) { + $regexParts[] = '^('.implode('|', $wordsMatchingExactly).')$'; + } + if ($wordsMatchingBeginningOrEnd) { + $regexParts[] = '^('.implode('|', $wordsMatchingBeginningOrEnd).')'; + $regexParts[] = '('.implode('|', $wordsMatchingBeginningOrEnd).')$'; + } + if ($wordMatchingAnywhere) { + $regexParts[] = '('.implode('|', $wordMatchingAnywhere).')'; + } + + if ($regexParts) { + return '/(' . implode('|', $regexParts) . ')/i'; + } + + return null; + } }