diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 978646e43337f..18a7c493dbc21 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -1207,13 +1207,16 @@ public function searchPrincipalUri(string $principalUri, * } $options * @return array */ - private function searchByAddressBookIds(array $addressBookIds, + private function searchByAddressBookIds( + array $addressBookIds, string $pattern, array $searchProperties, - array $options = []): array { + array $options = [], + ): array { if (empty($addressBookIds)) { return []; } + $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; $useWildcards = !\array_key_exists('wildcard', $options) || $options['wildcard'] !== false; @@ -1254,6 +1257,9 @@ private function searchByAddressBookIds(array $addressBookIds, $query2->andWhere($query2->expr()->ilike('cp.value', $query2->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); } } + if (isset($options['limit']) || isset($options['offset'])) { + $query2->orderBy('cp.cardid', 'ASC'); + } if (isset($options['limit'])) { $query2->setMaxResults($options['limit']); } @@ -1300,6 +1306,10 @@ private function searchByAddressBookIds(array $addressBookIds, ->from($this->dbCardsTable, 'c') ->where($query->expr()->in('c.id', $query->createParameter('matches'))); + if (isset($options['limit']) || isset($options['offset'])) { + $query->orderBy('c.id', 'ASC'); + } + foreach (array_chunk($matches, IQueryBuilder::MAX_IN_PARAMETERS) as $matchesChunk) { $query->setParameter('matches', $matchesChunk, IQueryBuilder::PARAM_INT_ARRAY); $result = $query->executeQuery(); diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php index df7b99a84bcc1..4dd2de9e8d982 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -682,6 +682,52 @@ public function testGetCardIdFailed(): void { #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataTestSearch')] public function testSearch(string $pattern, array $properties, array $options, array $expectedUris, array $expectedNeedles): void { + $this->seedSearchCards(); + + $result = $this->backend->search(0, $pattern, $properties, $options); + + $this->assertCount(count($expectedUris), $result); + + $actualUris = array_map(static fn (array $row): string => $row['uri'], $result); + sort($actualUris); + $expectedSortedUris = $expectedUris; + sort($expectedSortedUris); + + $this->assertSame($expectedSortedUris, $actualUris, 'Search returned unexpected URIs'); + + $expectedByUri = array_combine($expectedUris, $expectedNeedles); + $this->assertIsArray($expectedByUri); + + foreach ($result as $row) { + $this->assertArrayHasKey($row['uri'], $expectedByUri, 'Unexpected URI in search result'); + $this->assertNotFalse( + strpos($row['carddata'], $expectedByUri[$row['uri']]), + 'Returned carddata does not contain expected fragment for ' . $row['uri'] + ); + } + } + + public function testSearchPaginationUsesCardIdOrder(): void { + $this->seedSearchCards(); + + $firstPage = $this->backend->search(0, 'john', ['FN'], [ + 'limit' => 1, + 'offset' => 0, + ]); + $secondPage = $this->backend->search(0, 'john', ['FN'], [ + 'limit' => 1, + 'offset' => 1, + ]); + $twoResults = $this->backend->search(0, 'john', ['FN'], [ + 'limit' => 2, + ]); + + self::assertSame(['uri0'], array_column($firstPage, 'uri')); + self::assertSame(['uri1'], array_column($secondPage, 'uri')); + self::assertSame(['uri0', 'uri1'], array_column($twoResults, 'uri')); + } + + private function seedSearchCards(): void { $vCards = []; $vCards[0] = new VCard(); @@ -702,16 +748,14 @@ public function testSearch(string $pattern, array $properties, array $options, a $query = $this->db->getQueryBuilder(); for ($i = 0; $i < 3; $i++) { $query->insert($this->dbCardsTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB), - 'uri' => $query->createNamedParameter('uri' . $i), - 'lastmodified' => $query->createNamedParameter(time()), - 'etag' => $query->createNamedParameter('etag' . $i), - 'size' => $query->createNamedParameter(120), - ] - ); + ->values([ + 'addressbookid' => $query->createNamedParameter(0), + 'carddata' => $query->createNamedParameter($vCards[$i]->serialize(), IQueryBuilder::PARAM_LOB), + 'uri' => $query->createNamedParameter('uri' . $i), + 'lastmodified' => $query->createNamedParameter(time()), + 'etag' => $query->createNamedParameter('etag' . $i), + 'size' => $query->createNamedParameter(120), + ]); $query->executeStatement(); $vCardIds[] = $query->getLastInsertId(); } @@ -727,39 +771,15 @@ public function testSearch(string $pattern, array $properties, array $options, a foreach ($propertyRows as [$cardId, $name, $value]) { $query = $this->db->getQueryBuilder(); $query->insert($this->dbCardsPropertiesTable) - ->values( - [ - 'addressbookid' => $query->createNamedParameter(0), - 'cardid' => $query->createNamedParameter($cardId), - 'name' => $query->createNamedParameter($name), - 'value' => $query->createNamedParameter($value), - 'preferred' => $query->createNamedParameter(0), - ] - ); + ->values([ + 'addressbookid' => $query->createNamedParameter(0), + 'cardid' => $query->createNamedParameter($cardId), + 'name' => $query->createNamedParameter($name), + 'value' => $query->createNamedParameter($value), + 'preferred' => $query->createNamedParameter(0), + ]); $query->executeStatement(); } - - $result = $this->backend->search(0, $pattern, $properties, $options); - - $this->assertCount(count($expectedUris), $result); - - $actualUris = array_map(static fn (array $row): string => $row['uri'], $result); - sort($actualUris); - $expectedSortedUris = $expectedUris; - sort($expectedSortedUris); - - $this->assertSame($expectedSortedUris, $actualUris, 'Search returned unexpected URIs'); - - $expectedByUri = array_combine($expectedUris, $expectedNeedles); - $this->assertIsArray($expectedByUri); - - foreach ($result as $row) { - $this->assertArrayHasKey($row['uri'], $expectedByUri, 'Unexpected URI in search result'); - $this->assertNotFalse( - strpos($row['carddata'], $expectedByUri[$row['uri']]), - 'Returned carddata does not contain expected fragment for ' . $row['uri'] - ); - } } public static function dataTestSearch(): array {