diff options
Diffstat (limited to 'apps/dav/lib/BackgroundJob')
15 files changed, 609 insertions, 769 deletions
diff --git a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php index 3d4e4dd5e6b..1165367c33f 100644 --- a/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -32,7 +13,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\QueuedJob; use OCP\IDBConnection; -use OCP\ILogger; +use Psr\Log\LoggerInterface; /** * Class BuildReminderIndexBackgroundJob @@ -41,51 +22,28 @@ use OCP\ILogger; */ class BuildReminderIndexBackgroundJob extends QueuedJob { - /** @var IDBConnection */ - private $db; - - /** @var ReminderService */ - private $reminderService; - - /** @var ILogger */ - private $logger; - - /** @var IJobList */ - private $jobList; - /** @var ITimeFactory */ private $timeFactory; /** * BuildReminderIndexBackgroundJob constructor. - * - * @param IDBConnection $db - * @param ReminderService $reminderService - * @param ILogger $logger - * @param IJobList $jobList - * @param ITimeFactory $timeFactory */ - public function __construct(IDBConnection $db, - ReminderService $reminderService, - ILogger $logger, - IJobList $jobList, - ITimeFactory $timeFactory) { + public function __construct( + private IDBConnection $db, + private ReminderService $reminderService, + private LoggerInterface $logger, + private IJobList $jobList, + ITimeFactory $timeFactory, + ) { parent::__construct($timeFactory); - $this->db = $db; - $this->reminderService = $reminderService; - $this->logger = $logger; - $this->jobList = $jobList; $this->timeFactory = $timeFactory; } - /** - * @param $arguments - */ - public function run($arguments) { - $offset = (int) $arguments['offset']; - $stopAt = (int) $arguments['stopAt']; + public function run($argument) { + $offset = (int)$argument['offset']; + $stopAt = (int)$argument['stopAt']; - $this->logger->info('Building calendar reminder index (' . $offset .'/' . $stopAt . ')'); + $this->logger->info('Building calendar reminder index (' . $offset . '/' . $stopAt . ')'); $offset = $this->buildIndex($offset, $stopAt); @@ -115,9 +73,9 @@ class BuildReminderIndexBackgroundJob extends QueuedJob { ->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset))) ->orderBy('id', 'ASC'); - $stmt = $query->execute(); - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $offset = $row['id']; + $result = $query->executeQuery(); + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + $offset = (int)$row['id']; if (is_resource($row['calendardata'])) { $row['calendardata'] = stream_get_contents($row['calendardata']); } @@ -126,14 +84,16 @@ class BuildReminderIndexBackgroundJob extends QueuedJob { try { $this->reminderService->onCalendarObjectCreate($row); } catch (\Exception $ex) { - $this->logger->logException($ex); + $this->logger->error($ex->getMessage(), ['exception' => $ex]); } if (($this->timeFactory->getTime() - $startTime) > 15) { + $result->closeCursor(); return $offset; } } + $result->closeCursor(); return $stopAt; } } diff --git a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php index b57ed07d5c2..c6edac4f228 100644 --- a/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php +++ b/apps/dav/lib/BackgroundJob/CalendarRetentionJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -30,13 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; class CalendarRetentionJob extends TimedJob { - /** @var RetentionService */ - private $service; - - public function __construct(ITimeFactory $time, - RetentionService $service) { + public function __construct( + ITimeFactory $time, + private RetentionService $service, + ) { parent::__construct($time); - $this->service = $service; // Run four times a day $this->setInterval(6 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php index 073fc53e07a..49b6b1607ef 100644 --- a/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php +++ b/apps/dav/lib/BackgroundJob/CleanupDirectLinksJob.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -31,12 +13,11 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; class CleanupDirectLinksJob extends TimedJob { - /** @var DirectMapper */ - private $mapper; - - public function __construct(ITimeFactory $timeFactory, DirectMapper $mapper) { + public function __construct( + ITimeFactory $timeFactory, + private DirectMapper $mapper, + ) { parent::__construct($timeFactory); - $this->mapper = $mapper; // Run once a day at off-peak time $this->setInterval(24 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php index 6339e721c93..7b664d03181 100644 --- a/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php +++ b/apps/dav/lib/BackgroundJob/CleanupInvitationTokenJob.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -32,12 +14,11 @@ use OCP\IDBConnection; class CleanupInvitationTokenJob extends TimedJob { - /** @var IDBConnection */ - private $db; - - public function __construct(IDBConnection $db, ITimeFactory $time) { + public function __construct( + private IDBConnection $db, + ITimeFactory $time, + ) { parent::__construct($time); - $this->db = $db; // Run once a day at off-peak time $this->setInterval(24 * 60 * 60); diff --git a/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php new file mode 100644 index 00000000000..8a5e34381a7 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/CleanupOrphanedChildrenJob.php @@ -0,0 +1,89 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 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\IJobList; +use OCP\BackgroundJob\QueuedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use Psr\Log\LoggerInterface; + +class CleanupOrphanedChildrenJob extends QueuedJob { + public const ARGUMENT_CHILD_TABLE = 'childTable'; + public const ARGUMENT_PARENT_TABLE = 'parentTable'; + public const ARGUMENT_PARENT_ID = 'parentId'; + public const ARGUMENT_LOG_MESSAGE = 'logMessage'; + + private const BATCH_SIZE = 1000; + + public function __construct( + ITimeFactory $time, + private readonly IDBConnection $connection, + private readonly LoggerInterface $logger, + private readonly IJobList $jobList, + ) { + parent::__construct($time); + } + + protected function run($argument): void { + $childTable = $argument[self::ARGUMENT_CHILD_TABLE]; + $parentTable = $argument[self::ARGUMENT_PARENT_TABLE]; + $parentId = $argument[self::ARGUMENT_PARENT_ID]; + $logMessage = $argument[self::ARGUMENT_LOG_MESSAGE]; + + $orphanCount = $this->cleanUpOrphans($childTable, $parentTable, $parentId); + $this->logger->debug(sprintf($logMessage, $orphanCount)); + + // Requeue if there might be more orphans + if ($orphanCount >= self::BATCH_SIZE) { + $this->jobList->add(self::class, $argument); + } + } + + private function cleanUpOrphans( + string $childTable, + string $parentTable, + string $parentId, + ): int { + // We can't merge both queries into a single one here as DELETEing from a table while + // SELECTing it in a sub query is not supported by Oracle DB. + // Ref https://docs.oracle.com/cd/E17952_01/mysql-8.0-en/delete.html#idm46006185488144 + + $selectQb = $this->connection->getQueryBuilder(); + + $selectQb->select('c.id') + ->from($childTable, 'c') + ->leftJoin('c', $parentTable, 'p', $selectQb->expr()->eq('c.' . $parentId, 'p.id')) + ->where($selectQb->expr()->isNull('p.id')) + ->setMaxResults(self::BATCH_SIZE); + + if (\in_array($parentTable, ['calendars', 'calendarsubscriptions'], true)) { + $calendarType = $parentTable === 'calendarsubscriptions' ? CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION : CalDavBackend::CALENDAR_TYPE_CALENDAR; + $selectQb->andWhere($selectQb->expr()->eq('c.calendartype', $selectQb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); + } + + $result = $selectQb->executeQuery(); + $rows = $result->fetchAll(); + $result->closeCursor(); + if (empty($rows)) { + return 0; + } + + $orphanItems = array_map(static fn ($row) => $row['id'], $rows); + $deleteQb = $this->connection->getQueryBuilder(); + $deleteQb->delete($childTable) + ->where($deleteQb->expr()->in('id', $deleteQb->createNamedParameter($orphanItems, IQueryBuilder::PARAM_INT_ARRAY))); + $deleteQb->executeStatement(); + + return count($orphanItems); + } +} diff --git a/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php b/apps/dav/lib/BackgroundJob/DeleteOutdatedSchedulingObjects.php new file mode 100644 index 00000000000..bc306d58fe1 --- /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/BackgroundJob/EventReminderJob.php b/apps/dav/lib/BackgroundJob/EventReminderJob.php index ab7dadd8c0b..0e21e06fc35 100644 --- a/apps/dav/lib/BackgroundJob/EventReminderJob.php +++ b/apps/dav/lib/BackgroundJob/EventReminderJob.php @@ -3,29 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; +use OC\User\NoUserException; +use OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException; +use OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException; use OCA\DAV\CalDAV\Reminder\ReminderService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; @@ -33,18 +18,12 @@ use OCP\IConfig; class EventReminderJob extends TimedJob { - /** @var ReminderService */ - private $reminderService; - - /** @var IConfig */ - private $config; - - public function __construct(ITimeFactory $time, - ReminderService $reminderService, - IConfig $config) { + public function __construct( + ITimeFactory $time, + private ReminderService $reminderService, + private IConfig $config, + ) { parent::__construct($time); - $this->reminderService = $reminderService; - $this->config = $config; // Run every 5 minutes $this->setInterval(5 * 60); @@ -52,12 +31,11 @@ class EventReminderJob extends TimedJob { } /** - * @param $arg - * @throws \OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException - * @throws \OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException - * @throws \OC\User\NoUserException + * @throws ProviderNotAvailableException + * @throws NotificationTypeDoesNotExistException + * @throws NoUserException */ - public function run($arg):void { + public function run($argument):void { if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') { return; } diff --git a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php index a338a172d66..6d94f4810ed 100644 --- a/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2017 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -32,29 +15,19 @@ use OCP\IConfig; class GenerateBirthdayCalendarBackgroundJob extends QueuedJob { - /** @var BirthdayService */ - private $birthdayService; - - /** @var IConfig */ - private $config; - - public function __construct(ITimeFactory $time, - BirthdayService $birthdayService, - IConfig $config) { + public function __construct( + ITimeFactory $time, + private BirthdayService $birthdayService, + private IConfig $config, + ) { parent::__construct($time); - - $this->birthdayService = $birthdayService; - $this->config = $config; } - /** - * @param array $arguments - */ - public function run($arguments) { - $userId = $arguments['userId']; - $purgeBeforeGenerating = $arguments['purgeBeforeGenerating'] ?? false; + public function run($argument) { + $userId = $argument['userId']; + $purgeBeforeGenerating = $argument['purgeBeforeGenerating'] ?? false; - // make sure admin didn't change his mind + // make sure admin didn't change their mind $isGloballyEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes'); if ($isGloballyEnabled !== 'yes') { return; diff --git a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php new file mode 100644 index 00000000000..cc4fd5dce9d --- /dev/null +++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\AbsenceMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use Psr\Log\LoggerInterface; + +class OutOfOfficeEventDispatcherJob extends QueuedJob { + public const EVENT_START = 'start'; + public const EVENT_END = 'end'; + + public function __construct( + ITimeFactory $time, + private AbsenceMapper $absenceMapper, + private LoggerInterface $logger, + private IEventDispatcher $eventDispatcher, + private IUserManager $userManager, + private TimezoneService $timezoneService, + ) { + parent::__construct($time); + } + + public function run($argument): void { + $id = $argument['id']; + $event = $argument['event']; + + try { + $absence = $this->absenceMapper->findById($id); + } catch (DoesNotExistException|\OCP\DB\Exception $e) { + $this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [ + 'exception' => $e, + 'argument' => $argument, + ]); + return; + } + + $userId = $absence->getUserId(); + $user = $this->userManager->get($userId); + if ($user === null) { + $this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [ + 'argument' => $argument, + ]); + return; + } + + $data = $absence->toOutOufOfficeData( + $user, + $this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone(), + ); + if ($event === self::EVENT_START) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data)); + } elseif ($event === self::EVENT_END) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data)); + } else { + $this->logger->error("Invalid out-of-office event: $event", [ + 'argument' => $argument, + ]); + } + } +} diff --git a/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php new file mode 100644 index 00000000000..8746588acc7 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/PruneOutdatedSyncTokensJob.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\AppInfo\Application; +use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\IConfig; +use Psr\Log\LoggerInterface; + +class PruneOutdatedSyncTokensJob extends TimedJob { + + public function __construct( + ITimeFactory $timeFactory, + private CalDavBackend $calDavBackend, + private CardDavBackend $cardDavBackend, + private IConfig $config, + private LoggerInterface $logger, + ) { + parent::__construct($timeFactory); + $this->setInterval(60 * 60 * 24); // One day + $this->setTimeSensitivity(self::TIME_INSENSITIVE); + } + + public function run($argument) { + $limit = max(1, (int)$this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000')); + $retention = max(7, (int)$this->config->getAppValue(Application::APP_ID, 'syncTokensRetentionDays', '60')) * 24 * 3600; + + $prunedCalendarSyncTokens = $this->calDavBackend->pruneOutdatedSyncTokens($limit, $retention); + $prunedAddressBookSyncTokens = $this->cardDavBackend->pruneOutdatedSyncTokens($limit, $retention); + + $this->logger->info('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [ + 'calendarSyncTokensNumber' => $prunedCalendarSyncTokens, + 'addressBooksSyncTokensNumber' => $prunedAddressBookSyncTokens + ]); + } +} diff --git a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php index fbb944159fd..e96735ca50b 100644 --- a/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php +++ b/apps/dav/lib/BackgroundJob/RefreshWebcalJob.php @@ -3,28 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Citharel <nextcloud@tcit.fr> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -34,42 +14,18 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\Job; use OCP\IConfig; -use OCP\ILogger; +use Psr\Log\LoggerInterface; use Sabre\VObject\DateTimeParser; use Sabre\VObject\InvalidDataException; class RefreshWebcalJob extends Job { - - /** - * @var RefreshWebcalService - */ - private $refreshWebcalService; - - /** - * @var IConfig - */ - private $config; - - /** @var ILogger */ - private $logger; - - /** @var ITimeFactory */ - private $timeFactory; - - /** - * RefreshWebcalJob constructor. - * - * @param RefreshWebcalService $refreshWebcalService - * @param IConfig $config - * @param ILogger $logger - * @param ITimeFactory $timeFactory - */ - public function __construct(RefreshWebcalService $refreshWebcalService, IConfig $config, ILogger $logger, ITimeFactory $timeFactory) { + public function __construct( + private RefreshWebcalService $refreshWebcalService, + private IConfig $config, + private LoggerInterface $logger, + ITimeFactory $timeFactory, + ) { parent::__construct($timeFactory); - $this->refreshWebcalService = $refreshWebcalService; - $this->config = $config; - $this->logger = $logger; - $this->timeFactory = $timeFactory; } /** @@ -77,7 +33,7 @@ class RefreshWebcalJob extends Job { * * @inheritdoc */ - public function execute(IJobList $jobList, ILogger $logger = null) { + public function start(IJobList $jobList): void { $subscription = $this->refreshWebcalService->getSubscription($this->argument['principaluri'], $this->argument['uri']); if (!$subscription) { return; @@ -85,8 +41,8 @@ class RefreshWebcalJob extends Job { $this->fixSubscriptionRowTyping($subscription); - // if no refresh rate was configured, just refresh once a week - $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1W'); + // if no refresh rate was configured, just refresh once a day + $defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1D'); $refreshRate = $subscription[RefreshWebcalService::REFRESH_RATE] ?? $defaultRefreshRate; $subscriptionId = $subscription['id']; @@ -95,17 +51,19 @@ class RefreshWebcalJob extends Job { /** @var DateInterval $dateInterval */ $dateInterval = DateTimeParser::parseDuration($refreshRate); } catch (InvalidDataException $ex) { - $this->logger->logException($ex); - $this->logger->warning("Subscription $subscriptionId could not be refreshed, refreshrate in database is invalid"); + $this->logger->error( + "Subscription $subscriptionId could not be refreshed, refreshrate in database is invalid", + ['exception' => $ex] + ); return; } $interval = $this->getIntervalFromDateInterval($dateInterval); - if (($this->timeFactory->getTime() - $this->lastRun) <= $interval) { + if (($this->time->getTime() - $this->lastRun) <= $interval) { return; } - parent::execute($jobList, $logger); + parent::start($jobList); } /** @@ -146,7 +104,7 @@ class RefreshWebcalJob extends Job { foreach ($forceInt as $column) { if (isset($row[$column])) { - $row[$column] = (int) $row[$column]; + $row[$column] = (int)$row[$column]; } } } diff --git a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php index 85da81b3b91..7ec5b7fba79 100644 --- a/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php +++ b/apps/dav/lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright 2019 Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; @@ -35,12 +16,6 @@ use OCP\IUserManager; class RegisterRegenerateBirthdayCalendars extends QueuedJob { - /** @var IUserManager */ - private $userManager; - - /** @var IJobList */ - private $jobList; - /** * RegisterRegenerateBirthdayCalendars constructor. * @@ -48,19 +23,19 @@ class RegisterRegenerateBirthdayCalendars extends QueuedJob { * @param IUserManager $userManager * @param IJobList $jobList */ - public function __construct(ITimeFactory $time, - IUserManager $userManager, - IJobList $jobList) { + public function __construct( + ITimeFactory $time, + private IUserManager $userManager, + private IJobList $jobList, + ) { parent::__construct($time); - $this->userManager = $userManager; - $this->jobList = $jobList; } /** * @inheritDoc */ public function run($argument) { - $this->userManager->callForSeenUsers(function (IUser $user) { + $this->userManager->callForSeenUsers(function (IUser $user): void { $this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [ 'userId' => $user->getUID(), 'purgeBeforeGenerating' => true diff --git a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php index f7addd58248..b7688ea32d8 100644 --- a/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php +++ b/apps/dav/lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php @@ -3,452 +3,32 @@ declare(strict_types=1); /** - * @copyright 2019, Georg Ehrke <oc.list@georgehrke.com> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 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 OCP\Calendar\BackendTemporarilyUnavailableException; -use OCP\Calendar\IMetadataProvider; -use OCP\Calendar\Resource\IBackend as IResourceBackend; use OCP\Calendar\Resource\IManager as IResourceManager; -use OCP\Calendar\Resource\IResource; use OCP\Calendar\Room\IManager as IRoomManager; -use OCP\Calendar\Room\IRoom; -use OCP\IDBConnection; class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob { - - /** @var IResourceManager */ - private $resourceManager; - - /** @var IRoomManager */ - private $roomManager; - - /** @var IDBConnection */ - private $dbConnection; - - /** @var CalDavBackend */ - private $calDavBackend; - - public function __construct(ITimeFactory $time, - IResourceManager $resourceManager, - IRoomManager $roomManager, - IDBConnection $dbConnection, - CalDavBackend $calDavBackend) { + public function __construct( + ITimeFactory $time, + private IResourceManager $resourceManager, + private IRoomManager $roomManager, + ) { parent::__construct($time); - $this->resourceManager = $resourceManager; - $this->roomManager = $roomManager; - $this->dbConnection = $dbConnection; - $this->calDavBackend = $calDavBackend; // Run once an hour $this->setInterval(60 * 60); $this->setTimeSensitivity(self::TIME_SENSITIVE); } - /** - * @param $argument - */ public function run($argument): void { - $this->runForBackend( - $this->resourceManager, - 'calendar_resources', - 'calendar_resources_md', - 'resource_id', - 'principals/calendar-resources' - ); - $this->runForBackend( - $this->roomManager, - 'calendar_rooms', - 'calendar_rooms_md', - 'room_id', - 'principals/calendar-rooms' - ); - } - - /** - * Run background-job for one specific backendManager - * either ResourceManager or RoomManager - * - * @param IResourceManager|IRoomManager $backendManager - * @param string $dbTable - * @param string $dbTableMetadata - * @param string $foreignKey - * @param string $principalPrefix - */ - private function runForBackend($backendManager, - string $dbTable, - string $dbTableMetadata, - string $foreignKey, - string $principalPrefix): void { - $backends = $backendManager->getBackends(); - - foreach ($backends as $backend) { - $backendId = $backend->getBackendIdentifier(); - - try { - if ($backend instanceof IResourceBackend) { - $list = $backend->listAllResources(); - } else { - $list = $backend->listAllRooms(); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $cachedList = $this->getAllCachedByBackend($dbTable, $backendId); - $newIds = array_diff($list, $cachedList); - $deletedIds = array_diff($cachedList, $list); - $editedIds = array_intersect($list, $cachedList); - - foreach ($newIds as $newId) { - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($newId); - } else { - $resource = $backend->getRoom($newId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $id = $this->addToCache($dbTable, $backendId, $resource); - $this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata); - // we don't create the calendar here, it is created lazily - // when an event is actually scheduled with this resource / room - } - - foreach ($deletedIds as $deletedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId); - $this->deleteFromCache($dbTable, $id); - $this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id); - - $principalName = implode('-', [$backendId, $deletedId]); - $this->deleteCalendarDataForResource($principalPrefix, $principalName); - } - - foreach ($editedIds as $editedId) { - $id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId); - - try { - if ($backend instanceof IResourceBackend) { - $resource = $backend->getResource($editedId); - } else { - $resource = $backend->getRoom($editedId); - } - - $metadata = []; - if ($resource instanceof IMetadataProvider) { - $metadata = $this->getAllMetadataOfBackend($resource); - } - } catch (BackendTemporarilyUnavailableException $ex) { - continue; - } - - $this->updateCache($dbTable, $id, $resource); - - if ($resource instanceof IMetadataProvider) { - $cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id); - $this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata); - } - } - } - } - - /** - * add entry to cache that exists remotely but not yet in cache - * - * @param string $table - * @param string $backendId - * @param IResource|IRoom $remote - * - * @return int Insert id - */ - private function addToCache(string $table, - string $backendId, - $remote): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - 'backend_id' => $query->createNamedParameter($backendId), - 'resource_id' => $query->createNamedParameter($remote->getId()), - 'email' => $query->createNamedParameter($remote->getEMail()), - 'displayname' => $query->createNamedParameter($remote->getDisplayName()), - 'group_restrictions' => $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - )) - ]) - ->executeStatement(); - return $query->getLastInsertId(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $foreignId - * @param array $metadata - */ - private function addMetadataToCache(string $table, - string $foreignKey, - int $foreignId, - array $metadata): void { - foreach ($metadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($table) - ->values([ - $foreignKey => $query->createNamedParameter($foreignId), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - } - - /** - * delete entry from cache that does not exist anymore remotely - * - * @param string $table - * @param int $id - */ - private function deleteFromCache(string $table, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - */ - private function deleteMetadataFromCache(string $table, - string $foreignKey, - int $id): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * update an existing entry in cache - * - * @param string $table - * @param int $id - * @param IResource|IRoom $remote - */ - private function updateCache(string $table, - int $id, - $remote): void { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($table) - ->set('email', $query->createNamedParameter($remote->getEMail())) - ->set('displayname', $query->createNamedParameter($remote->getDisplayName())) - ->set('group_restrictions', $query->createNamedParameter( - $this->serializeGroupRestrictions( - $remote->getGroupRestrictions() - ))) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))) - ->executeStatement(); - } - - /** - * @param string $dbTable - * @param string $foreignKey - * @param int $id - * @param array $metadata - * @param array $cachedMetadata - */ - private function updateMetadataCache(string $dbTable, - string $foreignKey, - int $id, - array $metadata, - array $cachedMetadata): void { - $newMetadata = array_diff_key($metadata, $cachedMetadata); - $deletedMetadata = array_diff_key($cachedMetadata, $metadata); - - foreach ($newMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->insert($dbTable) - ->values([ - $foreignKey => $query->createNamedParameter($id), - 'key' => $query->createNamedParameter($key), - 'value' => $query->createNamedParameter($value), - ]) - ->executeStatement(); - } - - foreach ($deletedMetadata as $key => $value) { - $query = $this->dbConnection->getQueryBuilder(); - $query->delete($dbTable) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($key))) - ->executeStatement(); - } - - $existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata)); - foreach ($existingKeys as $existingKey) { - if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) { - $query = $this->dbConnection->getQueryBuilder(); - $query->update($dbTable) - ->set('value', $query->createNamedParameter($metadata[$existingKey])) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))) - ->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey))) - ->executeStatement(); - } - } - } - - /** - * serialize array of group restrictions to store them in database - * - * @param array $groups - * - * @return string - */ - private function serializeGroupRestrictions(array $groups): string { - return \json_encode($groups); - } - - /** - * Gets all metadata of a backend - * - * @param IResource|IRoom $resource - * - * @return array - */ - private function getAllMetadataOfBackend($resource): array { - if (!($resource instanceof IMetadataProvider)) { - return []; - } - - $keys = $resource->getAllAvailableMetadataKeys(); - $metadata = []; - foreach ($keys as $key) { - $metadata[$key] = $resource->getMetadataForKey($key); - } - - return $metadata; - } - - /** - * @param string $table - * @param string $foreignKey - * @param int $id - * - * @return array - */ - private function getAllMetadataOfCache(string $table, - string $foreignKey, - int $id): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select(['key', 'value']) - ->from($table) - ->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - $metadata = []; - foreach ($rows as $row) { - $metadata[$row['key']] = $row['value']; - } - - return $metadata; - } - - /** - * Gets all cached rooms / resources by backend - * - * @param $tableName - * @param $backendId - * - * @return array - */ - private function getAllCachedByBackend(string $tableName, - string $backendId): array { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('resource_id') - ->from($tableName) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))); - $result = $query->executeQuery(); - $rows = $result->fetchAll(); - $result->closeCursor(); - - return array_map(function ($row): string { - return $row['resource_id']; - }, $rows); - } - - /** - * @param $principalPrefix - * @param $principalUri - */ - private function deleteCalendarDataForResource(string $principalPrefix, - string $principalUri): void { - $calendar = $this->calDavBackend->getCalendarByUri( - implode('/', [$principalPrefix, $principalUri]), - CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI); - - if ($calendar !== null) { - $this->calDavBackend->deleteCalendar( - $calendar['id'], - true // Because this wasn't deleted by a user - ); - } - } - - /** - * @param $table - * @param $backendId - * @param $resourceId - * - * @return int - */ - private function getIdForBackendAndResource(string $table, - string $backendId, - string $resourceId): int { - $query = $this->dbConnection->getQueryBuilder(); - $query->select('id') - ->from($table) - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); - $result = $query->executeQuery(); - - $id = (int) $result->fetchOne(); - $result->closeCursor(); - return $id; + $this->resourceManager->update(); + $this->roomManager->update(); } } diff --git a/apps/dav/lib/BackgroundJob/UploadCleanup.php b/apps/dav/lib/BackgroundJob/UploadCleanup.php index 76906becb54..230cde61578 100644 --- a/apps/dav/lib/BackgroundJob/UploadCleanup.php +++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php @@ -3,56 +3,33 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\DAV\BackgroundJob; use OC\User\NoUserException; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\BackgroundJob\IJob; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\TimedJob; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use Psr\Log\LoggerInterface; class UploadCleanup extends TimedJob { - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IJobList */ - private $jobList; - - public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList) { + public function __construct( + ITimeFactory $time, + private IRootFolder $rootFolder, + private IJobList $jobList, + private LoggerInterface $logger, + ) { parent::__construct($time); - $this->rootFolder = $rootFolder; - $this->jobList = $jobList; // Run once a day $this->setInterval(60 * 60 * 24); - $this->setTimeSensitivity(IJob::TIME_INSENSITIVE); + $this->setTimeSensitivity(self::TIME_INSENSITIVE); } protected function run($argument) { @@ -64,18 +41,27 @@ class UploadCleanup extends TimedJob { $userRoot = $userFolder->getParent(); /** @var Folder $uploads */ $uploads = $userRoot->get('uploads'); - /** @var Folder $uploadFolder */ $uploadFolder = $uploads->get($folder); - } catch (NotFoundException | NoUserException $e) { + } catch (NotFoundException|NoUserException $e) { $this->jobList->remove(self::class, $argument); return; } - $files = $uploadFolder->getDirectoryListing(); - // Remove if all files have an mtime of more than a day $time = $this->time->getTime() - 60 * 60 * 24; + if (!($uploadFolder instanceof Folder)) { + $this->logger->error('Found a file inside the uploads folder. Uid: ' . $uid . ' folder: ' . $folder); + if ($uploadFolder->getMTime() < $time) { + $uploadFolder->delete(); + } + $this->jobList->remove(self::class, $argument); + return; + } + + /** @var File[] $files */ + $files = $uploadFolder->getDirectoryListing(); + // The folder has to be more than a day old $initial = $uploadFolder->getMTime() < $time; diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php new file mode 100644 index 00000000000..027b3349802 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -0,0 +1,243 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\IAvailabilityCoordinator; +use OCP\User\IOutOfOfficeData; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\Available; +use Sabre\VObject\Component\VAvailability; +use Sabre\VObject\Reader; +use Sabre\VObject\Recur\RRuleIterator; + +class UserStatusAutomation extends TimedJob { + public function __construct( + private ITimeFactory $timeFactory, + private IDBConnection $connection, + private IJobList $jobList, + private LoggerInterface $logger, + private IManager $manager, + private IConfig $config, + private IAvailabilityCoordinator $coordinator, + private IUserManager $userManager, + ) { + parent::__construct($timeFactory); + + // interval = 0 might look odd, but it's intentional. last_run is set to + // the user's next available time, so the job runs immediately when + // that time comes. + $this->setInterval(0); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + if (!isset($argument['userId'])) { + $this->jobList->remove(self::class, $argument); + $this->logger->info('Removing invalid ' . self::class . ' background job'); + return; + } + + $userId = $argument['userId']; + $user = $this->userManager->get($userId); + if ($user === null) { + return; + } + + $ooo = $this->coordinator->getCurrentOutOfOfficeData($user); + + $continue = $this->processOutOfOfficeData($user, $ooo); + if ($continue === false) { + return; + } + + $property = $this->getAvailabilityFromPropertiesTable($userId); + $hasDndForOfficeHours = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes'; + + if (!$property) { + // We found no ooo data and no availability settings, so we need to delete the job because there is no next runtime + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules and no OOO data set'); + $this->jobList->remove(self::class, $argument); + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); + return; + } + + $this->processAvailability($property, $user->getUID(), $hasDndForOfficeHours); + } + + protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void { + $query = $this->connection->getQueryBuilder(); + + $query->update('jobs') + ->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId); + } + + /** + * @param string $userId + * @return false|string + */ + protected function getAvailabilityFromPropertiesTable(string $userId) { + $propertyPath = 'calendars/' . $userId . '/inbox'; + $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + + $query = $this->connection->getQueryBuilder(); + $query->select('propertyvalue') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath))) + ->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $property = $result->fetchOne(); + $result->closeCursor(); + + return $property; + } + + /** + * @param string $property + * @param $userId + * @param $argument + * @return void + */ + private function processAvailability(string $property, string $userId, bool $hasDndForOfficeHours): void { + $isCurrentlyAvailable = false; + $nextPotentialToggles = []; + + $now = $this->time->getDateTime(); + $lastMidnight = (clone $now)->setTime(0, 0); + + $vObject = Reader::read($property); + foreach ($vObject->getComponents() as $component) { + if ($component->name !== 'VAVAILABILITY') { + continue; + } + /** @var VAvailability $component */ + $availables = $component->getComponents(); + foreach ($availables as $available) { + /** @var Available $available */ + if ($available->name === 'AVAILABLE') { + /** @var \DateTimeImmutable $originalStart */ + /** @var \DateTimeImmutable $originalEnd */ + [$originalStart, $originalEnd] = $available->getEffectiveStartEnd(); + + // Little shenanigans to fix the automation on the day the rules were adjusted + // Otherwise the $originalStart would match rules for Thursdays on a Friday, etc. + // So we simply wind back a week and then fastForward to the next occurrence + // since today's midnight, which then also accounts for the week days. + $effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D')); + $effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D')); + + try { + $it = new RRuleIterator((string)$available->RRULE, $effectiveStart); + $it->fastForward($lastMidnight); + + $startToday = $it->current(); + if ($startToday && $startToday <= $now) { + $duration = $effectiveStart->diff($effectiveEnd); + $endToday = $startToday->add($duration); + if ($endToday > $now) { + // User is currently available + // Also queuing the end time as next status toggle + $isCurrentlyAvailable = true; + $nextPotentialToggles[] = $endToday->getTimestamp(); + } + + // Availability enabling already done for today, + // so jump to the next recurrence to find the next status toggle + $it->next(); + } + + if ($it->current()) { + $nextPotentialToggles[] = $it->current()->getTimestamp(); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + } + } + + if (empty($nextPotentialToggles)) { + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules set'); + $this->jobList->remove(self::class, ['userId' => $userId]); + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + return; + } + + $nextAutomaticToggle = min($nextPotentialToggles); + $this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1); + + if ($isCurrentlyAvailable) { + $this->logger->debug('User is currently available, reverting DND status if applicable'); + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + $this->logger->debug('User status automation ran'); + return; + } + + if (!$hasDndForOfficeHours) { + // Office hours are not set to DND, so there is nothing to do. + return; + } + + $this->logger->debug('User is currently NOT available, reverting call and meeting status if applicable and then setting DND'); + $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + $this->logger->debug('User status automation ran'); + } + + private function processOutOfOfficeData(IUser $user, ?IOutOfOfficeData $ooo): bool { + if (empty($ooo)) { + // Reset the user status if the absence doesn't exist + $this->logger->debug('User has no OOO period in effect, reverting DND status if applicable'); + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); + // We need to also run the availability automation + return true; + } + + if (!$this->coordinator->isInEffect($ooo)) { + // Reset the user status if the absence is (no longer) in effect + $this->logger->debug('User has no OOO period in effect, reverting DND status if applicable'); + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); + + if ($ooo->getStartDate() > $this->time->getTime()) { + // Set the next run to take place at the start of the ooo period if it is in the future + // This might be overwritten if there is an availability setting, but we can't determine + // if this is the case here + $this->setLastRunToNextToggleTime($user->getUID(), $ooo->getStartDate()); + } + return true; + } + + $this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status'); + $this->manager->setUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage()); + + // Run at the end of an ooo period to return to availability / regular user status + // If it's overwritten by a custom status in the meantime, there's nothing we can do about it + $this->setLastRunToNextToggleTime($user->getUID(), $ooo->getEndDate()); + return false; + } +} |