diff options
Diffstat (limited to 'apps/dav')
-rw-r--r-- | apps/dav/appinfo/v1/carddav.php | 1 | ||||
-rw-r--r-- | apps/dav/l10n/da.js | 2 | ||||
-rw-r--r-- | apps/dav/l10n/da.json | 2 | ||||
-rw-r--r-- | apps/dav/l10n/eu.js | 23 | ||||
-rw-r--r-- | apps/dav/l10n/eu.json | 23 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/IMipService.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/AddressBook.php | 4 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/AddressBookImpl.php | 4 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/CardDavBackend.php | 77 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/SyncService.php | 61 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/SystemAddressbook.php | 8 | ||||
-rw-r--r-- | apps/dav/lib/Connector/Sabre/File.php | 12 | ||||
-rw-r--r-- | apps/dav/lib/RootCollection.php | 3 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/AddressBookImplTest.php | 17 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/CardDavBackendTest.php | 21 | ||||
-rw-r--r-- | apps/dav/tests/unit/CardDAV/SyncServiceTest.php | 8 |
16 files changed, 207 insertions, 61 deletions
diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index bcd66e47090..415a5c9634a 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -63,6 +63,7 @@ $cardDavBackend = new CardDavBackend( Server::get(IUserManager::class), Server::get(IEventDispatcher::class), Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class), + Server::get(IConfig::class), ); $debugging = Server::get(IConfig::class)->getSystemValue('debug', false); diff --git a/apps/dav/l10n/da.js b/apps/dav/l10n/da.js index c99b8d10b6e..1368b899919 100644 --- a/apps/dav/l10n/da.js +++ b/apps/dav/l10n/da.js @@ -80,7 +80,7 @@ OC.L10N.register( "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I en måned på %1$s for hele dagen","Om %n måneder den %1$s for hele dagen"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I et år på %1$s for hele dagen","Om %n år den %1$s for hele dagen"], "In the past on %1$s between %2$s - %3$s" : "Tidligere den %1$s mellem %2$s - %3$s", - "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem% %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"], + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"], "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I en time på %1$s mellem %2$s - %3$s","Om %n timer den %1$s mellem %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I en dag på %1$s mellem %2$s - %3$s","Om %n dage den %1$s mellem %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I en uge på %1$s mellem %2$s - %3$s","Om %n uger den %1$s mellem %2$s - %3$s"], diff --git a/apps/dav/l10n/da.json b/apps/dav/l10n/da.json index fa934db0daa..723ebc26b7d 100644 --- a/apps/dav/l10n/da.json +++ b/apps/dav/l10n/da.json @@ -78,7 +78,7 @@ "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["I en måned på %1$s for hele dagen","Om %n måneder den %1$s for hele dagen"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["I et år på %1$s for hele dagen","Om %n år den %1$s for hele dagen"], "In the past on %1$s between %2$s - %3$s" : "Tidligere den %1$s mellem %2$s - %3$s", - "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem% %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"], + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["I et minut på %1$s mellem %2$s - %3$s","Om %n minutter den %1$s mellem %2$s - %3$s"], "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["I en time på %1$s mellem %2$s - %3$s","Om %n timer den %1$s mellem %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["I en dag på %1$s mellem %2$s - %3$s","Om %n dage den %1$s mellem %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["I en uge på %1$s mellem %2$s - %3$s","Om %n uger den %1$s mellem %2$s - %3$s"], diff --git a/apps/dav/l10n/eu.js b/apps/dav/l10n/eu.js index 4deb8172bed..df586e1958e 100644 --- a/apps/dav/l10n/eu.js +++ b/apps/dav/l10n/eu.js @@ -189,6 +189,7 @@ OC.L10N.register( "Second" : "Bigarrena", "Third" : "Hirugarrena", "Fourth" : "Laugarrena", + "Fifth" : "Bosgarrena", "Last" : "Azkena", "Second Last" : "Azken aurrekoa", "Third Last" : "Hirugarren azkena", @@ -248,6 +249,11 @@ OC.L10N.register( "Completed on %s" : "%s-an osatua", "Due on %s by %s" : "%s-(e)an epemuga %s-(e)k", "Due on %s" : "%s-(e)an epemuga", + "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ongi etorri Nextcloud Egutegira!\n\nHau gertaera erakusgarria da - aztertu plangintzaren malgutasuna Nextcloud Egutegiarekin nahi dituzun edizioak eginez!\n\nNextcloud Egutegia aukerarekin, hau egin dezakezu:\n- Sortu, editatu eta kudeatu gertaerak esfortzurik gabe.\n- Egutegi ugari sortu eta taldekideekin, lagunekin edo familiarekin partekatu.\n- Egiaztatu libre egotea eta bistaratu zure laneko orduak beste batzuei.\n- Aplikazio eta gailuekin arazorik gabe integratzea CalDAV bidez.\n- Zure esperientzia pertsonalizatu: gertaera errepikariak programatu, jakinarazpenak doitu eta bestelako ezarpenak.", + "Example event - open me!" : "Gertaera adibidea - ireki nazazu!", + "System Address Book" : "Sistemaren helbide-liburua", + "The system address book contains contact information for all users in your instance." : "Sistemaren helbide-liburuak zure instantziako erabiltzaile guztien kontaktu-informazioa dauka.", + "Enable System Address Book" : "Gaitu sistemaren helbide-liburua", "DAV system address book" : "DAV sistemaren helbide-liburua", "No outstanding DAV system address book sync." : "Ez dago DAV sistema helbide-liburuaren sinkronizazio arrarorik.", "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV sistemaren helbide-liburuaren sinkronizazioa oraindik ez da martxan jarri zure instantziak 1000 erabiltzaile baino gehiago dituelako edo akats bat gertatu delako. Mesedez, exekutatu eskuz \"occ dav:sync-system-addressbook\" deituz.", @@ -288,7 +294,22 @@ OC.L10N.register( "Cancel" : "Utzi", "Import" : "Inportatu", "Error while saving settings" : "Errorea ezarpenak gordetzean", + "Contact reset successfully" : "Kontaktua behar bezala berrezarri da", + "Error while resetting contact" : "Errorea kontaktua berrezartzean", + "Contact imported successfully" : "Kontaktua behar bezala inportatu da", + "Error while importing contact" : "Errorea kontaktua inportatzean", + "Import contact" : "Inportatu kontaktua", "Reset to default" : "Berezarri balio lehenetsira", + "Import contacts" : "Inportatu kontaktuak", + "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : ".vcf fitxategi berri bat inportatzean, lehendik dagoen kontaktu lehenetsia ezabatu eta berriarekin ordeztuko da. Jarraitu nahi duzu?", + "Failed to save example event creation setting" : "Adibide gertaeraren sortze ezarpenak gordetzeak huts egin du", + "Failed to upload the example event" : "Adibide gertaera igotzeak huts egin du", + "Custom example event was saved successfully" : "Adibide gertaera pertsonalizatua behar bezala gorde da", + "Failed to delete the custom example event" : "Adibide gertaera pertsonalizatua ezabatzeak huts egin du", + "Custom example event was deleted successfully" : "Adibide gertaera pertsonalizatua behar bezala ezabatu da.", + "Import calendar event" : "Inportatu egutegiko gertaera", + "Uploading a new event will overwrite the existing one." : "Gertaera berri bat igotzeak dagoena gainidatz dezake", + "Upload event" : "Igo gertaera", "Availability" : "Eskuragarritasuna", "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste pertsonek bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.", "Absence" : "Absentzia", @@ -305,6 +326,8 @@ OC.L10N.register( "Send reminder notifications to calendar sharees as well" : "Bidali gogorarazpen jakinarazpenak egutegi partekatzea dutenei ere", "Reminders are always sent to organizers and attendees." : "Gogorarazpenak beti bidaltzen zaizkie antolatzaileei eta parte-hartzaileei.", "Enable notifications for events via push" : "Gaitu push bidezko jakinarazpenak gertaerentzat", + "Example content" : "Adibideko edukia", + "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Adibideko edukiak Nextcloud-en ezaugarriak erakusteko balio du. Eduki lehenetsia Nextcloud-ekin bidaltzen da, eta eduki pertsonalizatuarekin ordezka daiteke.", "There was an error updating your attendance status." : "Errore bat gertatu da zure parte-hartze egoera eguneratzerakoan.", "Please contact the organizer directly." : "Mesedez jarri harremanetan antolatzailearekin zuzenean.", "Are you accepting the invitation?" : "Gonbidapena onartzen duzu?", diff --git a/apps/dav/l10n/eu.json b/apps/dav/l10n/eu.json index ac11ede49c3..5d312d36973 100644 --- a/apps/dav/l10n/eu.json +++ b/apps/dav/l10n/eu.json @@ -187,6 +187,7 @@ "Second" : "Bigarrena", "Third" : "Hirugarrena", "Fourth" : "Laugarrena", + "Fifth" : "Bosgarrena", "Last" : "Azkena", "Second Last" : "Azken aurrekoa", "Third Last" : "Hirugarren azkena", @@ -246,6 +247,11 @@ "Completed on %s" : "%s-an osatua", "Due on %s by %s" : "%s-(e)an epemuga %s-(e)k", "Due on %s" : "%s-(e)an epemuga", + "Welcome to Nextcloud Calendar!\n\nThis is a sample event - explore the flexibility of planning with Nextcloud Calendar by making any edits you want!\n\nWith Nextcloud Calendar, you can:\n- Create, edit, and manage events effortlessly.\n- Create multiple calendars and share them with teammates, friends, or family.\n- Check availability and display your busy times to others.\n- Seamlessly integrate with apps and devices via CalDAV.\n- Customize your experience: schedule recurring events, adjust notifications and other settings." : "Ongi etorri Nextcloud Egutegira!\n\nHau gertaera erakusgarria da - aztertu plangintzaren malgutasuna Nextcloud Egutegiarekin nahi dituzun edizioak eginez!\n\nNextcloud Egutegia aukerarekin, hau egin dezakezu:\n- Sortu, editatu eta kudeatu gertaerak esfortzurik gabe.\n- Egutegi ugari sortu eta taldekideekin, lagunekin edo familiarekin partekatu.\n- Egiaztatu libre egotea eta bistaratu zure laneko orduak beste batzuei.\n- Aplikazio eta gailuekin arazorik gabe integratzea CalDAV bidez.\n- Zure esperientzia pertsonalizatu: gertaera errepikariak programatu, jakinarazpenak doitu eta bestelako ezarpenak.", + "Example event - open me!" : "Gertaera adibidea - ireki nazazu!", + "System Address Book" : "Sistemaren helbide-liburua", + "The system address book contains contact information for all users in your instance." : "Sistemaren helbide-liburuak zure instantziako erabiltzaile guztien kontaktu-informazioa dauka.", + "Enable System Address Book" : "Gaitu sistemaren helbide-liburua", "DAV system address book" : "DAV sistemaren helbide-liburua", "No outstanding DAV system address book sync." : "Ez dago DAV sistema helbide-liburuaren sinkronizazio arrarorik.", "The DAV system address book sync has not run yet as your instance has more than 1000 users or because an error occurred. Please run it manually by calling \"occ dav:sync-system-addressbook\"." : "DAV sistemaren helbide-liburuaren sinkronizazioa oraindik ez da martxan jarri zure instantziak 1000 erabiltzaile baino gehiago dituelako edo akats bat gertatu delako. Mesedez, exekutatu eskuz \"occ dav:sync-system-addressbook\" deituz.", @@ -286,7 +292,22 @@ "Cancel" : "Utzi", "Import" : "Inportatu", "Error while saving settings" : "Errorea ezarpenak gordetzean", + "Contact reset successfully" : "Kontaktua behar bezala berrezarri da", + "Error while resetting contact" : "Errorea kontaktua berrezartzean", + "Contact imported successfully" : "Kontaktua behar bezala inportatu da", + "Error while importing contact" : "Errorea kontaktua inportatzean", + "Import contact" : "Inportatu kontaktua", "Reset to default" : "Berezarri balio lehenetsira", + "Import contacts" : "Inportatu kontaktuak", + "Importing a new .vcf file will delete the existing default contact and replace it with the new one. Do you want to continue?" : ".vcf fitxategi berri bat inportatzean, lehendik dagoen kontaktu lehenetsia ezabatu eta berriarekin ordeztuko da. Jarraitu nahi duzu?", + "Failed to save example event creation setting" : "Adibide gertaeraren sortze ezarpenak gordetzeak huts egin du", + "Failed to upload the example event" : "Adibide gertaera igotzeak huts egin du", + "Custom example event was saved successfully" : "Adibide gertaera pertsonalizatua behar bezala gorde da", + "Failed to delete the custom example event" : "Adibide gertaera pertsonalizatua ezabatzeak huts egin du", + "Custom example event was deleted successfully" : "Adibide gertaera pertsonalizatua behar bezala ezabatu da.", + "Import calendar event" : "Inportatu egutegiko gertaera", + "Uploading a new event will overwrite the existing one." : "Gertaera berri bat igotzeak dagoena gainidatz dezake", + "Upload event" : "Igo gertaera", "Availability" : "Eskuragarritasuna", "If you configure your working hours, other people will see when you are out of office when they book a meeting." : "Zure lan orduak konfiguratzen badituzu, beste pertsonek bulegotik kanpo zaudela ikusiko dute bilera bat erreserbatzen dutenean.", "Absence" : "Absentzia", @@ -303,6 +324,8 @@ "Send reminder notifications to calendar sharees as well" : "Bidali gogorarazpen jakinarazpenak egutegi partekatzea dutenei ere", "Reminders are always sent to organizers and attendees." : "Gogorarazpenak beti bidaltzen zaizkie antolatzaileei eta parte-hartzaileei.", "Enable notifications for events via push" : "Gaitu push bidezko jakinarazpenak gertaerentzat", + "Example content" : "Adibideko edukia", + "Example content serves to showcase the features of Nextcloud. Default content is shipped with Nextcloud, and can be replaced by custom content." : "Adibideko edukiak Nextcloud-en ezaugarriak erakusteko balio du. Eduki lehenetsia Nextcloud-ekin bidaltzen da, eta eduki pertsonalizatuarekin ordezka daiteke.", "There was an error updating your attendance status." : "Errore bat gertatu da zure parte-hartze egoera eguneratzerakoan.", "Please contact the organizer directly." : "Mesedez jarri harremanetan antolatzailearekin zuzenean.", "Are you accepting the invitation?" : "Gonbidapena onartzen duzu?", diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index f7054eb2d34..54c0bc31849 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -1110,7 +1110,7 @@ class IMipService { $sequence = $iTipMessage->sequence; $recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : null; - $uid = $vevent->{'UID'}; + $uid = $vevent->{'UID'}?->getValue(); $query = $this->db->getQueryBuilder(); $query->insert('calendar_invitations') diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index d2391880585..4d30d507a7d 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -8,7 +8,6 @@ namespace OCA\DAV\CardDAV; use OCA\DAV\DAV\Sharing\IShareable; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCP\DB\Exception; use OCP\IL10N; use OCP\Server; @@ -234,9 +233,6 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable, IMov } public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } return parent::getChanges($syncToken, $syncLevel, $limit); } diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index 6bb8e24f628..ae77498539b 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -152,6 +152,10 @@ class AddressBookImpl implements IAddressBookEnabled { $permissions = $this->addressBook->getACL(); $result = 0; foreach ($permissions as $permission) { + if ($this->addressBookInfo['principaluri'] !== $permission['principal']) { + continue; + } + switch ($permission['privilege']) { case '{DAV:}read': $result |= Constants::PERMISSION_READ; diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 06bd8d8ee2c..a78686eb61d 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -23,6 +23,7 @@ use OCP\AppFramework\Db\TTransactional; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; use PDO; @@ -59,6 +60,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { private IUserManager $userManager, private IEventDispatcher $dispatcher, private Sharing\Backend $sharingBackend, + private IConfig $config, ) { } @@ -851,6 +853,8 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return array */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { + $maxLimit = $this->config->getSystemValueInt('carddav_sync_request_truncation', 2500); + $limit = ($limit === null) ? $maxLimit : min($limit, $maxLimit); // Current synctoken return $this->atomic(function () use ($addressBookId, $syncToken, $syncLevel, $limit) { $qb = $this->db->getQueryBuilder(); @@ -873,10 +877,35 @@ class CardDavBackend implements BackendInterface, SyncSupport { 'modified' => [], 'deleted' => [], ]; - - if ($syncToken) { + if (str_starts_with($syncToken, 'init_')) { + $syncValues = explode('_', $syncToken); + $lastID = $syncValues[1]; + $initialSyncToken = $syncValues[2]; $qb = $this->db->getQueryBuilder(); - $qb->select('uri', 'operation') + $qb->select('id', 'uri') + ->from('cards') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)), + $qb->expr()->gt('id', $qb->createNamedParameter($lastID))) + )->orderBy('id') + ->setMaxResults($limit); + $stmt = $qb->executeQuery(); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + if (count($values) === 0) { + $result['syncToken'] = $initialSyncToken; + $result['result_truncated'] = false; + $result['added'] = []; + } else { + $lastID = $values[array_key_last($values)]['id']; + $result['added'] = array_column($values, 'uri'); + $result['syncToken'] = count($result['added']) >= $limit ? "init_{$lastID}_$initialSyncToken" : $initialSyncToken ; + $result['result_truncated'] = count($result['added']) >= $limit; + } + } elseif ($syncToken) { + $qb = $this->db->getQueryBuilder(); + $qb->select('uri', 'operation', 'synctoken') ->from('addressbookchanges') ->where( $qb->expr()->andX( @@ -886,22 +915,31 @@ class CardDavBackend implements BackendInterface, SyncSupport { ) )->orderBy('synctoken'); - if (is_int($limit) && $limit > 0) { + if ($limit > 0) { $qb->setMaxResults($limit); } // Fetching all changes $stmt = $qb->executeQuery(); + $rowCount = $stmt->rowCount(); $changes = []; + $highestSyncToken = 0; // This loop ensures that any duplicates are overwritten, only the // last change on a node is relevant. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { $changes[$row['uri']] = $row['operation']; + $highestSyncToken = $row['synctoken']; } + $stmt->closeCursor(); + // No changes found, use current token + if (empty($changes)) { + $result['syncToken'] = $currentToken; + } + foreach ($changes as $uri => $operation) { switch ($operation) { case 1: @@ -915,16 +953,43 @@ class CardDavBackend implements BackendInterface, SyncSupport { break; } } + + /* + * The synctoken in oc_addressbooks is always the highest synctoken in oc_addressbookchanges for a given addressbook plus one (see addChange). + * + * For truncated results, it is expected that we return the highest token from the response, so the client can continue from the latest change. + * + * For non-truncated results, it is expected to return the currentToken. If we return the highest token, as with truncated results, the client will always think it is one change behind. + * + * Therefore, we differentiate between truncated and non-truncated results when returning the synctoken. + */ + if ($rowCount === $limit && $highestSyncToken < $currentToken) { + $result['syncToken'] = $highestSyncToken; + $result['result_truncated'] = true; + } } else { $qb = $this->db->getQueryBuilder(); - $qb->select('uri') + $qb->select('id', 'uri') ->from('cards') ->where( $qb->expr()->eq('addressbookid', $qb->createNamedParameter($addressBookId)) ); // No synctoken supplied, this is the initial sync. + $qb->setMaxResults($limit); $stmt = $qb->executeQuery(); - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + $values = $stmt->fetchAll(\PDO::FETCH_ASSOC); + if (empty($values)) { + $result['added'] = []; + return $result; + } + $lastID = $values[array_key_last($values)]['id']; + if (count($values) >= $limit) { + $result['syncToken'] = 'init_' . $lastID . '_' . $currentToken; + $result['result_truncated'] = true; + } + + $result['added'] = array_column($values, 'uri'); + $stmt->closeCursor(); } return $result; diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 4a75f8ced6c..e6da3ed5923 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -22,6 +22,7 @@ use Psr\Log\LoggerInterface; use Sabre\DAV\Xml\Response\MultiStatus; use Sabre\DAV\Xml\Service; use Sabre\VObject\Reader; +use Sabre\Xml\ParseException; use function is_null; class SyncService { @@ -43,9 +44,10 @@ class SyncService { } /** + * @psalm-return list{0: ?string, 1: boolean} * @throws \Exception */ - public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string { + public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): array { // 1. create addressbook $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties); $addressBookId = $book['id']; @@ -83,7 +85,10 @@ class SyncService { } } - return $response['token']; + return [ + $response['token'], + $response['truncated'], + ]; } /** @@ -127,7 +132,7 @@ class SyncService { private function prepareUri(string $host, string $path): string { /* - * The trailing slash is important for merging the uris together. + * The trailing slash is important for merging the uris. * * $host is stored in oc_trusted_servers.url and usually without a trailing slash. * @@ -158,7 +163,9 @@ class SyncService { } /** + * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool} * @throws ClientExceptionInterface + * @throws ParseException */ protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array { $client = $this->clientService->newClient(); @@ -181,7 +188,7 @@ class SyncService { $body = $response->getBody(); assert(is_string($body)); - return $this->parseMultiStatus($body); + return $this->parseMultiStatus($body, $addressBookUrl); } protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string { @@ -219,22 +226,50 @@ class SyncService { } /** - * @param string $body - * @return array - * @throws \Sabre\Xml\ParseException + * @return array{response: array<string, array<array-key, mixed>>, token: ?string, truncated: bool} + * @throws ParseException */ - private function parseMultiStatus($body) { - $xml = new Service(); - + private function parseMultiStatus(string $body, string $addressBookUrl): array { /** @var MultiStatus $multiStatus */ - $multiStatus = $xml->expect('{DAV:}multistatus', $body); + $multiStatus = (new Service())->expect('{DAV:}multistatus', $body); $result = []; + $truncated = false; + foreach ($multiStatus->getResponses() as $response) { - $result[$response->getHref()] = $response->getResponseProperties(); + $href = $response->getHref(); + if ($response->getHttpStatus() === '507' && $this->isResponseForRequestUri($href, $addressBookUrl)) { + $truncated = true; + } else { + $result[$response->getHref()] = $response->getResponseProperties(); + } } - return ['response' => $result, 'token' => $multiStatus->getSyncToken()]; + return ['response' => $result, 'token' => $multiStatus->getSyncToken(), 'truncated' => $truncated]; + } + + /** + * Determines whether the provided response URI corresponds to the given request URI. + */ + private function isResponseForRequestUri(string $responseUri, string $requestUri): bool { + /* + * Example response uri: + * + * /remote.php/dav/addressbooks/system/system/system/ + * /cloud/remote.php/dav/addressbooks/system/system/system/ (when installed in a subdirectory) + * + * Example request uri: + * + * remote.php/dav/addressbooks/system/system/system + * + * References: + * https://github.com/nextcloud/3rdparty/blob/e0a509739b13820f0a62ff9cad5d0fede00e76ee/sabre/dav/lib/DAV/Sync/Plugin.php#L172-L174 + * https://github.com/nextcloud/server/blob/b40acb34a39592070d8455eb91c5364c07928c50/apps/federation/lib/SyncFederationAddressBooks.php#L41 + */ + return str_ends_with( + rtrim($responseUri, '/'), + rtrim($requestUri, '/') + ); } /** diff --git a/apps/dav/lib/CardDAV/SystemAddressbook.php b/apps/dav/lib/CardDAV/SystemAddressbook.php index e0032044e70..912a2f1dcee 100644 --- a/apps/dav/lib/CardDAV/SystemAddressbook.php +++ b/apps/dav/lib/CardDAV/SystemAddressbook.php @@ -8,7 +8,6 @@ declare(strict_types=1); */ namespace OCA\DAV\CardDAV; -use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException; use OCA\Federation\TrustedServers; use OCP\Accounts\IAccountManager; use OCP\IConfig; @@ -212,14 +211,7 @@ class SystemAddressbook extends AddressBook { } return new Card($this->carddavBackend, $this->addressBookInfo, $obj); } - - /** - * @throws UnsupportedLimitOnInitialSyncException - */ public function getChanges($syncToken, $syncLevel, $limit = null) { - if (!$syncToken && $limit) { - throw new UnsupportedLimitOnInitialSyncException(); - } if (!$this->carddavBackend instanceof SyncSupport) { return null; diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index 218d38e1c4b..d2a71eb3e7b 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -204,6 +204,9 @@ class File extends Node implements IFile { } } + $lengthHeader = $this->request->getHeader('content-length'); + $expected = $lengthHeader !== '' ? (int)$lengthHeader : null; + if ($partStorage->instanceOfStorage(IWriteStreamStorage::class)) { $isEOF = false; $wrappedData = CallbackWrapper::wrap($data, null, null, null, null, function ($stream) use (&$isEOF): void { @@ -215,7 +218,7 @@ class File extends Node implements IFile { $count = -1; try { /** @var IWriteStreamStorage $partStorage */ - $count = $partStorage->writeStream($internalPartPath, $wrappedData); + $count = $partStorage->writeStream($internalPartPath, $wrappedData, $expected); } catch (GenericFileException $e) { $logger = Server::get(LoggerInterface::class); $logger->error('Error while writing stream to storage: ' . $e->getMessage(), ['exception' => $e, 'app' => 'webdav']); @@ -235,10 +238,7 @@ class File extends Node implements IFile { [$count, $result] = Files::streamCopy($data, $target, true); fclose($target); } - - $lengthHeader = $this->request->getHeader('content-length'); - $expected = $lengthHeader !== '' ? (int)$lengthHeader : -1; - if ($result === false && $expected >= 0) { + if ($result === false && $expected !== null) { throw new Exception( $this->l10n->t( 'Error while copying file to target location (copied: %1$s, expected filesize: %2$s)', @@ -253,7 +253,7 @@ class File extends Node implements IFile { // if content length is sent by client: // double check if the file was fully received // compare expected and actual size - if ($expected >= 0 + if ($expected !== null && $expected !== $count && $this->request->getMethod() === 'PUT' ) { diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index f1963c0ef01..870aa0d4540 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -132,6 +132,7 @@ class RootCollection extends SimpleCollection { ); $contactsSharingBackend = Server::get(\OCA\DAV\CardDAV\Sharing\Backend::class); + $config = Server::get(IConfig::class); $pluginManager = new PluginManager(\OC::$server, Server::get(IAppManager::class)); $usersCardDavBackend = new CardDavBackend( @@ -140,6 +141,7 @@ class RootCollection extends SimpleCollection { $userManager, $dispatcher, $contactsSharingBackend, + $config ); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; @@ -150,6 +152,7 @@ class RootCollection extends SimpleCollection { $userManager, $dispatcher, $contactsSharingBackend, + $config ); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, $userSession->getUser(), $groupManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php index f7daeb41cca..74699cf3925 100644 --- a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php +++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php @@ -241,14 +241,15 @@ class AddressBookImplTest extends TestCase { public static function dataTestGetPermissions(): array { return [ [[], 0], - [[['privilege' => '{DAV:}read']], 1], - [[['privilege' => '{DAV:}write']], 6], - [[['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 7], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}write']], 31], - [[['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write'],['privilege' => '{DAV:}all']], 31], - [[['privilege' => '{DAV:}all'],['privilege' => '{DAV:}read'],['privilege' => '{DAV:}write']], 31], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system']], 1], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'], ['privilege' => '{DAV:}write', 'principal' => 'principals/someone/else']], 1], + [[['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 6], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 7], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}all', 'principal' => 'principals/system/system']], 31], + [[['privilege' => '{DAV:}all', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}read', 'principal' => 'principals/system/system'],['privilege' => '{DAV:}write', 'principal' => 'principals/system/system']], 31], ]; } diff --git a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php index 1966a8d8c9a..c5eafa0764a 100644 --- a/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php +++ b/apps/dav/tests/unit/CardDAV/CardDavBackendTest.php @@ -50,6 +50,7 @@ class CardDavBackendTest extends TestCase { private IUserManager&MockObject $userManager; private IGroupManager&MockObject $groupManager; private IEventDispatcher&MockObject $dispatcher; + private IConfig&MockObject $config; private Backend $sharingBackend; private IDBConnection $db; private CardDavBackend $backend; @@ -96,6 +97,7 @@ class CardDavBackendTest extends TestCase { $this->userManager = $this->createMock(IUserManager::class); $this->groupManager = $this->createMock(IGroupManager::class); + $this->config = $this->createMock(IConfig::class); $this->principal = $this->getMockBuilder(Principal::class) ->setConstructorArgs([ $this->userManager, @@ -106,7 +108,7 @@ class CardDavBackendTest extends TestCase { $this->createMock(IAppManager::class), $this->createMock(ProxyMapper::class), $this->createMock(KnownUserService::class), - $this->createMock(IConfig::class), + $this->config, $this->createMock(IFactory::class) ]) ->onlyMethods(['getPrincipalByPath', 'getGroupMembership', 'findByUri']) @@ -135,6 +137,7 @@ class CardDavBackendTest extends TestCase { $this->userManager, $this->dispatcher, $this->sharingBackend, + $this->config, ); // start every test with a empty cards_properties and cards table $query = $this->db->getQueryBuilder(); @@ -231,7 +234,7 @@ class CardDavBackendTest extends TestCase { public function testCardOperations(): void { /** @var CardDavBackend&MockObject $backend */ $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) ->onlyMethods(['updateProperties', 'purgeProperties']) ->getMock(); @@ -291,7 +294,7 @@ class CardDavBackendTest extends TestCase { public function testMultiCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) ->onlyMethods(['updateProperties']) ->getMock(); @@ -345,7 +348,7 @@ class CardDavBackendTest extends TestCase { public function testMultipleUIDOnDifferentAddressbooks(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend,$this->config]) ->onlyMethods(['updateProperties']) ->getMock(); @@ -368,7 +371,7 @@ class CardDavBackendTest extends TestCase { public function testMultipleUIDDenied(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods(['updateProperties']) ->getMock(); @@ -390,7 +393,7 @@ class CardDavBackendTest extends TestCase { public function testNoValidUID(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods(['updateProperties']) ->getMock(); @@ -408,7 +411,7 @@ class CardDavBackendTest extends TestCase { public function testDeleteWithoutCard(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods([ 'getCardId', 'addChange', @@ -453,7 +456,7 @@ class CardDavBackendTest extends TestCase { public function testSyncSupport(): void { $this->backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods(['updateProperties']) ->getMock(); @@ -522,7 +525,7 @@ class CardDavBackendTest extends TestCase { $cardId = 2; $backend = $this->getMockBuilder(CardDavBackend::class) - ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend]) + ->setConstructorArgs([$this->db, $this->principal, $this->userManager, $this->dispatcher, $this->sharingBackend, $this->config]) ->onlyMethods(['getCardId'])->getMock(); $backend->expects($this->any())->method('getCardId')->willReturn($cardId); diff --git a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php index ea4886a67e6..77caed336f4 100644 --- a/apps/dav/tests/unit/CardDAV/SyncServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/SyncServiceTest.php @@ -108,7 +108,7 @@ class SyncServiceTest extends TestCase { '1', 'principals/system/system', [] - ); + )[0]; $this->assertEquals('http://sabre.io/ns/sync/1', $token); } @@ -179,7 +179,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )[0]; $this->assertEquals('http://sabre.io/ns/sync/2', $token); } @@ -250,7 +250,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )[0]; $this->assertEquals('http://sabre.io/ns/sync/3', $token); } @@ -291,7 +291,7 @@ END:VCARD'; '1', 'principals/system/system', [] - ); + )[0]; $this->assertEquals('http://sabre.io/ns/sync/4', $token); } |