aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib')
-rw-r--r--apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php35
-rw-r--r--apps/dav/lib/CalDAV/CalDavBackend.php212
-rw-r--r--apps/dav/lib/CalDAV/Status/StatusService.php27
-rw-r--r--apps/dav/lib/Migration/DeleteSchedulingObjects.php38
-rw-r--r--apps/dav/lib/Migration/Version1004Date20170825134824.php1
5 files changed, 259 insertions, 54 deletions
diff --git a/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php
new file mode 100644
index 00000000000..fa53a8be4f0
--- /dev/null
+++ b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php
@@ -0,0 +1,35 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\BackgroundJob;
+
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use Psr\Log\LoggerInterface;
+
+class DeleteOutdatedSchedulingObjects extends TimedJob {
+ public function __construct(
+ private CalDavBackend $calDavBackend,
+ private LoggerInterface $logger,
+ ITimeFactory $timeFactory,
+ ) {
+ parent::__construct($timeFactory);
+ $this->setInterval(23 * 60 * 60);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
+ }
+
+ /**
+ * @param array $argument
+ */
+ protected function run($argument): void {
+ $time = $this->time->getTime() - (60 * 60);
+ $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000);
+ $this->logger->info("Removed outdated scheduling objects");
+ }
+}
diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php
index 0aa4426c786..58deffc1536 100644
--- a/apps/dav/lib/CalDAV/CalDavBackend.php
+++ b/apps/dav/lib/CalDAV/CalDavBackend.php
@@ -40,6 +40,7 @@
namespace OCA\DAV\CalDAV;
use DateTime;
+use DateTimeImmutable;
use DateTimeInterface;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\Sharing\Backend;
@@ -1920,15 +1921,34 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$this->db->escapeLikeParameter($pattern) . '%')));
}
- if (isset($options['timerange'])) {
- if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence',
- $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp())));
- }
- if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
- $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence',
- $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp())));
- }
+ $start = null;
+ $end = null;
+
+ $hasLimit = is_int($limit);
+ $hasTimeRange = false;
+
+ if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $start */
+ $start = $options['timerange']['start'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->gt(
+ 'lastoccurence',
+ $outerQuery->createNamedParameter($start->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
+ }
+
+ if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
+ /** @var DateTimeInterface $end */
+ $end = $options['timerange']['end'];
+ $outerQuery->andWhere(
+ $outerQuery->expr()->lt(
+ 'firstoccurence',
+ $outerQuery->createNamedParameter($end->getTimestamp())
+ )
+ );
+ $hasTimeRange = true;
}
if (isset($options['uid'])) {
@@ -1946,54 +1966,46 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
$outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
- if ($offset) {
- $outerQuery->setFirstResult($offset);
- }
- if ($limit) {
- $outerQuery->setMaxResults($limit);
- }
+ // Without explicit order by its undefined in which order the SQL server returns the events.
+ // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
+ $outerQuery->addOrderBy('id');
- $result = $outerQuery->executeQuery();
- $calendarObjects = [];
- while (($row = $result->fetch()) !== false) {
- $start = $options['timerange']['start'] ?? null;
- $end = $options['timerange']['end'] ?? null;
+ $offset = (int)$offset;
+ $outerQuery->setFirstResult($offset);
- if ($start === null || !($start instanceof DateTimeInterface) || $end === null || !($end instanceof DateTimeInterface)) {
- // No filter required
- $calendarObjects[] = $row;
- continue;
- }
+ $calendarObjects = [];
- $isValid = $this->validateFilterForObject($row, [
- 'name' => 'VCALENDAR',
- 'comp-filters' => [
- [
- 'name' => 'VEVENT',
- 'comp-filters' => [],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => [
- 'start' => $start,
- 'end' => $end,
- ],
- ],
- ],
- 'prop-filters' => [],
- 'is-not-defined' => false,
- 'time-range' => null,
- ]);
- if (is_resource($row['calendardata'])) {
- // Put the stream back to the beginning so it can be read another time
- rewind($row['calendardata']);
- }
- if ($isValid) {
- $calendarObjects[] = $row;
- }
+ if ($hasLimit && $hasTimeRange) {
+ /**
+ * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
+ *
+ * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
+ * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
+ *
+ * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
+ * and discard the events after evaluating the reoccurrence rules because they are not due within
+ * the next 14 days and end up with an empty result even if there are two events to show.
+ *
+ * The workaround for search requests with a limit and time range is asking for more row than requested
+ * and retrying if we have not reached the limit.
+ *
+ * 25 rows and 3 retries is entirely arbitrary.
+ */
+ $maxResults = (int)max($limit, 25);
+ $outerQuery->setMaxResults($maxResults);
+
+ for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
+ $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
+ $outerQuery->setFirstResult($offset += $maxResults);
+ }
+
+ $calendarObjects = array_slice($calendarObjects, 0, $limit, false);
+ } else {
+ $outerQuery->setMaxResults($limit);
+ $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
}
- $result->closeCursor();
- return array_map(function ($o) use ($options) {
+ $calendarObjects = array_map(function ($o) use ($options) {
$calendarData = Reader::read($o['calendardata']);
// Expand recurrences if an explicit time range is requested
@@ -2029,6 +2041,64 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}, $timezones),
];
}, $calendarObjects);
+
+ usort($calendarObjects, function (array $a, array $b) {
+ /** @var DateTimeImmutable $startA */
+ $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+ /** @var DateTimeImmutable $startB */
+ $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
+
+ return $startA->getTimestamp() <=> $startB->getTimestamp();
+ });
+
+ return $calendarObjects;
+ }
+
+ private function searchCalendarObjects(IQueryBuilder $query, DateTimeInterface|null $start, DateTimeInterface|null $end): array {
+ $calendarObjects = [];
+ $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
+
+ $result = $query->executeQuery();
+
+ while (($row = $result->fetch()) !== false) {
+ if ($filterByTimeRange === false) {
+ // No filter required
+ $calendarObjects[] = $row;
+ continue;
+ }
+
+ $isValid = $this->validateFilterForObject($row, [
+ 'name' => 'VCALENDAR',
+ 'comp-filters' => [
+ [
+ 'name' => 'VEVENT',
+ 'comp-filters' => [],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => [
+ 'start' => $start,
+ 'end' => $end,
+ ],
+ ],
+ ],
+ 'prop-filters' => [],
+ 'is-not-defined' => false,
+ 'time-range' => null,
+ ]);
+
+ if (is_resource($row['calendardata'])) {
+ // Put the stream back to the beginning so it can be read another time
+ rewind($row['calendardata']);
+ }
+
+ if ($isValid) {
+ $calendarObjects[] = $row;
+ }
+ }
+
+ $result->closeCursor();
+
+ return $calendarObjects;
}
/**
@@ -2739,6 +2809,44 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription
}
/**
+ * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
+ *
+ * @param int $modifiedBefore
+ * @param int $limit
+ * @return void
+ */
+ public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
+ $query = $this->db->getQueryBuilder();
+ $query->select('id')
+ ->from('schedulingobjects')
+ ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
+ ->setMaxResults($limit);
+ $result = $query->executeQuery();
+ $count = $result->rowCount();
+ if($count === 0) {
+ return;
+ }
+ $ids = array_map(static function (array $id) {
+ return (int)$id[0];
+ }, $result->fetchAll(\PDO::FETCH_NUM));
+ $result->closeCursor();
+
+ $numDeleted = 0;
+ $deleteQuery = $this->db->getQueryBuilder();
+ $deleteQuery->delete('schedulingobjects')
+ ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
+ foreach(array_chunk($ids, 1000) as $chunk) {
+ $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
+ $numDeleted += $deleteQuery->executeStatement();
+ }
+
+ if($numDeleted === $limit) {
+ $this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
+ $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
+ }
+ }
+
+ /**
* Creates a new scheduling object. This should land in a users' inbox.
*
* @param string $principalUri
diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php
index 29129a3b073..ed2c233055c 100644
--- a/apps/dav/lib/CalDAV/Status/StatusService.php
+++ b/apps/dav/lib/CalDAV/Status/StatusService.php
@@ -32,6 +32,7 @@ use OCA\UserStatus\Service\StatusService as UserStatusService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\IManager;
+use OCP\DB\Exception;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUser as User;
@@ -72,7 +73,18 @@ class StatusService {
}
if(empty($calendarEvents)) {
- $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
$this->logger->debug('No calendar events found for status check', ['user' => $userId]);
return;
}
@@ -118,7 +130,18 @@ class StatusService {
});
if(empty($applicableEvents)) {
- $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ try {
+ $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
+ } catch (Exception $e) {
+ if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ // A different process might have written another status
+ // update to the DB while we're processing our stuff.
+ // We cannot safely restore the status as we don't know which one is valid at this point
+ // So let's silently log this one and exit
+ $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]);
+ return;
+ }
+ }
$this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
return;
}
diff --git a/apps/dav/lib/Migration/DeleteSchedulingObjects.php b/apps/dav/lib/Migration/DeleteSchedulingObjects.php
new file mode 100644
index 00000000000..3919236788b
--- /dev/null
+++ b/apps/dav/lib/Migration/DeleteSchedulingObjects.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Migration;
+
+use OCA\DAV\BackgroundJob\DeleteOutdatedSchedulingObjects;
+use OCA\DAV\CalDAV\CalDavBackend;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class DeleteSchedulingObjects implements IRepairStep {
+ public function __construct(private IJobList $jobList,
+ private ITimeFactory $time,
+ private CalDavBackend $calDavBackend
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Handle outdated scheduling events';
+ }
+
+ public function run(IOutput $output): void {
+ $output->info('Cleaning up old scheduling events');
+ $time = $this->time->getTime() - (60 * 60);
+ $this->calDavBackend->deleteOutdatedSchedulingObjects($time, 50000);
+ if (!$this->jobList->has(DeleteOutdatedSchedulingObjects::class, null)) {
+ $output->info('Adding background job to delete old scheduling objects');
+ $this->jobList->add(DeleteOutdatedSchedulingObjects::class, null);
+ }
+ }
+}
diff --git a/apps/dav/lib/Migration/Version1004Date20170825134824.php b/apps/dav/lib/Migration/Version1004Date20170825134824.php
index a7cbaa78ef2..7321bba62ff 100644
--- a/apps/dav/lib/Migration/Version1004Date20170825134824.php
+++ b/apps/dav/lib/Migration/Version1004Date20170825134824.php
@@ -383,6 +383,7 @@ class Version1004Date20170825134824 extends SimpleMigrationStep {
]);
$table->setPrimaryKey(['id']);
$table->addIndex(['principaluri'], 'schedulobj_principuri_index');
+ $table->addIndex(['lastmodified'], 'schedulobj_lastmodified_idx');
}
if (!$schema->hasTable('cards_properties')) {