From f19645adab404a9c2642b42ec335bf2830dd8aa7 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Fri, 24 Nov 2023 01:49:30 +0100 Subject: [PATCH] enh(userstatus): add OOO automation and remove calendar automation Signed-off-by: Anna Larch --- .../BackgroundJob/UserStatusAutomation.php | 169 ++-- apps/dav/lib/CalDAV/Status/Status.php | 17 +- apps/dav/lib/CalDAV/Status/StatusService.php | 24 +- .../AvailabilitySettingsController.php | 4 + apps/dav/lib/Db/Absence.php | 4 +- apps/dav/lib/Listener/OutOfOfficeListener.php | 12 +- apps/dav/lib/Service/AbsenceService.php | 20 + .../UserStatusAutomationTest.php | 134 ++- .../unit/CalDAV/Status/StatusServiceTest.php | 850 +----------------- .../unit/Listener/OutOfOfficeListenerTest.php | 12 + .../composer/composer/ClassLoader.php | 108 +-- .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + apps/user_status/lib/AppInfo/Application.php | 7 + .../lib/Connector/UserStatusProvider.php | 4 +- apps/user_status/lib/Db/UserStatusMapper.php | 20 - .../Listener/OutOfOfficeStatusListener.php | 67 ++ .../lib/Service/PredefinedStatusService.php | 1 + .../user_status/lib/Service/StatusService.php | 63 +- .../tests/Unit/Service/StatusServiceTest.php | 294 +----- lib/private/User/AvailabilityCoordinator.php | 50 +- lib/private/UserStatus/ISettableProvider.php | 3 +- lib/private/UserStatus/Manager.php | 4 +- lib/public/User/IAvailabilityCoordinator.php | 16 + lib/public/UserStatus/IManager.php | 4 +- lib/public/UserStatus/IUserStatus.php | 6 + .../lib/User/AvailabilityCoordinatorTest.php | 90 +- 27 files changed, 554 insertions(+), 1431 deletions(-) create mode 100644 apps/user_status/lib/Listener/OutOfOfficeStatusListener.php diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php index 53184be6f25..60c53e9bd08 100644 --- a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -30,6 +30,10 @@ 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; @@ -39,24 +43,15 @@ use Sabre\VObject\Reader; use Sabre\VObject\Recur\RRuleIterator; class UserStatusAutomation extends TimedJob { - protected IDBConnection $connection; - protected IJobList $jobList; - protected LoggerInterface $logger; - protected IManager $manager; - protected IConfig $config; - - public function __construct(ITimeFactory $timeFactory, - IDBConnection $connection, - IJobList $jobList, - LoggerInterface $logger, - IManager $manager, - IConfig $config) { + 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); - $this->connection = $connection; - $this->jobList = $jobList; - $this->logger = $logger; - $this->manager = $manager; - $this->config = $config; // Interval 0 might look weird, but the last_checked is always moved // to the next time we need this and then it's 0 seconds ago. @@ -74,21 +69,74 @@ class UserStatusAutomation extends TimedJob { } $userId = $argument['userId']; - $automationEnabled = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes'; - if (!$automationEnabled) { - $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the setting is disabled'); - $this->jobList->remove(self::class, $argument); + $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) { - $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no availability settings'); + // 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_VACATION, 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 = []; @@ -117,7 +165,7 @@ class UserStatusAutomation extends TimedJob { $effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D')); try { - $it = new RRuleIterator((string) $available->RRULE, $effectiveStart); + $it = new RRuleIterator((string)$available->RRULE, $effectiveStart); $it->fastForward($lastMidnight); $startToday = $it->current(); @@ -148,7 +196,7 @@ class UserStatusAutomation extends TimedJob { 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, $argument); + $this->jobList->remove(self::class, ['userId' => $userId]); $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); return; } @@ -159,46 +207,53 @@ class UserStatusAutomation extends TimedJob { if ($isCurrentlyAvailable) { $this->logger->debug('User is currently available, reverting DND status if applicable'); $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); - } else { - $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND'); - // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. - $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); - $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + $this->logger->debug('User status automation ran'); + return; } - $this->logger->debug('User status automation ran'); - } - - 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(); + if(!$hasDndForOfficeHours) { + // Office hours are not set to DND, so there is nothing to do. + return; + } - $this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId); + $this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND'); + // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + $this->logger->debug('User status automation ran'); } - /** - * @param string $userId - * @return false|string - */ - protected function getAvailabilityFromPropertiesTable(string $userId) { - $propertyPath = 'calendars/' . $userId . '/inbox'; - $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + 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_VACATION, IUserStatus::DND); + // We need to also run the availability automation + return true; + } - $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); + 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_VACATION, IUserStatus::DND); - $result = $query->executeQuery(); - $property = $result->fetchOne(); - $result->closeCursor(); + 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; + } - return $property; + $this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status'); + // Revert both a possible 'CALL - away' and 'office hours - DND' status + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_CALL, IUserStatus::DND); + $this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + $this->manager->setUserStatus($user->getUID(), IUserStatus::MESSAGE_VACATION, 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; } } diff --git a/apps/dav/lib/CalDAV/Status/Status.php b/apps/dav/lib/CalDAV/Status/Status.php index d1c35002fcd..da08d3fe285 100644 --- a/apps/dav/lib/CalDAV/Status/Status.php +++ b/apps/dav/lib/CalDAV/Status/Status.php @@ -26,8 +26,7 @@ namespace OCA\DAV\CalDAV\Status; class Status { - - public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null) { + public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null, private ?int $timestamp = null, private ?string $customEmoji = null) { } public function getStatus(): string { @@ -54,5 +53,19 @@ class Status { $this->customMessage = $customMessage; } + public function setEndTime(?int $timestamp): void { + $this->timestamp = $timestamp; + } + + public function getEndTime(): ?int { + return $this->timestamp; + } + public function getCustomEmoji(): ?string { + return $this->customEmoji; + } + + public function setCustomEmoji(?string $emoji): void { + $this->customEmoji = $emoji; + } } diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php index 1dce2c4c3a3..eea4c12db23 100644 --- a/apps/dav/lib/CalDAV/Status/StatusService.php +++ b/apps/dav/lib/CalDAV/Status/StatusService.php @@ -66,7 +66,6 @@ use Sabre\VObject\Component; use Sabre\VObject\Component\VEvent; use Sabre\VObject\Parameter; use Sabre\VObject\Property; -use Sabre\VObject\Reader; class StatusService { public function __construct(private ITimeFactory $timeFactory, @@ -76,7 +75,7 @@ class StatusService { private FreeBusyGenerator $generator) { } - public function processCalendarAvailability(User $user, ?string $availability): ?Status { + public function processCalendarAvailability(User $user): ?Status { $userId = $user->getUID(); $email = $user->getEMailAddress(); if($email === null) { @@ -160,8 +159,7 @@ class StatusService { } // @todo we can cache that - if(empty($availability) && empty($calendarEvents)) { - // No availability settings and no calendar events, we can stop here + if(empty($calendarEvents)) { return null; } @@ -181,15 +179,6 @@ class StatusService { $this->generator->setObjects($calendar); $this->generator->setTimeRange($dtStart, $dtEnd); $this->generator->setTimeZone($calendarTimeZone); - - if (!empty($availability)) { - $this->generator->setVAvailability( - Reader::read( - $availability - ) - ); - } - // Generate the intersection of VAVILABILITY and all VEVENTS in all calendars $result = $this->generator->getResult(); if (!isset($result->VFREEBUSY)) { @@ -200,9 +189,8 @@ class StatusService { $freeBusyComponent = $result->VFREEBUSY; $freeBusyProperties = $freeBusyComponent->select('FREEBUSY'); // If there is no FreeBusy property, the time-range is empty and available - // so set the status to online as otherwise we will never recover from a BUSY status if (count($freeBusyProperties) === 0) { - return new Status(IUserStatus::ONLINE); + return null; } /** @var Property $freeBusyProperty */ @@ -220,12 +208,10 @@ class StatusService { } $fbType = $fbTypeParameter->getValue(); switch ($fbType) { + // Ignore BUSY-UNAVAILABLE, that's for the automation case 'BUSY': - return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting')); - case 'BUSY-UNAVAILABLE': - return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY); case 'BUSY-TENTATIVE': - return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE); + return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting')); default: return null; } diff --git a/apps/dav/lib/Controller/AvailabilitySettingsController.php b/apps/dav/lib/Controller/AvailabilitySettingsController.php index 3e10162dd84..daa39df470e 100644 --- a/apps/dav/lib/Controller/AvailabilitySettingsController.php +++ b/apps/dav/lib/Controller/AvailabilitySettingsController.php @@ -36,12 +36,14 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\IRequest; use OCP\IUserSession; +use OCP\User\IAvailabilityCoordinator; class AvailabilitySettingsController extends Controller { public function __construct( IRequest $request, private ?IUserSession $userSession, private AbsenceService $absenceService, + private IAvailabilityCoordinator $coordinator, ) { parent::__construct(Application::APP_ID, $request); } @@ -75,6 +77,7 @@ class AvailabilitySettingsController extends Controller { $status, $message, ); + $this->coordinator->clearCache($user->getUID()); return new JSONResponse($absence); } @@ -89,6 +92,7 @@ class AvailabilitySettingsController extends Controller { } $this->absenceService->clearAbsence($user); + $this->coordinator->clearCache($user->getUID()); return new JSONResponse([]); } diff --git a/apps/dav/lib/Db/Absence.php b/apps/dav/lib/Db/Absence.php index 3cd8037d57e..dc9afb59653 100644 --- a/apps/dav/lib/Db/Absence.php +++ b/apps/dav/lib/Db/Absence.php @@ -27,7 +27,6 @@ declare(strict_types=1); namespace OCA\DAV\Db; use DateTime; -use DateTimeZone; use Exception; use InvalidArgumentException; use JsonSerializable; @@ -58,6 +57,7 @@ class Absence extends Entity implements JsonSerializable { protected string $lastDay = ''; protected string $status = ''; + protected string $message = ''; public function __construct() { @@ -76,7 +76,7 @@ class Absence extends Entity implements JsonSerializable { throw new Exception('Creating out-of-office data without ID'); } - $tz = new DateTimeZone($timezone); + $tz = new \DateTimeZone($timezone); $startDate = new DateTime($this->getFirstDay(), $tz); $endDate = new DateTime($this->getLastDay(), $tz); $endDate->setTime(23, 59); diff --git a/apps/dav/lib/Listener/OutOfOfficeListener.php b/apps/dav/lib/Listener/OutOfOfficeListener.php index 609c32f5067..3d3bbfb2f42 100644 --- a/apps/dav/lib/Listener/OutOfOfficeListener.php +++ b/apps/dav/lib/Listener/OutOfOfficeListener.php @@ -52,21 +52,21 @@ use function rewind; * @template-implements IEventListener */ class OutOfOfficeListener implements IEventListener { - public function __construct(private ServerFactory $serverFactory, + public function __construct( + private ServerFactory $serverFactory, private IConfig $appConfig, - private LoggerInterface $logger) { + private LoggerInterface $logger + ) { } public function handle(Event $event): void { if ($event instanceof OutOfOfficeScheduledEvent) { $userId = $event->getData()->getUser()->getUID(); $principal = "principals/users/$userId"; - $calendarNode = $this->getCalendarNode($principal, $userId); if ($calendarNode === null) { return; } - $tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null; $vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz); $stream = fopen('php://memory', 'rb+'); @@ -83,7 +83,6 @@ class OutOfOfficeListener implements IEventListener { } elseif ($event instanceof OutOfOfficeChangedEvent) { $userId = $event->getData()->getUser()->getUID(); $principal = "principals/users/$userId"; - $calendarNode = $this->getCalendarNode($principal, $userId); if ($calendarNode === null) { return; @@ -110,17 +109,16 @@ class OutOfOfficeListener implements IEventListener { } elseif ($event instanceof OutOfOfficeClearedEvent) { $userId = $event->getData()->getUser()->getUID(); $principal = "principals/users/$userId"; - $calendarNode = $this->getCalendarNode($principal, $userId); if ($calendarNode === null) { return; } - try { $oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId())); $oldEvent->delete(); } catch (NotFound) { // The user must have deleted it or the default calendar changed -> ignore + return; } } } diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 3f5168e386d..874e86f6e1c 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -32,12 +32,16 @@ use OCA\DAV\CalDAV\TimezoneService; use OCA\DAV\Db\Absence; use OCA\DAV\Db\AbsenceMapper; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; +use OCP\Calendar\IManager; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; use OCP\IUser; use OCP\User\Events\OutOfOfficeChangedEvent; use OCP\User\Events\OutOfOfficeClearedEvent; use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\IOutOfOfficeData; class AbsenceService { public function __construct( @@ -45,6 +49,9 @@ class AbsenceService { private IEventDispatcher $eventDispatcher, private IJobList $jobList, private TimezoneService $timezoneService, + private ITimeFactory $timeFactory, + private IConfig $appConfig, + private IManager $calendarManager, ) { } @@ -128,4 +135,17 @@ class AbsenceService { ); $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData)); } + + public function getAbsence(string $userId): ?Absence { + try { + return $this->absenceMapper->findByUserId($userId); + } catch (DoesNotExistException $e) { + return null; + } + } + + public function isInEffect(IOutOfOfficeData $absence): bool { + $now = $this->timeFactory->getTime(); + return $absence->getStartDate() <= $now && $absence->getEndDate() >= $now; + } } diff --git a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php index 59438c7cd28..fe6919d8dae 100644 --- a/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php +++ b/apps/dav/tests/unit/BackgroundJob/UserStatusAutomationTest.php @@ -26,10 +26,14 @@ declare(strict_types=1); namespace OCA\DAV\Tests\unit\BackgroundJob; +use OC\User\OutOfOfficeData; use OCA\DAV\BackgroundJob\UserStatusAutomation; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\IAvailabilityCoordinator; use OCP\UserStatus\IManager; use OCP\UserStatus\IUserStatus; use PHPUnit\Framework\MockObject\MockObject; @@ -46,6 +50,8 @@ class UserStatusAutomationTest extends TestCase { protected MockObject|LoggerInterface $logger; protected MockObject|IManager $statusManager; protected MockObject|IConfig $config; + private IAvailabilityCoordinator|MockObject $coordinator; + private IUserManager|MockObject $userManager; protected function setUp(): void { parent::setUp(); @@ -55,6 +61,8 @@ class UserStatusAutomationTest extends TestCase { $this->logger = $this->createMock(LoggerInterface::class); $this->statusManager = $this->createMock(IManager::class); $this->config = $this->createMock(IConfig::class); + $this->coordinator = $this->createMock(IAvailabilityCoordinator::class); + $this->userManager = $this->createMock(IUserManager::class); } @@ -67,6 +75,8 @@ class UserStatusAutomationTest extends TestCase { $this->logger, $this->statusManager, $this->config, + $this->coordinator, + $this->userManager, ); } @@ -78,6 +88,8 @@ class UserStatusAutomationTest extends TestCase { $this->logger, $this->statusManager, $this->config, + $this->coordinator, + $this->userManager, ]) ->setMethods($methods) ->getMock(); @@ -95,14 +107,31 @@ class UserStatusAutomationTest extends TestCase { /** * @dataProvider dataRun */ - public function testRun(string $ruleDay, string $currentTime, bool $isAvailable): void { + public function testRunNoOOO(string $ruleDay, string $currentTime, bool $isAvailable): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); $this->config->method('getUserValue') ->with('user', 'dav', 'user_status_automation', 'no') ->willReturn('yes'); - $this->time->method('getDateTime') ->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC'))); - + $this->logger->expects(self::exactly(4)) + ->method('debug'); + $this->statusManager->expects(self::exactly(2)) + ->method('revertUserStatus'); + if (!$isAvailable) { + $this->statusManager->expects(self::once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']); $automation->method('getAvailabilityFromPropertiesTable') ->with('user') @@ -141,63 +170,74 @@ END:AVAILABLE END:VAVAILABILITY END:VCALENDAR'); - if ($isAvailable) { - $this->statusManager->expects($this->once()) - ->method('revertUserStatus') - ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); - } else { - $this->statusManager->expects($this->once()) - ->method('revertUserStatus') - ->with('user', IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); - $this->statusManager->expects($this->once()) - ->method('setUserStatus') - ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); - } - self::invokePrivate($automation, 'run', [['userId' => 'user']]); } - public function testRunNoMoreAvailabilityDefined(): void { + public function testRunNoAvailabilityNoOOO(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn(null); $this->config->method('getUserValue') ->with('user', 'dav', 'user_status_automation', 'no') ->willReturn('yes'); - $this->time->method('getDateTime') ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC'))); - + $this->statusManager->expects($this->exactly(3)) + ->method('revertUserStatus'); + $this->jobList->expects($this->once()) + ->method('remove') + ->with(UserStatusAutomation::class, ['userId' => 'user']); + $this->logger->expects(self::once()) + ->method('debug'); + $this->logger->expects(self::once()) + ->method('info'); $automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']); $automation->method('getAvailabilityFromPropertiesTable') ->with('user') - ->willReturn('BEGIN:VCALENDAR -PRODID:Nextcloud DAV app -BEGIN:VTIMEZONE -TZID:Europe/Berlin -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19701025T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19700329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VAVAILABILITY -END:VAVAILABILITY -END:VCALENDAR'); + ->willReturn(false); - $this->statusManager->expects($this->once()) - ->method('revertUserStatus') - ->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + self::invokePrivate($automation, 'run', [['userId' => 'user']]); + } - $this->jobList->expects($this->once()) - ->method('remove') - ->with(UserStatusAutomation::class, ['userId' => 'user']); + public function testRunNoAvailabilityWithOOO(): void { + $user = $this->createConfiguredMock(IUser::class, [ + 'getUID' => 'user' + ]); + $ooo = $this->createConfiguredMock(OutOfOfficeData::class, [ + 'getShortMessage' => 'On Vacation', + 'getEndDate' => 123456, + ]); + + $this->userManager->expects(self::once()) + ->method('get') + ->willReturn($user); + $this->coordinator->expects(self::once()) + ->method('getCurrentOutOfOfficeData') + ->willReturn($ooo); + $this->coordinator->expects(self::once()) + ->method('isInEffect') + ->willReturn(true); + $this->statusManager->expects($this->exactly(2)) + ->method('revertUserStatus'); + $this->statusManager->expects(self::once()) + ->method('setUserStatus') + ->with('user', IUserStatus::MESSAGE_VACATION, IUserStatus::DND, true, $ooo->getShortMessage()); + $this->config->expects(self::never()) + ->method('getUserValue'); + $this->time->method('getDateTime') + ->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC'))); + $this->jobList->expects($this->never()) + ->method('remove'); + $this->logger->expects(self::exactly(2)) + ->method('debug'); + $automation = $this->getAutomationMock([]); self::invokePrivate($automation, 'run', [['userId' => 'user']]); } diff --git a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php index ef77d715180..705298de125 100644 --- a/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php +++ b/apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php @@ -27,14 +27,12 @@ use OCA\DAV\CalDAV\CalendarImpl; use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator; use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; use OCA\DAV\CalDAV\Schedule\Plugin; -use OCA\DAV\CalDAV\Status\Status; use OCA\DAV\CalDAV\Status\StatusService; use OCA\DAV\Connector\Sabre\Server; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\IManager; use OCP\IL10N; use OCP\IUser; -use OCP\UserStatus\IUserStatus; use PHPUnit\Framework\MockObject\MockObject; use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; use Sabre\DAV\Exception\NotAuthenticated; @@ -42,7 +40,6 @@ use Sabre\DAV\Xml\Property\LocalHref; use Sabre\DAVACL\Exception\NeedPrivileges; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VTimeZone; -use Sabre\VObject\Document; use Sabre\VObject\Reader; use Test\TestCase; @@ -75,7 +72,6 @@ class StatusServiceTest extends TestCase { 'getUID' => 'admin', 'getEMailAddress' => null, ]); - $availability = ''; $user->expects(self::once()) ->method('getUID') @@ -103,12 +99,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -161,12 +155,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -219,12 +211,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -284,12 +274,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -348,12 +336,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -417,12 +403,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -496,12 +480,10 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } @@ -599,16 +581,14 @@ class StatusServiceTest extends TestCase { ->method('setTimeRange'); $this->generator->expects(self::never()) ->method('setTimeZone'); - $this->generator->expects(self::never()) - ->method('setVAvailability'); $this->generator->expects(self::never()) ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } - public function testAvailabilityAndSearchCalendarsNoResults(): void { + public function testSearchCalendarsNoResults(): void { $user = $this->createConfiguredMock(IUser::class, [ 'getUID' => 'admin', 'getEMailAddress' => 'test@test.com', @@ -627,7 +607,6 @@ class StatusServiceTest extends TestCase { $timezoneObj = $this->createMock(VTimeZone::class); $calendar = $this->createMock(CalendarImpl::class); $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); $result = Reader::read('BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Sabre//Sabre VObject 4.5.3//EN @@ -701,809 +680,20 @@ END:VCALENDAR'); ->method('searchForPrincipal') ->with($query) ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); + $this->generator->expects(self::never()) + ->method('getVCalendar'); $vCalendar->expects(self::never()) ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); + $this->generator->expects(self::never()) + ->method('setObjects'); + $this->generator->expects(self::never()) + ->method('setTimeRange'); + $this->generator->expects(self::never()) + ->method('setTimeZone'); + $this->generator->expects(self::never()) + ->method('getResult'); - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); + $status = $this->service->processCalendarAvailability($user); $this->assertNull($status); } - - public function testAvailabilityAndSearchCalendarsStatusOnline(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertEquals(new Status(IUserStatus::ONLINE), $status); - } - - public function testAvailabilityAndSearchCalendarsStatusBusyNoFBType(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -FREEBUSY:19700101T000000Z/19700101T003600Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY), $status); - } - - public function testAvailabilityAndSearchCalendarsStatusBusy(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -FREEBUSY;FBTYPE=BUSY:19700101T000000Z/19700101T003600Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - $this->l10n->expects(self::once()) - ->method('t') - ->with('In a meeting') - ->willReturn('In a meeting'); - - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertEquals(new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, 'In a meeting'), $status); - } - - public function testAvailabilityAndSearchCalendarsStatusBusyUnavailable(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:19700101T000000Z/19700101T003600Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - $this->l10n->expects(self::never()) - ->method('t'); - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertEquals(new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY), $status); - } - - public function testAvailabilityAndSearchCalendarsStatusBusyTentative(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -FREEBUSY;FBTYPE=BUSY-TENTATIVE:19700101T000000Z/19700101T003600Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - $this->l10n->expects(self::never()) - ->method('t'); - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertEquals(new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE), $status); - } - - public function testAvailabilityAndSearchCalendarsStatusBusyUnknownProperty(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $server = $this->createMock(Server::class); - $schedulingPlugin = $this->createMock(Plugin::class); - $aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class); - $calendarHome = $this->createMock(LocalHref::class); - $acl = [[200 => ['{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL' => $calendarHome]]]; - $now = new \DateTimeImmutable('1970-1-1 00:00', new \DateTimeZone('UTC')); - $inTenMinutes = new \DateTime('1970-1-1 01:00'); - $immutableInTenMinutes = \DateTimeImmutable::createFromMutable($inTenMinutes); - $principal = 'principals/users/admin'; - $query = $this->createMock(CalendarQuery::class); - $timezone = new \DateTimeZone('UTC'); - $timezoneObj = $this->createMock(VTimeZone::class); - $calendar = $this->createMock(CalendarImpl::class); - $vCalendar = $this->createMock(VCalendar::class); - $availability = $this->getVAvailability(); - $result = Reader::read('BEGIN:VCALENDAR -VERSION:2.0 -PRODID:-//Sabre//Sabre VObject 4.5.3//EN - CALSCALE:GREGORIAN -METHOD:REQUEST -BEGIN:VFREEBUSY -DTSTART:19700101T000000Z -DTEND:19700101T003600Z -DTSTAMP:19700101T000200Z -FREEBUSY;FBTYPE=X-MEETING:19700101T000000Z/19700101T003600Z -END:VFREEBUSY -END:VCALENDAR'); - - $user->expects(self::once()) - ->method('getUID') - ->willReturn('admin'); - $user->expects(self::once()) - ->method('getEMailAddress') - ->willReturn('test@test.com'); - $this->server->expects(self::once()) - ->method('getServer') - ->willReturn($server); - $server->expects(self::exactly(2)) - ->method('getPlugin') - ->withConsecutive( - ['caldav-schedule'], - ['acl'], - )->willReturnOnConsecutiveCalls($schedulingPlugin, $aclPlugin); - $aclPlugin->expects(self::once()) - ->method('principalSearch') - ->with(['{http://sabredav.org/ns}email-address' => 'test@test.com']) - ->willReturn($acl); - $calendarHome->expects(self::once()) - ->method('getHref') - ->willReturn('calendars/admin/inbox/'); - $aclPlugin->expects(self::once()) - ->method('checkPrivileges') - ->willReturn(true); - $this->timeFactory->expects(self::once()) - ->method('now') - ->willReturn($now); - $this->calendarManager->expects(self::once()) - ->method('getCalendarsForPrincipal') - ->with($principal) - ->willReturn([$calendar]); - $this->calendarManager->expects(self::once()) - ->method('newQuery') - ->with($principal) - ->willReturn($query); - $calendar->expects(self::once()) - ->method('getSchedulingTransparency') - ->willReturn(new ScheduleCalendarTransp('opaque')); - $calendar->expects(self::once()) - ->method('getSchedulingTimezone') - ->willReturn($timezoneObj); - $timezoneObj->expects(self::once()) - ->method('getTimeZone') - ->willReturn($timezone); - $calendar->expects(self::once()) - ->method('getUri'); - $query->expects(self::once()) - ->method('addSearchCalendar'); - $query->expects(self::once()) - ->method('getCalendarUris') - ->willReturn([$calendar]); - $this->timeFactory->expects(self::once()) - ->method('getDateTime') - ->with('+10 minutes') - ->willReturn($inTenMinutes); - $query->expects(self::once()) - ->method('setTimerangeStart') - ->with($now); - $query->expects(self::once()) - ->method('setTimerangeEnd') - ->with($immutableInTenMinutes); - $this->calendarManager->expects(self::once()) - ->method('searchForPrincipal') - ->with($query) - ->willReturn([]); - $this->generator->expects(self::once()) - ->method('getVCalendar') - ->willReturn($vCalendar); - $vCalendar->expects(self::never()) - ->method('add'); - $this->generator->expects(self::once()) - ->method('setObjects') - ->with($vCalendar); - $this->generator->expects(self::once()) - ->method('setTimeRange') - ->with($now, $immutableInTenMinutes); - $this->generator->expects(self::once()) - ->method('setTimeZone') - ->with($timezone); - $this->generator->expects(self::once()) - ->method('setVAvailability') - ->with($availability); - $this->generator->expects(self::once()) - ->method('getResult') - ->willReturn($result); - $this->l10n->expects(self::never()) - ->method('t'); - $status = $this->service->processCalendarAvailability($user, $availability->serialize()); - $this->assertNull($status); - } - - private function getVAvailability(): Document { - return Reader::read('BEGIN:VCALENDAR -PRODID:Nextcloud DAV app -BEGIN:VTIMEZONE -TZID:Europe/Vienna -BEGIN:STANDARD -TZNAME:CET -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -DTSTART:19701025T030000 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -END:STANDARD -BEGIN:DAYLIGHT -TZNAME:CEST -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -DTSTART:19700329T020000 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -END:DAYLIGHT -END:VTIMEZONE -BEGIN:VAVAILABILITY -BEGIN:AVAILABLE -DTSTART;TZID=Europe/Vienna:20231025T000000 -DTEND;TZID=Europe/Vienna:20231025T235900 -UID:d866782e-e003-4906-9ece-303f270a2c6b -RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU -END:AVAILABLE -END:VAVAILABILITY -END:VCALENDAR'); - } } diff --git a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php index 02227adfbfc..919cf0a9ccf 100644 --- a/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php +++ b/apps/dav/tests/unit/Listener/OutOfOfficeListenerTest.php @@ -40,6 +40,7 @@ use OCP\User\Events\OutOfOfficeChangedEvent; use OCP\User\Events\OutOfOfficeClearedEvent; use OCP\User\Events\OutOfOfficeScheduledEvent; use OCP\User\IOutOfOfficeData; +use OCP\UserStatus\IManager; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\NotFound; @@ -55,6 +56,7 @@ class OutOfOfficeListenerTest extends TestCase { private IConfig|MockObject $appConfig; private LoggerInterface|MockObject $loggerInterface; private OutOfOfficeListener $listener; + private IManager|MockObject $manager; protected function setUp(): void { parent::setUp(); @@ -62,11 +64,13 @@ class OutOfOfficeListenerTest extends TestCase { $this->serverFactory = $this->createMock(ServerFactory::class); $this->appConfig = $this->createMock(IConfig::class); $this->loggerInterface = $this->createMock(LoggerInterface::class); + $this->manager = $this->createMock(IManager::class); $this->listener = new OutOfOfficeListener( $this->serverFactory, $this->appConfig, $this->loggerInterface, + $this->manager ); } @@ -389,6 +393,8 @@ class OutOfOfficeListenerTest extends TestCase { ->method('getPlugin') ->with('caldav') ->willReturn($caldavPlugin); + $this->manager->expects(self::never()) + ->method('revertUserStatus'); $event = new OutOfOfficeClearedEvent($data); $this->listener->handle($event); @@ -417,6 +423,8 @@ class OutOfOfficeListenerTest extends TestCase { ->method('getNodeForPath') ->with('/home/calendar') ->willThrowException(new NotFound('nope')); + $this->manager->expects(self::never()) + ->method('revertUserStatus'); $event = new OutOfOfficeClearedEvent($data); $this->listener->handle($event); @@ -454,6 +462,8 @@ class OutOfOfficeListenerTest extends TestCase { ->method('getChild') ->with('personal-1') ->willThrowException(new NotFound('nope')); + $this->manager->expects(self::never()) + ->method('revertUserStatus'); $event = new OutOfOfficeClearedEvent($data); $this->listener->handle($event); @@ -495,6 +505,8 @@ class OutOfOfficeListenerTest extends TestCase { $calendar->expects(self::once()) ->method('getChild') ->willThrowException(new NotFound()); + $this->manager->expects(self::never()) + ->method('revertUserStatus'); $event = new OutOfOfficeClearedEvent($data); $this->listener->handle($event); diff --git a/apps/user_status/composer/composer/ClassLoader.php b/apps/user_status/composer/composer/ClassLoader.php index 7824d8f7eaf..fd56bd7d840 100644 --- a/apps/user_status/composer/composer/ClassLoader.php +++ b/apps/user_status/composer/composer/ClassLoader.php @@ -45,34 +45,35 @@ class ClassLoader /** @var \Closure(string):void */ private static $includeFile; - /** @var string|null */ + /** @var ?string */ private $vendorDir; // PSR-4 /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixLengthsPsr4 = array(); /** - * @var array> + * @var array[] + * @psalm-var array> */ private $prefixDirsPsr4 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr4 = array(); // PSR-0 /** - * List of PSR-0 prefixes - * - * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) - * - * @var array>> + * @var array[] + * @psalm-var array> */ private $prefixesPsr0 = array(); /** - * @var list + * @var array[] + * @psalm-var array */ private $fallbackDirsPsr0 = array(); @@ -80,7 +81,8 @@ class ClassLoader private $useIncludePath = false; /** - * @var array + * @var string[] + * @psalm-var array */ private $classMap = array(); @@ -88,20 +90,21 @@ class ClassLoader private $classMapAuthoritative = false; /** - * @var array + * @var bool[] + * @psalm-var array */ private $missingClasses = array(); - /** @var string|null */ + /** @var ?string */ private $apcuPrefix; /** - * @var array + * @var self[] */ private static $registeredLoaders = array(); /** - * @param string|null $vendorDir + * @param ?string $vendorDir */ public function __construct($vendorDir = null) { @@ -110,7 +113,7 @@ class ClassLoader } /** - * @return array> + * @return string[] */ public function getPrefixes() { @@ -122,7 +125,8 @@ class ClassLoader } /** - * @return array> + * @return array[] + * @psalm-return array> */ public function getPrefixesPsr4() { @@ -130,7 +134,8 @@ class ClassLoader } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirs() { @@ -138,7 +143,8 @@ class ClassLoader } /** - * @return list + * @return array[] + * @psalm-return array */ public function getFallbackDirsPsr4() { @@ -146,7 +152,8 @@ class ClassLoader } /** - * @return array Array of classname => path + * @return string[] Array of classname => path + * @psalm-return array */ public function getClassMap() { @@ -154,7 +161,8 @@ class ClassLoader } /** - * @param array $classMap Class to filename map + * @param string[] $classMap Class to filename map + * @psalm-param array $classMap * * @return void */ @@ -171,25 +179,24 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 root directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories * * @return void */ public function add($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, - $paths + (array) $paths ); } @@ -198,19 +205,19 @@ class ClassLoader $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { - $this->prefixesPsr0[$first][$prefix] = $paths; + $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], - $paths + (array) $paths ); } } @@ -219,9 +226,9 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories - * @param bool $prepend Whether to prepend the directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException * @@ -229,18 +236,17 @@ class ClassLoader */ public function addPsr4($prefix, $paths, $prepend = false) { - $paths = (array) $paths; if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( - $paths, + (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, - $paths + (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { @@ -250,18 +256,18 @@ class ClassLoader throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; - $this->prefixDirsPsr4[$prefix] = $paths; + $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( - $paths, + (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], - $paths + (array) $paths ); } } @@ -270,8 +276,8 @@ class ClassLoader * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * - * @param string $prefix The prefix - * @param list|string $paths The PSR-0 base directories + * @param string $prefix The prefix + * @param string[]|string $paths The PSR-0 base directories * * @return void */ @@ -288,8 +294,8 @@ class ClassLoader * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * - * @param string $prefix The prefix/namespace, with trailing '\\' - * @param list|string $paths The PSR-4 base directories + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param string[]|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException * @@ -423,8 +429,7 @@ class ClassLoader public function loadClass($class) { if ($file = $this->findFile($class)) { - $includeFile = self::$includeFile; - $includeFile($file); + (self::$includeFile)($file); return true; } @@ -475,9 +480,9 @@ class ClassLoader } /** - * Returns the currently registered loaders keyed by their corresponding vendor directories. + * Returns the currently registered loaders indexed by their corresponding vendor directories. * - * @return array + * @return self[] */ public static function getRegisteredLoaders() { @@ -555,10 +560,7 @@ class ClassLoader return false; } - /** - * @return void - */ - private static function initializeIncludeClosure() + private static function initializeIncludeClosure(): void { if (self::$includeFile !== null) { return; @@ -572,8 +574,8 @@ class ClassLoader * @param string $file * @return void */ - self::$includeFile = \Closure::bind(static function($file) { + self::$includeFile = static function($file) { include $file; - }, null, null); + }; } } diff --git a/apps/user_status/composer/composer/autoload_classmap.php b/apps/user_status/composer/composer/autoload_classmap.php index ecdd83dbb68..b57df813bc9 100644 --- a/apps/user_status/composer/composer/autoload_classmap.php +++ b/apps/user_status/composer/composer/autoload_classmap.php @@ -26,6 +26,7 @@ return array( 'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => $baseDir . '/../lib/Exception/InvalidStatusTypeException.php', 'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => $baseDir . '/../lib/Exception/StatusMessageTooLongException.php', 'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => $baseDir . '/../lib/Listener/BeforeTemplateRenderedListener.php', + 'OCA\\UserStatus\\Listener\\OutOfOfficeStatusListener' => $baseDir . '/../lib/Listener/OutOfOfficeStatusListener.php', 'OCA\\UserStatus\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php', 'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => $baseDir . '/../lib/Listener/UserLiveStatusListener.php', 'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => $baseDir . '/../lib/Migration/Version0001Date20200602134824.php', diff --git a/apps/user_status/composer/composer/autoload_static.php b/apps/user_status/composer/composer/autoload_static.php index 55be0c04d43..7e494344490 100644 --- a/apps/user_status/composer/composer/autoload_static.php +++ b/apps/user_status/composer/composer/autoload_static.php @@ -41,6 +41,7 @@ class ComposerStaticInitUserStatus 'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => __DIR__ . '/..' . '/../lib/Exception/InvalidStatusTypeException.php', 'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => __DIR__ . '/..' . '/../lib/Exception/StatusMessageTooLongException.php', 'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeTemplateRenderedListener.php', + 'OCA\\UserStatus\\Listener\\OutOfOfficeStatusListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeStatusListener.php', 'OCA\\UserStatus\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php', 'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => __DIR__ . '/..' . '/../lib/Listener/UserLiveStatusListener.php', 'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => __DIR__ . '/..' . '/../lib/Migration/Version0001Date20200602134824.php', diff --git a/apps/user_status/lib/AppInfo/Application.php b/apps/user_status/lib/AppInfo/Application.php index 68e5e6169ee..26f736bbc24 100644 --- a/apps/user_status/lib/AppInfo/Application.php +++ b/apps/user_status/lib/AppInfo/Application.php @@ -29,6 +29,7 @@ use OCA\UserStatus\Capabilities; use OCA\UserStatus\Connector\UserStatusProvider; use OCA\UserStatus\Dashboard\UserStatusWidget; use OCA\UserStatus\Listener\BeforeTemplateRenderedListener; +use OCA\UserStatus\Listener\OutOfOfficeStatusListener; use OCA\UserStatus\Listener\UserDeletedListener; use OCA\UserStatus\Listener\UserLiveStatusListener; use OCP\AppFramework\App; @@ -37,6 +38,9 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; use OCP\IConfig; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IManager; @@ -71,6 +75,9 @@ class Application extends App implements IBootstrap { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + $context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeStatusListener::class); $config = $this->getContainer()->query(IConfig::class); $shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; diff --git a/apps/user_status/lib/Connector/UserStatusProvider.php b/apps/user_status/lib/Connector/UserStatusProvider.php index 4bc2645dceb..700cd3c10fc 100644 --- a/apps/user_status/lib/Connector/UserStatusProvider.php +++ b/apps/user_status/lib/Connector/UserStatusProvider.php @@ -57,8 +57,8 @@ class UserStatusProvider implements IProvider, ISettableProvider { return $userStatuses; } - public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void { - $this->service->setUserStatus($userId, $status, $messageId, $createBackup); + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void { + $this->service->setUserStatus($userId, $status, $messageId, $createBackup, $customMessage); } public function revertUserStatus(string $userId, string $messageId, string $status): void { diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php index febee32c821..3621c1bfa96 100644 --- a/apps/user_status/lib/Db/UserStatusMapper.php +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -30,7 +30,6 @@ use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\UserStatus\IUserStatus; -use Sabre\CalDAV\Schedule\Plugin; /** * @template-extends QBMapper @@ -211,23 +210,4 @@ class UserStatusMapper extends QBMapper { $qb->executeStatement(); } - - public function getAvailabilityFromPropertiesTable(string $userId): ?string { - $propertyPath = 'calendars/' . $userId . '/inbox'; - $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; - - $query = $this->db->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 === false ? null : $property); - } } diff --git a/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php new file mode 100644 index 00000000000..0d793c3f306 --- /dev/null +++ b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php @@ -0,0 +1,67 @@ + + * + * @author Anna Larch + * + * @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 . + * + */ +namespace OCA\UserStatus\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; + +/** + * Class UserDeletedListener + * + * @template-implements IEventListener + * + */ +class OutOfOfficeStatusListener implements IEventListener { + public function __construct(private IJobList $jobsList, + private ITimeFactory $time, + private IManager $manager) { + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if($event instanceof OutOfOfficeClearedEvent) { + $this->manager->revertUserStatus($event->getData()->getUser()->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND); + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + return; + } + + if ($event instanceof OutOfOfficeScheduledEvent + || $event instanceof OutOfOfficeChangedEvent) { + // This might be overwritten by the office hours automation, but that is ok. This is just in case no office hours are set + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + } + } +} diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php index 516680ba683..c77ca588ebb 100644 --- a/apps/user_status/lib/Service/PredefinedStatusService.php +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -202,6 +202,7 @@ class PredefinedStatusService { self::REMOTE_WORK, IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_AVAILABILITY, + IUserStatus::MESSAGE_VACATION, IUserStatus::MESSAGE_CALENDAR_BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE, ], true); diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php index 829c6c58570..114764f193d 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -137,37 +137,8 @@ class StatusService { * @return UserStatus * @throws DoesNotExistException */ - public function findByUserId(string $userId): UserStatus { - $userStatus = $this->mapper->findByUserId($userId); - // If the status is user-defined and one of the persistent status, we - // will not override it. - if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { - return $this->processStatus($userStatus); - } - - $calendarStatus = $this->getCalendarStatus($userId); - // We found no status from the calendar, proceed with the existing status - if($calendarStatus === null) { - return $this->processStatus($userStatus); - } - - // if we have the same status result for the calendar and the current status, - // and a custom message to boot, we leave the existing status alone - // as to not overwrite a custom message / emoji - if($userStatus->getIsUserDefined() - && $calendarStatus->getStatus() === $userStatus->getStatus() - && !empty($userStatus->getCustomMessage())) { - return $this->processStatus($userStatus); - } - - // If the new status is null, there's already an identical status in place - $newUserStatus = $this->setUserStatus($userId, - $calendarStatus->getStatus(), - $calendarStatus->getMessage() ?? IUserStatus::MESSAGE_AVAILABILITY, - true, - $calendarStatus->getCustomMessage() ?? ''); - - return $newUserStatus === null ? $this->processStatus($userStatus) : $this->processStatus($newUserStatus); + public function findByUserId(string $userId):UserStatus { + return $this->processStatus($this->mapper->findByUserId($userId)); } /** @@ -271,6 +242,7 @@ class StatusService { * @param string $status * @param string $messageId * @param bool $createBackup + * @param string|null $customMessage * @throws InvalidStatusTypeException * @throws InvalidMessageIdException */ @@ -278,7 +250,7 @@ class StatusService { string $status, string $messageId, bool $createBackup, - string $customMessage = null): ?UserStatus { + ?string $customMessage = null): ?UserStatus { // Check if status-type is valid if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); @@ -313,13 +285,7 @@ class StatusService { $userStatus->setCustomIcon(null); $userStatus->setCustomMessage($customMessage); $userStatus->setClearAt(null); - if ($this->predefinedStatusService->getTranslatedStatusForId($messageId) !== null - || ($customMessage !== null && $customMessage !== '')) { - // Only track status message ID if there is one - $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); - } else { - $userStatus->setStatusMessageTimestamp(0); - } + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); if ($userStatus->getId() !== null) { return $this->mapper->update($userStatus); @@ -506,8 +472,14 @@ class StatusService { private function addDefaultMessage(UserStatus $status): void { // If the message is predefined, insert the translated message and icon $predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId()); - if ($predefinedMessage !== null) { + if ($predefinedMessage === null) { + return; + } + // If there is a custom message, don't overwrite it + if(empty($status->getCustomMessage())) { $status->setCustomMessage($predefinedMessage['message']); + } + if(empty($status->getCustomIcon())) { $status->setCustomIcon($predefinedMessage['icon']); } } @@ -588,8 +560,7 @@ class StatusService { } /** - * Calculate a users' status according to their availabilit settings and their calendar - * events + * Calculate a users' status according to their calendar events * * There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE', * but 'X-' properties are possible @@ -598,11 +569,10 @@ class StatusService { * * The status will be changed for types * - 'BUSY' - * - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect) * - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively) * and all FREEBUSY components without a type (implicitly a 'BUSY' status) * - * 'X-' properties are not handled for now + * 'X-' properties and BUSY-UNAVAILABLE is not handled * * @param string $userId * @return CalendarStatus|null @@ -612,9 +582,6 @@ class StatusService { if ($user === null) { return null; } - - $availability = $this->mapper->getAvailabilityFromPropertiesTable($userId); - - return $this->calendarStatusService->processCalendarAvailability($user, $availability); + return $this->calendarStatusService->processCalendarAvailability($user); } } diff --git a/apps/user_status/tests/Unit/Service/StatusServiceTest.php b/apps/user_status/tests/Unit/Service/StatusServiceTest.php index 8789a425622..2de041712bd 100644 --- a/apps/user_status/tests/Unit/Service/StatusServiceTest.php +++ b/apps/user_status/tests/Unit/Service/StatusServiceTest.php @@ -845,15 +845,13 @@ class StatusServiceTest extends TestCase { ->method('get') ->with($userId) ->willReturn(null); - $this->mapper->expects(self::never()) - ->method('getAvailabilityFromPropertiesTable'); $this->calendarStatusService->expects(self::never()) ->method('processCalendarAvailability'); $this->service->getCalendarStatus($userId); } - public function testCalendarAvailabilityNoVavailablility(): void { + public function testCalendarAvailabilityNoStatus(): void { $user = $this->createConfiguredMock(IUser::class, [ 'getUID' => 'admin', 'getEMailAddress' => 'test@test.com', @@ -863,299 +861,11 @@ class StatusServiceTest extends TestCase { ->method('get') ->with($user->getUID()) ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn(''); $this->calendarStatusService->expects(self::once()) ->method('processCalendarAvailability') - ->with($user, '') + ->with($user) ->willReturn(null); $this->service->getCalendarStatus($user->getUID()); } - - public function testCalendarAvailabilityVavailablilityAvailable(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - - $vavailability = <<userManager->expects(self::once()) - ->method('get') - ->with($user->getUID()) - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn($vavailability); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, $vavailability) - ->willReturn($status); - - $this->service->getCalendarStatus($user->getUID()); - } - - public function testCalendarAvailabilityVavailablilityUpdate(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting'); - $vavailability = <<userManager->expects(self::once()) - ->method('get') - ->with($user->getUID()) - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn($vavailability); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, $vavailability) - ->willReturn($calDavStatus); - - $this->service->getCalendarStatus($user->getUID()); - } - - public function testFindByUserIdUserDefinedAndPersistent(): void { - $status = new UserStatus(); - $status->setIsUserDefined(true); - $status->setStatus(IUserStatus::DND); - - $this->mapper->expects($this->once()) - ->method('findByUserId') - ->with('admin') - ->willReturn($status); - $this->mapper->expects(self::never()) - ->method('getAvailabilityFromPropertiesTable'); - $this->calendarStatusService->expects(self::never()) - ->method('processCalendarAvailability'); - - $this->assertEquals($status, $this->service->findByUserId('admin')); - } - - public function testFindByUserIdUserDefinedNoCalStatus(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $status = new UserStatus(); - $status->setIsUserDefined(true); - $status->setStatus(IUserStatus::ONLINE); - - $this->mapper->expects($this->once()) - ->method('findByUserId') - ->with($user->getUID()) - ->willReturn($status); - $this->userManager->expects(self::once()) - ->method('get') - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn(''); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, '') - ->willReturn(null); - - $this->assertEquals($status, $this->service->findByUserId('admin')); - } - - public function testFindByUserIdUserDefinedCalStatusIdentical(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $calDavStatus = new Status(IUserStatus::ONLINE); - $userStatus = new UserStatus(); - $userStatus->setStatus(IUserStatus::ONLINE); - $userStatus->setIsUserDefined(true); - $userStatus->setCustomMessage('Test'); - - $this->mapper->expects(self::once()) - ->method('findByUserId') - ->with($user->getUID()) - ->willReturn($userStatus); - $this->userManager->expects(self::once()) - ->method('get') - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn(''); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, '') - ->willReturn($calDavStatus); - - $this->assertEquals($userStatus, $this->service->findByUserId('admin')); - } - - public function testFindByUserIdUserDefinedCalStatusUpdate(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting'); - - $oldStatus = new UserStatus(); - $oldStatus->setId(42); - $oldStatus->setUserId($user->getUID()); - $oldStatus->setStatus(IUserStatus::ONLINE); - $oldStatus->setStatusTimestamp(0); - $oldStatus->setIsUserDefined(true); - - $expected = new UserStatus(); - $expected->setUserId($user->getUID()); - $expected->setStatus(IUserStatus::BUSY); - $expected->setStatusTimestamp(0); - $expected->setIsUserDefined(true); - $expected->setIsBackup(false); - - $this->mapper->expects(self::once()) - ->method('findByUserId') - ->with($user->getUID()) - ->willReturn($oldStatus); - $this->userManager->expects(self::once()) - ->method('get') - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn(''); - $this->mapper->expects(self::once()) - ->method('createBackupStatus') - ->with($user->getUID()) - ->willReturn(true); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, '') - ->willReturn($calDavStatus); - $this->predefinedStatusService->expects(self::once()) - ->method('isValidId') - ->with($calDavStatus->getMessage()) - ->willReturn(true); - $this->mapper->expects(self::once()) - ->method('insert') - ->willReturn($expected); - - $actual = $this->service->findByUserId('admin'); - $this->assertEquals($expected->getStatus(), $actual->getStatus()); - $this->assertEquals($expected->getCustomMessage(), $actual->getCustomMessage()); - } - - public function testFindByUserIdSystemDefined(): void { - $user = $this->createConfiguredMock(IUser::class, [ - 'getUID' => 'admin', - 'getEMailAddress' => 'test@test.com', - ]); - $status = new UserStatus(); - $status->setIsUserDefined(false); - $status->setStatus(IUserStatus::ONLINE); - - $this->mapper->expects($this->once()) - ->method('findByUserId') - ->with($user->getUID()) - ->willReturn($status); - $this->userManager->expects(self::once()) - ->method('get') - ->willReturn($user); - $this->mapper->expects(self::once()) - ->method('getAvailabilityFromPropertiesTable') - ->willReturn(''); - $this->calendarStatusService->expects(self::once()) - ->method('processCalendarAvailability') - ->with($user, '') - ->willReturn(null); - - $this->assertEquals($status, $this->service->findByUserId('admin')); - } - - public function testSetStatusWithoutMessage(): void { - $this->predefinedStatusService->expects(self::once()) - ->method('isValidId') - ->with(IUserStatus::MESSAGE_AVAILABILITY) - ->willReturn(true); - $this->timeFactory - ->method('getTime') - ->willReturn(1234); - $status = new UserStatus(); - $status->setUserId('admin'); - $status->setStatusTimestamp(1234); - $status->setIsUserDefined(true); - $status->setStatus(IUserStatus::DND); - $status->setIsBackup(false); - $status->setMessageId(IUserStatus::MESSAGE_AVAILABILITY); - $this->mapper->expects(self::once()) - ->method('insert') - ->with($this->equalTo($status)) - ->willReturnArgument(0); - - $result = $this->service->setUserStatus( - 'admin', - IUserStatus::DND, - IUserStatus::MESSAGE_AVAILABILITY, - true, - ); - - self::assertNotNull($result); - } } diff --git a/lib/private/User/AvailabilityCoordinator.php b/lib/private/User/AvailabilityCoordinator.php index e33a0aa1558..c32c3005c32 100644 --- a/lib/private/User/AvailabilityCoordinator.php +++ b/lib/private/User/AvailabilityCoordinator.php @@ -29,8 +29,7 @@ namespace OC\User; use JsonException; use OCA\DAV\AppInfo\Application; use OCA\DAV\CalDAV\TimezoneService; -use OCA\DAV\Db\AbsenceMapper; -use OCP\AppFramework\Db\DoesNotExistException; +use OCA\DAV\Service\AbsenceService; use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; @@ -44,8 +43,8 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator { public function __construct( ICacheFactory $cacheFactory, - private AbsenceMapper $absenceMapper, private IConfig $config, + private AbsenceService $absenceService, private LoggerInterface $logger, private TimezoneService $timezoneService, ) { @@ -53,11 +52,7 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator { } public function isEnabled(): bool { - return $this->config->getAppValue( - Application::APP_ID, - 'hide_absence_settings', - 'no', - ) === 'no'; + return $this->config->getAppValue(Application::APP_ID, 'hide_absence_settings', 'no') === 'no'; } private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData { @@ -106,22 +101,39 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator { } public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData { - $cachedData = $this->getCachedOutOfOfficeData($user); - if ($cachedData !== null) { - return $cachedData; + $timezone = $this->getCachedTimezone($user->getUID()); + if ($timezone === null) { + $timezone = $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(); + $this->setCachedTimezone($user->getUID(), $timezone); } - try { - $absenceData = $this->absenceMapper->findByUserId($user->getUID()); - } catch (DoesNotExistException $e) { - return null; + $data = $this->getCachedOutOfOfficeData($user); + if ($data === null) { + $absenceData = $this->absenceService->getAbsence($user->getUID()); + if ($absenceData === null) { + return null; + } + $data = $absenceData->toOutOufOfficeData($user, $timezone); } - $data = $absenceData->toOutOufOfficeData( - $user, - $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(), - ); $this->setCachedOutOfOfficeData($data); return $data; } + + private function getCachedTimezone(string $userId): ?string { + return $this->cache->get($userId . '_timezone') ?? null; + } + + private function setCachedTimezone(string $userId, string $timezone): void { + $this->cache->set($userId . '_timezone', $timezone, 3600); + } + + public function clearCache(string $userId): void { + $this->cache->set($userId, null, 300); + $this->cache->set($userId . '_timezone', null, 3600); + } + + public function isInEffect(IOutOfOfficeData $data): bool { + return $this->absenceService->isInEffect($data); + } } diff --git a/lib/private/UserStatus/ISettableProvider.php b/lib/private/UserStatus/ISettableProvider.php index 88a107d1f86..957d3274f1d 100644 --- a/lib/private/UserStatus/ISettableProvider.php +++ b/lib/private/UserStatus/ISettableProvider.php @@ -39,8 +39,9 @@ interface ISettableProvider extends IProvider { * @param string $messageId The new message id. * @param string $status The new status. * @param bool $createBackup If true, this will store the old status so that it is possible to revert it later (e.g. after a call). + * @param string|null $customMessage */ - public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void; + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void; /** * Revert an automatically set user status. For example after leaving a call, diff --git a/lib/private/UserStatus/Manager.php b/lib/private/UserStatus/Manager.php index e255e24c70e..a5594158c1e 100644 --- a/lib/private/UserStatus/Manager.php +++ b/lib/private/UserStatus/Manager.php @@ -104,13 +104,13 @@ class Manager implements IManager { $this->provider = $provider; } - public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void { + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false, ?string $customMessage = null): void { $this->setupProvider(); if (!$this->provider || !($this->provider instanceof ISettableProvider)) { return; } - $this->provider->setUserStatus($userId, $messageId, $status, $createBackup); + $this->provider->setUserStatus($userId, $messageId, $status, $createBackup, $customMessage); } public function revertUserStatus(string $userId, string $messageId, string $status): void { diff --git a/lib/public/User/IAvailabilityCoordinator.php b/lib/public/User/IAvailabilityCoordinator.php index 749241f13bc..3a79e39b7b7 100644 --- a/lib/public/User/IAvailabilityCoordinator.php +++ b/lib/public/User/IAvailabilityCoordinator.php @@ -48,4 +48,20 @@ interface IAvailabilityCoordinator { * @since 28.0.0 */ public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData; + + /** + * Reset the absence cache to null + * + * @since 28.0.0 + */ + public function clearCache(string $userId): void; + + /** + * Is the absence in effect at this moment + * + * @param IOutOfOfficeData $data + * @return bool + * @since 28.0.0 + */ + public function isInEffect(IOutOfOfficeData $data): bool; } diff --git a/lib/public/UserStatus/IManager.php b/lib/public/UserStatus/IManager.php index 9cc8eaad8ee..a85c1894c65 100644 --- a/lib/public/UserStatus/IManager.php +++ b/lib/public/UserStatus/IManager.php @@ -52,9 +52,11 @@ interface IManager { * @param string $messageId The id of the predefined message. * @param string $status The status to assign * @param bool $createBackup If true, this will store the old status so that it is possible to revert it later (e.g. after a call). + * @param string|null $customMessage * @since 23.0.0 + * @since 28.0.0 Optional parameter $customMessage was added */ - public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void; + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false, ?string $customMessage = null): void; /** * Revert an automatically set user status. For example after leaving a call, diff --git a/lib/public/UserStatus/IUserStatus.php b/lib/public/UserStatus/IUserStatus.php index c96d07d298b..f5005e5616b 100644 --- a/lib/public/UserStatus/IUserStatus.php +++ b/lib/public/UserStatus/IUserStatus.php @@ -81,6 +81,12 @@ interface IUserStatus { */ public const MESSAGE_AVAILABILITY = 'availability'; + /** + * @var string + * @since 28.0.0 + */ + public const MESSAGE_VACATION = 'vacationing'; + /** * @var string * @since 28.0.0 diff --git a/tests/lib/User/AvailabilityCoordinatorTest.php b/tests/lib/User/AvailabilityCoordinatorTest.php index 8a0b66181d2..b41b1fbac2a 100644 --- a/tests/lib/User/AvailabilityCoordinatorTest.php +++ b/tests/lib/User/AvailabilityCoordinatorTest.php @@ -30,7 +30,7 @@ use OC\User\AvailabilityCoordinator; use OC\User\OutOfOfficeData; use OCA\DAV\CalDAV\TimezoneService; use OCA\DAV\Db\Absence; -use OCA\DAV\Db\AbsenceMapper; +use OCA\DAV\Service\AbsenceService; use OCP\ICache; use OCP\ICacheFactory; use OCP\IConfig; @@ -44,7 +44,7 @@ class AvailabilityCoordinatorTest extends TestCase { private ICacheFactory $cacheFactory; private ICache $cache; private IConfig|MockObject $config; - private AbsenceMapper $absenceMapper; + private AbsenceService $absenceService; private LoggerInterface $logger; private MockObject|TimezoneService $timezoneService; @@ -53,7 +53,7 @@ class AvailabilityCoordinatorTest extends TestCase { $this->cacheFactory = $this->createMock(ICacheFactory::class); $this->cache = $this->createMock(ICache::class); - $this->absenceMapper = $this->createMock(AbsenceMapper::class); + $this->absenceService = $this->createMock(AbsenceService::class); $this->config = $this->createMock(IConfig::class); $this->logger = $this->createMock(LoggerInterface::class); $this->timezoneService = $this->createMock(TimezoneService::class); @@ -64,8 +64,8 @@ class AvailabilityCoordinatorTest extends TestCase { $this->availabilityCoordinator = new AvailabilityCoordinator( $this->cacheFactory, - $this->absenceMapper, $this->config, + $this->absenceService, $this->logger, $this->timezoneService, ); @@ -82,7 +82,7 @@ class AvailabilityCoordinatorTest extends TestCase { self::assertTrue($isEnabled); } - public function testGetOutOfOfficeData(): void { + public function testGetOutOfOfficeDataInEffect(): void { $absence = new Absence(); $absence->setId(420); $absence->setUserId('user'); @@ -96,17 +96,17 @@ class AvailabilityCoordinatorTest extends TestCase { $user->method('getUID') ->willReturn('user'); - $this->cache->expects(self::once()) + $this->cache->expects(self::exactly(2)) ->method('get') - ->with('user') - ->willReturn(null); - $this->absenceMapper->expects(self::once()) - ->method('findByUserId') - ->with('user') + ->willReturnOnConsecutiveCalls(null, null); + $this->absenceService->expects(self::once()) + ->method('getAbsence') + ->with($user->getUID()) ->willReturn($absence); - $this->cache->expects(self::once()) + $this->cache->expects(self::exactly(2)) ->method('set') - ->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300); + ->withConsecutive([$user->getUID() . '_timezone', 'Europe/Berlin', 3600], + [$user->getUID(), '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300]); $expected = new OutOfOfficeData( '420', @@ -120,25 +120,32 @@ class AvailabilityCoordinatorTest extends TestCase { self::assertEquals($expected, $actual); } - public function testGetOutOfOfficeDataWithCachedData(): void { + public function testGetOutOfOfficeDataCachedAll(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + $user = $this->createMock(IUser::class); $user->method('getUID') ->willReturn('user'); - $this->cache->expects(self::once()) + $this->cache->expects(self::exactly(2)) ->method('get') - ->with('user') - ->willReturn('{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}'); - $this->absenceMapper->expects(self::never()) - ->method('findByUserId'); - $this->cache->expects(self::never()) + ->willReturnOnConsecutiveCalls('UTC', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation"}'); + $this->absenceService->expects(self::never()) + ->method('getAbsence'); + $this->cache->expects(self::exactly(1)) ->method('set'); $expected = new OutOfOfficeData( '420', $user, 1696118400, - 1696723200, + 1696809540, 'Vacation', 'On vacation', ); @@ -146,6 +153,32 @@ class AvailabilityCoordinatorTest extends TestCase { self::assertEquals($expected, $actual); } + public function testGetOutOfOfficeDataNoData(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls('UTC', null); + $this->absenceService->expects(self::once()) + ->method('getAbsence') + ->willReturn(null); + $this->cache->expects(self::never()) + ->method('set'); + + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertNull($actual); + } + public function testGetOutOfOfficeDataWithInvalidCachedData(): void { $absence = new Absence(); $absence->setId(420); @@ -160,23 +193,22 @@ class AvailabilityCoordinatorTest extends TestCase { $user->method('getUID') ->willReturn('user'); - $this->cache->expects(self::once()) + $this->cache->expects(self::exactly(2)) ->method('get') - ->with('user') - ->willReturn('{"id":"420",}'); - $this->absenceMapper->expects(self::once()) - ->method('findByUserId') + ->willReturnOnConsecutiveCalls('UTC', '{"id":"420",}'); + $this->absenceService->expects(self::once()) + ->method('getAbsence') ->with('user') ->willReturn($absence); $this->cache->expects(self::once()) ->method('set') - ->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300); + ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation"}', 300); $expected = new OutOfOfficeData( '420', $user, - 1696111200, - 1696802340, + 1696118400, + 1696809540, 'Vacation', 'On vacation', ); -- 2.39.5