Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 93 additions & 19 deletions apps/comments/lib/Search/CommentsSearchProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
* SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Comments\Search;

use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
Expand All @@ -16,15 +23,16 @@
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use OCP\Search\SearchResultEntry;
use function array_map;
use function pathinfo;
use Psr\Log\LoggerInterface;

class CommentsSearchProvider implements IProvider {
public function __construct(
private IUserManager $userManager,
private IL10N $l10n,
private IURLGenerator $urlGenerator,
private LegacyProvider $legacyProvider,
private ICommentsManager $commentsManager,
private IRootFolder $rootFolder,
private LoggerInterface $logger,
) {
}

Expand All @@ -45,27 +53,93 @@ public function getOrder(string $route, array $routeParameters): int {
}

public function search(IUser $user, ISearchQuery $query): SearchResult {
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$result = $this->findCommentsBySearchQuery($query, $userFolder);

return SearchResult::complete(
$this->l10n->t('Comments'),
array_map(function (Result $result) {
$path = $result->path;
$pathInfo = pathinfo($path);
$isUser = $this->userManager->userExists($result->authorId);
$result
);
}

/**
* @return list<SearchResultEntry>
*/
private function findCommentsBySearchQuery(ISearchQuery $query, Folder $userFolder): array {
$result = [];
$numComments = 50;
$offset = 0;
$limit = $numComments;

while (count($result) < $numComments) {
$comments = $this->commentsManager->search(
$query->getTerm(),
'files',
'',
'comment',
$offset,
$limit,
);

foreach ($comments as $comment) {
if ($comment->getActorType() !== 'users') {
continue;
}

try {
$node = $this->getFileForComment($userFolder, $comment);
} catch (\Throwable $e) {
$this->logger->debug('Found comment for a file, but obtaining the file thrown an exception', ['exception' => $e]);
continue;
}

$actorId = $comment->getActorId();
$isUser = $this->userManager->userExists($actorId);

$avatarUrl = $isUser
? $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $result->authorId, 'size' => 42])
: $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $result->authorId, 'size' => 42]);
return new SearchResultEntry(
? $this->urlGenerator->linkToRouteAbsolute('core.avatar.getAvatar', ['userId' => $actorId, 'size' => 42])
: $this->urlGenerator->linkToRouteAbsolute('core.GuestAvatar.getAvatar', ['guestName' => $actorId, 'size' => 42]);

$path = $userFolder->getRelativePath($node->getPath());

// Use shortened link to centralize the various
// files/folder url redirection in files.View.showFile
$link = $this->urlGenerator->linkToRoute(
'files.View.showFile',
['fileid' => $node->getId()]
);

$searchResultEntry = new SearchResultEntry(
$avatarUrl,
$result->name,
$path,
$this->urlGenerator->linkToRouteAbsolute('files.view.index', [
'dir' => $pathInfo['dirname'],
'scrollto' => $pathInfo['basename'],
]),
$comment->getMessage(),
ltrim($path, '/'),
$this->urlGenerator->getAbsoluteURL($link),
'',
true
);
}, $this->legacyProvider->search($query->getTerm()))
);
$searchResultEntry->addAttribute('fileId', (string)$node->getId());
$searchResultEntry->addAttribute('path', $path);

$result[] = $searchResultEntry;
}

if (count($comments) < $limit) {
// Didn't find more comments when we tried to get, so there are no more comments.
break;
}

$offset += $limit;
$limit = $numComments - count($result);
}

return $result;
}

private function getFileForComment(Folder $userFolder, IComment $comment): Node {
$node = $userFolder->getFirstNodeById((int)$comment->getObjectId());
if ($node === null) {
throw new NotFoundException('File not found');
}

return $node;
}
}
164 changes: 164 additions & 0 deletions apps/comments/tests/Unit/Search/CommentsSearchProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Comments\Tests\Unit\Search;

use OC\Comments\Comment;
use OCA\Comments\Search\CommentsSearchProvider;
use OCP\Comments\IComment;
use OCP\Comments\ICommentsManager;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Search\IFilter;
use OCP\Search\ISearchQuery;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\NullLogger;
use Test\TestCase;

