fix(performance): Parse file shares inaccurately to prevent setupFS

In some cases we only use the "plain text" version of a chat message.
We can render the conversation list subline without knowing the exact
path of a file. All other information is still available on the cache
entry. The path is only needed in some special cases when afterwards
the viewer is proposed to the user.

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2025-04-02 08:39:44 +02:00
parent b21659365b
commit fa7d240896
No known key found for this signature in database
GPG key ID: F72FA5B49FFA96B0
12 changed files with 56 additions and 30 deletions

View file

@ -66,7 +66,12 @@ class MessageParser {
return $message; return $message;
} }
public function parseMessage(Message $message): void { /**
* @param bool $allowInaccurate File share messages will not have fully correct data for the file object
* E.g. path is only the file name and preview generation is estimated by
* mimetype only. This is done to prevent a filesystem setup.
*/
public function parseMessage(Message $message, bool $allowInaccurate = false): void {
$message->setMessage($message->getComment()->getMessage(), []); $message->setMessage($message->getComment()->getMessage(), []);
$verb = $message->getComment()->getVerb(); $verb = $message->getComment()->getVerb();
@ -77,7 +82,7 @@ class MessageParser {
$this->setMessageActor($message); $this->setMessageActor($message);
$this->setLastEditInfo($message); $this->setLastEditInfo($message);
$event = new MessageParseEvent($message->getRoom(), $message); $event = new MessageParseEvent($message->getRoom(), $message, $allowInaccurate);
$this->dispatcher->dispatchTyped($event); $this->dispatcher->dispatchTyped($event);
} }

View file

@ -28,6 +28,7 @@ use OCP\Comments\ICommentsManager;
use OCP\EventDispatcher\Event; use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener; use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudIdManager; use OCP\Federation\ICloudIdManager;
use OCP\Files\FileInfo;
use OCP\Files\InvalidPathException; use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder; use OCP\Files\IRootFolder;
use OCP\Files\Node; use OCP\Files\Node;
@ -92,7 +93,7 @@ class SystemMessage implements IEventListener {
if ($event->getMessage()->getMessageType() === ChatManager::VERB_SYSTEM) { if ($event->getMessage()->getMessageType() === ChatManager::VERB_SYSTEM) {
try { try {
$this->parseMessage($event->getMessage()); $this->parseMessage($event->getMessage(), $event->allowInaccurate());
// Disabled so we can parse mentions in captions: $event->stopPropagation(); // Disabled so we can parse mentions in captions: $event->stopPropagation();
} catch (\OutOfBoundsException $e) { } catch (\OutOfBoundsException $e) {
// Unknown message, ignore // Unknown message, ignore
@ -111,7 +112,7 @@ class SystemMessage implements IEventListener {
* @param Message $chatMessage * @param Message $chatMessage
* @throws \OutOfBoundsException * @throws \OutOfBoundsException
*/ */
protected function parseMessage(Message $chatMessage): void { protected function parseMessage(Message $chatMessage, $allowInaccurate): void {
$this->l = $chatMessage->getL10n(); $this->l = $chatMessage->getL10n();
$comment = $chatMessage->getComment(); $comment = $chatMessage->getComment();
$room = $chatMessage->getRoom(); $room = $chatMessage->getRoom();
@ -515,7 +516,7 @@ class SystemMessage implements IEventListener {
} }
} elseif ($message === 'file_shared') { } elseif ($message === 'file_shared') {
try { try {
$parsedParameters['file'] = $this->getFileFromShare($room, $participant, $parameters['share']); $parsedParameters['file'] = $this->getFileFromShare($room, $participant, $parameters['share'], $allowInaccurate);
$parsedMessage = '{file}'; $parsedMessage = '{file}';
$metaData = $parameters['metaData'] ?? []; $metaData = $parameters['metaData'] ?? [];
if (isset($metaData['messageType'])) { if (isset($metaData['messageType'])) {
@ -759,18 +760,24 @@ class SystemMessage implements IEventListener {
} }
/** /**
* @param ?Participant $participant
* @param string $shareId
* @return array
* @throws InvalidPathException * @throws InvalidPathException
* @throws NotFoundException * @throws NotFoundException
* @throws ShareNotFound * @throws ShareNotFound
*/ */
protected function getFileFromShare(Room $room, ?Participant $participant, string $shareId): array { protected function getFileFromShare(Room $room, ?Participant $participant, string $shareId, bool $allowInaccurate): array {
$share = $this->shareProvider->getShareById((int)$shareId); $share = $this->shareProvider->getShareById((int)$shareId);
if ($participant && $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { if ($participant && $participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) {
if ($share->getShareOwner() !== $participant->getAttendee()->getActorId()) { if ($allowInaccurate) {
$node = $share->getNodeCacheEntry();
if ($node === null) {
throw new ShareNotFound();
}
$name = $node->getName();
$size = $node->getSize();
$path = $name;
} elseif ($share->getShareOwner() !== $participant->getAttendee()->getActorId()) {
$userFolder = $this->rootFolder->getUserFolder($participant->getAttendee()->getActorId()); $userFolder = $this->rootFolder->getUserFolder($participant->getAttendee()->getActorId());
if (!$userFolder instanceof Node) { if (!$userFolder instanceof Node) {
throw new ShareNotFound(); throw new ShareNotFound();
@ -814,7 +821,12 @@ class SystemMessage implements IEventListener {
} elseif ($participant && $room->getType() !== Room::TYPE_PUBLIC && $participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { } elseif ($participant && $room->getType() !== Room::TYPE_PUBLIC && $participant->getAttendee()->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
throw new ShareNotFound(); throw new ShareNotFound();
} else { } else {
$node = $share->getNode(); if ($allowInaccurate) {
$node = $share->getNodeCacheEntry();
} else {
$node = $share->getNode();
}
$name = $node->getName(); $name = $node->getName();
$size = $node->getSize(); $size = $node->getSize();
$path = $name; $path = $name;
@ -825,7 +837,11 @@ class SystemMessage implements IEventListener {
} }
$fileId = $node->getId(); $fileId = $node->getId();
$isPreviewAvailable = $this->previewManager->isAvailable($node); if ($node instanceof FileInfo) {
$isPreviewAvailable = $this->previewManager->isAvailable($node);
} else {
$isPreviewAvailable = $size > 0 && $this->previewManager->isMimeSupported($node->getMimeType());
}
$data = [ $data = [
'type' => 'file', 'type' => 'file',

View file

@ -469,7 +469,7 @@ class Listener implements IEventListener {
try { try {
$parentComment = $this->chatManager->getParentComment($room, (string)$replyTo); $parentComment = $this->chatManager->getParentComment($room, (string)$replyTo);
$parentMessage = $this->messageParser->createMessage($room, $participant, $parentComment, $this->l); $parentMessage = $this->messageParser->createMessage($room, $participant, $parentComment, $this->l);
$this->messageParser->parseMessage($parentMessage); $this->messageParser->parseMessage($parentMessage, true);
if ($parentMessage->isReplyable()) { if ($parentMessage->isReplyable()) {
$parent = $parentComment; $parent = $parentComment;
} }

View file

@ -173,7 +173,7 @@ class TalkReferenceProvider extends ADiscoverableReferenceProvider implements IS
throw new RoomNotFoundException(); throw new RoomNotFoundException();
} }
$message = $this->messageParser->createMessage($room, $participant, $comment, $this->l); $message = $this->messageParser->createMessage($room, $participant, $comment, $this->l);
$this->messageParser->parseMessage($message); $this->messageParser->parseMessage($message, true);
} else { } else {
try { try {
$proxy = $this->proxyCacheMessageMapper->findById($room, (int)$messageId); $proxy = $this->proxyCacheMessageMapper->findById($room, (int)$messageId);

View file

@ -553,7 +553,7 @@ class ChatController extends AEnvironmentAwareOCSController {
foreach ($comments as $comment) { foreach ($comments as $comment) {
$nextOffset = (int)$comment->getId(); $nextOffset = (int)$comment->getId();
$message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l); $message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
$this->messageParser->parseMessage($message); $this->messageParser->parseMessage($message, true);
if (!$message->getVisibility()) { if (!$message->getVisibility()) {
continue; continue;

View file

@ -281,7 +281,7 @@ class TalkWidget implements IAPIWidget, IIconWidget, IButtonWidget, IOptionWidge
} }
} elseif ($room->getLastMessageId() && $room->getLastMessage() && !$room->isFederatedConversation()) { } elseif ($room->getLastMessageId() && $room->getLastMessage() && !$room->isFederatedConversation()) {
$message = $this->messageParser->createMessage($room, $participant, $room->getLastMessage(), $this->l10n); $message = $this->messageParser->createMessage($room, $participant, $room->getLastMessage(), $this->l10n);
$this->messageParser->parseMessage($message); $this->messageParser->parseMessage($message, true);
$subtitle = $this->getSubtitleFromMessage($message); $subtitle = $this->getSubtitleFromMessage($message);
} }

View file

@ -15,6 +15,7 @@ class MessageParseEvent extends ARoomEvent {
public function __construct( public function __construct(
Room $room, Room $room,
protected Message $message, protected Message $message,
protected bool $allowInaccurate,
) { ) {
parent::__construct($room); parent::__construct($room);
} }
@ -22,4 +23,8 @@ class MessageParseEvent extends ARoomEvent {
public function getMessage(): Message { public function getMessage(): Message {
return $this->message; return $this->message;
} }
public function allowInaccurate(): bool {
return $this->allowInaccurate;
}
} }

View file

@ -525,7 +525,7 @@ class Notifier implements INotifier {
} }
$message = $this->messageParser->createMessage($room, $participant, $comment, $l); $message = $this->messageParser->createMessage($room, $participant, $comment, $l);
$this->messageParser->parseMessage($message); $this->messageParser->parseMessage($message, true);
if (!$message->getVisibility()) { if (!$message->getVisibility()) {
throw new AlreadyProcessedException(); throw new AlreadyProcessedException();

View file

@ -106,7 +106,7 @@ class BotService {
$parent, $parent,
$this->l10nFactory->get('spreed', 'en', 'en') $this->l10nFactory->get('spreed', 'en', 'en')
); );
$messageParser->parseMessage($parentMessage); $messageParser->parseMessage($parentMessage, true);
$parentMessageData = [ $parentMessageData = [
'message' => $parentMessage->getMessage(), 'message' => $parentMessage->getMessage(),
'parameters' => $parentMessage->getMessageParameters(), 'parameters' => $parentMessage->getMessageParameters(),
@ -125,7 +125,7 @@ class BotService {
$event->getComment(), $event->getComment(),
$this->l10nFactory->get('spreed', 'en', 'en') $this->l10nFactory->get('spreed', 'en', 'en')
); );
$messageParser->parseMessage($message); $messageParser->parseMessage($message, true);
$messageData = [ $messageData = [
'message' => $message->getMessage(), 'message' => $message->getMessage(),
'parameters' => $message->getMessageParameters(), 'parameters' => $message->getMessageParameters(),

View file

@ -435,7 +435,7 @@ class RoomFormatter {
IComment $lastMessage, IComment $lastMessage,
): ?array { ): ?array {
$message = $this->messageParser->createMessage($room, $participant, $lastMessage, $this->l10n); $message = $this->messageParser->createMessage($room, $participant, $lastMessage, $this->l10n);
$this->messageParser->parseMessage($message); $this->messageParser->parseMessage($message, true);
if (!$message->getVisibility()) { if (!$message->getVisibility()) {
return null; return null;

View file

@ -632,7 +632,7 @@ class SystemMessageTest extends TestCase {
'parameters' => $parameters, 'parameters' => $parameters,
]), [], $message); ]), [], $message);
self::invokePrivate($parser, 'parseMessage', [$chatMessage]); self::invokePrivate($parser, 'parseMessage', [$chatMessage, false]);
$this->assertSame($expectedMessage, $chatMessage->getMessage()); $this->assertSame($expectedMessage, $chatMessage->getMessage());
$this->assertSame($expectedParameters, $chatMessage->getMessageParameters()); $this->assertSame($expectedParameters, $chatMessage->getMessageParameters());
@ -672,7 +672,7 @@ class SystemMessageTest extends TestCase {
$chatMessage->setMessage($return, []); $chatMessage->setMessage($return, []);
$this->expectException(\OutOfBoundsException::class); $this->expectException(\OutOfBoundsException::class);
self::invokePrivate($parser, 'parseMessage', [$chatMessage]); self::invokePrivate($parser, 'parseMessage', [$chatMessage, false]);
} }
public function testGetFileFromShareForGuest(): void { public function testGetFileFromShareForGuest(): void {
@ -743,7 +743,7 @@ class SystemMessageTest extends TestCase {
'preview-available' => 'yes', 'preview-available' => 'yes',
'width' => '1234', 'width' => '1234',
'height' => '4567', 'height' => '4567',
], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23'])); ], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]));
} }
public function testGetFileFromShareForGuestWithBlurhash(): void { public function testGetFileFromShareForGuestWithBlurhash(): void {
@ -820,7 +820,7 @@ class SystemMessageTest extends TestCase {
'width' => '1234', 'width' => '1234',
'height' => '4567', 'height' => '4567',
'blurhash' => 'LEHV9uae2yk8pyo0adR*.7kCMdnj', 'blurhash' => 'LEHV9uae2yk8pyo0adR*.7kCMdnj',
], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23'])); ], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]));
} }
public function testGetFileFromShareForOwner(): void { public function testGetFileFromShareForOwner(): void {
@ -897,7 +897,7 @@ class SystemMessageTest extends TestCase {
'permissions' => '27', 'permissions' => '27',
'mimetype' => 'httpd/unix-directory', 'mimetype' => 'httpd/unix-directory',
'preview-available' => 'no', 'preview-available' => 'no',
], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23'])); ], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]));
} }
public function testGetFileFromShareForRecipient(): void { public function testGetFileFromShareForRecipient(): void {
@ -982,7 +982,7 @@ class SystemMessageTest extends TestCase {
'permissions' => '27', 'permissions' => '27',
'mimetype' => 'application/octet-stream', 'mimetype' => 'application/octet-stream',
'preview-available' => 'no', 'preview-available' => 'no',
], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23'])); ], self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]));
} }
public function testGetFileFromShareForRecipientThrows(): void { public function testGetFileFromShareForRecipientThrows(): void {
@ -1026,7 +1026,7 @@ class SystemMessageTest extends TestCase {
$parser = $this->getParser(); $parser = $this->getParser();
$this->expectException(NotFoundException::class); $this->expectException(NotFoundException::class);
self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23']); self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]);
} }
public function testGetFileFromShareThrows(): void { public function testGetFileFromShareThrows(): void {
@ -1046,7 +1046,7 @@ class SystemMessageTest extends TestCase {
->willReturn($attendee); ->willReturn($attendee);
$parser = $this->getParser(); $parser = $this->getParser();
$this->expectException(ShareNotFound::class); $this->expectException(ShareNotFound::class);
self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23']); self::invokePrivate($parser, 'getFileFromShare', [$room, $participant, '23', false]);
} }
public static function dataGetActor(): array { public static function dataGetActor(): array {

View file

@ -421,8 +421,8 @@ class ChatControllerTest extends TestCase {
$i = 0; $i = 0;
$expectedCalls = [ $expectedCalls = [
[$parentMessage], [$parentMessage, false],
[$chatMessage], [$chatMessage, false],
]; ];
$this->messageParser->expects($this->exactly(2)) $this->messageParser->expects($this->exactly(2))
->method('parseMessage') ->method('parseMessage')