diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php index 26fa64f8a5cf3..0856f68c7ee72 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -118,7 +118,7 @@ public function setStatus(string $userId, throw new InvalidStatusTypeException('Status-type "' . $statusType . '" is not supported'); } // Check if statusIcon contains only one character - if (\mb_strlen($statusIcon) > 1) { + if ($statusIcon !== null && !$this->isValidEmoji($statusIcon)) { throw new InvalidStatusIconException('Status-Icon is longer than one character'); } // Check for maximum length of custom message @@ -158,4 +158,48 @@ public function removeUserStatus(string $userId): bool { $this->mapper->delete($userStatus); return true; } + + /** + * @param string $emoji + * @return bool + */ + private function isValidEmoji(string $emoji): bool { + $intlBreakIterator = \IntlBreakIterator::createCharacterInstance(); + $intlBreakIterator->setText($emoji); + + $characterCount = 0; + while ($intlBreakIterator->next() !== \IntlBreakIterator::DONE) { + $characterCount++; + } + + if ($characterCount !== 1) { + return false; + } + + $codePointIterator = \IntlBreakIterator::createCodePointInstance(); + $codePointIterator->setText($emoji); + + foreach($codePointIterator->getPartsIterator() as $codePoint) { + $codePointType = \IntlChar::charType($codePoint); + + // If the current code-point is an emoji or a modifier (like a skin-tone) + // just continue and check the next character + if ($codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_SYMBOL || + $codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_LETTER || + $codePointType === \IntlChar::CHAR_CATEGORY_OTHER_SYMBOL) { + continue; + } + + // If it's neither a modifier nor an emoji, we only allow + // a zero-width-joiner or a variation selector 16 + $codePointValue = \IntlChar::ord($codePoint); + if ($codePointValue === 8205 || $codePointValue === 65039) { + continue; + } + + return false; + } + + return true; + } } diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php index 92023ccb9a419..ee29ba2c249af 100644 --- a/apps/user_status/tests/Unit/Service/StatusServiceTest.php +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -199,6 +199,12 @@ public function setStatusDataProvider(): array { // Clear at is in the past ['john.doe', 'busy', '📱', 'In a phone call', 10, true, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], ['john.doe', 'busy', '📱', 'In a phone call', 10, false, false, true, InvalidClearAtException::class, 'ClearAt is in the past'], + // Test some more complex emojis with modifiers and zero-width-joiner + ['john.doe', 'busy', '👩🏿‍💻', 'In a phone call', 42, true, true, false, null, null], + ['john.doe', 'busy', '🤷🏼‍♀️', 'In a phone call', 42, true, true, false, null, null], + ['john.doe', 'busy', '🏳️‍🌈', 'In a phone call', 42, true, true, false, null, null], + ['john.doe', 'busy', '👨‍👨‍👦‍👦', 'In a phone call', 42, true, true, false, null, null], + ['john.doe', 'busy', '👩‍❤️‍👩', 'In a phone call', 42, true, true, false, null, null], ]; }