class CommentsSearchProviderTest extends TestCase {

private IUserManager&MockObject $userManager;
private IL10N&MockObject $l10n;
private IURLGenerator&MockObject $urlGenerator;
private ICommentsManager&MockObject $commentsManager;
private IRootFolder&MockObject $rootFolder;
private CommentsSearchProvider $provider;


protected function setUp(): void {
parent::setUp();

$this->userManager = $this->createMock(IUserManager::class);
$this->l10n = $this->createMock(IL10N::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->commentsManager = $this->createMock(ICommentsManager::class);
$this->rootFolder = $this->createMock(IRootFolder::class);

$userFolder = $this->createMock(Folder::class);
$userFolder->method('getFirstNodeById')->willReturnCallback(function (int $id) {
if ($id % 4 === 0) {
// Returning null for every fourth file to simulate a file not found case.
return null;
}
$node = $this->createMock(File::class);
$node->method('getId')->willReturn($id);
$node->method('getPath')->willReturn('/' . $id . '.txt');
return $node;
});
$userFolder->method('getRelativePath')->willReturnArgument(0);
$this->rootFolder->method('getUserFolder')->willReturn($userFolder);

$this->userManager->method('userExists')->willReturn(true);

$this->l10n->method('t')->willReturnArgument(0);

$this->provider = new CommentsSearchProvider(
$this->userManager,
$this->l10n,
$this->urlGenerator,
$this->commentsManager,
$this->rootFolder,
new NullLogger(),
);
}

public function testGetId(): void {
$this->assertEquals('comments', $this->provider->getId());
}

public function testGetName(): void {
$this->l10n->expects($this->once())
->method('t')
->with('Comments')
->willReturnArgument(0);

$this->assertEquals('Comments', $this->provider->getName());
}

public function testSearch(): void {
$this->commentsManager->method('search')->willReturnCallback(function (string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50) {
// The search method is call until 50 comments are found or there are no more comments to search.
$comments = [];
for ($i = 1; $i <= $limit; $i++) {
$comments[] = $this->mockComment(($offset + $i));
}
return $comments;
});
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('alice');
$searchTermFilter = $this->createMock(IFilter::class);
$searchTermFilter->method('get')->willReturn('search term');
$searchQuery = $this->createMock(ISearchQuery::class);
$searchQuery->method('getFilter')->willReturnCallback(function ($name) use ($searchTermFilter) {
return match ($name) {
'term' => $searchTermFilter,
default => null,
};
});

$result = $this->provider->search($user, $searchQuery);
$data = $result->jsonSerialize();

$this->assertCount(50, $data['entries']);
}

public function testSearchNoMoreComments(): void {
$this->commentsManager->method('search')->willReturnCallback(function (string $search, string $objectType, string $objectId, string $verb, int $offset, int $limit = 50) {
// Decrease the limit to simulate no more comments to search -> the break case.
if ($offset > 0) {
$limit--;
}
$comments = [];
for ($i = 1; $i <= $limit; $i++) {
$comments[] = $this->mockComment(($offset + $i));
}
return $comments;
});
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('alice');
$searchTermFilter = $this->createMock(IFilter::class);
$searchTermFilter->method('get')->willReturn('search term');
$searchQuery = $this->createMock(ISearchQuery::class);
$searchQuery->method('getFilter')->willReturnCallback(function ($name) use ($searchTermFilter) {
return match ($name) {
'term' => $searchTermFilter,
default => null,
};
});


$result = $this->provider->search($user, $searchQuery);
$data = $result->jsonSerialize();

$this->assertCount(46, $data['entries']);
}

private function mockComment(int $id): IComment {
return new Comment([
'id' => (string)$id,
'parent_id' => '0',
'topmost_parent_id' => '0',
'children_count' => 0,
'actor_type' => 'users',
'actor_id' => 'user' . $id,
'message' => 'Comment ' . $id,
'verb' => 'comment',
'creation_timestamp' => new \DateTime(),
'latest_child_timestamp' => null,
'object_type' => 'files',
'object_id' => (string)$id
]);
}

}
8 changes: 4 additions & 4 deletions apps/dav/tests/unit/Search/EventsSearchProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,14 @@ public function testSearch(): void {
$user = $this->createMock(IUser::class);
$user->method('getUID')->willReturn('john.doe');
$query = $this->createMock(ISearchQuery::class);
$seachTermFilter = $this->createMock(IFilter::class);
$query->method('getFilter')->willReturnCallback(function ($name) use ($seachTermFilter) {
$searchTermFilter = $this->createMock(IFilter::class);
$query->method('getFilter')->willReturnCallback(function ($name) use ($searchTermFilter) {
return match ($name) {
'term' => $seachTermFilter,
'term' => $searchTermFilter,
default => null,
};
});
$seachTermFilter->method('get')->willReturn('search term');
$searchTermFilter->method('get')->willReturn('search term');
$query->method('getLimit')->willReturn(5);
$query->method('getCursor')->willReturn(20);
$this->appManager->expects($this->once())
Expand Down
12 changes: 0 additions & 12 deletions build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,6 @@
<code><![CDATA[CommentsEvent::EVENT_PRE_UPDATE]]></code>
</DeprecatedConstant>
</file>
<file src="apps/comments/lib/Search/CommentsSearchProvider.php">
<DeprecatedClass>
<code><![CDATA[Result]]></code>
</DeprecatedClass>
<DeprecatedProperty>
<code><![CDATA[$result->authorId]]></code>
<code><![CDATA[$result->authorId]]></code>
<code><![CDATA[$result->authorId]]></code>
<code><![CDATA[$result->name]]></code>
<code><![CDATA[$result->path]]></code>
</DeprecatedProperty>
</file>
<file src="apps/comments/lib/Search/LegacyProvider.php">
<DeprecatedClass>
<code><![CDATA[Provider]]></code>
Expand Down
Loading