migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Tobia De Koninck <tobia@ledfan.be>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\AppInfo;
use OCA\DAV\CalDAV\Activity\Backend;
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\CalendarManager;
use OCA\DAV\CalDAV\CalendarProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
use OCA\DAV\CalDAV\Reminder\Notifier;
use OCA\DAV\Capabilities;
use OCA\DAV\CardDAV\ContactsManager;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
use OCA\DAV\Events\AddressBookCreatedEvent;
use OCA\DAV\Events\AddressBookDeletedEvent;
use OCA\DAV\Events\AddressBookShareUpdatedEvent;
use OCA\DAV\Events\AddressBookUpdatedEvent;
use OCA\DAV\Events\CalendarCreatedEvent;
use OCA\DAV\Events\CalendarDeletedEvent;
use OCA\DAV\Events\CalendarMovedToTrashEvent;
use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarObjectDeletedEvent;
use OCA\DAV\Events\CalendarObjectMovedEvent;
use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
use OCA\DAV\Events\CalendarObjectRestoredEvent;
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\DAV\Events\CalendarPublishedEvent;
use OCA\DAV\Events\CalendarRestoredEvent;
use OCA\DAV\Events\CalendarShareUpdatedEvent;
use OCA\DAV\Events\CalendarUnpublishedEvent;
use OCA\DAV\Events\CalendarUpdatedEvent;
use OCA\DAV\Events\CardCreatedEvent;
use OCA\DAV\Events\CardDeletedEvent;
use OCA\DAV\Events\CardUpdatedEvent;
use OCA\DAV\Events\SubscriptionCreatedEvent;
use OCA\DAV\Events\SubscriptionDeletedEvent;
use OCA\DAV\HookManager;
use OCA\DAV\Listener\ActivityUpdaterListener;
use OCA\DAV\Listener\AddressbookListener;
use OCA\DAV\Listener\BirthdayListener;
use OCA\DAV\Listener\CalendarContactInteractionListener;
use OCA\DAV\Listener\CalendarDeletionDefaultUpdaterListener;
use OCA\DAV\Listener\CalendarObjectReminderUpdaterListener;
use OCA\DAV\Listener\CalendarPublicationListener;
use OCA\DAV\Listener\CalendarShareUpdateListener;
use OCA\DAV\Listener\CardListener;
use OCA\DAV\Listener\ClearPhotoCacheListener;
use OCA\DAV\Listener\OutOfOfficeListener;
use OCA\DAV\Listener\SubscriptionListener;
use OCA\DAV\Listener\TrustedServerRemovedListener;
use OCA\DAV\Listener\UserPreferenceListener;
use OCA\DAV\Search\ContactsSearchProvider;
use OCA\DAV\Search\EventsSearchProvider;
use OCA\DAV\Search\TasksSearchProvider;
use OCA\DAV\SetupChecks\NeedsSystemAddressBookSync;
use OCA\DAV\UserMigration\CalendarMigrator;
use OCA\DAV\UserMigration\ContactsMigrator;
use OCP\Accounts\UserUpdatedEvent;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\IAppContainer;
use OCP\Calendar\IManager as ICalendarManager;
use OCP\Config\BeforePreferenceDeletedEvent;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\Contacts\IManager as IContactsManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\Events\TrustedServerRemovedEvent;
use OCP\Files\AppData\IAppDataFactory;
use OCP\IUser;
use OCP\User\Events\OutOfOfficeChangedEvent;
use OCP\User\Events\OutOfOfficeClearedEvent;
use OCP\User\Events\OutOfOfficeScheduledEvent;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use Throwable;
use function is_null;
class Application extends App implements IBootstrap {
public const APP_ID = 'dav';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void {
$context->registerServiceAlias('CardDAVSyncService', SyncService::class);
$context->registerService(PhotoCache::class, function (ContainerInterface $c) {
return new PhotoCache(
$c->get(IAppDataFactory::class)->get('dav-photocache'),
$c->get(LoggerInterface::class)
);
});
$context->registerService(AppCalendarPlugin::class, function (ContainerInterface $c) {
return new AppCalendarPlugin(
$c->get(ICalendarManager::class),
$c->get(LoggerInterface::class)
);
});
/*
* Register capabilities
*/
$context->registerCapability(Capabilities::class);
/*
* Register Search Providers
*/
$context->registerSearchProvider(ContactsSearchProvider::class);
$context->registerSearchProvider(EventsSearchProvider::class);
$context->registerSearchProvider(TasksSearchProvider::class);
/**
* Register event listeners
*/
$context->registerEventListener(CalendarCreatedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarDeletedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarDeletedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarDeletedEvent::class, CalendarDeletionDefaultUpdaterListener::class);
$context->registerEventListener(CalendarMovedToTrashEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarUpdatedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarRestoredEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarRestoredEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectCreatedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectCreatedEvent::class, CalendarContactInteractionListener::class);
$context->registerEventListener(CalendarObjectCreatedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectUpdatedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarContactInteractionListener::class);
$context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectDeletedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedToTrashEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarObjectRestoredEvent::class, ActivityUpdaterListener::class);
$context->registerEventListener(CalendarObjectRestoredEvent::class, CalendarObjectReminderUpdaterListener::class);
$context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarContactInteractionListener::class);
$context->registerEventListener(CalendarPublishedEvent::class, CalendarPublicationListener::class);
$context->registerEventListener(CalendarUnpublishedEvent::class, CalendarPublicationListener::class);
$context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarShareUpdateListener::class);
$context->registerEventListener(SubscriptionCreatedEvent::class, SubscriptionListener::class);
$context->registerEventListener(SubscriptionDeletedEvent::class, SubscriptionListener::class);
$context->registerEventListener(AddressBookCreatedEvent::class, AddressbookListener::class);
$context->registerEventListener(AddressBookDeletedEvent::class, AddressbookListener::class);
$context->registerEventListener(AddressBookUpdatedEvent::class, AddressbookListener::class);
$context->registerEventListener(AddressBookShareUpdatedEvent::class, AddressbookListener::class);
$context->registerEventListener(CardCreatedEvent::class, CardListener::class);
$context->registerEventListener(CardDeletedEvent::class, CardListener::class);
$context->registerEventListener(CardUpdatedEvent::class, CardListener::class);
$context->registerEventListener(CardCreatedEvent::class, BirthdayListener::class);
$context->registerEventListener(CardDeletedEvent::class, BirthdayListener::class);
$context->registerEventListener(CardUpdatedEvent::class, BirthdayListener::class);
$context->registerEventListener(CardDeletedEvent::class, ClearPhotoCacheListener::class);
$context->registerEventListener(CardUpdatedEvent::class, ClearPhotoCacheListener::class);
$context->registerEventListener(TrustedServerRemovedEvent::class, TrustedServerRemovedListener::class);
$context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class);
$context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class);
$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);
$context->registerNotifierService(Notifier::class);
$context->registerCalendarProvider(CalendarProvider::class);
$context->registerUserMigrator(CalendarMigrator::class);
$context->registerUserMigrator(ContactsMigrator::class);
$context->registerSetupCheck(NeedsSystemAddressBookSync::class);
}
public function boot(IBootContext $context): void {
// Load all dav apps
\OC_App::loadApps(['dav']);
$context->injectFn([$this, 'registerHooks']);
$context->injectFn([$this, 'registerContactsManager']);
$context->injectFn([$this, 'registerCalendarManager']);
$context->injectFn([$this, 'registerCalendarReminders']);
}
public function registerHooks(HookManager $hm,
IEventDispatcher $dispatcher,
IAppContainer $container) {
$hm->setup();
// first time login event setup
$dispatcher->addListener(IUser::class . '::firstLogin', function ($event) use ($hm) {
if ($event instanceof GenericEvent) {
$hm->firstLogin($event->getSubject());
}
});
$dispatcher->addListener(UserUpdatedEvent::class, function (UserUpdatedEvent $event) use ($container) {
/** @var SyncService $syncService */
$syncService = \OCP\Server::get(SyncService::class);
$syncService->updateUser($event->getUser());
});
$dispatcher->addListener(CalendarShareUpdatedEvent::class, function (CalendarShareUpdatedEvent $event) use ($container) {
/** @var Backend $backend */
$backend = $container->query(Backend::class);
$backend->onCalendarUpdateShares(
$event->getCalendarData(),
$event->getOldShares(),
$event->getAdded(),
$event->getRemoved()
);
// Here we should recalculate if reminders should be sent to new or old sharees
});
}
public function registerContactsManager(IContactsManager $cm, IAppContainer $container): void {
$cm->register(function () use ($container, $cm): void {
$user = \OC::$server->getUserSession()->getUser();
if (!is_null($user)) {
$this->setupContactsProvider($cm, $container, $user->getUID());
} else {
$this->setupSystemContactsProvider($cm, $container);
}
});
}
private function setupContactsProvider(IContactsManager $contactsManager,
IAppContainer $container,
string $userID): void {
/** @var ContactsManager $cm */
$cm = $container->query(ContactsManager::class);
$urlGenerator = $container->getServer()->getURLGenerator();
$cm->setupContactsProvider($contactsManager, $userID, $urlGenerator);
}
private function setupSystemContactsProvider(IContactsManager $contactsManager,
IAppContainer $container): void {
/** @var ContactsManager $cm */
$cm = $container->query(ContactsManager::class);
$urlGenerator = $container->getServer()->getURLGenerator();
$cm->setupSystemContactsProvider($contactsManager, $urlGenerator);
}
public function registerCalendarManager(ICalendarManager $calendarManager,
IAppContainer $container): void {
$calendarManager->register(function () use ($container, $calendarManager) {
$user = \OC::$server->getUserSession()->getUser();
if ($user !== null) {
$this->setupCalendarProvider($calendarManager, $container, $user->getUID());
}
});
}
private function setupCalendarProvider(ICalendarManager $calendarManager,
IAppContainer $container,
$userId) {
$cm = $container->query(CalendarManager::class);
$cm->setupCalendarProvider($calendarManager, $userId);
}
public function registerCalendarReminders(NotificationProviderManager $manager,
LoggerInterface $logger): void {
try {
$manager->registerProvider(AudioProvider::class);
$manager->registerProvider(EmailProvider::class);
$manager->registerProvider(PushProvider::class);
} catch (Throwable $ex) {
$logger->error($ex->getMessage(), ['exception' => $ex]);
}
}
}
@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud GmbH.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Vincent Petry <vincent@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\AppInfo;
use OC\ServerContainer;
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
use OCP\App\IAppManager;
use OCP\AppFramework\QueryException;
use Sabre\DAV\Collection;
use Sabre\DAV\ServerPlugin;
use function array_map;
use function class_exists;
use function is_array;
/**
* Manager for DAV plugins from apps, used to register them
* to the Sabre server.
*/
class PluginManager {
/**
* @var ServerContainer
*/
private $container;
/**
* @var IAppManager
*/
private $appManager;
/**
* App plugins
*
* @var ServerPlugin[]
*/
private $plugins = [];
/**
* App collections
*
* @var Collection[]
*/
private $collections = [];
/**
* Address book plugins
*
* @var IAddressBookProvider[]
*/
private $addressBookPlugins = [];
/**
* Calendar plugins
*
* @var ICalendarProvider[]
*/
private $calendarPlugins = [];
/** @var bool */
private $populated = false;
/**
* Contstruct a PluginManager
*
* @param ServerContainer $container server container for resolving plugin classes
* @param IAppManager $appManager app manager to loading apps and their info
*/
public function __construct(ServerContainer $container, IAppManager $appManager) {
$this->container = $container;
$this->appManager = $appManager;
}
/**
* Returns an array of app-registered plugins
*
* @return ServerPlugin[]
*/
public function getAppPlugins() {
$this->populate();
return $this->plugins;
}
/**
* Returns an array of app-registered collections
*
* @return array
*/
public function getAppCollections() {
$this->populate();
return $this->collections;
}
/**
* @return IAddressBookProvider[]
*/
public function getAddressBookPlugins(): array {
$this->populate();
return $this->addressBookPlugins;
}
/**
* Returns an array of app-registered calendar plugins
*
* @return ICalendarProvider[]
*/
public function getCalendarPlugins():array {
$this->populate();
return $this->calendarPlugins;
}
/**
* Retrieve plugin and collection list and populate attributes
*/
private function populate(): void {
if ($this->populated) {
return;
}
$this->populated = true;
$this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class);
foreach ($this->appManager->getInstalledApps() as $app) {
// load plugins and collections from info.xml
$info = $this->appManager->getAppInfo($app);
if (!isset($info['types']) || !in_array('dav', $info['types'], true)) {
continue;
}
$plugins = $this->loadSabrePluginsFromInfoXml($this->extractPluginList($info));
foreach ($plugins as $plugin) {
$this->plugins[] = $plugin;
}
$collections = $this->loadSabreCollectionsFromInfoXml($this->extractCollectionList($info));
foreach ($collections as $collection) {
$this->collections[] = $collection;
}
$addresbookPlugins = $this->loadSabreAddressBookPluginsFromInfoXml($this->extractAddressBookPluginList($info));
foreach ($addresbookPlugins as $addresbookPlugin) {
$this->addressBookPlugins[] = $addresbookPlugin;
}
$calendarPlugins = $this->loadSabreCalendarPluginsFromInfoXml($this->extractCalendarPluginList($info));
foreach ($calendarPlugins as $calendarPlugin) {
$this->calendarPlugins[] = $calendarPlugin;
}
}
}
/**
* @param array $array
* @return string[]
*/
private function extractPluginList(array $array): array {
if (isset($array['sabre']) && is_array($array['sabre'])) {
if (isset($array['sabre']['plugins']) && is_array($array['sabre']['plugins'])) {
if (isset($array['sabre']['plugins']['plugin'])) {
$items = $array['sabre']['plugins']['plugin'];
if (!is_array($items)) {
$items = [$items];
}
return $items;
}
}
}
return [];
}
/**
* @param array $array
* @return string[]
*/
private function extractCollectionList(array $array): array {
if (isset($array['sabre']) && is_array($array['sabre'])) {
if (isset($array['sabre']['collections']) && is_array($array['sabre']['collections'])) {
if (isset($array['sabre']['collections']['collection'])) {
$items = $array['sabre']['collections']['collection'];
if (!is_array($items)) {
$items = [$items];
}
return $items;
}
}
}
return [];
}
/**
* @param array $array
* @return string[]
*/
private function extractAddressBookPluginList(array $array): array {
if (!isset($array['sabre']) || !is_array($array['sabre'])) {
return [];
}
if (!isset($array['sabre']['address-book-plugins']) || !is_array($array['sabre']['address-book-plugins'])) {
return [];
}
if (!isset($array['sabre']['address-book-plugins']['plugin'])) {
return [];
}
$items = $array['sabre']['address-book-plugins']['plugin'];
if (!is_array($items)) {
$items = [$items];
}
return $items;
}
/**
* @param array $array
* @return string[]
*/
private function extractCalendarPluginList(array $array): array {
if (isset($array['sabre']) && is_array($array['sabre'])) {
if (isset($array['sabre']['calendar-plugins']) && is_array($array['sabre']['calendar-plugins'])) {
if (isset($array['sabre']['calendar-plugins']['plugin'])) {
$items = $array['sabre']['calendar-plugins']['plugin'];
if (!is_array($items)) {
$items = [$items];
}
return $items;
}
}
}
return [];
}
private function createClass(string $className): object {
try {
return $this->container->get($className);
} catch (QueryException $e) {
if (class_exists($className)) {
return new $className();
}
}
throw new \Exception('Could not load ' . $className, 0, $e);
}
/**
* @param string[] $classes
* @return ServerPlugin[]
* @throws \Exception
*/
private function loadSabrePluginsFromInfoXml(array $classes): array {
return array_map(function (string $className): ServerPlugin {
$instance = $this->createClass($className);
if (!($instance instanceof ServerPlugin)) {
throw new \Exception('Sabre server plugin ' . $className . ' does not implement the ' . ServerPlugin::class . ' interface');
}
return $instance;
}, $classes);
}
/**
* @param string[] $classes
* @return Collection[]
*/
private function loadSabreCollectionsFromInfoXml(array $classes): array {
return array_map(function (string $className): Collection {
$instance = $this->createClass($className);
if (!($instance instanceof Collection)) {
throw new \Exception('Sabre collection plugin ' . $className . ' does not implement the ' . Collection::class . ' interface');
}
return $instance;
}, $classes);
}
/**
* @param string[] $classes
* @return IAddressBookProvider[]
*/
private function loadSabreAddressBookPluginsFromInfoXml(array $classes): array {
return array_map(function (string $className): IAddressBookProvider {
$instance = $this->createClass($className);
if (!($instance instanceof IAddressBookProvider)) {
throw new \Exception('Sabre address book plugin class ' . $className . ' does not implement the ' . IAddressBookProvider::class . ' interface');
}
return $instance;
}, $classes);
}
/**
* @param string[] $classes
* @return ICalendarProvider[]
*/
private function loadSabreCalendarPluginsFromInfoXml(array $classes): array {
return array_map(function (string $className): ICalendarProvider {
$instance = $this->createClass($className);
if (!($instance instanceof ICalendarProvider)) {
throw new \Exception('Sabre calendar plugin class ' . $className . ' does not implement the ' . ICalendarProvider::class . ' interface');
}
return $instance;
}, $classes);
}
}
@@ -0,0 +1,119 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud GmbH
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Avatars;
use OCP\IAvatarManager;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
use Sabre\Uri;
class AvatarHome implements ICollection {
/** @var array */
private $principalInfo;
/** @var IAvatarManager */
private $avatarManager;
/**
* AvatarHome constructor.
*
* @param array $principalInfo
* @param IAvatarManager $avatarManager
*/
public function __construct($principalInfo, IAvatarManager $avatarManager) {
$this->principalInfo = $principalInfo;
$this->avatarManager = $avatarManager;
}
public function createFile($name, $data = null) {
throw new Forbidden('Permission denied to create a file');
}
public function createDirectory($name) {
throw new Forbidden('Permission denied to create a folder');
}
public function getChild($name) {
$elements = pathinfo($name);
$ext = $elements['extension'] ?? '';
$size = (int)($elements['filename'] ?? '64');
if (!in_array($ext, ['jpeg', 'png'], true)) {
throw new MethodNotAllowed('File format not allowed');
}
if ($size <= 0 || $size > 1024) {
throw new MethodNotAllowed('Invalid image size');
}
$avatar = $this->avatarManager->getAvatar($this->getName());
if (!$avatar->exists()) {
throw new NotFound();
}
return new AvatarNode($size, $ext, $avatar);
}
public function getChildren() {
try {
return [
$this->getChild('96.jpeg')
];
} catch (NotFound $exception) {
return [];
}
}
public function childExists($name) {
try {
$ret = $this->getChild($name);
return $ret !== null;
} catch (NotFound $ex) {
return false;
} catch (MethodNotAllowed $ex) {
return false;
}
}
public function delete() {
throw new Forbidden('Permission denied to delete this folder');
}
public function getName() {
[,$name] = Uri\split($this->principalInfo['uri']);
return $name;
}
public function setName($name) {
throw new Forbidden('Permission denied to rename this folder');
}
/**
* Returns the last modification time, as a unix timestamp
*
* @return int|null
*/
public function getLastModified() {
return null;
}
}
@@ -0,0 +1,96 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud GmbH
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\Avatars;
use OCP\IAvatar;
use Sabre\DAV\File;
class AvatarNode extends File {
private $ext;
private $size;
private $avatar;
/**
* AvatarNode constructor.
*
* @param integer $size
* @param string $ext
* @param IAvatar $avatar
*/
public function __construct($size, $ext, $avatar) {
$this->size = $size;
$this->ext = $ext;
$this->avatar = $avatar;
}
/**
* Returns the name of the node.
*
* This is used to generate the url.
*
* @return string
*/
public function getName() {
return "$this->size.$this->ext";
}
public function get() {
$image = $this->avatar->get($this->size);
$res = $image->resource();
ob_start();
if ($this->ext === 'png') {
imagepng($res);
} else {
imagejpeg($res);
}
return ob_get_clean();
}
/**
* Returns the mime-type for a file
*
* If null is returned, we'll assume application/octet-stream
*
* @return string|null
*/
public function getContentType() {
if ($this->ext === 'png') {
return 'image/png';
}
return 'image/jpeg';
}
public function getETag() {
return $this->avatar->getFile($this->size)->getEtag();
}
public function getLastModified() {
$timestamp = $this->avatar->getFile($this->size)->getMTime();
if (!empty($timestamp)) {
return (int)$timestamp;
}
return $timestamp;
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @copyright Copyright (c) 2016 Thomas Müller <thomas.mueller@tmit.eu>
*
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\Avatars;
use Sabre\DAVACL\AbstractPrincipalCollection;
class RootCollection extends AbstractPrincipalCollection {
/**
* This method returns a node for a principal.
*
* The passed array contains principal information, and is guaranteed to
* at least contain a uri item. Other properties may or may not be
* supplied by the authentication backend.
*
* @param array $principalInfo
* @return AvatarHome
*/
public function getChildForPrincipal(array $principalInfo) {
$avatarManager = \OC::$server->getAvatarManager();
return new AvatarHome($principalInfo, $avatarManager);
}
public function getName() {
return 'avatars';
}
}
@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\Reminder\ReminderService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* Class BuildReminderIndexBackgroundJob
*
* @package OCA\DAV\BackgroundJob
*/
class BuildReminderIndexBackgroundJob extends QueuedJob {
/** @var IDBConnection */
private $db;
/** @var ReminderService */
private $reminderService;
private LoggerInterface $logger;
/** @var IJobList */
private $jobList;
/** @var ITimeFactory */
private $timeFactory;
/**
* BuildReminderIndexBackgroundJob constructor.
*/
public function __construct(IDBConnection $db,
ReminderService $reminderService,
LoggerInterface $logger,
IJobList $jobList,
ITimeFactory $timeFactory) {
parent::__construct($timeFactory);
$this->db = $db;
$this->reminderService = $reminderService;
$this->logger = $logger;
$this->jobList = $jobList;
$this->timeFactory = $timeFactory;
}
public function run($argument) {
$offset = (int) $argument['offset'];
$stopAt = (int) $argument['stopAt'];
$this->logger->info('Building calendar reminder index (' . $offset .'/' . $stopAt . ')');
$offset = $this->buildIndex($offset, $stopAt);
if ($offset >= $stopAt) {
$this->logger->info('Building calendar reminder index done');
} else {
$this->jobList->add(self::class, [
'offset' => $offset,
'stopAt' => $stopAt
]);
$this->logger->info('Scheduled a new BuildReminderIndexBackgroundJob with offset ' . $offset);
}
}
/**
* @param int $offset
* @param int $stopAt
* @return int
*/
private function buildIndex(int $offset, int $stopAt):int {
$startTime = $this->timeFactory->getTime();
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('calendarobjects')
->where($query->expr()->lte('id', $query->createNamedParameter($stopAt)))
->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset)))
->orderBy('id', 'ASC');
$result = $query->executeQuery();
while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
$offset = (int) $row['id'];
if (is_resource($row['calendardata'])) {
$row['calendardata'] = stream_get_contents($row['calendardata']);
}
$row['component'] = $row['componenttype'];
try {
$this->reminderService->onCalendarObjectCreate($row);
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage(), ['exception' => $ex]);
}
if (($this->timeFactory->getTime() - $startTime) > 15) {
$result->closeCursor();
return $offset;
}
}
$result->closeCursor();
return $stopAt;
}
}
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\RetentionService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
class CalendarRetentionJob extends TimedJob {
/** @var RetentionService */
private $service;
public function __construct(ITimeFactory $time,
RetentionService $service) {
parent::__construct($time);
$this->service = $service;
// Run four times a day
$this->setInterval(6 * 60 * 60);
$this->setTimeSensitivity(self::TIME_SENSITIVE);
}
protected function run($argument): void {
$this->service->cleanUp();
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\Db\DirectMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
class CleanupDirectLinksJob extends TimedJob {
/** @var DirectMapper */
private $mapper;
public function __construct(ITimeFactory $timeFactory, DirectMapper $mapper) {
parent::__construct($timeFactory);
$this->mapper = $mapper;
// Run once a day at off-peak time
$this->setInterval(24 * 60 * 60);
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
protected function run($argument) {
// Delete all shares expired 24 hours ago
$this->mapper->deleteExpired($this->time->getTime() - 60 * 60 * 24);
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IDBConnection;
class CleanupInvitationTokenJob extends TimedJob {
/** @var IDBConnection */
private $db;
public function __construct(IDBConnection $db, ITimeFactory $time) {
parent::__construct($time);
$this->db = $db;
// Run once a day at off-peak time
$this->setInterval(24 * 60 * 60);
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
public function run($argument) {
$query = $this->db->getQueryBuilder();
$query->delete('calendar_invitations')
->where($query->expr()->lt('expiration',
$query->createNamedParameter($this->time->getTime())))
->execute();
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016 Thomas Citharel <nextcloud@tcit.fr>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\Reminder\ReminderService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
class EventReminderJob extends TimedJob {
/** @var ReminderService */
private $reminderService;
/** @var IConfig */
private $config;
public function __construct(ITimeFactory $time,
ReminderService $reminderService,
IConfig $config) {
parent::__construct($time);
$this->reminderService = $reminderService;
$this->config = $config;
// Run every 5 minutes
$this->setInterval(5 * 60);
$this->setTimeSensitivity(self::TIME_SENSITIVE);
}
/**
* @throws \OCA\DAV\CalDAV\Reminder\NotificationProvider\ProviderNotAvailableException
* @throws \OCA\DAV\CalDAV\Reminder\NotificationTypeDoesNotExistException
* @throws \OC\User\NoUserException
*/
public function run($argument):void {
if ($this->config->getAppValue('dav', 'sendEventReminders', 'yes') !== 'yes') {
return;
}
if ($this->config->getAppValue('dav', 'sendEventRemindersMode', 'backgroundjob') !== 'backgroundjob') {
return;
}
$this->reminderService->processReminders();
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\BirthdayService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\IConfig;
class GenerateBirthdayCalendarBackgroundJob extends QueuedJob {
/** @var BirthdayService */
private $birthdayService;
/** @var IConfig */
private $config;
public function __construct(ITimeFactory $time,
BirthdayService $birthdayService,
IConfig $config) {
parent::__construct($time);
$this->birthdayService = $birthdayService;
$this->config = $config;
}
public function run($argument) {
$userId = $argument['userId'];
$purgeBeforeGenerating = $argument['purgeBeforeGenerating'] ?? false;
// make sure admin didn't change his mind
$isGloballyEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes');
if ($isGloballyEnabled !== 'yes') {
return;
}
// did the user opt out?
$isUserEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
if ($isUserEnabled !== 'yes') {
return;
}
if ($purgeBeforeGenerating) {
$this->birthdayService->resetForUser($userId);
}
$this->birthdayService->syncUser($userId);
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Richard Steinmetz <richard@steinmetz.cloud>
*
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\TimezoneService;
use OCA\DAV\Db\AbsenceMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\QueuedJob;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IUserManager;
use OCP\User\Events\OutOfOfficeEndedEvent;
use OCP\User\Events\OutOfOfficeStartedEvent;
use Psr\Log\LoggerInterface;
class OutOfOfficeEventDispatcherJob extends QueuedJob {
public const EVENT_START = 'start';
public const EVENT_END = 'end';
public function __construct(
ITimeFactory $time,
private AbsenceMapper $absenceMapper,
private LoggerInterface $logger,
private IEventDispatcher $eventDispatcher,
private IUserManager $userManager,
private TimezoneService $timezoneService,
) {
parent::__construct($time);
}
public function run($argument): void {
$id = $argument['id'];
$event = $argument['event'];
try {
$absence = $this->absenceMapper->findById($id);
} catch (DoesNotExistException | \OCP\DB\Exception $e) {
$this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [
'exception' => $e,
'argument' => $argument,
]);
return;
}
$userId = $absence->getUserId();
$user = $this->userManager->get($userId);
if ($user === null) {
$this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [
'argument' => $argument,
]);
return;
}
$data = $absence->toOutOufOfficeData(
$user,
$this->timezoneService->getUserTimezone($userId) ?? $this->timezoneService->getDefaultTimezone(),
);
if ($event === self::EVENT_START) {
$this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data));
} elseif ($event === self::EVENT_END) {
$this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data));
} else {
$this->logger->error("Invalid out-of-office event: $event", [
'argument' => $argument,
]);
}
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Thomas Citharel <nextcloud@tcit.fr>
*
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\AppInfo\Application;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CardDAV\CardDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
class PruneOutdatedSyncTokensJob extends TimedJob {
private IConfig $config;
private LoggerInterface $logger;
private CardDavBackend $cardDavBackend;
private CalDavBackend $calDavBackend;
public function __construct(ITimeFactory $timeFactory, CalDavBackend $calDavBackend, CardDavBackend $cardDavBackend, IConfig $config, LoggerInterface $logger) {
parent::__construct($timeFactory);
$this->calDavBackend = $calDavBackend;
$this->cardDavBackend = $cardDavBackend;
$this->config = $config;
$this->logger = $logger;
$this->setInterval(60 * 60 * 24); // One day
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}
public function run($argument) {
$limit = max(1, (int) $this->config->getAppValue(Application::APP_ID, 'totalNumberOfSyncTokensToKeep', '10000'));
$prunedCalendarSyncTokens = $this->calDavBackend->pruneOutdatedSyncTokens($limit);
$prunedAddressBookSyncTokens = $this->cardDavBackend->pruneOutdatedSyncTokens($limit);
$this->logger->info('Pruned {calendarSyncTokensNumber} calendar sync tokens and {addressBooksSyncTokensNumber} address book sync tokens', [
'calendarSyncTokensNumber' => $prunedCalendarSyncTokens,
'addressBooksSyncTokensNumber' => $prunedAddressBookSyncTokens
]);
}
}
@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use DateInterval;
use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\Job;
use OCP\IConfig;
use OCP\ILogger;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
class RefreshWebcalJob extends Job {
/**
* @var RefreshWebcalService
*/
private $refreshWebcalService;
/**
* @var IConfig
*/
private $config;
/** @var ILogger */
private $logger;
/** @var ITimeFactory */
private $timeFactory;
/**
* RefreshWebcalJob constructor.
*
* @param RefreshWebcalService $refreshWebcalService
* @param IConfig $config
* @param ILogger $logger
* @param ITimeFactory $timeFactory
*/
public function __construct(RefreshWebcalService $refreshWebcalService, IConfig $config, ILogger $logger, ITimeFactory $timeFactory) {
parent::__construct($timeFactory);
$this->refreshWebcalService = $refreshWebcalService;
$this->config = $config;
$this->logger = $logger;
$this->timeFactory = $timeFactory;
}
/**
* this function is called at most every hour
*
* @inheritdoc
*/
public function execute(IJobList $jobList, ILogger $logger = null) {
$subscription = $this->refreshWebcalService->getSubscription($this->argument['principaluri'], $this->argument['uri']);
if (!$subscription) {
return;
}
$this->fixSubscriptionRowTyping($subscription);
// if no refresh rate was configured, just refresh once a week
$defaultRefreshRate = $this->config->getAppValue('dav', 'calendarSubscriptionRefreshRate', 'P1W');
$refreshRate = $subscription[RefreshWebcalService::REFRESH_RATE] ?? $defaultRefreshRate;
$subscriptionId = $subscription['id'];
try {
/** @var DateInterval $dateInterval */
$dateInterval = DateTimeParser::parseDuration($refreshRate);
} catch (InvalidDataException $ex) {
$this->logger->logException($ex);
$this->logger->warning("Subscription $subscriptionId could not be refreshed, refreshrate in database is invalid");
return;
}
$interval = $this->getIntervalFromDateInterval($dateInterval);
if (($this->timeFactory->getTime() - $this->lastRun) <= $interval) {
return;
}
parent::execute($jobList, $logger);
}
/**
* @param array $argument
*/
protected function run($argument) {
$this->refreshWebcalService->refreshSubscription($argument['principaluri'], $argument['uri']);
}
/**
* get total number of seconds from DateInterval object
*
* @param DateInterval $interval
* @return int
*/
private function getIntervalFromDateInterval(DateInterval $interval):int {
return $interval->s
+ ($interval->i * 60)
+ ($interval->h * 60 * 60)
+ ($interval->d * 60 * 60 * 24)
+ ($interval->m * 60 * 60 * 24 * 30)
+ ($interval->y * 60 * 60 * 24 * 365);
}
/**
* Fixes types of rows
*
* @param array $row
*/
private function fixSubscriptionRowTyping(array &$row):void {
$forceInt = [
'id',
'lastmodified',
RefreshWebcalService::STRIP_ALARMS,
RefreshWebcalService::STRIP_ATTACHMENTS,
RefreshWebcalService::STRIP_TODOS,
];
foreach ($forceInt as $column) {
if (isset($row[$column])) {
$row[$column] = (int) $row[$column];
}
}
}
}
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\IUser;
use OCP\IUserManager;
class RegisterRegenerateBirthdayCalendars extends QueuedJob {
/** @var IUserManager */
private $userManager;
/** @var IJobList */
private $jobList;
/**
* RegisterRegenerateBirthdayCalendars constructor.
*
* @param ITimeFactory $time
* @param IUserManager $userManager
* @param IJobList $jobList
*/
public function __construct(ITimeFactory $time,
IUserManager $userManager,
IJobList $jobList) {
parent::__construct($time);
$this->userManager = $userManager;
$this->jobList = $jobList;
}
/**
* @inheritDoc
*/
public function run($argument) {
$this->userManager->callForSeenUsers(function (IUser $user) {
$this->jobList->add(GenerateBirthdayCalendarBackgroundJob::class, [
'userId' => $user->getUID(),
'purgeBeforeGenerating' => true
]);
});
}
}
@@ -0,0 +1,454 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\Calendar\BackendTemporarilyUnavailableException;
use OCP\Calendar\IMetadataProvider;
use OCP\Calendar\Resource\IBackend as IResourceBackend;
use OCP\Calendar\Resource\IManager as IResourceManager;
use OCP\Calendar\Resource\IResource;
use OCP\Calendar\Room\IManager as IRoomManager;
use OCP\Calendar\Room\IRoom;
use OCP\IDBConnection;
class UpdateCalendarResourcesRoomsBackgroundJob extends TimedJob {
/** @var IResourceManager */
private $resourceManager;
/** @var IRoomManager */
private $roomManager;
/** @var IDBConnection */
private $dbConnection;
/** @var CalDavBackend */
private $calDavBackend;
public function __construct(ITimeFactory $time,
IResourceManager $resourceManager,
IRoomManager $roomManager,
IDBConnection $dbConnection,
CalDavBackend $calDavBackend) {
parent::__construct($time);
$this->resourceManager = $resourceManager;
$this->roomManager = $roomManager;
$this->dbConnection = $dbConnection;
$this->calDavBackend = $calDavBackend;
// Run once an hour
$this->setInterval(60 * 60);
$this->setTimeSensitivity(self::TIME_SENSITIVE);
}
/**
* @param $argument
*/
public function run($argument): void {
$this->runForBackend(
$this->resourceManager,
'calendar_resources',
'calendar_resources_md',
'resource_id',
'principals/calendar-resources'
);
$this->runForBackend(
$this->roomManager,
'calendar_rooms',
'calendar_rooms_md',
'room_id',
'principals/calendar-rooms'
);
}
/**
* Run background-job for one specific backendManager
* either ResourceManager or RoomManager
*
* @param IResourceManager|IRoomManager $backendManager
* @param string $dbTable
* @param string $dbTableMetadata
* @param string $foreignKey
* @param string $principalPrefix
*/
private function runForBackend($backendManager,
string $dbTable,
string $dbTableMetadata,
string $foreignKey,
string $principalPrefix): void {
$backends = $backendManager->getBackends();
foreach ($backends as $backend) {
$backendId = $backend->getBackendIdentifier();
try {
if ($backend instanceof IResourceBackend) {
$list = $backend->listAllResources();
} else {
$list = $backend->listAllRooms();
}
} catch (BackendTemporarilyUnavailableException $ex) {
continue;
}
$cachedList = $this->getAllCachedByBackend($dbTable, $backendId);
$newIds = array_diff($list, $cachedList);
$deletedIds = array_diff($cachedList, $list);
$editedIds = array_intersect($list, $cachedList);
foreach ($newIds as $newId) {
try {
if ($backend instanceof IResourceBackend) {
$resource = $backend->getResource($newId);
} else {
$resource = $backend->getRoom($newId);
}
$metadata = [];
if ($resource instanceof IMetadataProvider) {
$metadata = $this->getAllMetadataOfBackend($resource);
}
} catch (BackendTemporarilyUnavailableException $ex) {
continue;
}
$id = $this->addToCache($dbTable, $backendId, $resource);
$this->addMetadataToCache($dbTableMetadata, $foreignKey, $id, $metadata);
// we don't create the calendar here, it is created lazily
// when an event is actually scheduled with this resource / room
}
foreach ($deletedIds as $deletedId) {
$id = $this->getIdForBackendAndResource($dbTable, $backendId, $deletedId);
$this->deleteFromCache($dbTable, $id);
$this->deleteMetadataFromCache($dbTableMetadata, $foreignKey, $id);
$principalName = implode('-', [$backendId, $deletedId]);
$this->deleteCalendarDataForResource($principalPrefix, $principalName);
}
foreach ($editedIds as $editedId) {
$id = $this->getIdForBackendAndResource($dbTable, $backendId, $editedId);
try {
if ($backend instanceof IResourceBackend) {
$resource = $backend->getResource($editedId);
} else {
$resource = $backend->getRoom($editedId);
}
$metadata = [];
if ($resource instanceof IMetadataProvider) {
$metadata = $this->getAllMetadataOfBackend($resource);
}
} catch (BackendTemporarilyUnavailableException $ex) {
continue;
}
$this->updateCache($dbTable, $id, $resource);
if ($resource instanceof IMetadataProvider) {
$cachedMetadata = $this->getAllMetadataOfCache($dbTableMetadata, $foreignKey, $id);
$this->updateMetadataCache($dbTableMetadata, $foreignKey, $id, $metadata, $cachedMetadata);
}
}
}
}
/**
* add entry to cache that exists remotely but not yet in cache
*
* @param string $table
* @param string $backendId
* @param IResource|IRoom $remote
*
* @return int Insert id
*/
private function addToCache(string $table,
string $backendId,
$remote): int {
$query = $this->dbConnection->getQueryBuilder();
$query->insert($table)
->values([
'backend_id' => $query->createNamedParameter($backendId),
'resource_id' => $query->createNamedParameter($remote->getId()),
'email' => $query->createNamedParameter($remote->getEMail()),
'displayname' => $query->createNamedParameter($remote->getDisplayName()),
'group_restrictions' => $query->createNamedParameter(
$this->serializeGroupRestrictions(
$remote->getGroupRestrictions()
))
])
->executeStatement();
return $query->getLastInsertId();
}
/**
* @param string $table
* @param string $foreignKey
* @param int $foreignId
* @param array $metadata
*/
private function addMetadataToCache(string $table,
string $foreignKey,
int $foreignId,
array $metadata): void {
foreach ($metadata as $key => $value) {
$query = $this->dbConnection->getQueryBuilder();
$query->insert($table)
->values([
$foreignKey => $query->createNamedParameter($foreignId),
'key' => $query->createNamedParameter($key),
'value' => $query->createNamedParameter($value),
])
->executeStatement();
}
}
/**
* delete entry from cache that does not exist anymore remotely
*
* @param string $table
* @param int $id
*/
private function deleteFromCache(string $table,
int $id): void {
$query = $this->dbConnection->getQueryBuilder();
$query->delete($table)
->where($query->expr()->eq('id', $query->createNamedParameter($id)))
->executeStatement();
}
/**
* @param string $table
* @param string $foreignKey
* @param int $id
*/
private function deleteMetadataFromCache(string $table,
string $foreignKey,
int $id): void {
$query = $this->dbConnection->getQueryBuilder();
$query->delete($table)
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
->executeStatement();
}
/**
* update an existing entry in cache
*
* @param string $table
* @param int $id
* @param IResource|IRoom $remote
*/
private function updateCache(string $table,
int $id,
$remote): void {
$query = $this->dbConnection->getQueryBuilder();
$query->update($table)
->set('email', $query->createNamedParameter($remote->getEMail()))
->set('displayname', $query->createNamedParameter($remote->getDisplayName()))
->set('group_restrictions', $query->createNamedParameter(
$this->serializeGroupRestrictions(
$remote->getGroupRestrictions()
)))
->where($query->expr()->eq('id', $query->createNamedParameter($id)))
->executeStatement();
}
/**
* @param string $dbTable
* @param string $foreignKey
* @param int $id
* @param array $metadata
* @param array $cachedMetadata
*/
private function updateMetadataCache(string $dbTable,
string $foreignKey,
int $id,
array $metadata,
array $cachedMetadata): void {
$newMetadata = array_diff_key($metadata, $cachedMetadata);
$deletedMetadata = array_diff_key($cachedMetadata, $metadata);
foreach ($newMetadata as $key => $value) {
$query = $this->dbConnection->getQueryBuilder();
$query->insert($dbTable)
->values([
$foreignKey => $query->createNamedParameter($id),
'key' => $query->createNamedParameter($key),
'value' => $query->createNamedParameter($value),
])
->executeStatement();
}
foreach ($deletedMetadata as $key => $value) {
$query = $this->dbConnection->getQueryBuilder();
$query->delete($dbTable)
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
->andWhere($query->expr()->eq('key', $query->createNamedParameter($key)))
->executeStatement();
}
$existingKeys = array_keys(array_intersect_key($metadata, $cachedMetadata));
foreach ($existingKeys as $existingKey) {
if ($metadata[$existingKey] !== $cachedMetadata[$existingKey]) {
$query = $this->dbConnection->getQueryBuilder();
$query->update($dbTable)
->set('value', $query->createNamedParameter($metadata[$existingKey]))
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)))
->andWhere($query->expr()->eq('key', $query->createNamedParameter($existingKey)))
->executeStatement();
}
}
}
/**
* serialize array of group restrictions to store them in database
*
* @param array $groups
*
* @return string
*/
private function serializeGroupRestrictions(array $groups): string {
return \json_encode($groups, JSON_THROW_ON_ERROR);
}
/**
* Gets all metadata of a backend
*
* @param IResource|IRoom $resource
*
* @return array
*/
private function getAllMetadataOfBackend($resource): array {
if (!($resource instanceof IMetadataProvider)) {
return [];
}
$keys = $resource->getAllAvailableMetadataKeys();
$metadata = [];
foreach ($keys as $key) {
$metadata[$key] = $resource->getMetadataForKey($key);
}
return $metadata;
}
/**
* @param string $table
* @param string $foreignKey
* @param int $id
*
* @return array
*/
private function getAllMetadataOfCache(string $table,
string $foreignKey,
int $id): array {
$query = $this->dbConnection->getQueryBuilder();
$query->select(['key', 'value'])
->from($table)
->where($query->expr()->eq($foreignKey, $query->createNamedParameter($id)));
$result = $query->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
$metadata = [];
foreach ($rows as $row) {
$metadata[$row['key']] = $row['value'];
}
return $metadata;
}
/**
* Gets all cached rooms / resources by backend
*
* @param $tableName
* @param $backendId
*
* @return array
*/
private function getAllCachedByBackend(string $tableName,
string $backendId): array {
$query = $this->dbConnection->getQueryBuilder();
$query->select('resource_id')
->from($tableName)
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)));
$result = $query->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
return array_map(function ($row): string {
return $row['resource_id'];
}, $rows);
}
/**
* @param $principalPrefix
* @param $principalUri
*/
private function deleteCalendarDataForResource(string $principalPrefix,
string $principalUri): void {
$calendar = $this->calDavBackend->getCalendarByUri(
implode('/', [$principalPrefix, $principalUri]),
CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI);
if ($calendar !== null) {
$this->calDavBackend->deleteCalendar(
$calendar['id'],
true // Because this wasn't deleted by a user
);
}
}
/**
* @param $table
* @param $backendId
* @param $resourceId
*
* @return int
*/
private function getIdForBackendAndResource(string $table,
string $backendId,
string $resourceId): int {
$query = $this->dbConnection->getQueryBuilder();
$query->select('id')
->from($table)
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
$result = $query->executeQuery();
$id = (int) $result->fetchOne();
$result->closeCursor();
return $id;
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Julius Härtl <jus@bitgrid.net>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OC\User\NoUserException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use Psr\Log\LoggerInterface;
class UploadCleanup extends TimedJob {
private IRootFolder $rootFolder;
private IJobList $jobList;
private LoggerInterface $logger;
public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList, LoggerInterface $logger) {
parent::__construct($time);
$this->rootFolder = $rootFolder;
$this->jobList = $jobList;
$this->logger = $logger;
// Run once a day
$this->setInterval(60 * 60 * 24);
$this->setTimeSensitivity(IJob::TIME_INSENSITIVE);
}
protected function run($argument) {
$uid = $argument['uid'];
$folder = $argument['folder'];
try {
$userFolder = $this->rootFolder->getUserFolder($uid);
$userRoot = $userFolder->getParent();
/** @var Folder $uploads */
$uploads = $userRoot->get('uploads');
$uploadFolder = $uploads->get($folder);
} catch (NotFoundException | NoUserException $e) {
$this->jobList->remove(self::class, $argument);
return;
}
// Remove if all files have an mtime of more than a day
$time = $this->time->getTime() - 60 * 60 * 24;
if (!($uploadFolder instanceof Folder)) {
$this->logger->error("Found a file inside the uploads folder. Uid: " . $uid . ' folder: ' . $folder);
if ($uploadFolder->getMTime() < $time) {
$uploadFolder->delete();
}
$this->jobList->remove(self::class, $argument);
return;
}
/** @var File[] $files */
$files = $uploadFolder->getDirectoryListing();
// The folder has to be more than a day old
$initial = $uploadFolder->getMTime() < $time;
$expire = array_reduce($files, function (bool $carry, File $file) use ($time) {
return $carry && $file->getMTime() < $time;
}, $initial);
if ($expire) {
$uploadFolder->delete();
$this->jobList->remove(self::class, $argument);
}
}
}
@@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\BackgroundJob;
use OCA\DAV\CalDAV\Schedule\Plugin;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
use OCP\User\IAvailabilityCoordinator;
use OCP\User\IOutOfOfficeData;
use OCP\UserStatus\IManager;
use OCP\UserStatus\IUserStatus;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\Available;
use Sabre\VObject\Component\VAvailability;
use Sabre\VObject\Reader;
use Sabre\VObject\Recur\RRuleIterator;
class UserStatusAutomation extends TimedJob {
public function __construct(private ITimeFactory $timeFactory,
private IDBConnection $connection,
private IJobList $jobList,
private LoggerInterface $logger,
private IManager $manager,
private IConfig $config,
private IAvailabilityCoordinator $coordinator,
private IUserManager $userManager) {
parent::__construct($timeFactory);
// Interval 0 might look weird, but the last_checked is always moved
// to the next time we need this and then it's 0 seconds ago.
$this->setInterval(0);
}
/**
* @inheritDoc
*/
protected function run($argument) {
if (!isset($argument['userId'])) {
$this->jobList->remove(self::class, $argument);
$this->logger->info('Removing invalid ' . self::class . ' background job');
return;
}
$userId = $argument['userId'];
$user = $this->userManager->get($userId);
if($user === null) {
return;
}
$ooo = $this->coordinator->getCurrentOutOfOfficeData($user);
$continue = $this->processOutOfOfficeData($user, $ooo);
if($continue === false) {
return;
}
$property = $this->getAvailabilityFromPropertiesTable($userId);
$hasDndForOfficeHours = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes';
if (!$property) {
// We found no ooo data and no availability settings, so we need to delete the job because there is no next runtime
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules and no OOO data set');
$this->jobList->remove(self::class, $argument);
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
return;
}
$this->processAvailability($property, $user->getUID(), $hasDndForOfficeHours);
}
protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void {
$query = $this->connection->getQueryBuilder();
$query->update('jobs')
->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
$query->executeStatement();
$this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId);
}
/**
* @param string $userId
* @return false|string
*/
protected function getAvailabilityFromPropertiesTable(string $userId) {
$propertyPath = 'calendars/' . $userId . '/inbox';
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';
$query = $this->connection->getQueryBuilder();
$query->select('propertyvalue')
->from('properties')
->where($query->expr()->eq('userid', $query->createNamedParameter($userId)))
->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath)))
->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName)))
->setMaxResults(1);
$result = $query->executeQuery();
$property = $result->fetchOne();
$result->closeCursor();
return $property;
}
/**
* @param string $property
* @param $userId
* @param $argument
* @return void
*/
private function processAvailability(string $property, string $userId, bool $hasDndForOfficeHours): void {
$isCurrentlyAvailable = false;
$nextPotentialToggles = [];
$now = $this->time->getDateTime();
$lastMidnight = (clone $now)->setTime(0, 0);
$vObject = Reader::read($property);
foreach ($vObject->getComponents() as $component) {
if ($component->name !== 'VAVAILABILITY') {
continue;
}
/** @var VAvailability $component */
$availables = $component->getComponents();
foreach ($availables as $available) {
/** @var Available $available */
if ($available->name === 'AVAILABLE') {
/** @var \DateTimeImmutable $originalStart */
/** @var \DateTimeImmutable $originalEnd */
[$originalStart, $originalEnd] = $available->getEffectiveStartEnd();
// Little shenanigans to fix the automation on the day the rules were adjusted
// Otherwise the $originalStart would match rules for Thursdays on a Friday, etc.
// So we simply wind back a week and then fastForward to the next occurrence
// since today's midnight, which then also accounts for the week days.
$effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D'));
$effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D'));
try {
$it = new RRuleIterator((string)$available->RRULE, $effectiveStart);
$it->fastForward($lastMidnight);
$startToday = $it->current();
if ($startToday && $startToday <= $now) {
$duration = $effectiveStart->diff($effectiveEnd);
$endToday = $startToday->add($duration);
if ($endToday > $now) {
// User is currently available
// Also queuing the end time as next status toggle
$isCurrentlyAvailable = true;
$nextPotentialToggles[] = $endToday->getTimestamp();
}
// Availability enabling already done for today,
// so jump to the next recurrence to find the next status toggle
$it->next();
}
if ($it->current()) {
$nextPotentialToggles[] = $it->current()->getTimestamp();
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
}
}
}
if (empty($nextPotentialToggles)) {
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules set');
$this->jobList->remove(self::class, ['userId' => $userId]);
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
return;
}
$nextAutomaticToggle = min($nextPotentialToggles);
$this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1);
if ($isCurrentlyAvailable) {
$this->logger->debug('User is currently available, reverting DND status if applicable');
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
$this->logger->debug('User status automation ran');
return;
}
if(!$hasDndForOfficeHours) {
// Office hours are not set to DND, so there is nothing to do.
return;
}
$this->logger->debug('User is currently NOT available, reverting call 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');
}
private function processOutOfOfficeData(IUser $user, ?IOutOfOfficeData $ooo): bool {
if(empty($ooo)) {
// Reset the user status if the absence doesn't exist
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
// We need to also run the availability automation
return true;
}
if(!$this->coordinator->isInEffect($ooo)) {
// Reset the user status if the absence is (no longer) in effect
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND);
if($ooo->getStartDate() > $this->time->getTime()) {
// Set the next run to take place at the start of the ooo period if it is in the future
// This might be overwritten if there is an availability setting, but we can't determine
// if this is the case here
$this->setLastRunToNextToggleTime($user->getUID(), $ooo->getStartDate());
}
return true;
}
$this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status');
// 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_OUT_OF_OFFICE, IUserStatus::DND, true, $ooo->getShortMessage());
// Run at the end of an ooo period to return to availability / regular user status
// If it's overwritten by a custom status in the meantime, there's nothing we can do about it
$this->setLastRunToNextToggleTime($user->getUID(), $ooo->getEndDate());
return false;
}
}
@@ -0,0 +1,116 @@
<?php
/**
* @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\BulkUpload;
use OCA\DAV\Connector\Sabre\MtimeSanitizer;
use OCP\AppFramework\Http;
use OCP\Files\DavUtil;
use OCP\Files\Folder;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class BulkUploadPlugin extends ServerPlugin {
private Folder $userFolder;
private LoggerInterface $logger;
public function __construct(
Folder $userFolder,
LoggerInterface $logger
) {
$this->userFolder = $userFolder;
$this->logger = $logger;
}
/**
* Register listener on POST requests with the httpPost method.
*/
public function initialize(Server $server): void {
$server->on('method:POST', [$this, 'httpPost'], 10);
}
/**
* Handle POST requests on /dav/bulk
* - parsing is done with a MultipartContentsParser object
* - writing is done with the userFolder service
*
* Will respond with an object containing an ETag for every written files.
*/
public function httpPost(RequestInterface $request, ResponseInterface $response): bool {
// Limit bulk upload to the /dav/bulk endpoint
if ($request->getPath() !== "bulk") {
return true;
}
$multiPartParser = new MultipartRequestParser($request, $this->logger);
$writtenFiles = [];
while (!$multiPartParser->isAtLastBoundary()) {
try {
[$headers, $content] = $multiPartParser->parseNextPart();
} catch (\Exception $e) {
// Return early if an error occurs during parsing.
$this->logger->error($e->getMessage());
$response->setStatus(Http::STATUS_BAD_REQUEST);
$response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
try {
// TODO: Remove 'x-file-mtime' when the desktop client no longer use it.
if (isset($headers['x-file-mtime'])) {
$mtime = MtimeSanitizer::sanitizeMtime($headers['x-file-mtime']);
} elseif (isset($headers['x-oc-mtime'])) {
$mtime = MtimeSanitizer::sanitizeMtime($headers['x-oc-mtime']);
} else {
$mtime = null;
}
$node = $this->userFolder->newFile($headers['x-file-path'], $content);
$node->touch($mtime);
$node = $this->userFolder->getById($node->getId())[0];
$writtenFiles[$headers['x-file-path']] = [
"error" => false,
"etag" => $node->getETag(),
"fileid" => DavUtil::getDavFileId($node->getId()),
"permissions" => DavUtil::getDavPermissions($node),
];
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['path' => $headers['x-file-path']]);
$writtenFiles[$headers['x-file-path']] = [
"error" => true,
"message" => $e->getMessage(),
];
}
}
$response->setStatus(Http::STATUS_OK);
$response->setBody(json_encode($writtenFiles, JSON_THROW_ON_ERROR));
return false;
}
}
@@ -0,0 +1,252 @@
<?php
/**
* @copyright Copyright (c) 2021, Louis Chemineau <louis@chmn.me>
*
* @author Louis Chemineau <louis@chmn.me>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\BulkUpload;
use OCP\AppFramework\Http;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\LengthRequired;
use Sabre\HTTP\RequestInterface;
class MultipartRequestParser {
/** @var resource */
private $stream;
/** @var string */
private $boundary = "";
/** @var string */
private $lastBoundary = "";
/**
* @throws BadRequest
*/
public function __construct(
RequestInterface $request,
protected LoggerInterface $logger,
) {
$stream = $request->getBody();
$contentType = $request->getHeader('Content-Type');
if (!is_resource($stream)) {
throw new BadRequest('Body should be of type resource');
}
if ($contentType === null) {
throw new BadRequest("Content-Type can not be null");
}
$this->stream = $stream;
$boundary = $this->parseBoundaryFromHeaders($contentType);
$this->boundary = '--'.$boundary."\r\n";
$this->lastBoundary = '--'.$boundary."--\r\n";
}
/**
* Parse the boundary from the Content-Type header.
* Example: Content-Type: "multipart/related; boundary=boundary_bf38b9b4b10a303a28ed075624db3978"
*
* @throws BadRequest
*/
private function parseBoundaryFromHeaders(string $contentType): string {
try {
[$mimeType, $boundary] = explode(';', $contentType);
[$boundaryKey, $boundaryValue] = explode('=', $boundary);
} catch (\Exception $e) {
throw new BadRequest("Error while parsing boundary in Content-Type header.", Http::STATUS_BAD_REQUEST, $e);
}
$boundaryValue = trim($boundaryValue);
// Remove potential quotes around boundary value.
if (substr($boundaryValue, 0, 1) === '"' && substr($boundaryValue, -1) === '"') {
$boundaryValue = substr($boundaryValue, 1, -1);
}
if (trim($mimeType) !== 'multipart/related') {
throw new BadRequest('Content-Type must be multipart/related');
}
if (trim($boundaryKey) !== 'boundary') {
throw new BadRequest('Boundary is invalid');
}
return $boundaryValue;
}
/**
* Check whether the stream's cursor is sitting right before the provided string.
*
* @throws Exception
*/
private function isAt(string $expectedContent): bool {
$expectedContentLength = strlen($expectedContent);
$content = fread($this->stream, $expectedContentLength);
if ($content === false) {
throw new Exception('An error occurred while checking content');
}
$seekBackResult = fseek($this->stream, -$expectedContentLength, SEEK_CUR);
if ($seekBackResult === -1) {
throw new Exception("Unknown error while seeking content", Http::STATUS_INTERNAL_SERVER_ERROR);
}
return $expectedContent === $content;
}
/**
* Check whether the stream's cursor is sitting right before the boundary.
*/
private function isAtBoundary(): bool {
return $this->isAt($this->boundary);
}
/**
* Check whether the stream's cursor is sitting right before the last boundary.
*/
public function isAtLastBoundary(): bool {
return $this->isAt($this->lastBoundary);
}
/**
* Parse and return the next part of the multipart headers.
*
* Example:
* --boundary_azertyuiop
* Header1: value
* Header2: value
*
* Content of
* the part
*
*/
public function parseNextPart(): array {
$this->readBoundary();
$headers = $this->readPartHeaders();
$content = $this->readPartContent($headers["content-length"], $headers["x-file-md5"]);
return [$headers, $content];
}
/**
* Read the boundary and check its content.
*
* @throws BadRequest
*/
private function readBoundary(): string {
if (!$this->isAtBoundary()) {
throw new BadRequest("Boundary not found where it should be.");
}
return fread($this->stream, strlen($this->boundary));
}
/**
* Return the headers of a part of the multipart body.
*
* @throws Exception
* @throws BadRequest
* @throws LengthRequired
*/
private function readPartHeaders(): array {
$headers = [];
while (($line = fgets($this->stream)) !== "\r\n") {
if ($line === false) {
throw new Exception('An error occurred while reading headers of a part');
}
if (!str_contains($line, ':')) {
$this->logger->error('Header missing ":" on bulk request: ' . json_encode($line));
throw new Exception('An error occurred while reading headers of a part', Http::STATUS_BAD_REQUEST);
}
try {
[$key, $value] = explode(':', $line, 2);
$headers[strtolower(trim($key))] = trim($value);
} catch (\Exception $e) {
throw new BadRequest('An error occurred while parsing headers of a part', Http::STATUS_BAD_REQUEST, $e);
}
}
if (!isset($headers["content-length"])) {
throw new LengthRequired("The Content-Length header must not be null.");
}
if (!isset($headers["x-file-md5"])) {
throw new BadRequest("The X-File-MD5 header must not be null.");
}
return $headers;
}
/**
* Return the content of a part of the multipart body.
*
* @throws Exception
* @throws BadRequest
*/
private function readPartContent(int $length, string $md5): string {
$computedMd5 = $this->computeMd5Hash($length);
if ($md5 !== $computedMd5) {
throw new BadRequest("Computed md5 hash is incorrect.");
}
if ($length === 0) {
$content = '';
} else {
$content = stream_get_line($this->stream, $length);
}
if ($content === false) {
throw new Exception("Fail to read part's content.");
}
if ($length !== 0 && feof($this->stream)) {
throw new Exception("Unexpected EOF while reading stream.");
}
// Read '\r\n'.
stream_get_contents($this->stream, 2);
return $content;
}
/**
* Compute the MD5 hash of the next x bytes.
*/
private function computeMd5Hash(int $length): string {
$context = hash_init('md5');
hash_update_stream($context, $this->stream, $length);
fseek($this->stream, -$length, SEEK_CUR);
return hash_final($context);
}
}
@@ -0,0 +1,661 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity;
use OCA\DAV\CalDAV\Activity\Provider\Calendar;
use OCA\DAV\CalDAV\Activity\Provider\Event;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\Activity\IEvent;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\IAppManager;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use Sabre\VObject\Reader;
/**
* Class Backend
*
* @package OCA\DAV\CalDAV\Activity
*/
class Backend {
/** @var IActivityManager */
protected $activityManager;
/** @var IGroupManager */
protected $groupManager;
/** @var IUserSession */
protected $userSession;
/** @var IAppManager */
protected $appManager;
/** @var IUserManager */
protected $userManager;
public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager, IUserManager $userManager) {
$this->activityManager = $activityManager;
$this->groupManager = $groupManager;
$this->userSession = $userSession;
$this->appManager = $appManager;
$this->userManager = $userManager;
}
/**
* Creates activities when a calendar was creates
*
* @param array $calendarData
*/
public function onCalendarAdd(array $calendarData) {
$this->triggerCalendarActivity(Calendar::SUBJECT_ADD, $calendarData);
}
/**
* Creates activities when a calendar was updated
*
* @param array $calendarData
* @param array $shares
* @param array $properties
*/
public function onCalendarUpdate(array $calendarData, array $shares, array $properties) {
$this->triggerCalendarActivity(Calendar::SUBJECT_UPDATE, $calendarData, $shares, $properties);
}
/**
* Creates activities when a calendar was moved to trash
*
* @param array $calendarData
* @param array $shares
*/
public function onCalendarMovedToTrash(array $calendarData, array $shares): void {
$this->triggerCalendarActivity(Calendar::SUBJECT_MOVE_TO_TRASH, $calendarData, $shares);
}
/**
* Creates activities when a calendar was restored
*
* @param array $calendarData
* @param array $shares
*/
public function onCalendarRestored(array $calendarData, array $shares): void {
$this->triggerCalendarActivity(Calendar::SUBJECT_RESTORE, $calendarData, $shares);
}
/**
* Creates activities when a calendar was deleted
*
* @param array $calendarData
* @param array $shares
*/
public function onCalendarDelete(array $calendarData, array $shares): void {
$this->triggerCalendarActivity(Calendar::SUBJECT_DELETE, $calendarData, $shares);
}
/**
* Creates activities when a calendar was (un)published
*
* @param array $calendarData
* @param bool $publishStatus
*/
public function onCalendarPublication(array $calendarData, bool $publishStatus): void {
$this->triggerCalendarActivity($publishStatus ? Calendar::SUBJECT_PUBLISH : Calendar::SUBJECT_UNPUBLISH, $calendarData);
}
/**
* Creates activities for all related users when a calendar was touched
*
* @param string $action
* @param array $calendarData
* @param array $shares
* @param array $changedProperties
*/
protected function triggerCalendarActivity($action, array $calendarData, array $shares = [], array $changedProperties = []) {
if (!isset($calendarData['principaluri'])) {
return;
}
$principal = explode('/', $calendarData['principaluri']);
$owner = array_pop($principal);
$currentUser = $this->userSession->getUser();
if ($currentUser instanceof IUser) {
$currentUser = $currentUser->getUID();
} else {
$currentUser = $owner;
}
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
->setObject('calendar', (int) $calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
$changedVisibleInformation = array_intersect([
'{DAV:}displayname',
'{http://apple.com/ns/ical/}calendar-color'
], array_keys($changedProperties));
if (empty($shares) || ($action === Calendar::SUBJECT_UPDATE && empty($changedVisibleInformation))) {
$users = [$owner];
} else {
$users = $this->getUsersForShares($shares);
$users[] = $owner;
}
foreach ($users as $user) {
if ($action === Calendar::SUBJECT_DELETE && !$this->userManager->userExists($user)) {
// Avoid creating calendar_delete activities for deleted users
continue;
}
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? $action . '_self' : $action,
[
'actor' => $currentUser,
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
]
);
$this->activityManager->publish($event);
}
}
/**
* Creates activities for all related users when a calendar was (un-)shared
*
* @param array $calendarData
* @param array $shares
* @param array $add
* @param array $remove
*/
public function onCalendarUpdateShares(array $calendarData, array $shares, array $add, array $remove) {
$principal = explode('/', $calendarData['principaluri']);
$owner = $principal[2];
$currentUser = $this->userSession->getUser();
if ($currentUser instanceof IUser) {
$currentUser = $currentUser->getUID();
} else {
$currentUser = $owner;
}
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
->setObject('calendar', (int) $calendarData['id'])
->setType('calendar')
->setAuthor($currentUser);
foreach ($remove as $principal) {
// principal:principals/users/test
$parts = explode(':', $principal, 2);
if ($parts[0] !== 'principal') {
continue;
}
$principal = explode('/', $parts[1]);
if ($principal[1] === 'users') {
$this->triggerActivityUser(
$principal[2],
$event,
$calendarData,
Calendar::SUBJECT_UNSHARE_USER,
Calendar::SUBJECT_DELETE . '_self'
);
if ($owner !== $principal[2]) {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
'user' => $principal[2],
];
if ($owner === $event->getAuthor()) {
$subject = Calendar::SUBJECT_UNSHARE_USER . '_you';
} elseif ($principal[2] === $event->getAuthor()) {
$subject = Calendar::SUBJECT_UNSHARE_USER . '_self';
} else {
$event->setAffectedUser($event->getAuthor())
->setSubject(Calendar::SUBJECT_UNSHARE_USER . '_you', $parameters);
$this->activityManager->publish($event);
$subject = Calendar::SUBJECT_UNSHARE_USER . '_by';
}
$event->setAffectedUser($owner)
->setSubject($subject, $parameters);
$this->activityManager->publish($event);
}
} elseif ($principal[1] === 'groups') {
$this->triggerActivityGroup($principal[2], $event, $calendarData, Calendar::SUBJECT_UNSHARE_USER);
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
'group' => $principal[2],
];
if ($owner === $event->getAuthor()) {
$subject = Calendar::SUBJECT_UNSHARE_GROUP . '_you';
} else {
$event->setAffectedUser($event->getAuthor())
->setSubject(Calendar::SUBJECT_UNSHARE_GROUP . '_you', $parameters);
$this->activityManager->publish($event);
$subject = Calendar::SUBJECT_UNSHARE_GROUP . '_by';
}
$event->setAffectedUser($owner)
->setSubject($subject, $parameters);
$this->activityManager->publish($event);
}
}
foreach ($add as $share) {
if ($this->isAlreadyShared($share['href'], $shares)) {
continue;
}
// principal:principals/users/test
$parts = explode(':', $share['href'], 2);
if ($parts[0] !== 'principal') {
continue;
}
$principal = explode('/', $parts[1]);
if ($principal[1] === 'users') {
$this->triggerActivityUser($principal[2], $event, $calendarData, Calendar::SUBJECT_SHARE_USER);
if ($owner !== $principal[2]) {
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
'user' => $principal[2],
];
if ($owner === $event->getAuthor()) {
$subject = Calendar::SUBJECT_SHARE_USER . '_you';
} else {
$event->setAffectedUser($event->getAuthor())
->setSubject(Calendar::SUBJECT_SHARE_USER . '_you', $parameters);
$this->activityManager->publish($event);
$subject = Calendar::SUBJECT_SHARE_USER . '_by';
}
$event->setAffectedUser($owner)
->setSubject($subject, $parameters);
$this->activityManager->publish($event);
}
} elseif ($principal[1] === 'groups') {
$this->triggerActivityGroup($principal[2], $event, $calendarData, Calendar::SUBJECT_SHARE_USER);
$parameters = [
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
'group' => $principal[2],
];
if ($owner === $event->getAuthor()) {
$subject = Calendar::SUBJECT_SHARE_GROUP . '_you';
} else {
$event->setAffectedUser($event->getAuthor())
->setSubject(Calendar::SUBJECT_SHARE_GROUP . '_you', $parameters);
$this->activityManager->publish($event);
$subject = Calendar::SUBJECT_SHARE_GROUP . '_by';
}
$event->setAffectedUser($owner)
->setSubject($subject, $parameters);
$this->activityManager->publish($event);
}
}
}
/**
* Checks if a calendar is already shared with a principal
*
* @param string $principal
* @param array[] $shares
* @return bool
*/
protected function isAlreadyShared($principal, $shares) {
foreach ($shares as $share) {
if ($principal === $share['href']) {
return true;
}
}
return false;
}
/**
* Creates the given activity for all members of the given group
*
* @param string $gid
* @param IEvent $event
* @param array $properties
* @param string $subject
*/
protected function triggerActivityGroup($gid, IEvent $event, array $properties, $subject) {
$group = $this->groupManager->get($gid);
if ($group instanceof IGroup) {
foreach ($group->getUsers() as $user) {
// Exclude current user
if ($user->getUID() !== $event->getAuthor()) {
$this->triggerActivityUser($user->getUID(), $event, $properties, $subject);
}
}
}
}
/**
* Creates the given activity for the given user
*
* @param string $user
* @param IEvent $event
* @param array $properties
* @param string $subject
* @param string $subjectSelf
*/
protected function triggerActivityUser($user, IEvent $event, array $properties, $subject, $subjectSelf = '') {
$event->setAffectedUser($user)
->setSubject(
$user === $event->getAuthor() && $subjectSelf ? $subjectSelf : $subject,
[
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $properties['id'],
'uri' => $properties['uri'],
'name' => $properties['{DAV:}displayname'],
],
]
);
$this->activityManager->publish($event);
}
/**
* Creates activities when a calendar object was created/updated/deleted
*
* @param string $action
* @param array $calendarData
* @param array $shares
* @param array $objectData
*/
public function onTouchCalendarObject($action, array $calendarData, array $shares, array $objectData) {
if (!isset($calendarData['principaluri'])) {
return;
}
$principal = explode('/', $calendarData['principaluri']);
$owner = array_pop($principal);
$currentUser = $this->userSession->getUser();
if ($currentUser instanceof IUser) {
$currentUser = $currentUser->getUID();
} else {
$currentUser = $owner;
}
$classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
$object = $this->getObjectNameAndType($objectData);
if (!$object) {
return;
}
$action = $action . '_' . $object['type'];
if ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'COMPLETED') {
$action .= '_completed';
} elseif ($object['type'] === 'todo' && str_starts_with($action, Event::SUBJECT_OBJECT_UPDATE) && $object['status'] === 'NEEDS-ACTION') {
$action .= '_needs_action';
}
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
->setObject('calendar', (int) $calendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
$users = $this->getUsersForShares($shares);
$users[] = $owner;
// Users for share can return the owner itself if the calendar is published
foreach (array_unique($users) as $user) {
if ($classification === CalDavBackend::CLASSIFICATION_PRIVATE && $user !== $owner) {
// Private events are only shown to the owner
continue;
}
$params = [
'actor' => $event->getAuthor(),
'calendar' => [
'id' => (int) $calendarData['id'],
'uri' => $calendarData['uri'],
'name' => $calendarData['{DAV:}displayname'],
],
'object' => [
'id' => $object['id'],
'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $owner ? 'Busy' : $object['name'],
'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $owner,
],
];
if ($object['type'] === 'event' && !str_contains($action, Event::SUBJECT_OBJECT_DELETE) && $this->appManager->isEnabledForUser('calendar')) {
$params['object']['link']['object_uri'] = $objectData['uri'];
$params['object']['link']['calendar_uri'] = $calendarData['uri'];
$params['object']['link']['owner'] = $owner;
}
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? $action . '_self' : $action,
$params
);
$this->activityManager->publish($event);
}
}
/**
* Creates activities when a calendar object was moved
*/
public function onMovedCalendarObject(array $sourceCalendarData, array $targetCalendarData, array $sourceShares, array $targetShares, array $objectData): void {
if (!isset($targetCalendarData['principaluri'])) {
return;
}
$sourcePrincipal = explode('/', $sourceCalendarData['principaluri']);
$sourceOwner = array_pop($sourcePrincipal);
$targetPrincipal = explode('/', $targetCalendarData['principaluri']);
$targetOwner = array_pop($targetPrincipal);
if ($sourceOwner !== $targetOwner) {
$this->onTouchCalendarObject(
Event::SUBJECT_OBJECT_DELETE,
$sourceCalendarData,
$sourceShares,
$objectData
);
$this->onTouchCalendarObject(
Event::SUBJECT_OBJECT_ADD,
$targetCalendarData,
$targetShares,
$objectData
);
return;
}
$currentUser = $this->userSession->getUser();
if ($currentUser instanceof IUser) {
$currentUser = $currentUser->getUID();
} else {
$currentUser = $targetOwner;
}
$classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC;
$object = $this->getObjectNameAndType($objectData);
if (!$object) {
return;
}
$event = $this->activityManager->generateEvent();
$event->setApp('dav')
->setObject('calendar', (int) $targetCalendarData['id'])
->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo')
->setAuthor($currentUser);
$users = $this->getUsersForShares(array_intersect($sourceShares, $targetShares));
$users[] = $targetOwner;
// Users for share can return the owner itself if the calendar is published
foreach (array_unique($users) as $user) {
if ($classification === CalDavBackend::CLASSIFICATION_PRIVATE && $user !== $targetOwner) {
// Private events are only shown to the owner
continue;
}
$params = [
'actor' => $event->getAuthor(),
'sourceCalendar' => [
'id' => (int) $sourceCalendarData['id'],
'uri' => $sourceCalendarData['uri'],
'name' => $sourceCalendarData['{DAV:}displayname'],
],
'targetCalendar' => [
'id' => (int) $targetCalendarData['id'],
'uri' => $targetCalendarData['uri'],
'name' => $targetCalendarData['{DAV:}displayname'],
],
'object' => [
'id' => $object['id'],
'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner ? 'Busy' : $object['name'],
'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner,
],
];
if ($object['type'] === 'event' && $this->appManager->isEnabledForUser('calendar')) {
$params['object']['link']['object_uri'] = $objectData['uri'];
$params['object']['link']['calendar_uri'] = $targetCalendarData['uri'];
$params['object']['link']['owner'] = $targetOwner;
}
$event->setAffectedUser($user)
->setSubject(
$user === $currentUser ? Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'] . '_self' : Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'],
$params
);
$this->activityManager->publish($event);
}
}
/**
* @param array $objectData
* @return string[]|false
*/
protected function getObjectNameAndType(array $objectData) {
$vObject = Reader::read($objectData['calendardata']);
$component = $componentType = null;
foreach ($vObject->getComponents() as $component) {
if (in_array($component->name, ['VEVENT', 'VTODO'])) {
$componentType = $component->name;
break;
}
}
if (!$componentType) {
// Calendar objects must have a VEVENT or VTODO component
return false;
}
if ($componentType === 'VEVENT') {
return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event'];
}
return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS];
}
/**
* Get all users that have access to a given calendar
*
* @param array $shares
* @return string[]
*/
protected function getUsersForShares(array $shares) {
$users = $groups = [];
foreach ($shares as $share) {
$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
if ($principal[1] === 'users') {
$users[] = $principal[2];
} elseif ($principal[1] === 'groups') {
$groups[] = $principal[2];
}
}
if (!empty($groups)) {
foreach ($groups as $gid) {
$group = $this->groupManager->get($gid);
if ($group instanceof IGroup) {
foreach ($group->getUsers() as $user) {
$users[] = $user->getUID();
}
}
}
}
return array_unique($users);
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class Calendar implements IFilter {
/** @var IL10N */
protected $l;
/** @var IURLGenerator */
protected $url;
public function __construct(IL10N $l, IURLGenerator $url) {
$this->l = $l;
$this->url = $url;
}
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'calendar';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l->t('Calendar');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 40;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 11.0.0
*/
public function getIcon() {
return $this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar.svg'));
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function filterTypes(array $types) {
return array_intersect(['calendar', 'calendar_event'], $types);
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function allowedApps() {
return [];
}
}
@@ -0,0 +1,92 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Filter;
use OCP\Activity\IFilter;
use OCP\IL10N;
use OCP\IURLGenerator;
class Todo implements IFilter {
/** @var IL10N */
protected $l;
/** @var IURLGenerator */
protected $url;
public function __construct(IL10N $l, IURLGenerator $url) {
$this->l = $l;
$this->url = $url;
}
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'calendar_todo';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l->t('To-dos');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 40;
}
/**
* @return string Full URL to an icon, empty string when none is given
* @since 11.0.0
*/
public function getIcon() {
return $this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/checkmark.svg'));
}
/**
* @param string[] $types
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function filterTypes(array $types) {
return array_intersect(['calendar_todo'], $types);
}
/**
* @return string[] An array of allowed apps from which activities should be displayed
* @since 11.0.0
*/
public function allowedApps() {
return [];
}
}
@@ -0,0 +1,133 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
use OCA\DAV\CalDAV\CalDavBackend;
use OCP\Activity\IEvent;
use OCP\Activity\IProvider;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
abstract class Base implements IProvider {
/** @var IUserManager */
protected $userManager;
/** @var IGroupManager */
protected $groupManager;
/** @var string[] */
protected $groupDisplayNames = [];
/** @var IURLGenerator */
protected $url;
/**
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param IURLGenerator $urlGenerator
*/
public function __construct(IUserManager $userManager, IGroupManager $groupManager, IURLGenerator $urlGenerator) {
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->url = $urlGenerator;
}
protected function setSubjects(IEvent $event, string $subject, array $parameters): void {
$event->setRichSubject($subject, $parameters);
}
/**
* @param array $data
* @param IL10N $l
* @return array
*/
protected function generateCalendarParameter($data, IL10N $l) {
if ($data['uri'] === CalDavBackend::PERSONAL_CALENDAR_URI &&
$data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
return [
'type' => 'calendar',
'id' => $data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'calendar',
'id' => $data['id'],
'name' => $data['name'],
];
}
/**
* @param int $id
* @param string $name
* @return array
*/
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
'id' => $id,
'name' => $name,
];
}
protected function generateUserParameter(string $uid): array {
return [
'type' => 'user',
'id' => $uid,
'name' => $this->userManager->getDisplayName($uid) ?? $uid,
];
}
/**
* @param string $gid
* @return array
*/
protected function generateGroupParameter($gid) {
if (!isset($this->groupDisplayNames[$gid])) {
$this->groupDisplayNames[$gid] = $this->getGroupDisplayName($gid);
}
return [
'type' => 'user-group',
'id' => $gid,
'name' => $this->groupDisplayNames[$gid],
];
}
/**
* @param string $gid
* @return string
*/
protected function getGroupDisplayName($gid) {
$group = $this->groupManager->get($gid);
if ($group instanceof IGroup) {
return $group->getDisplayName();
}
return $gid;
}
}
@@ -0,0 +1,277 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
class Calendar extends Base {
public const SUBJECT_ADD = 'calendar_add';
public const SUBJECT_UPDATE = 'calendar_update';
public const SUBJECT_MOVE_TO_TRASH = 'calendar_move_to_trash';
public const SUBJECT_RESTORE = 'calendar_restore';
public const SUBJECT_DELETE = 'calendar_delete';
public const SUBJECT_PUBLISH = 'calendar_publish';
public const SUBJECT_UNPUBLISH = 'calendar_unpublish';
public const SUBJECT_SHARE_USER = 'calendar_user_share';
public const SUBJECT_SHARE_GROUP = 'calendar_group_share';
public const SUBJECT_UNSHARE_USER = 'calendar_user_unshare';
public const SUBJECT_UNSHARE_GROUP = 'calendar_group_unshare';
/** @var IFactory */
protected $languageFactory;
/** @var IL10N */
protected $l;
/** @var IManager */
protected $activityManager;
/** @var IEventMerger */
protected $eventMerger;
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
* @param IManager $activityManager
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param IEventMerger $eventMerger
*/
public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger) {
parent::__construct($userManager, $groupManager, $url);
$this->languageFactory = $languageFactory;
$this->activityManager = $activityManager;
$this->eventMerger = $eventMerger;
}
/**
* @param string $language
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
public function parse($language, IEvent $event, IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar') {
throw new \InvalidArgumentException();
}
$this->l = $this->languageFactory->get('dav', $language);
if ($this->activityManager->getRequirePNG()) {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar-dark.png')));
} else {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar.svg')));
}
if ($event->getSubject() === self::SUBJECT_ADD) {
$subject = $this->l->t('{actor} created calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_ADD . '_self') {
$subject = $this->l->t('You created calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_DELETE) {
$subject = $this->l->t('{actor} deleted calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_DELETE . '_self') {
$subject = $this->l->t('You deleted calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_UPDATE) {
$subject = $this->l->t('{actor} updated calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') {
$subject = $this->l->t('You updated calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH) {
$subject = $this->l->t('{actor} deleted calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_MOVE_TO_TRASH . '_self') {
$subject = $this->l->t('You deleted calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_RESTORE) {
$subject = $this->l->t('{actor} restored calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_RESTORE . '_self') {
$subject = $this->l->t('You restored calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_PUBLISH . '_self') {
$subject = $this->l->t('You shared calendar {calendar} as public link');
} elseif ($event->getSubject() === self::SUBJECT_UNPUBLISH . '_self') {
$subject = $this->l->t('You removed public link for calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_SHARE_USER) {
$subject = $this->l->t('{actor} shared calendar {calendar} with you');
} elseif ($event->getSubject() === self::SUBJECT_SHARE_USER . '_you') {
$subject = $this->l->t('You shared calendar {calendar} with {user}');
} elseif ($event->getSubject() === self::SUBJECT_SHARE_USER . '_by') {
$subject = $this->l->t('{actor} shared calendar {calendar} with {user}');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER) {
$subject = $this->l->t('{actor} unshared calendar {calendar} from you');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_you') {
$subject = $this->l->t('You unshared calendar {calendar} from {user}');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_by') {
$subject = $this->l->t('{actor} unshared calendar {calendar} from {user}');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_self') {
$subject = $this->l->t('{actor} unshared calendar {calendar} from themselves');
} elseif ($event->getSubject() === self::SUBJECT_SHARE_GROUP . '_you') {
$subject = $this->l->t('You shared calendar {calendar} with group {group}');
} elseif ($event->getSubject() === self::SUBJECT_SHARE_GROUP . '_by') {
$subject = $this->l->t('{actor} shared calendar {calendar} with group {group}');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_you') {
$subject = $this->l->t('You unshared calendar {calendar} from group {group}');
} elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') {
$subject = $this->l->t('{actor} unshared calendar {calendar} from group {group}');
} else {
throw new \InvalidArgumentException();
}
$parsedParameters = $this->getParameters($event);
$this->setSubjects($event, $subject, $parsedParameters);
$event = $this->eventMerger->mergeEvents('calendar', $event, $previousEvent);
if ($event->getChildEvent() === null) {
if (isset($parsedParameters['user'])) {
// Couldn't group by calendar, maybe we can group by users
$event = $this->eventMerger->mergeEvents('user', $event, $previousEvent);
} elseif (isset($parsedParameters['group'])) {
// Couldn't group by calendar, maybe we can group by groups
$event = $this->eventMerger->mergeEvents('group', $event, $previousEvent);
}
}
return $event;
}
/**
* @param IEvent $event
* @return array
*/
protected function getParameters(IEvent $event) {
$subject = $event->getSubject();
$parameters = $event->getSubjectParameters();
// Nextcloud 13+
if (isset($parameters['calendar'])) {
switch ($subject) {
case self::SUBJECT_ADD:
case self::SUBJECT_ADD . '_self':
case self::SUBJECT_DELETE:
case self::SUBJECT_DELETE . '_self':
case self::SUBJECT_UPDATE:
case self::SUBJECT_UPDATE . '_self':
case self::SUBJECT_MOVE_TO_TRASH:
case self::SUBJECT_MOVE_TO_TRASH . '_self':
case self::SUBJECT_RESTORE:
case self::SUBJECT_RESTORE . '_self':
case self::SUBJECT_PUBLISH . '_self':
case self::SUBJECT_UNPUBLISH . '_self':
case self::SUBJECT_SHARE_USER:
case self::SUBJECT_UNSHARE_USER:
case self::SUBJECT_UNSHARE_USER . '_self':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
];
case self::SUBJECT_SHARE_USER . '_you':
case self::SUBJECT_UNSHARE_USER . '_you':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'user' => $this->generateUserParameter($parameters['user']),
];
case self::SUBJECT_SHARE_USER . '_by':
case self::SUBJECT_UNSHARE_USER . '_by':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'user' => $this->generateUserParameter($parameters['user']),
];
case self::SUBJECT_SHARE_GROUP . '_you':
case self::SUBJECT_UNSHARE_GROUP . '_you':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'group' => $this->generateGroupParameter($parameters['group']),
];
case self::SUBJECT_SHARE_GROUP . '_by':
case self::SUBJECT_UNSHARE_GROUP . '_by':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'group' => $this->generateGroupParameter($parameters['group']),
];
}
}
// Legacy - Do NOT Remove unless necessary
// Removing this will break parsing of activities that were created on
// Nextcloud 12, so we should keep this as long as it's acceptable.
// Otherwise if people upgrade over multiple releases in a short period,
// they will get the dead entries in their stream.
switch ($subject) {
case self::SUBJECT_ADD:
case self::SUBJECT_ADD . '_self':
case self::SUBJECT_DELETE:
case self::SUBJECT_DELETE . '_self':
case self::SUBJECT_UPDATE:
case self::SUBJECT_UPDATE . '_self':
case self::SUBJECT_PUBLISH . '_self':
case self::SUBJECT_UNPUBLISH . '_self':
case self::SUBJECT_SHARE_USER:
case self::SUBJECT_UNSHARE_USER:
case self::SUBJECT_UNSHARE_USER . '_self':
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
];
case self::SUBJECT_SHARE_USER . '_you':
case self::SUBJECT_UNSHARE_USER . '_you':
return [
'user' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
];
case self::SUBJECT_SHARE_USER . '_by':
case self::SUBJECT_UNSHARE_USER . '_by':
return [
'user' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'actor' => $this->generateUserParameter($parameters[2]),
];
case self::SUBJECT_SHARE_GROUP . '_you':
case self::SUBJECT_UNSHARE_GROUP . '_you':
return [
'group' => $this->generateGroupParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
];
case self::SUBJECT_SHARE_GROUP . '_by':
case self::SUBJECT_UNSHARE_GROUP . '_by':
return [
'group' => $this->generateGroupParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'actor' => $this->generateUserParameter($parameters[2]),
];
}
throw new \InvalidArgumentException();
}
}
@@ -0,0 +1,257 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
use OC_App;
use OCP\Activity\IEvent;
use OCP\Activity\IEventMerger;
use OCP\Activity\IManager;
use OCP\App\IAppManager;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\L10N\IFactory;
class Event extends Base {
public const SUBJECT_OBJECT_ADD = 'object_add';
public const SUBJECT_OBJECT_UPDATE = 'object_update';
public const SUBJECT_OBJECT_MOVE = 'object_move';
public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash';
public const SUBJECT_OBJECT_RESTORE = 'object_restore';
public const SUBJECT_OBJECT_DELETE = 'object_delete';
/** @var IFactory */
protected $languageFactory;
/** @var IL10N */
protected $l;
/** @var IManager */
protected $activityManager;
/** @var IEventMerger */
protected $eventMerger;
/** @var IAppManager */
protected $appManager;
/**
* @param IFactory $languageFactory
* @param IURLGenerator $url
* @param IManager $activityManager
* @param IUserManager $userManager
* @param IGroupManager $groupManager
* @param IEventMerger $eventMerger
* @param IAppManager $appManager
*/
public function __construct(IFactory $languageFactory, IURLGenerator $url, IManager $activityManager, IUserManager $userManager, IGroupManager $groupManager, IEventMerger $eventMerger, IAppManager $appManager) {
parent::__construct($userManager, $groupManager, $url);
$this->languageFactory = $languageFactory;
$this->activityManager = $activityManager;
$this->eventMerger = $eventMerger;
$this->appManager = $appManager;
}
/**
* @param array $eventData
* @return array
*/
protected function generateObjectParameter(array $eventData) {
if (!isset($eventData['id']) || !isset($eventData['name'])) {
throw new \InvalidArgumentException();
}
$params = [
'type' => 'calendar-event',
'id' => $eventData['id'],
'name' => trim($eventData['name']) !== '' ? $eventData['name'] : $this->l->t('Untitled event'),
];
if (isset($eventData['link']) && is_array($eventData['link']) && $this->appManager->isEnabledForUser('calendar')) {
try {
// The calendar app needs to be manually loaded for the routes to be loaded
OC_App::loadApp('calendar');
$linkData = $eventData['link'];
$objectId = base64_encode($this->url->getWebroot() . '/remote.php/dav/calendars/' . $linkData['owner'] . '/' . $linkData['calendar_uri'] . '/' . $linkData['object_uri']);
$link = [
'view' => 'dayGridMonth',
'timeRange' => 'now',
'mode' => 'sidebar',
'objectId' => $objectId,
'recurrenceId' => 'next'
];
$params['link'] = $this->url->linkToRouteAbsolute('calendar.view.indexview.timerange.edit', $link);
} catch (\Exception $error) {
// Do nothing
}
}
return $params;
}
/**
* @param string $language
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
public function parse($language, IEvent $event, IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_event') {
throw new \InvalidArgumentException();
}
$this->l = $this->languageFactory->get('dav', $language);
if ($this->activityManager->getRequirePNG()) {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar-dark.png')));
} else {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/calendar.svg')));
}
if ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_event') {
$subject = $this->l->t('{actor} created event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_event_self') {
$subject = $this->l->t('You created event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_event') {
$subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_event_self') {
$subject = $this->l->t('You deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event') {
$subject = $this->l->t('{actor} updated event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') {
$subject = $this->l->t('You updated event {event} in calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event') {
$subject = $this->l->t('{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event_self') {
$subject = $this->l->t('You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') {
$subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') {
$subject = $this->l->t('You deleted event {event} from calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event') {
$subject = $this->l->t('{actor} restored event {event} of calendar {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_RESTORE . '_event_self') {
$subject = $this->l->t('You restored event {event} of calendar {calendar}');
} else {
throw new \InvalidArgumentException();
}
$parsedParameters = $this->getParameters($event);
$this->setSubjects($event, $subject, $parsedParameters);
$event = $this->eventMerger->mergeEvents('event', $event, $previousEvent);
return $event;
}
/**
* @param IEvent $event
* @return array
*/
protected function getParameters(IEvent $event) {
$subject = $event->getSubject();
$parameters = $event->getSubjectParameters();
// Nextcloud 13+
if (isset($parameters['calendar'])) {
switch ($subject) {
case self::SUBJECT_OBJECT_ADD . '_event':
case self::SUBJECT_OBJECT_DELETE . '_event':
case self::SUBJECT_OBJECT_UPDATE . '_event':
case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event':
case self::SUBJECT_OBJECT_RESTORE . '_event':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'event' => $this->generateClassifiedObjectParameter($parameters['object']),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
case self::SUBJECT_OBJECT_UPDATE . '_event_self':
case self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self':
case self::SUBJECT_OBJECT_RESTORE . '_event_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'event' => $this->generateClassifiedObjectParameter($parameters['object']),
];
}
}
if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
switch ($subject) {
case self::SUBJECT_OBJECT_MOVE . '_event':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
'event' => $this->generateClassifiedObjectParameter($parameters['object']),
];
case self::SUBJECT_OBJECT_MOVE . '_event_self':
return [
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
'event' => $this->generateClassifiedObjectParameter($parameters['object']),
];
}
}
// Legacy - Do NOT Remove unless necessary
// Removing this will break parsing of activities that were created on
// Nextcloud 12, so we should keep this as long as it's acceptable.
// Otherwise if people upgrade over multiple releases in a short period,
// they will get the dead entries in their stream.
switch ($subject) {
case self::SUBJECT_OBJECT_ADD . '_event':
case self::SUBJECT_OBJECT_DELETE . '_event':
case self::SUBJECT_OBJECT_UPDATE . '_event':
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'event' => $this->generateObjectParameter($parameters[2]),
];
case self::SUBJECT_OBJECT_ADD . '_event_self':
case self::SUBJECT_OBJECT_DELETE . '_event_self':
case self::SUBJECT_OBJECT_UPDATE . '_event_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'event' => $this->generateObjectParameter($parameters[2]),
];
}
throw new \InvalidArgumentException();
}
private function generateClassifiedObjectParameter(array $eventData) {
$parameter = $this->generateObjectParameter($eventData);
if (!empty($eventData['classified'])) {
$parameter['name'] = $this->l->t('Busy');
}
return $parameter;
}
}
@@ -0,0 +1,168 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Provider;
use OCP\Activity\IEvent;
class Todo extends Event {
/**
* @param string $language
* @param IEvent $event
* @param IEvent|null $previousEvent
* @return IEvent
* @throws \InvalidArgumentException
* @since 11.0.0
*/
public function parse($language, IEvent $event, IEvent $previousEvent = null) {
if ($event->getApp() !== 'dav' || $event->getType() !== 'calendar_todo') {
throw new \InvalidArgumentException();
}
$this->l = $this->languageFactory->get('dav', $language);
if ($this->activityManager->getRequirePNG()) {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/checkmark.png')));
} else {
$event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/checkmark.svg')));
}
if ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo') {
$subject = $this->l->t('{actor} created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo_self') {
$subject = $this->l->t('You created to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo') {
$subject = $this->l->t('{actor} deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo_self') {
$subject = $this->l->t('You deleted to-do {todo} from list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo') {
$subject = $this->l->t('{actor} updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_self') {
$subject = $this->l->t('You updated to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed') {
$subject = $this->l->t('{actor} solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self') {
$subject = $this->l->t('You solved to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action') {
$subject = $this->l->t('{actor} reopened to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self') {
$subject = $this->l->t('You reopened to-do {todo} in list {calendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo') {
$subject = $this->l->t('{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') {
$subject = $this->l->t('You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}');
} else {
throw new \InvalidArgumentException();
}
$parsedParameters = $this->getParameters($event);
$this->setSubjects($event, $subject, $parsedParameters);
$event = $this->eventMerger->mergeEvents('todo', $event, $previousEvent);
return $event;
}
/**
* @param IEvent $event
* @return array
*/
protected function getParameters(IEvent $event) {
$subject = $event->getSubject();
$parameters = $event->getSubjectParameters();
// Nextcloud 13+
if (isset($parameters['calendar'])) {
switch ($subject) {
case self::SUBJECT_OBJECT_ADD . '_todo':
case self::SUBJECT_OBJECT_DELETE . '_todo':
case self::SUBJECT_OBJECT_UPDATE . '_todo':
case self::SUBJECT_OBJECT_UPDATE . '_todo_completed':
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'todo' => $this->generateObjectParameter($parameters['object']),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateCalendarParameter($parameters['calendar'], $this->l),
'todo' => $this->generateObjectParameter($parameters['object']),
];
}
}
if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) {
switch ($subject) {
case self::SUBJECT_OBJECT_MOVE . '_todo':
return [
'actor' => $this->generateUserParameter($parameters['actor']),
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
'todo' => $this->generateObjectParameter($parameters['object']),
];
case self::SUBJECT_OBJECT_MOVE . '_todo_self':
return [
'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l),
'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l),
'todo' => $this->generateObjectParameter($parameters['object']),
];
}
}
// Legacy - Do NOT Remove unless necessary
// Removing this will break parsing of activities that were created on
// Nextcloud 12, so we should keep this as long as it's acceptable.
// Otherwise if people upgrade over multiple releases in a short period,
// they will get the dead entries in their stream.
switch ($subject) {
case self::SUBJECT_OBJECT_ADD . '_todo':
case self::SUBJECT_OBJECT_DELETE . '_todo':
case self::SUBJECT_OBJECT_UPDATE . '_todo':
case self::SUBJECT_OBJECT_UPDATE . '_todo_completed':
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action':
return [
'actor' => $this->generateUserParameter($parameters[0]),
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'todo' => $this->generateObjectParameter($parameters[2]),
];
case self::SUBJECT_OBJECT_ADD . '_todo_self':
case self::SUBJECT_OBJECT_DELETE . '_todo_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self':
case self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self':
return [
'calendar' => $this->generateLegacyCalendarParameter($event->getObjectId(), $parameters[1]),
'todo' => $this->generateObjectParameter($parameters[2]),
];
}
throw new \InvalidArgumentException();
}
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020 Robin Appelman <robin@icewind.nl>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
use OCP\Activity\ActivitySettings;
use OCP\IL10N;
abstract class CalDAVSetting extends ActivitySettings {
/** @var IL10N */
protected $l;
/**
* @param IL10N $l
*/
public function __construct(IL10N $l) {
$this->l = $l;
}
public function getGroupIdentifier() {
return 'calendar';
}
public function getGroupName() {
return $this->l->t('Calendar, contacts and tasks');
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
class Calendar extends CalDAVSetting {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'calendar';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l->t('A <strong>calendar</strong> was modified');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 50;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function canChangeStream() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledStream() {
return true;
}
/**
* @return bool True when the option can be changed for the mail
* @since 11.0.0
*/
public function canChangeMail() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledMail() {
return false;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
class Event extends CalDAVSetting {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'calendar_event';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l->t('A calendar <strong>event</strong> was modified');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 50;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function canChangeStream() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledStream() {
return true;
}
/**
* @return bool True when the option can be changed for the mail
* @since 11.0.0
*/
public function canChangeMail() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledMail() {
return false;
}
}
@@ -0,0 +1,85 @@
<?php
/**
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Robin Appelman <robin@icewind.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Activity\Setting;
class Todo extends CalDAVSetting {
/**
* @return string Lowercase a-z and underscore only identifier
* @since 11.0.0
*/
public function getIdentifier() {
return 'calendar_todo';
}
/**
* @return string A translated string
* @since 11.0.0
*/
public function getName() {
return $this->l->t('A calendar <strong>to-do</strong> was modified');
}
/**
* @return int whether the filter should be rather on the top or bottom of
* the admin section. The filters are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
* @since 11.0.0
*/
public function getPriority() {
return 50;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function canChangeStream() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledStream() {
return true;
}
/**
* @return bool True when the option can be changed for the mail
* @since 11.0.0
*/
public function canChangeMail() {
return true;
}
/**
* @return bool True when the option can be changed for the stream
* @since 11.0.0
*/
public function isDefaultEnabledMail() {
return false;
}
}
@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\AppCalendar;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Plugin;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICreateFromString;
use OCP\Constants;
use Sabre\CalDAV\CalendarQueryValidator;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Reader;
class AppCalendar extends ExternalCalendar {
protected string $principal;
protected ICalendar $calendar;
public function __construct(string $appId, ICalendar $calendar, string $principal) {
parent::__construct($appId, $calendar->getUri());
$this->principal = $principal;
$this->calendar = $calendar;
}
/**
* Return permissions supported by the backend calendar
* @return int Permissions based on \OCP\Constants
*/
public function getPermissions(): int {
// Make sure to only promote write support if the backend implement the correct interface
if ($this->calendar instanceof ICreateFromString) {
return $this->calendar->getPermissions();
}
return Constants::PERMISSION_READ;
}
public function getOwner(): ?string {
return $this->principal;
}
public function getGroup(): ?string {
return null;
}
public function getACL(): array {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner(),
'protected' => true,
]
];
if ($this->getPermissions() & Constants::PERMISSION_CREATE) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
];
}
return $acl;
}
public function setACL(array $acl): void {
throw new Forbidden('Setting ACL is not supported on this node');
}
public function getSupportedPrivilegeSet(): ?array {
// Use the default one
return null;
}
public function getLastModified(): ?int {
// unknown
return null;
}
public function delete(): void {
// No method for deleting a calendar in OCP\Calendar\ICalendar
throw new Forbidden('Deleting an entry is not implemented');
}
public function createFile($name, $data = null) {
if ($this->calendar instanceof ICreateFromString) {
if (is_resource($data)) {
$data = stream_get_contents($data) ?: null;
}
$this->calendar->createFromString($name, is_null($data) ? '' : $data);
return null;
} else {
throw new Forbidden('Creating a new entry is not allowed');
}
}
public function getProperties($properties) {
return [
'{DAV:}displayname' => $this->calendar->getDisplayName() ?: $this->calendar->getKey(),
'{http://apple.com/ns/ical/}calendar-color' => $this->calendar->getDisplayColor() ?: '#0082c9',
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VJOURNAL', 'VTODO']),
];
}
public function calendarQuery(array $filters) {
$result = [];
$objects = $this->getChildren();
foreach ($objects as $object) {
if ($this->validateFilterForObject($object, $filters)) {
$result[] = $object->getName();
}
}
return $result;
}
protected function validateFilterForObject(ICalendarObject $object, array $filters): bool {
/** @var \Sabre\VObject\Component\VCalendar */
$vObject = Reader::read($object->get());
$validator = new CalendarQueryValidator();
$result = $validator->validate($vObject, $filters);
// Destroy circular references so PHP will GC the object.
$vObject->destroy();
return $result;
}
public function childExists($name): bool {
try {
$this->getChild($name);
return true;
} catch (NotFound $error) {
return false;
}
}
public function getChild($name) {
// Try to get calendar by filename
$children = $this->calendar->search($name, ['X-FILENAME']);
if (count($children) === 0) {
// If nothing found try to get by UID from filename
$pos = strrpos($name, '.ics');
$children = $this->calendar->search(substr($name, 0, $pos ?: null), ['UID']);
}
if (count($children) > 0) {
return new CalendarObject($this, $this->calendar, new VCalendar($children));
}
throw new NotFound('Node not found');
}
/**
* @return ICalendarObject[]
*/
public function getChildren(): array {
$objects = $this->calendar->search('');
// We need to group by UID (actually by filename but we do not have that information)
$result = [];
foreach ($objects as $object) {
$uid = (string)$object['UID'] ?: uniqid();
if (!isset($result[$uid])) {
$result[$uid] = [];
}
$result[$uid][] = $object;
}
return array_map(function (array $children) {
return new CalendarObject($this, $this->calendar, new VCalendar($children));
}, $result);
}
public function propPatch(PropPatch $propPatch): void {
// no setDisplayColor or setDisplayName in \OCP\Calendar\ICalendar
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\AppCalendar;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCP\Calendar\IManager;
use Psr\Log\LoggerInterface;
/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
class AppCalendarPlugin implements ICalendarProvider {
protected IManager $manager;
protected LoggerInterface $logger;
public function __construct(IManager $manager, LoggerInterface $logger) {
$this->manager = $manager;
$this->logger = $logger;
}
public function getAppID(): string {
return 'dav-wrapper';
}
public function fetchAllForCalendarHome(string $principalUri): array {
return array_map(function ($calendar) use (&$principalUri) {
return new AppCalendar($this->getAppID(), $calendar, $principalUri);
}, $this->getWrappedCalendars($principalUri));
}
public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
return count($this->getWrappedCalendars($principalUri, [ $calendarUri ])) > 0;
}
public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar {
$calendars = $this->getWrappedCalendars($principalUri, [ $calendarUri ]);
if (count($calendars) > 0) {
return new AppCalendar($this->getAppID(), $calendars[0], $principalUri);
}
return null;
}
protected function getWrappedCalendars(string $principalUri, array $calendarUris = []): array {
return array_values(
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
// We must not provide a wrapper for DAV calendars
return ! ($c instanceof \OCA\DAV\CalDAV\CalendarImpl);
})
);
}
}
@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\AppCalendar;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICreateFromString;
use OCP\Constants;
use Sabre\CalDAV\ICalendarObject;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAVACL\IACL;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Property\ICalendar\DateTime;
class CalendarObject implements ICalendarObject, IACL {
private VCalendar $vobject;
private AppCalendar $calendar;
private ICalendar|ICreateFromString $backend;
public function __construct(AppCalendar $calendar, ICalendar $backend, VCalendar $vobject) {
$this->backend = $backend;
$this->calendar = $calendar;
$this->vobject = $vobject;
}
public function getOwner() {
return $this->calendar->getOwner();
}
public function getGroup() {
return $this->calendar->getGroup();
}
public function getACL(): array {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
]
];
if ($this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
$acl[] = [
'privilege' => '{DAV:}write-content',
'principal' => $this->getOwner(),
'protected' => true,
];
}
return $acl;
}
public function setACL(array $acl): void {
throw new Forbidden('Setting ACL is not supported on this node');
}
public function getSupportedPrivilegeSet(): ?array {
return null;
}
public function put($data): void {
if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) {
if (is_resource($data)) {
$data = stream_get_contents($data) ?: '';
}
$this->backend->createFromString($this->getName(), $data);
} else {
throw new Forbidden('This calendar-object is read-only');
}
}
public function get(): string {
return $this->vobject->serialize();
}
public function getContentType(): string {
return 'text/calendar; charset=utf-8';
}
public function getETag(): ?string {
return null;
}
public function getSize() {
return mb_strlen($this->vobject->serialize());
}
public function delete(): void {
if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_DELETE) {
/** @var \Sabre\VObject\Component[] */
$components = $this->vobject->getBaseComponents();
foreach ($components as $key => $component) {
$components[$key]->STATUS = 'CANCELLED';
$components[$key]->SEQUENCE = isset($component->SEQUENCE) ? ((int)$component->SEQUENCE->getValue()) + 1 : 1;
if ($component->name === 'VEVENT') {
$components[$key]->METHOD = 'CANCEL';
}
}
$this->backend->createFromString($this->getName(), (new VCalendar($components))->serialize());
} else {
throw new Forbidden('This calendar-object is read-only');
}
}
public function getName(): string {
// Every object is required to have an UID
$base = $this->vobject->getBaseComponent();
// This should never happen except the app provides invalid calendars (VEvent, VTodo... all require UID to be present)
if ($base === null) {
throw new NotFound('Invalid node');
}
if (isset($base->{'X-FILENAME'})) {
return (string)$base->{'X-FILENAME'};
}
return (string)$base->UID . '.ics';
}
public function setName($name): void {
throw new Forbidden('This calendar-object is read-only');
}
public function getLastModified(): ?int {
$base = $this->vobject->getBaseComponent();
if ($base !== null && $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) {
/** @var DateTime */
$lastModified = $this->vobject->getBaseComponent()->{'LAST-MODIFIED'};
return $lastModified->getDateTime()->getTimestamp();
}
return null;
}
}
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* CalDAV App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Auth;
use Sabre\DAV\Auth\Plugin;
/**
* Set a custom principal uri to allow public requests to its calendar
*/
class CustomPrincipalPlugin extends Plugin {
public function setCurrentPrincipal(?string $currentPrincipal): void {
$this->currentPrincipal = $currentPrincipal;
}
}
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* CalDAV App
*
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Auth;
use Sabre\DAV\Auth\Plugin;
/**
* Defines the public facing principal option
*/
class PublicPrincipalPlugin extends Plugin {
public function getCurrentPrincipal(): ?string {
return 'principals/system/public';
}
}
@@ -0,0 +1,149 @@
<?php
/**
* @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\BirthdayCalendar;
use OCA\DAV\CalDAV\BirthdayService;
use OCA\DAV\CalDAV\CalendarHome;
use OCP\IConfig;
use OCP\IUser;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
/**
* Class EnablePlugin
* allows users to re-enable the birthday calendar via CalDAV
*
* @package OCA\DAV\CalDAV\BirthdayCalendar
*/
class EnablePlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
* @var IConfig
*/
protected $config;
/**
* @var BirthdayService
*/
protected $birthdayService;
/**
* @var Server
*/
protected $server;
/** @var IUser */
private $user;
/**
* PublishPlugin constructor.
*
* @param IConfig $config
* @param BirthdayService $birthdayService
* @param IUser $user
*/
public function __construct(IConfig $config, BirthdayService $birthdayService, IUser $user) {
$this->config = $config;
$this->birthdayService = $birthdayService;
$this->user = $user;
}
/**
* This method should return a list of server-features.
*
* This is for example 'versioning' and is added to the DAV: header
* in an OPTIONS response.
*
* @return string[]
*/
public function getFeatures() {
return ['nc-enable-birthday-calendar'];
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using Sabre\DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName() {
return 'nc-enable-birthday-calendar';
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param Server $server
*/
public function initialize(Server $server) {
$this->server = $server;
$this->server->on('method:POST', [$this, 'httpPost']);
}
/**
* We intercept this to handle POST requests on calendar homes.
*
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return bool|void
*/
public function httpPost(RequestInterface $request, ResponseInterface $response) {
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
if (!($node instanceof CalendarHome)) {
return;
}
$requestBody = $request->getBodyAsString();
$this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
if ($documentType !== '{'.self::NS_Nextcloud.'}enable-birthday-calendar') {
return;
}
$owner = substr($node->getOwner(), 17);
if($owner !== $this->user->getUID()) {
$this->server->httpResponse->setStatus(403);
return false;
}
$this->config->setUserValue($this->user->getUID(), 'dav', 'generateBirthdayCalendar', 'yes');
$this->birthdayService->syncUser($this->user->getUID());
$this->server->httpResponse->setStatus(204);
return false;
}
}
@@ -0,0 +1,527 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Achim Königs <garfonso@tratschtante.de>
* @author Christian Weiske <cweiske@cweiske.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Robin Appelman <robin@icewind.nl>
* @author Sven Strickroth <email@cs-ware.de>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Valdnet <47037905+Valdnet@users.noreply.github.com>
* @author Cédric Neukom <github@webguy.ch>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use Exception;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\DAV\GroupPrincipalBackend;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Document;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Property\VCard\DateAndOrTime;
use Sabre\VObject\Reader;
/**
* Class BirthdayService
*
* @package OCA\DAV\CalDAV
*/
class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
private GroupPrincipalBackend $principalBackend;
private CalDavBackend $calDavBackEnd;
private CardDavBackend $cardDavBackEnd;
private IConfig $config;
private IDBConnection $dbConnection;
private IL10N $l10n;
/**
* BirthdayService constructor.
*/
public function __construct(CalDavBackend $calDavBackEnd,
CardDavBackend $cardDavBackEnd,
GroupPrincipalBackend $principalBackend,
IConfig $config,
IDBConnection $dbConnection,
IL10N $l10n) {
$this->calDavBackEnd = $calDavBackEnd;
$this->cardDavBackEnd = $cardDavBackEnd;
$this->principalBackend = $principalBackend;
$this->config = $config;
$this->dbConnection = $dbConnection;
$this->l10n = $l10n;
}
public function onCardChanged(int $addressBookId,
string $cardUri,
string $cardData): void {
if (!$this->isGloballyEnabled()) {
return;
}
$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
if ($book === null) {
return;
}
$targetPrincipals[] = $book['principaluri'];
$datesToSync = [
['postfix' => '', 'field' => 'BDAY'],
['postfix' => '-death', 'field' => 'DEATHDATE'],
['postfix' => '-anniversary', 'field' => 'ANNIVERSARY'],
];
foreach ($targetPrincipals as $principalUri) {
if (!$this->isUserEnabled($principalUri)) {
continue;
}
$reminderOffset = $this->getReminderOffsetForUser($principalUri);
$calendar = $this->ensureCalendarExists($principalUri);
if ($calendar === null) {
return;
}
foreach ($datesToSync as $type) {
$this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset);
}
}
}
public function onCardDeleted(int $addressBookId,
string $cardUri): void {
if (!$this->isGloballyEnabled()) {
return;
}
$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
$targetPrincipals[] = $book['principaluri'];
foreach ($targetPrincipals as $principalUri) {
if (!$this->isUserEnabled($principalUri)) {
continue;
}
$calendar = $this->ensureCalendarExists($principalUri);
foreach (['', '-death', '-anniversary'] as $tag) {
$objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
}
/**
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function ensureCalendarExists(string $principal): ?array {
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!is_null($calendar)) {
return $calendar;
}
$this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
'{DAV:}displayname' => $this->l10n->t('Contact birthdays'),
'{http://apple.com/ns/ical/}calendar-color' => '#E9D859',
'components' => 'VEVENT',
]);
return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
}
/**
* @param $cardData
* @param $dateField
* @param $postfix
* @param $reminderOffset
* @return VCalendar|null
* @throws InvalidDataException
*/
public function buildDateFromContact(string $cardData,
string $dateField,
string $postfix,
?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
}
try {
$doc = Reader::read($cardData);
// We're always converting to vCard 4.0 so we can rely on the
// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
if (!$doc instanceof VCard) {
return null;
}
$doc = $doc->convert(Document::VCARD40);
} catch (Exception $e) {
return null;
}
if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
return null;
}
if (!isset($doc->{$dateField})) {
return null;
}
if (!isset($doc->FN)) {
return null;
}
$birthday = $doc->{$dateField};
if (!(string)$birthday) {
return null;
}
// Skip if the BDAY property is not of the right type.
if (!$birthday instanceof DateAndOrTime) {
return null;
}
// Skip if we can't parse the BDAY value.
try {
$dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
} catch (InvalidDataException $e) {
return null;
}
if ($dateParts['year'] !== null) {
$parameters = $birthday->parameters();
$omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
&& $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
// 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
if ($omitYear || (int)$dateParts['year'] === 1604) {
$dateParts['year'] = null;
}
}
$originalYear = null;
if ($dateParts['year'] !== null) {
$originalYear = (int)$dateParts['year'];
}
$leapDay = ((int)$dateParts['month'] === 2
&& (int)$dateParts['date'] === 29);
if ($dateParts['year'] === null || $originalYear < 1970) {
$birthday = ($leapDay ? '1972-' : '1970-')
. $dateParts['month'] . '-' . $dateParts['date'];
}
try {
if ($birthday instanceof DateAndOrTime) {
$date = $birthday->getDateTime();
} else {
$date = new \DateTimeImmutable($birthday);
}
} catch (Exception $e) {
return null;
}
$summary = $this->formatTitle($dateField, $doc->FN->getValue(), $originalYear, $this->dbConnection->supports4ByteText());
$vCal = new VCalendar();
$vCal->VERSION = '2.0';
$vCal->PRODID = '-//IDN nextcloud.com//Birthday calendar//EN';
$vEvent = $vCal->createComponent('VEVENT');
$vEvent->add('DTSTART');
$vEvent->DTSTART->setDateTime(
$date
);
$vEvent->DTSTART['VALUE'] = 'DATE';
$vEvent->add('DTEND');
$dtEndDate = (new \DateTime())->setTimestamp($date->getTimeStamp());
$dtEndDate->add(new \DateInterval('P1D'));
$vEvent->DTEND->setDateTime(
$dtEndDate
);
$vEvent->DTEND['VALUE'] = 'DATE';
$vEvent->{'UID'} = $doc->UID . $postfix;
$vEvent->{'RRULE'} = 'FREQ=YEARLY';
if ($leapDay) {
/* Sabre\VObject supports BYMONTHDAY only if BYMONTH
* is also set */
$vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';
}
$vEvent->{'SUMMARY'} = $summary;
$vEvent->{'TRANSP'} = 'TRANSPARENT';
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
$vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
if ($originalYear !== null) {
$vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
}
if ($reminderOffset) {
$alarm = $vCal->createComponent('VALARM');
$alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION']));
$alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
$alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
$vEvent->add($alarm);
}
$vCal->add($vEvent);
return $vCal;
}
/**
* @param string $user
*/
public function resetForUser(string $user):void {
$principal = 'principals/users/'.$user;
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!$calendar) {
return; // The user's birthday calendar doesn't exist, no need to purge it
}
$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
foreach ($calendarObjects as $calendarObject) {
$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $calendarObject['uri'], CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
/**
* @param string $user
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function syncUser(string $user):void {
$principal = 'principals/users/'.$user;
$this->ensureCalendarExists($principal);
$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
foreach ($books as $book) {
$cards = $this->cardDavBackEnd->getCards($book['id']);
foreach ($cards as $card) {
$this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
}
}
}
/**
* @param string $existingCalendarData
* @param VCalendar $newCalendarData
* @return bool
*/
public function birthdayEvenChanged(string $existingCalendarData,
VCalendar $newCalendarData):bool {
try {
$existingBirthday = Reader::read($existingCalendarData);
} catch (Exception $ex) {
return true;
}
return (
$newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
$newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
);
}
/**
* @param integer $addressBookId
* @return mixed
*/
protected function getAllAffectedPrincipals(int $addressBookId) {
$targetPrincipals = [];
$shares = $this->cardDavBackEnd->getShares($addressBookId);
foreach ($shares as $share) {
if ($share['{http://owncloud.org/ns}group-share']) {
$users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']);
foreach ($users as $user) {
$targetPrincipals[] = $user['uri'];
}
} else {
$targetPrincipals[] = $share['{http://owncloud.org/ns}principal'];
}
}
return array_values(array_unique($targetPrincipals, SORT_STRING));
}
/**
* @param string $cardUri
* @param string $cardData
* @param array $book
* @param int $calendarId
* @param array $type
* @param string $reminderOffset
* @throws InvalidDataException
* @throws \Sabre\DAV\Exception\BadRequest
*/
private function updateCalendar(string $cardUri,
string $cardData,
array $book,
int $calendarId,
array $type,
?string $reminderOffset):void {
$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
if ($calendarData === null) {
if ($existing !== null) {
$this->calDavBackEnd->deleteCalendarObject($calendarId, $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
} else {
if ($existing === null) {
// not found by URI, but maybe by UID
// happens when a contact with birthday is moved to a different address book
$calendarInfo = $this->calDavBackEnd->getCalendarById($calendarId);
$extraData = $this->calDavBackEnd->getDenormalizedData($calendarData->serialize());
if ($calendarInfo && array_key_exists('principaluri', $calendarInfo)) {
$existing2path = $this->calDavBackEnd->getCalendarObjectByUID($calendarInfo['principaluri'], $extraData['uid']);
if ($existing2path !== null && array_key_exists('uri', $calendarInfo)) {
// delete the old birthday entry first so that we do not get duplicate UIDs
$existing2objectUri = substr($existing2path, strlen($calendarInfo['uri']) + 1);
$this->calDavBackEnd->deleteCalendarObject($calendarId, $existing2objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
$this->calDavBackEnd->createCalendarObject($calendarId, $objectUri, $calendarData->serialize());
} else {
if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) {
$this->calDavBackEnd->updateCalendarObject($calendarId, $objectUri, $calendarData->serialize());
}
}
}
}
/**
* checks if the admin opted-out of birthday calendars
*
* @return bool
*/
private function isGloballyEnabled():bool {
return $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes') === 'yes';
}
/**
* Extracts the userId part of a principal
*
* @param string $userPrincipal
* @return string|null
*/
private function principalToUserId(string $userPrincipal):?string {
if (substr($userPrincipal, 0, 17) === 'principals/users/') {
return substr($userPrincipal, 17);
}
return null;
}
/**
* Checks if the user opted-out of birthday calendars
*
* @param string $userPrincipal The user principal to check for
* @return bool
*/
private function isUserEnabled(string $userPrincipal):bool {
$userId = $this->principalToUserId($userPrincipal);
if ($userId !== null) {
$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
return $isEnabled === 'yes';
}
// not sure how we got here, just be on the safe side and return true
return true;
}
/**
* Get the reminder offset value for a user. This is a duration string (e.g.
* PT9H) or null if no reminder is wanted.
*
* @param string $userPrincipal
* @return string|null
*/
private function getReminderOffsetForUser(string $userPrincipal):?string {
$userId = $this->principalToUserId($userPrincipal);
if ($userId !== null) {
return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
}
// not sure how we got here, just be on the safe side and return the default value
return 'PT9H';
}
/**
* Formats title of Birthday event
*
* @param string $field Field name like BDAY, ANNIVERSARY, ...
* @param string $name Name of contact
* @param int|null $year Year of birth, anniversary, ...
* @param bool $supports4Byte Whether or not the database supports 4 byte chars
* @return string The formatted title
*/
private function formatTitle(string $field,
string $name,
int $year = null,
bool $supports4Byte = true):string {
if ($supports4Byte) {
switch ($field) {
case 'BDAY':
return implode('', [
'🎂 ',
$name,
$year ? (' (' . $year . ')') : '',
]);
case 'DEATHDATE':
return implode('', [
$this->l10n->t('Death of %s', [$name]),
$year ? (' (' . $year . ')') : '',
]);
case 'ANNIVERSARY':
return implode('', [
'💍 ',
$name,
$year ? (' (' . $year . ')') : '',
]);
default:
return '';
}
} else {
switch ($field) {
case 'BDAY':
return implode('', [
$name,
' ',
$year ? ('(*' . $year . ')') : '*',
]);
case 'DEATHDATE':
return implode('', [
$this->l10n->t('Death of %s', [$name]),
$year ? (' (' . $year . ')') : '',
]);
case 'ANNIVERSARY':
return implode('', [
$name,
' ',
$year ? ('(⚭' . $year . ')') : '⚭',
]);
default:
return '';
}
}
}
}
@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;
/**
* Class CachedSubscription
*
* @package OCA\DAV\CalDAV
* @property CalDavBackend $caldavBackend
*/
class CachedSubscription extends \Sabre\CalDAV\Calendar {
/**
* @return string
*/
public function getPrincipalURI():string {
return $this->calendarInfo['principaluri'];
}
/**
* @return array
*/
public function getACL() {
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
[
'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
'principal' => '{DAV:}authenticated',
'protected' => true,
],
];
}
/**
* @return array
*/
public function getChildACL() {
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
];
}
/**
* @return null|string
*/
public function getOwner() {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
}
return parent::getOwner();
}
public function delete() {
$this->caldavBackend->deleteSubscription($this->calendarInfo['id']);
}
/**
* @param PropPatch $propPatch
*/
public function propPatch(PropPatch $propPatch) {
$this->caldavBackend->updateSubscription($this->calendarInfo['id'], $propPatch);
}
/**
* @param string $name
* @return CalendarObject|\Sabre\CalDAV\ICalendarObject
* @throws NotFound
*/
public function getChild($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
if (!$obj) {
throw new NotFound('Calendar object not found');
}
$obj['acl'] = $this->getChildACL();
return new CachedSubscriptionObject($this->caldavBackend, $this->calendarInfo, $obj);
}
/**
* @return INode[]
*/
public function getChildren(): array {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
$children = [];
foreach ($objs as $obj) {
$children[] = new CachedSubscriptionObject($this->caldavBackend, $this->calendarInfo, $obj);
}
return $children;
}
/**
* @param array $paths
* @return array
*/
public function getMultipleChildren(array $paths):array {
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
$children = [];
foreach ($objs as $obj) {
$children[] = new CachedSubscriptionObject($this->caldavBackend, $this->calendarInfo, $obj);
}
return $children;
}
/**
* @param string $name
* @param null|resource|string $data
* @return null|string
* @throws MethodNotAllowed
*/
public function createFile($name, $data = null) {
throw new MethodNotAllowed('Creating objects in cached subscription is not allowed');
}
/**
* @param string $name
* @return bool
*/
public function childExists($name):bool {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
if (!$obj) {
return false;
}
return true;
}
/**
* @param array $filters
* @return array
*/
public function calendarQuery(array $filters):array {
return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
}
/**
* @inheritDoc
*/
public function getChanges($syncToken, $syncLevel, $limit = null) {
if (!$syncToken && $limit) {
throw new UnsupportedLimitOnInitialSyncException();
}
return parent::getChanges($syncToken, $syncLevel, $limit);
}
}
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use Sabre\DAV\Exception\MethodNotAllowed;
/**
* Class CachedSubscriptionObject
*
* @package OCA\DAV\CalDAV
* @property CalDavBackend $caldavBackend
*/
class CachedSubscriptionObject extends \Sabre\CalDAV\CalendarObject {
/**
* @inheritdoc
*/
public function get() {
// Pre-populating the 'calendardata' is optional, if we don't have it
// already we fetch it from the backend.
if (!isset($this->objectData['calendardata'])) {
$this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION);
}
return $this->objectData['calendardata'];
}
/**
* @param resource|string $calendarData
* @return string
* @throws MethodNotAllowed
*/
public function put($calendarData) {
throw new MethodNotAllowed('Creating objects in a cached subscription is not allowed');
}
/**
* @throws MethodNotAllowed
*/
public function delete() {
throw new MethodNotAllowed('Deleting objects in a cached subscription is not allowed');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,429 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Gary Kim <gary@garykim.dev>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use DateTimeImmutable;
use DateTimeInterface;
use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin;
use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\Exception\UnsupportedLimitOnInitialSyncException;
use OCP\DB\Exception;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\IMoveTarget;
use Sabre\DAV\INode;
use Sabre\DAV\PropPatch;
/**
* Class Calendar
*
* @package OCA\DAV\CalDAV
* @property CalDavBackend $caldavBackend
*/
class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget {
private IConfig $config;
protected IL10N $l10n;
private bool $useTrashbin = true;
private LoggerInterface $logger;
public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
// Convert deletion date to ISO8601 string
if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
$calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] = (new DateTimeImmutable())
->setTimestamp($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])
->format(DateTimeInterface::ATOM);
}
parent::__construct($caldavBackend, $calendarInfo);
if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Contact birthdays');
}
if ($this->getName() === CalDavBackend::PERSONAL_CALENDAR_URI &&
$this->calendarInfo['{DAV:}displayname'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
$this->calendarInfo['{DAV:}displayname'] = $l10n->t('Personal');
}
$this->config = $config;
$this->l10n = $l10n;
$this->logger = $logger;
}
/**
* {@inheritdoc}
* @throws Forbidden
*/
public function updateShares(array $add, array $remove): void {
if ($this->isShared()) {
throw new Forbidden();
}
$this->caldavBackend->updateShares($this, $add, $remove);
}
/**
* Returns the list of people whom this resource is shared with.
*
* Every element in this array should have the following properties:
* * href - Often a mailto: address
* * commonName - Optional, for example a first + last name
* * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
* * readOnly - boolean
* * summary - Optional, a description for the share
*
* @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
*/
public function getShares(): array {
if ($this->isShared()) {
return [];
}
return $this->caldavBackend->getShares($this->getResourceId());
}
public function getResourceId(): int {
return $this->calendarInfo['id'];
}
/**
* @return string
*/
public function getPrincipalURI() {
return $this->calendarInfo['principaluri'];
}
/**
* @param int $resourceId
* @param list<array{privilege: string, principal: string, protected: bool}> $acl
* @return list<array{privilege: string, principal: ?string, protected: bool}>
*/
public function getACL() {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
];
if ($this->getName() !== BirthdayService::BIRTHDAY_CALENDAR_URI) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
];
} else {
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner(),
'protected' => true,
];
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
];
}
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
];
if (!$this->isShared()) {
return $acl;
}
if ($this->getOwner() !== parent::getOwner()) {
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => parent::getOwner(),
'protected' => true,
];
if ($this->canWrite()) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => parent::getOwner(),
'protected' => true,
];
} else {
$acl[] = [
'privilege' => '{DAV:}write-properties',
'principal' => parent::getOwner(),
'protected' => true,
];
}
}
if ($this->isPublic()) {
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => 'principals/system/public',
'protected' => true,
];
}
$acl = $this->caldavBackend->applyShareAcl($this->getResourceId(), $acl);
$allowedPrincipals = [
$this->getOwner(),
$this->getOwner(). '/calendar-proxy-read',
$this->getOwner(). '/calendar-proxy-write',
parent::getOwner(),
'principals/system/public'
];
/** @var list<array{privilege: string, principal: string, protected: bool}> $acl */
$acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool {
return \in_array($rule['principal'], $allowedPrincipals, true);
});
return $acl;
}
public function getChildACL() {
return $this->getACL();
}
public function getOwner(): ?string {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
}
return parent::getOwner();
}
public function delete() {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal']) &&
$this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri']) {
$principal = 'principal:' . parent::getOwner();
$shares = $this->caldavBackend->getShares($this->getResourceId());
$shares = array_filter($shares, function ($share) use ($principal) {
return $share['href'] === $principal;
});
if (empty($shares)) {
throw new Forbidden();
}
$this->caldavBackend->updateShares($this, [], [
$principal
]);
return;
}
// Remember when a user deleted their birthday calendar
// in order to not regenerate it on the next contacts change
if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
$principalURI = $this->getPrincipalURI();
$userId = substr($principalURI, 17);
$this->config->setUserValue($userId, 'dav', 'generateBirthdayCalendar', 'no');
}
$this->caldavBackend->deleteCalendar(
$this->calendarInfo['id'],
!$this->useTrashbin
);
}
public function propPatch(PropPatch $propPatch) {
// parent::propPatch will only update calendars table
// if calendar is shared, changes have to be made to the properties table
if (!$this->isShared()) {
parent::propPatch($propPatch);
}
}
public function getChild($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
if (!$obj) {
throw new NotFound('Calendar object not found');
}
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
throw new NotFound('Calendar object not found');
}
$obj['acl'] = $this->getChildACL();
return new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
public function getChildren() {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
$children = [];
foreach ($objs as $obj) {
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
continue;
}
$obj['acl'] = $this->getChildACL();
$children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
return $children;
}
public function getMultipleChildren(array $paths) {
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
$children = [];
foreach ($objs as $obj) {
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
continue;
}
$obj['acl'] = $this->getChildACL();
$children[] = new CalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
return $children;
}
public function childExists($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
if (!$obj) {
return false;
}
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE && $this->isShared()) {
return false;
}
return true;
}
public function calendarQuery(array $filters) {
$uris = $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
if ($this->isShared()) {
return array_filter($uris, function ($uri) {
return $this->childExists($uri);
});
}
return $uris;
}
/**
* @param boolean $value
* @return string|null
*/
public function setPublishStatus($value) {
$publicUri = $this->caldavBackend->setPublishStatus($value, $this);
$this->calendarInfo['publicuri'] = $publicUri;
return $publicUri;
}
/**
* @return mixed $value
*/
public function getPublishStatus() {
return $this->caldavBackend->getPublishStatus($this);
}
public function canWrite() {
if ($this->getName() === BirthdayService::BIRTHDAY_CALENDAR_URI) {
return false;
}
if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
}
return true;
}
private function isPublic() {
return isset($this->calendarInfo['{http://owncloud.org/ns}public']);
}
public function isShared() {
if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
}
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
}
public function isSubscription() {
return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']);
}
public function isDeleted(): bool {
if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) {
return false;
}
return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null;
}
/**
* @inheritDoc
*/
public function getChanges($syncToken, $syncLevel, $limit = null) {
if (!$syncToken && $limit) {
throw new UnsupportedLimitOnInitialSyncException();
}
return parent::getChanges($syncToken, $syncLevel, $limit);
}
/**
* @inheritDoc
*/
public function restore(): void {
$this->caldavBackend->restoreCalendar((int) $this->calendarInfo['id']);
}
public function disableTrashbin(): void {
$this->useTrashbin = false;
}
/**
* @inheritDoc
*/
public function moveInto($targetName, $sourcePath, INode $sourceNode) {
if (!($sourceNode instanceof CalendarObject)) {
return false;
}
try {
return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner());
} catch (Exception $e) {
$this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]);
return false;
}
}
}
@@ -0,0 +1,226 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CalDAV\Trashbin\TrashbinHome;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend\BackendInterface;
use Sabre\CalDAV\Backend\NotificationSupport;
use Sabre\CalDAV\Backend\SchedulingSupport;
use Sabre\CalDAV\Backend\SubscriptionSupport;
use Sabre\CalDAV\Schedule\Inbox;
use Sabre\CalDAV\Subscriptions\Subscription;
use Sabre\DAV\Exception\MethodNotAllowed;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\MkCol;
class CalendarHome extends \Sabre\CalDAV\CalendarHome {
/** @var \OCP\IL10N */
private $l10n;
/** @var \OCP\IConfig */
private $config;
/** @var PluginManager */
private $pluginManager;
/** @var bool */
private $returnCachedSubscriptions = false;
/** @var LoggerInterface */
private $logger;
private ?array $cachedChildren = null;
public function __construct(BackendInterface $caldavBackend, $principalInfo, LoggerInterface $logger) {
parent::__construct($caldavBackend, $principalInfo);
$this->l10n = \OC::$server->getL10N('dav');
$this->config = \OC::$server->getConfig();
$this->pluginManager = new PluginManager(
\OC::$server,
\OC::$server->getAppManager()
);
$this->logger = $logger;
}
/**
* @return BackendInterface
*/
public function getCalDAVBackend() {
return $this->caldavBackend;
}
/**
* @inheritdoc
*/
public function createExtendedCollection($name, MkCol $mkCol): void {
$reservedNames = [
BirthdayService::BIRTHDAY_CALENDAR_URI,
TrashbinHome::NAME,
];
if (\in_array($name, $reservedNames, true) || ExternalCalendar::doesViolateReservedName($name)) {
throw new MethodNotAllowed('The resource you tried to create has a reserved name');
}
parent::createExtendedCollection($name, $mkCol);
}
/**
* @inheritdoc
*/
public function getChildren() {
if ($this->cachedChildren) {
return $this->cachedChildren;
}
$calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
$objects = [];
foreach ($calendars as $calendar) {
$objects[] = new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
if ($this->caldavBackend instanceof SchedulingSupport) {
$objects[] = new Inbox($this->caldavBackend, $this->principalInfo['uri']);
$objects[] = new Outbox($this->config, $this->principalInfo['uri']);
}
// We're adding a notifications node, if it's supported by the backend.
if ($this->caldavBackend instanceof NotificationSupport) {
$objects[] = new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
if ($this->caldavBackend instanceof CalDavBackend) {
$objects[] = new TrashbinHome($this->caldavBackend, $this->principalInfo);
}
// If the backend supports subscriptions, we'll add those as well,
if ($this->caldavBackend instanceof SubscriptionSupport) {
foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
if ($this->returnCachedSubscriptions) {
$objects[] = new CachedSubscription($this->caldavBackend, $subscription);
} else {
$objects[] = new Subscription($this->caldavBackend, $subscription);
}
}
}
foreach ($this->pluginManager->getCalendarPlugins() as $calendarPlugin) {
/** @var ICalendarProvider $calendarPlugin */
$calendars = $calendarPlugin->fetchAllForCalendarHome($this->principalInfo['uri']);
foreach ($calendars as $calendar) {
$objects[] = $calendar;
}
}
$this->cachedChildren = $objects;
return $objects;
}
/**
* @param string $name
*
* @return INode
*/
public function getChild($name) {
// Special nodes
if ($name === 'inbox' && $this->caldavBackend instanceof SchedulingSupport) {
return new Inbox($this->caldavBackend, $this->principalInfo['uri']);
}
if ($name === 'outbox' && $this->caldavBackend instanceof SchedulingSupport) {
return new Outbox($this->config, $this->principalInfo['uri']);
}
if ($name === 'notifications' && $this->caldavBackend instanceof NotificationSupport) {
return new \Sabre\CalDAV\Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
}
if ($name === TrashbinHome::NAME && $this->caldavBackend instanceof CalDavBackend) {
return new TrashbinHome($this->caldavBackend, $this->principalInfo);
}
// Calendar - this covers all "regular" calendars, but not shared
// only check if the method is available
if($this->caldavBackend instanceof CalDavBackend) {
$calendar = $this->caldavBackend->getCalendarByUri($this->principalInfo['uri'], $name);
if(!empty($calendar)) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
}
// Fallback to cover shared calendars
foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
if ($calendar['uri'] === $name) {
return new Calendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
}
if ($this->caldavBackend instanceof SubscriptionSupport) {
foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
if ($subscription['uri'] === $name) {
if ($this->returnCachedSubscriptions) {
return new CachedSubscription($this->caldavBackend, $subscription);
}
return new Subscription($this->caldavBackend, $subscription);
}
}
}
if (ExternalCalendar::isAppGeneratedCalendar($name)) {
[$appId, $calendarUri] = ExternalCalendar::splitAppGeneratedCalendarUri($name);
foreach ($this->pluginManager->getCalendarPlugins() as $calendarPlugin) {
/** @var ICalendarProvider $calendarPlugin */
if ($calendarPlugin->getAppId() !== $appId) {
continue;
}
if ($calendarPlugin->hasCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri)) {
return $calendarPlugin->getCalendarInCalendarHome($this->principalInfo['uri'], $calendarUri);
}
}
}
throw new NotFound('Node with name \'' . $name . '\' could not be found');
}
/**
* @param array $filters
* @param integer|null $limit
* @param integer|null $offset
*/
public function calendarSearch(array $filters, $limit = null, $offset = null) {
$principalUri = $this->principalInfo['uri'];
return $this->caldavBackend->calendarSearch($principalUri, $filters, $limit, $offset);
}
public function enableCachedSubscriptionsForThisRequest() {
$this->returnCachedSubscriptions = true;
}
}
@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
/**
* @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Constants;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\Conflict;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
class CalendarImpl implements ICreateFromString, IHandleImipMessage {
private CalDavBackend $backend;
private Calendar $calendar;
/** @var array<string, mixed> */
private array $calendarInfo;
public function __construct(Calendar $calendar,
array $calendarInfo,
CalDavBackend $backend) {
$this->calendar = $calendar;
$this->calendarInfo = $calendarInfo;
$this->backend = $backend;
}
/**
* @return string defining the technical unique key
* @since 13.0.0
*/
public function getKey(): string {
return (string) $this->calendarInfo['id'];
}
/**
* {@inheritDoc}
*/
public function getUri(): string {
return $this->calendarInfo['uri'];
}
/**
* In comparison to getKey() this function returns a human readable (maybe translated) name
* @since 13.0.0
*/
public function getDisplayName(): ?string {
return $this->calendarInfo['{DAV:}displayname'];
}
/**
* Calendar color
* @since 13.0.0
*/
public function getDisplayColor(): ?string {
return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color'];
}
public function getSchedulingTransparency(): ?ScheduleCalendarTransp {
return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp'];
}
public function getSchedulingTimezone(): ?VTimeZone {
$tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone';
if (!isset($this->calendarInfo[$tzProp])) {
return null;
}
// This property contains a VCALENDAR with a single VTIMEZONE
/** @var string $timezoneProp */
$timezoneProp = $this->calendarInfo[$tzProp];
/** @var VCalendar $vobj */
$vobj = Reader::read($timezoneProp);
$components = $vobj->getComponents();
if(empty($components)) {
return null;
}
/** @var VTimeZone $vtimezone */
$vtimezone = $components[0];
return $vtimezone;
}
/**
* @param string $pattern which should match within the $searchProperties
* @param array $searchProperties defines the properties within the query pattern should match
* @param array $options - optional parameters:
* ['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
* @param int|null $limit - limit number of search results
* @param int|null $offset - offset for paging of search results
* @return array an array of events/journals/todos which are arrays of key-value-pairs
* @since 13.0.0
*/
public function search(string $pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null): array {
return $this->backend->search($this->calendarInfo, $pattern,
$searchProperties, $options, $limit, $offset);
}
/**
* @return int build up using \OCP\Constants
* @since 13.0.0
*/
public function getPermissions(): int {
$permissions = $this->calendar->getACL();
$result = 0;
foreach ($permissions as $permission) {
switch ($permission['privilege']) {
case '{DAV:}read':
$result |= Constants::PERMISSION_READ;
break;
case '{DAV:}write':
$result |= Constants::PERMISSION_CREATE;
$result |= Constants::PERMISSION_UPDATE;
break;
case '{DAV:}all':
$result |= Constants::PERMISSION_ALL;
break;
}
}
return $result;
}
/**
* @since 26.0.0
*/
public function isDeleted(): bool {
return $this->calendar->isDeleted();
}
/**
* Create a new calendar event for this calendar
* by way of an ICS string
*
* @param string $name the file name - needs to contain the .ics ending
* @param string $calendarData a string containing a valid VEVENT ics
*
* @throws CalendarException
*/
public function createFromString(string $name, string $calendarData): void {
$server = new InvitationResponseServer(false);
/** @var CustomPrincipalPlugin $plugin */
$plugin = $server->getServer()->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
$plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
if (empty($this->calendarInfo['uri'])) {
throw new CalendarException('Could not write to calendar as URI parameter is missing');
}
// Build full calendar path
[, $user] = uriSplit($this->calendar->getPrincipalURI());
$fullCalendarFilename = sprintf('calendars/%s/%s/%s', $user, $this->calendarInfo['uri'], $name);
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
$schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
$schedulingPlugin->setPathOfCalendarObjectChange($fullCalendarFilename);
$stream = fopen('php://memory', 'rb+');
fwrite($stream, $calendarData);
rewind($stream);
try {
$server->getServer()->createFile($fullCalendarFilename, $stream);
} catch (Conflict $e) {
throw new CalendarException('Could not create new calendar event: ' . $e->getMessage(), 0, $e);
} finally {
fclose($stream);
}
}
/**
* @throws CalendarException
*/
public function handleIMipMessage(string $name, string $calendarData): void {
$server = $this->getInvitationResponseServer();
/** @var CustomPrincipalPlugin $plugin */
$plugin = $server->getServer()->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
$plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
if (empty($this->calendarInfo['uri'])) {
throw new CalendarException('Could not write to calendar as URI parameter is missing');
}
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
$schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
// Let sabre handle the rest
$iTipMessage = new Message();
/** @var VCalendar $vObject */
$vObject = Reader::read($calendarData);
/** @var VEvent $vEvent */
$vEvent = $vObject->{'VEVENT'};
if ($vObject->{'METHOD'} === null) {
throw new CalendarException('No Method provided for scheduling data. Could not process message');
}
if (!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
}
$organizer = $vEvent->{'ORGANIZER'}->getValue();
$attendee = $vEvent->{'ATTENDEE'}->getValue();
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
if ($iTipMessage->method === 'REPLY') {
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
$iTipMessage->recipient = $organizer;
} else {
$iTipMessage->recipient = $attendee;
}
$iTipMessage->sender = $attendee;
} elseif ($iTipMessage->method === 'CANCEL') {
$iTipMessage->recipient = $attendee;
$iTipMessage->sender = $organizer;
}
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
$iTipMessage->component = 'VEVENT';
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
$iTipMessage->message = $vObject;
$server->server->emit('schedule', [$iTipMessage]);
}
public function getInvitationResponseServer(): InvitationResponseServer {
return new InvitationResponseServer(false);
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright 2017, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Morris Jobke <hey@morrisjobke.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCP\Calendar\IManager;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
class CalendarManager {
/** @var CalDavBackend */
private $backend;
/** @var IL10N */
private $l10n;
/** @var IConfig */
private $config;
/** @var LoggerInterface */
private $logger;
/**
* CalendarManager constructor.
*
* @param CalDavBackend $backend
* @param IL10N $l10n
* @param IConfig $config
*/
public function __construct(CalDavBackend $backend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
$this->backend = $backend;
$this->l10n = $l10n;
$this->config = $config;
$this->logger = $logger;
}
/**
* @param IManager $cm
* @param string $userId
*/
public function setupCalendarProvider(IManager $cm, $userId) {
$calendars = $this->backend->getCalendarsForUser("principals/users/$userId");
$this->register($cm, $calendars);
}
/**
* @param IManager $cm
* @param array $calendars
*/
private function register(IManager $cm, array $calendars) {
foreach ($calendars as $calendarInfo) {
$calendar = new Calendar($this->backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$cm->registerCalendar(new CalendarImpl(
$calendar,
$calendarInfo,
$this->backend
));
}
}
}
@@ -0,0 +1,172 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2017, Georg Ehrke
* @copyright Copyright (c) 2020, Gary Kim <gary@garykim.dev>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Gary Kim <gary@garykim.dev>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use OCP\IL10N;
use Sabre\VObject\Component;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
class CalendarObject extends \Sabre\CalDAV\CalendarObject {
/** @var IL10N */
protected $l10n;
/**
* CalendarObject constructor.
*
* @param CalDavBackend $caldavBackend
* @param IL10N $l10n
* @param array $calendarInfo
* @param array $objectData
*/
public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
array $calendarInfo,
array $objectData) {
parent::__construct($caldavBackend, $calendarInfo, $objectData);
if ($this->isShared()) {
unset($this->objectData['size']);
}
$this->l10n = $l10n;
}
/**
* @inheritdoc
*/
public function get() {
$data = parent::get();
if (!$this->isShared()) {
return $data;
}
$vObject = Reader::read($data);
// remove VAlarms if calendar is shared read-only
if (!$this->canWrite()) {
$this->removeVAlarms($vObject);
}
// shows as busy if event is declared confidential
if ($this->objectData['classification'] === CalDavBackend::CLASSIFICATION_CONFIDENTIAL) {
$this->createConfidentialObject($vObject);
}
return $vObject->serialize();
}
public function getId(): int {
return (int) $this->objectData['id'];
}
protected function isShared() {
if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return false;
}
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'] !== $this->calendarInfo['principaluri'];
}
/**
* @param Component\VCalendar $vObject
* @return void
*/
private function createConfidentialObject(Component\VCalendar $vObject) {
/** @var Component $vElement */
$vElement = null;
if (isset($vObject->VEVENT)) {
$vElement = $vObject->VEVENT;
}
if (isset($vObject->VJOURNAL)) {
$vElement = $vObject->VJOURNAL;
}
if (isset($vObject->VTODO)) {
$vElement = $vObject->VTODO;
}
if (!is_null($vElement)) {
foreach ($vElement->children() as &$property) {
/** @var Property $property */
switch ($property->name) {
case 'CREATED':
case 'DTSTART':
case 'RRULE':
case 'DURATION':
case 'DTEND':
case 'CLASS':
case 'UID':
break;
case 'SUMMARY':
$property->setValue($this->l10n->t('Busy'));
break;
default:
$vElement->__unset($property->name);
unset($property);
break;
}
}
}
}
/**
* @param Component\VCalendar $vObject
* @return void
*/
private function removeVAlarms(Component\VCalendar $vObject) {
$subcomponents = $vObject->getComponents();
foreach ($subcomponents as $subcomponent) {
unset($subcomponent->VALARM);
}
}
/**
* @return bool
*/
private function canWrite() {
if (isset($this->calendarInfo['{http://owncloud.org/ns}read-only'])) {
return !$this->calendarInfo['{http://owncloud.org/ns}read-only'];
}
return true;
}
public function getCalendarId(): int {
return (int)$this->objectData['calendarid'];
}
public function getPrincipalUri(): string {
return $this->calendarInfo['principaluri'];
}
public function getOwner(): ?string {
if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) {
return $this->calendarInfo['{http://owncloud.org/ns}owner-principal'];
}
return parent::getOwner();
}
}
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCP\Calendar\ICalendarProvider;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
class CalendarProvider implements ICalendarProvider {
/** @var CalDavBackend */
private $calDavBackend;
/** @var IL10N */
private $l10n;
/** @var IConfig */
private $config;
/** @var LoggerInterface */
private $logger;
public function __construct(CalDavBackend $calDavBackend, IL10N $l10n, IConfig $config, LoggerInterface $logger) {
$this->calDavBackend = $calDavBackend;
$this->l10n = $l10n;
$this->config = $config;
$this->logger = $logger;
}
public function getCalendars(string $principalUri, array $calendarUris = []): array {
$calendarInfos = [];
if (empty($calendarUris)) {
$calendarInfos = $this->calDavBackend->getCalendarsForUser($principalUri);
} else {
foreach ($calendarUris as $calendarUri) {
$calendarInfos[] = $this->calDavBackend->getCalendarByUri($principalUri, $calendarUri);
}
}
$calendarInfos = array_filter($calendarInfos);
$iCalendars = [];
foreach ($calendarInfos as $calendarInfo) {
$calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$iCalendars[] = new CalendarImpl(
$calendar,
$calendarInfo,
$this->calDavBackend,
);
}
return $iCalendars;
}
}
@@ -0,0 +1,59 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Backend;
use Sabre\DAVACL\PrincipalBackend;
class CalendarRoot extends \Sabre\CalDAV\CalendarRoot {
private LoggerInterface $logger;
public function __construct(
PrincipalBackend\BackendInterface $principalBackend,
Backend\BackendInterface $caldavBackend,
$principalPrefix,
LoggerInterface $logger
) {
parent::__construct($principalBackend, $caldavBackend, $principalPrefix);
$this->logger = $logger;
}
public function getChildForPrincipal(array $principal) {
return new CalendarHome($this->caldavBackend, $principal, $this->logger);
}
public function getName() {
if ($this->principalPrefix === 'principals/calendar-resources' ||
$this->principalPrefix === 'principals/calendar-rooms') {
$parts = explode('/', $this->principalPrefix);
return $parts[1];
}
return parent::getName();
}
}
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Anna Larch <anna.larch@gmx.net>
*
* @author 2022 Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\CalDAV\Schedule\IMipService;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
class EventComparisonService {
/** @var string[] */
private const EVENT_DIFF = [
'RECURRENCE-ID',
'RRULE',
'SEQUENCE',
'LAST-MODIFIED'
];
/**
* If found, remove the event from $eventsToFilter that
* is identical to the passed $filterEvent
* and return whether an identical event was found
*
* This function takes into account the SEQUENCE,
* RRULE, RECURRENCE-ID and LAST-MODIFIED parameters
*
* @param VEvent $filterEvent
* @param array $eventsToFilter
* @return bool true if there was an identical event found and removed, false if there wasn't
*/
private function removeIfUnchanged(VEvent $filterEvent, array &$eventsToFilter): bool {
$filterEventData = [];
foreach(self::EVENT_DIFF as $eventDiff) {
$filterEventData[] = IMipService::readPropertyWithDefault($filterEvent, $eventDiff, '');
}
/** @var VEvent $component */
foreach ($eventsToFilter as $k => $eventToFilter) {
$eventToFilterData = [];
foreach(self::EVENT_DIFF as $eventDiff) {
$eventToFilterData[] = IMipService::readPropertyWithDefault($eventToFilter, $eventDiff, '');
}
// events are identical and can be removed
if (empty(array_diff($filterEventData, $eventToFilterData))) {
unset($eventsToFilter[$k]);
return true;
}
}
return false;
}
/**
* Compare two VCalendars with each other and find all changed elements
*
* Returns an array of old and new events
*
* Old events are only detected if they are also changed
* If there is no corresponding old event for a VEvent, it
* has been newly created
*
* @param VCalendar $new
* @param VCalendar|null $old
* @return array<string, VEvent[]|null>
*/
public function findModified(VCalendar $new, ?VCalendar $old): array {
$newEventComponents = $new->getComponents();
foreach ($newEventComponents as $k => $event) {
if(!$event instanceof VEvent) {
unset($newEventComponents[$k]);
}
}
if(empty($old)) {
return ['old' => null, 'new' => $newEventComponents];
}
$oldEventComponents = $old->getComponents();
if(is_array($oldEventComponents) && !empty($oldEventComponents)) {
foreach ($oldEventComponents as $k => $event) {
if(!$event instanceof VEvent) {
unset($oldEventComponents[$k]);
continue;
}
if($this->removeIfUnchanged($event, $newEventComponents)) {
unset($oldEventComponents[$k]);
}
}
}
return ['old' => array_values($oldEventComponents), 'new' => array_values($newEventComponents)];
}
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\FreeBusy;
use Sabre\VObject\Component\VCalendar;
/**
* @psalm-suppress PropertyNotSetInConstructor
*/
class FreeBusyGenerator extends \Sabre\VObject\FreeBusyGenerator {
public function __construct() {
parent::__construct();
}
public function getVCalendar(): VCalendar {
return new VCalendar();
}
}
@@ -0,0 +1,90 @@
<?php
/**
* @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\ICSExportPlugin;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\Property\ICalendar\Duration;
/**
* Class ICSExportPlugin
*
* @package OCA\DAV\CalDAV\ICSExportPlugin
*/
class ICSExportPlugin extends \Sabre\CalDAV\ICSExportPlugin {
private IConfig $config;
private LoggerInterface $logger;
/** @var string */
private const DEFAULT_REFRESH_INTERVAL = 'PT4H';
/**
* ICSExportPlugin constructor.
*/
public function __construct(IConfig $config, LoggerInterface $logger) {
$this->config = $config;
$this->logger = $logger;
}
/**
* @inheritDoc
*/
protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
if (!isset($properties['{http://nextcloud.com/ns}refresh-interval'])) {
$value = $this->config->getAppValue('dav', 'defaultRefreshIntervalExportedCalendars', self::DEFAULT_REFRESH_INTERVAL);
$properties['{http://nextcloud.com/ns}refresh-interval'] = $value;
}
return parent::generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
}
/**
* @inheritDoc
*/
public function mergeObjects(array $properties, array $inputObjects) {
$vcalendar = parent::mergeObjects($properties, $inputObjects);
if (isset($properties['{http://nextcloud.com/ns}refresh-interval'])) {
$refreshIntervalValue = $properties['{http://nextcloud.com/ns}refresh-interval'];
try {
DateTimeParser::parseDuration($refreshIntervalValue);
} catch (InvalidDataException $ex) {
$this->logger->debug('Invalid refresh interval for exported calendar, falling back to default value ...');
$refreshIntervalValue = self::DEFAULT_REFRESH_INTERVAL;
}
// https://tools.ietf.org/html/rfc7986#section-5.7
$refreshInterval = new Duration($vcalendar, 'REFRESH-INTERVAL', $refreshIntervalValue);
$refreshInterval->add('VALUE', 'DURATION');
$vcalendar->add($refreshInterval);
// Legacy property for compatibility
$vcalendar->{'X-PUBLISHED-TTL'} = $refreshIntervalValue;
}
return $vcalendar;
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use Sabre\DAV\Exception;
/**
* Interface for nodes that can be restored from the trashbin
*/
interface IRestorable {
/**
* Restore this node
*
* @throws Exception
*/
public function restore(): void;
}
@@ -0,0 +1,131 @@
<?php
/**
* @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Integration;
use Sabre\CalDAV;
use Sabre\DAV;
/**
* Class ExternalCalendar
*
* @package OCA\DAV\CalDAV\Integration
* @since 19.0.0
*/
abstract class ExternalCalendar implements CalDAV\ICalendar, DAV\IProperties {
/** @var string */
private const PREFIX = 'app-generated';
/**
* @var string
*
* Double dash is a valid delimiter,
* because it will always split the calendarURIs correctly:
* - our prefix contains only one dash and won't be split
* - appIds are not allowed to contain dashes as per spec:
* > must contain only lowercase ASCII characters and underscore
* - explode has a limit of three, so even if the app-generated
* calendar uri has double dashes, it won't be split
*/
private const DELIMITER = '--';
/** @var string */
private $appId;
/** @var string */
private $calendarUri;
/**
* ExternalCalendar constructor.
*
* @param string $appId
* @param string $calendarUri
*/
public function __construct(string $appId, string $calendarUri) {
$this->appId = $appId;
$this->calendarUri = $calendarUri;
}
/**
* @inheritDoc
*/
final public function getName() {
return implode(self::DELIMITER, [
self::PREFIX,
$this->appId,
$this->calendarUri,
]);
}
/**
* @inheritDoc
*/
final public function setName($name) {
throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
}
/**
* @inheritDoc
*/
final public function createDirectory($name) {
throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
}
/**
* Checks whether the calendar uri is app-generated
*
* @param string $calendarUri
* @return bool
*/
public static function isAppGeneratedCalendar(string $calendarUri):bool {
return str_starts_with($calendarUri, self::PREFIX) && substr_count($calendarUri, self::DELIMITER) >= 2;
}
/**
* Splits an app-generated calendar-uri into appId and calendarUri
*
* @param string $calendarUri
* @return array
*/
public static function splitAppGeneratedCalendarUri(string $calendarUri):array {
$array = array_slice(explode(self::DELIMITER, $calendarUri, 3), 1);
// Check the array has expected amount of elements
// and none of them is an empty string
if (\count($array) !== 2 || \in_array('', $array, true)) {
throw new \InvalidArgumentException('Provided calendar uri was not app-generated');
}
return $array;
}
/**
* Checks whether a calendar-name, the user wants to create, violates
* the reserved name for calendar uris
*
* @param string $calendarUri
* @return bool
*/
public static function doesViolateReservedName(string $calendarUri):bool {
return str_starts_with($calendarUri, self::PREFIX);
}
}
@@ -0,0 +1,70 @@
<?php
/**
* @copyright 2020, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Integration;
/**
* Interface ICalendarProvider
*
* @package OCA\DAV\CalDAV\Integration
* @since 19.0.0
*/
interface ICalendarProvider {
/**
* Provides the appId of the plugin
*
* @since 19.0.0
* @return string AppId
*/
public function getAppId(): string;
/**
* Fetches all calendars for a given principal uri
*
* @since 19.0.0
* @param string $principalUri E.g. principals/users/user1
* @return ExternalCalendar[] Array of all calendars
*/
public function fetchAllForCalendarHome(string $principalUri): array;
/**
* Checks whether plugin has a calendar for a given principalUri and calendarUri
*
* @since 19.0.0
* @param string $principalUri E.g. principals/users/user1
* @param string $calendarUri E.g. personal
* @return bool True if calendar for principalUri and calendarUri exists, false otherwise
*/
public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool;
/**
* Fetches a calendar for a given principalUri and calendarUri
* Returns null if calendar does not exist
*
* @since 19.0.0
* @param string $principalUri E.g. principals/users/user1
* @param string $calendarUri E.g. personal
* @return ExternalCalendar|null Calendar if it exists, null otherwise
*/
public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar;
}
@@ -0,0 +1,136 @@
<?php
/**
* @copyright Copyright (c) 2018, Georg Ehrke.
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\InvitationResponse;
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin;
use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin;
use OCA\DAV\Connector\Sabre\CachingTree;
use OCA\DAV\Connector\Sabre\DavAclPlugin;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\DAV\RootCollection;
use OCP\EventDispatcher\IEventDispatcher;
use Psr\Log\LoggerInterface;
use Sabre\VObject\ITip\Message;
class InvitationResponseServer {
/** @var \OCA\DAV\Connector\Sabre\Server */
public $server;
/**
* InvitationResponseServer constructor.
*/
public function __construct(bool $public = true) {
$baseUri = \OC::$WEBROOT . '/remote.php/dav/';
$logger = \OC::$server->get(LoggerInterface::class);
/** @var IEventDispatcher $dispatcher */
$dispatcher = \OC::$server->query(IEventDispatcher::class);
$root = new RootCollection();
$this->server = new \OCA\DAV\Connector\Sabre\Server(new CachingTree($root));
// Add maintenance plugin
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\MaintenancePlugin(\OC::$server->getConfig(), \OC::$server->getL10N('dav')));
// Set URL explicitly due to reverse-proxy situations
$this->server->httpRequest->setUrl($baseUri);
$this->server->setBaseUri($baseUri);
$this->server->addPlugin(new BlockLegacyClientPlugin(\OC::$server->getConfig()));
$this->server->addPlugin(new AnonymousOptionsPlugin());
// allow custom principal uri option
if ($public) {
$this->server->addPlugin(new PublicPrincipalPlugin());
} else {
$this->server->addPlugin(new CustomPrincipalPlugin());
}
// allow setup of additional auth backends
$event = new SabrePluginAuthInitEvent($this->server);
$dispatcher->dispatchTyped($event);
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin('webdav', $logger));
$this->server->addPlugin(new \OCA\DAV\Connector\Sabre\LockPlugin());
$this->server->addPlugin(new \Sabre\DAV\Sync\Plugin());
// acl
$acl = new DavAclPlugin();
$acl->principalCollectionSet = [
'principals/users', 'principals/groups'
];
$this->server->addPlugin($acl);
// calendar plugins
$this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin());
$this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class)));
$this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin());
$this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin());
//$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest()));
$this->server->addPlugin(new \OCA\DAV\CalDAV\Publishing\PublishPlugin(
\OC::$server->getConfig(),
\OC::$server->getURLGenerator()
));
// wait with registering these until auth is handled and the filesystem is setup
$this->server->on('beforeMethod:*', function () use ($root): void {
// register plugins from apps
$pluginManager = new PluginManager(
\OC::$server,
\OC::$server->getAppManager()
);
foreach ($pluginManager->getAppPlugins() as $appPlugin) {
$this->server->addPlugin($appPlugin);
}
foreach ($pluginManager->getAppCollections() as $appCollection) {
$root->addChild($appCollection);
}
});
}
/**
* @param Message $iTipMessage
* @return void
*/
public function handleITipMessage(Message $iTipMessage) {
/** @var \OCA\DAV\CalDAV\Schedule\Plugin $schedulingPlugin */
$schedulingPlugin = $this->server->getPlugin('caldav-schedule');
$schedulingPlugin->scheduleLocalDelivery($iTipMessage);
}
public function isExternalAttendee(string $principalUri): bool {
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
$aclPlugin = $this->getServer()->getPlugin('acl');
return $aclPlugin->getPrincipalByUri($principalUri) === null;
}
public function getServer(): \OCA\DAV\Connector\Sabre\Server {
return $this->server;
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
/**
* @copyright Copyright (c) 2018, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCP\IConfig;
use Sabre\CalDAV\Plugin as CalDAVPlugin;
/**
* Class Outbox
*
* @package OCA\DAV\CalDAV
*/
class Outbox extends \Sabre\CalDAV\Schedule\Outbox {
/** @var IConfig */
private $config;
/** @var null|bool */
private $disableFreeBusy = null;
/**
* Outbox constructor.
*
* @param IConfig $config
* @param string $principalUri
*/
public function __construct(IConfig $config, string $principalUri) {
parent::__construct($principalUri);
$this->config = $config;
}
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL() {
// getACL is called so frequently that we cache the config result
if ($this->disableFreeBusy === null) {
$this->disableFreeBusy = ($this->config->getAppValue('dav', 'disableFreeBusy', 'no') === 'yes');
}
$commonAcl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-read',
'protected' => true,
],
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
];
// schedule-send is an aggregate privilege for:
// - schedule-send-invite
// - schedule-send-reply
// - schedule-send-freebusy
//
// If FreeBusy is disabled, we have to remove the latter privilege
if ($this->disableFreeBusy) {
return array_merge($commonAcl, [
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send-invite',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send-invite',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send-reply',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send-reply',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
]);
}
return array_merge($commonAcl, [
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{' . CalDAVPlugin::NS_CALDAV . '}schedule-send',
'principal' => $this->getOwner() . '/calendar-proxy-write',
'protected' => true,
],
]);
}
}
@@ -0,0 +1,53 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud GmbH.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
class Plugin extends \Sabre\CalDAV\Plugin {
public const SYSTEM_CALENDAR_ROOT = 'system-calendars';
/**
* Returns the path to a principal's calendar home.
*
* The return url must not end with a slash.
* This function should return null in case a principal did not have
* a calendar home.
*
* @param string $principalUrl
* @return string|null
*/
public function getCalendarHomeForPrincipal($principalUrl) {
if (strrpos($principalUrl, 'principals/users', -strlen($principalUrl)) !== false) {
[, $principalId] = \Sabre\Uri\split($principalUrl);
return self::CALENDAR_ROOT . '/' . $principalId;
}
if (strrpos($principalUrl, 'principals/calendar-resources', -strlen($principalUrl)) !== false) {
[, $principalId] = \Sabre\Uri\split($principalUrl);
return self::SYSTEM_CALENDAR_ROOT . '/calendar-resources/' . $principalId;
}
if (strrpos($principalUrl, 'principals/calendar-rooms', -strlen($principalUrl)) !== false) {
[, $principalId] = \Sabre\Uri\split($principalUrl);
return self::SYSTEM_CALENDAR_ROOT . '/calendar-rooms/' . $principalId;
}
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
*
* @author Christoph Seitz <christoph.seitz@posteo.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Principal;
/**
* Class Collection
*
* @package OCA\DAV\CalDAV\Principal
*/
class Collection extends \Sabre\CalDAV\Principal\Collection {
/**
* Returns a child object based on principal information
*
* @param array $principalInfo
* @return User
*/
public function getChildForPrincipal(array $principalInfo) {
return new User($this->principalBackend, $principalInfo);
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @copyright Copyright (c) 2017, Christoph Seitz <christoph.seitz@posteo.de>
*
* @author Christoph Seitz <christoph.seitz@posteo.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Principal;
/**
* Class User
*
* @package OCA\DAV\CalDAV\Principal
*/
class User extends \Sabre\CalDAV\Principal\User {
/**
* Returns a list of ACE's for this node.
*
* Each ACE has the following properties:
* * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
* currently the only supported privileges
* * 'principal', a url to the principal who owns the node
* * 'protected' (optional), indicating that this ACE is not allowed to
* be updated.
*
* @return array
*/
public function getACL() {
$acl = parent::getACL();
$acl[] = [
'privilege' => '{DAV:}read',
'principal' => '{DAV:}authenticated',
'protected' => true,
];
return $acl;
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Proxy;
use OCP\AppFramework\Db\Entity;
/**
* @method string getOwnerId()
* @method void setOwnerId(string $ownerId)
* @method string getProxyId()
* @method void setProxyId(string $proxyId)
* @method int getPermissions()
* @method void setPermissions(int $permissions)
*/
class Proxy extends Entity {
/** @var string */
protected $ownerId;
/** @var string */
protected $proxyId;
/** @var int */
protected $permissions;
public function __construct() {
$this->addType('ownerId', 'string');
$this->addType('proxyId', 'string');
$this->addType('permissions', 'int');
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Proxy;
use OCP\AppFramework\Db\QBMapper;
use OCP\IDBConnection;
/**
* Class ProxyMapper
*
* @package OCA\DAV\CalDAV\Proxy
*
* @template-extends QBMapper<Proxy>
*/
class ProxyMapper extends QBMapper {
public const PERMISSION_READ = 1;
public const PERMISSION_WRITE = 2;
/**
* ProxyMapper constructor.
*
* @param IDBConnection $db
*/
public function __construct(IDBConnection $db) {
parent::__construct($db, 'dav_cal_proxy', Proxy::class);
}
/**
* @param string $proxyId The principal uri that can act as a proxy for the resulting calendars
*
* @return Proxy[]
*/
public function getProxiesFor(string $proxyId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('proxy_id', $qb->createNamedParameter($proxyId)));
return $this->findEntities($qb);
}
/**
* @param string $ownerId The principal uri that has the resulting proxies for their calendars
*
* @return Proxy[]
*/
public function getProxiesOf(string $ownerId): array {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('owner_id', $qb->createNamedParameter($ownerId)));
return $this->findEntities($qb);
}
}
@@ -0,0 +1,90 @@
<?php
/**
* @copyright Copyright (c) 2017, Georg Ehrke
*
* @author Gary Kim <gary@garykim.dev>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use Sabre\DAV\Exception\NotFound;
class PublicCalendar extends Calendar {
/**
* @param string $name
* @throws NotFound
* @return PublicCalendarObject
*/
public function getChild($name) {
$obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
if (!$obj) {
throw new NotFound('Calendar object not found');
}
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE) {
throw new NotFound('Calendar object not found');
}
$obj['acl'] = $this->getChildACL();
return new PublicCalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
/**
* @return PublicCalendarObject[]
*/
public function getChildren() {
$objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
$children = [];
foreach ($objs as $obj) {
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE) {
continue;
}
$obj['acl'] = $this->getChildACL();
$children[] = new PublicCalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
return $children;
}
/**
* @param string[] $paths
* @return PublicCalendarObject[]
*/
public function getMultipleChildren(array $paths) {
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
$children = [];
foreach ($objs as $obj) {
if ($obj['classification'] === CalDavBackend::CLASSIFICATION_PRIVATE) {
continue;
}
$obj['acl'] = $this->getChildACL();
$children[] = new PublicCalendarObject($this->caldavBackend, $this->l10n, $this->calendarInfo, $obj);
}
return $children;
}
/**
* public calendars are always shared
* @return bool
*/
public function isShared() {
return true;
}
}
@@ -0,0 +1,35 @@
<?php
/**
* @copyright Copyright (c) 2017, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
class PublicCalendarObject extends CalendarObject {
/**
* public calendars are always shared
* @return bool
*/
protected function isShared() {
return true;
}
}
@@ -0,0 +1,83 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Collection;
class PublicCalendarRoot extends Collection {
/** @var CalDavBackend */
protected $caldavBackend;
/** @var \OCP\IL10N */
protected $l10n;
/** @var \OCP\IConfig */
protected $config;
/** @var LoggerInterface */
private $logger;
/**
* PublicCalendarRoot constructor.
*
* @param CalDavBackend $caldavBackend
* @param IL10N $l10n
* @param IConfig $config
*/
public function __construct(CalDavBackend $caldavBackend, IL10N $l10n,
IConfig $config, LoggerInterface $logger) {
$this->caldavBackend = $caldavBackend;
$this->l10n = $l10n;
$this->config = $config;
$this->logger = $logger;
}
/**
* @inheritdoc
*/
public function getName() {
return 'public-calendars';
}
/**
* @inheritdoc
*/
public function getChild($name) {
$calendar = $this->caldavBackend->getPublicCalendar($name);
return new PublicCalendar($this->caldavBackend, $calendar, $this->l10n, $this->config, $this->logger);
}
/**
* @inheritdoc
*/
public function getChildren() {
return [];
}
}
@@ -0,0 +1,256 @@
<?php
/**
* @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Publishing;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\Publishing\Xml\Publisher;
use OCP\IConfig;
use OCP\IURLGenerator;
use Sabre\CalDAV\Xml\Property\AllowedSharingModes;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
class PublishPlugin extends ServerPlugin {
public const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
/**
* Reference to SabreDAV server object.
*
* @var \Sabre\DAV\Server
*/
protected $server;
/**
* Config instance to get instance secret.
*
* @var IConfig
*/
protected $config;
/**
* URL Generator for absolute URLs.
*
* @var IURLGenerator
*/
protected $urlGenerator;
/**
* PublishPlugin constructor.
*
* @param IConfig $config
* @param IURLGenerator $urlGenerator
*/
public function __construct(IConfig $config, IURLGenerator $urlGenerator) {
$this->config = $config;
$this->urlGenerator = $urlGenerator;
}
/**
* This method should return a list of server-features.
*
* This is for example 'versioning' and is added to the DAV: header
* in an OPTIONS response.
*
* @return string[]
*/
public function getFeatures() {
// May have to be changed to be detected
return ['oc-calendar-publishing', 'calendarserver-sharing'];
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using Sabre\DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName() {
return 'oc-calendar-publishing';
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param Server $server
*/
public function initialize(Server $server) {
$this->server = $server;
$this->server->on('method:POST', [$this, 'httpPost']);
$this->server->on('propFind', [$this, 'propFind']);
}
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof Calendar) {
$propFind->handle('{'.self::NS_CALENDARSERVER.'}publish-url', function () use ($node) {
if ($node->getPublishStatus()) {
// We return the publish-url only if the calendar is published.
$token = $node->getPublishStatus();
$publishUrl = $this->urlGenerator->getAbsoluteURL($this->server->getBaseUri().'public-calendars/').$token;
return new Publisher($publishUrl, true);
}
});
$propFind->handle('{'.self::NS_CALENDARSERVER.'}allowed-sharing-modes', function () use ($node) {
$canShare = (!$node->isSubscription() && $node->canWrite());
$canPublish = (!$node->isSubscription() && $node->canWrite());
if ($this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes') {
$canShare = $canShare && ($node->getOwner() === $node->getPrincipalURI());
$canPublish = $canPublish && ($node->getOwner() === $node->getPrincipalURI());
}
return new AllowedSharingModes($canShare, $canPublish);
});
}
}
/**
* We intercept this to handle POST requests on calendars.
*
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return void|bool
*/
public function httpPost(RequestInterface $request, ResponseInterface $response) {
$path = $request->getPath();
// Only handling xml
$contentType = (string) $request->getHeader('Content-Type');
if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
return;
}
// Making sure the node exists
try {
$node = $this->server->tree->getNodeForPath($path);
} catch (NotFound $e) {
return;
}
$requestBody = $request->getBodyAsString();
// If this request handler could not deal with this POST request, it
// will return 'null' and other plugins get a chance to handle the
// request.
//
// However, we already requested the full body. This is a problem,
// because a body can only be read once. This is why we preemptively
// re-populated the request body with the existing data.
$request->setBody($requestBody);
$this->server->xml->parse($requestBody, $request->getUrl(), $documentType);
switch ($documentType) {
case '{'.self::NS_CALENDARSERVER.'}publish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-publish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
/** @var \Sabre\DAVACL\Plugin $acl */
$acl->checkPrivileges($path, '{DAV:}write');
$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
if ($limitSharingToOwner && !$isOwner) {
return;
}
}
$node->setPublishStatus(true);
// iCloud sends back the 202, so we will too.
$response->setStatus(202);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
// Breaking the event chain
return false;
case '{'.self::NS_CALENDARSERVER.'}unpublish-calendar':
// We can only deal with IShareableCalendar objects
if (!$node instanceof Calendar) {
return;
}
$this->server->transactionType = 'post-unpublish-calendar';
// Getting ACL info
$acl = $this->server->getPlugin('acl');
// If there's no ACL support, we allow everything
if ($acl) {
/** @var \Sabre\DAVACL\Plugin $acl */
$acl->checkPrivileges($path, '{DAV:}write');
$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
if ($limitSharingToOwner && !$isOwner) {
return;
}
}
$node->setPublishStatus(false);
$response->setStatus(200);
// Adding this because sending a response body may cause issues,
// and I wanted some type of indicator the response was handled.
$response->setHeader('X-Sabre-Status', 'everything-went-well');
// Breaking the event chain
return false;
}
}
}
@@ -0,0 +1,85 @@
<?php
/**
* @copyright Copyright (c) 2016 Thomas Citharel <tcit@tcit.fr>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Publishing\Xml;
use Sabre\Xml\Writer;
use Sabre\Xml\XmlSerializable;
class Publisher implements XmlSerializable {
/**
* @var string $publishUrl
*/
protected $publishUrl;
/**
* @var boolean $isPublished
*/
protected $isPublished;
/**
* @param string $publishUrl
* @param boolean $isPublished
*/
public function __construct($publishUrl, $isPublished) {
$this->publishUrl = $publishUrl;
$this->isPublished = $isPublished;
}
/**
* @return string
*/
public function getValue() {
return $this->publishUrl;
}
/**
* The xmlSerialize method is called during xml writing.
*
* Use the $writer argument to write its own xml serialization.
*
* An important note: do _not_ create a parent element. Any element
* implementing XmlSerializble should only ever write what's considered
* its 'inner xml'.
*
* The parent of the current element is responsible for writing a
* containing element.
*
* This allows serializers to be re-used for different element names.
*
* If you are opening new elements, you must also close them again.
*
* @param Writer $writer
* @return void
*/
public function xmlSerialize(Writer $writer) {
if (!$this->isPublished) {
// for pre-publish-url
$writer->write($this->publishUrl);
} else {
// for publish-url
$writer->writeElement('{DAV:}href', $this->publishUrl);
}
}
}
@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
/**
* Class Backend
*
* @package OCA\DAV\CalDAV\Reminder
*/
class Backend {
/** @var IDBConnection */
protected $db;
/** @var ITimeFactory */
private $timeFactory;
/**
* Backend constructor.
*
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
*/
public function __construct(IDBConnection $db,
ITimeFactory $timeFactory) {
$this->db = $db;
$this->timeFactory = $timeFactory;
}
/**
* Get all reminders with a notification date before now
*
* @return array
* @throws \Exception
*/
public function getRemindersToProcess():array {
$query = $this->db->getQueryBuilder();
$query->select(['cr.*', 'co.calendardata', 'c.displayname', 'c.principaluri'])
->from('calendar_reminders', 'cr')
->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime())))
->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id'))
->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'));
$stmt = $query->execute();
return array_map(
[$this, 'fixRowTyping'],
$stmt->fetchAll()
);
}
/**
* Get all scheduled reminders for an event
*
* @param int $objectId
* @return array
*/
public function getAllScheduledRemindersForEvent(int $objectId):array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)));
$stmt = $query->execute();
return array_map(
[$this, 'fixRowTyping'],
$stmt->fetchAll()
);
}
/**
* Insert a new reminder into the database
*
* @param int $calendarId
* @param int $objectId
* @param string $uid
* @param bool $isRecurring
* @param int $recurrenceId
* @param bool $isRecurrenceException
* @param string $eventHash
* @param string $alarmHash
* @param string $type
* @param bool $isRelative
* @param int $notificationDate
* @param bool $isRepeatBased
* @return int The insert id
*/
public function insertReminder(int $calendarId,
int $objectId,
string $uid,
bool $isRecurring,
int $recurrenceId,
bool $isRecurrenceException,
string $eventHash,
string $alarmHash,
string $type,
bool $isRelative,
int $notificationDate,
bool $isRepeatBased):int {
$query = $this->db->getQueryBuilder();
$query->insert('calendar_reminders')
->values([
'calendar_id' => $query->createNamedParameter($calendarId),
'object_id' => $query->createNamedParameter($objectId),
'uid' => $query->createNamedParameter($uid),
'is_recurring' => $query->createNamedParameter($isRecurring ? 1 : 0),
'recurrence_id' => $query->createNamedParameter($recurrenceId),
'is_recurrence_exception' => $query->createNamedParameter($isRecurrenceException ? 1 : 0),
'event_hash' => $query->createNamedParameter($eventHash),
'alarm_hash' => $query->createNamedParameter($alarmHash),
'type' => $query->createNamedParameter($type),
'is_relative' => $query->createNamedParameter($isRelative ? 1 : 0),
'notification_date' => $query->createNamedParameter($notificationDate),
'is_repeat_based' => $query->createNamedParameter($isRepeatBased ? 1 : 0),
])
->execute();
return $query->getLastInsertId();
}
/**
* Sets a new notificationDate on an existing reminder
*
* @param int $reminderId
* @param int $newNotificationDate
*/
public function updateReminder(int $reminderId,
int $newNotificationDate):void {
$query = $this->db->getQueryBuilder();
$query->update('calendar_reminders')
->set('notification_date', $query->createNamedParameter($newNotificationDate))
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
->execute();
}
/**
* Remove a reminder by it's id
*
* @param integer $reminderId
* @return void
*/
public function removeReminder(int $reminderId):void {
$query = $this->db->getQueryBuilder();
$query->delete('calendar_reminders')
->where($query->expr()->eq('id', $query->createNamedParameter($reminderId)))
->execute();
}
/**
* Cleans reminders in database
*
* @param int $objectId
*/
public function cleanRemindersForEvent(int $objectId):void {
$query = $this->db->getQueryBuilder();
$query->delete('calendar_reminders')
->where($query->expr()->eq('object_id', $query->createNamedParameter($objectId)))
->execute();
}
/**
* Remove all reminders for a calendar
*
* @param int $calendarId
* @return void
*/
public function cleanRemindersForCalendar(int $calendarId):void {
$query = $this->db->getQueryBuilder();
$query->delete('calendar_reminders')
->where($query->expr()->eq('calendar_id', $query->createNamedParameter($calendarId)))
->execute();
}
/**
* @param array $row
* @return array
*/
private function fixRowTyping(array $row): array {
$row['id'] = (int) $row['id'];
$row['calendar_id'] = (int) $row['calendar_id'];
$row['object_id'] = (int) $row['object_id'];
$row['is_recurring'] = (bool) $row['is_recurring'];
$row['recurrence_id'] = (int) $row['recurrence_id'];
$row['is_recurrence_exception'] = (bool) $row['is_recurrence_exception'];
$row['is_relative'] = (bool) $row['is_relative'];
$row['notification_date'] = (int) $row['notification_date'];
$row['is_repeat_based'] = (bool) $row['is_repeat_based'];
return $row;
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
use OCP\IUser;
use Sabre\VObject\Component\VEvent;
/**
* Interface INotificationProvider
*
* @package OCA\DAV\CalDAV\Reminder
*/
interface INotificationProvider {
/**
* Send notification
*
* @param VEvent $vevent
* @param string|null $calendarDisplayName
* @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object
* @param IUser[] $users
* @return void
*/
public function send(VEvent $vevent,
?string $calendarDisplayName,
array $principalEmailAddresses,
array $users = []): void;
}
@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\CalDAV\Reminder\INotificationProvider;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\Property;
/**
* Class AbstractProvider
*
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
abstract class AbstractProvider implements INotificationProvider {
/** @var string */
public const NOTIFICATION_TYPE = '';
protected LoggerInterface $logger;
/** @var L10NFactory */
protected $l10nFactory;
/** @var IL10N[] */
private $l10ns;
/** @var string */
private $fallbackLanguage;
/** @var IURLGenerator */
protected $urlGenerator;
/** @var IConfig */
protected $config;
public function __construct(LoggerInterface $logger,
L10NFactory $l10nFactory,
IURLGenerator $urlGenerator,
IConfig $config) {
$this->logger = $logger;
$this->l10nFactory = $l10nFactory;
$this->urlGenerator = $urlGenerator;
$this->config = $config;
}
/**
* Send notification
*
* @param VEvent $vevent
* @param string|null $calendarDisplayName
* @param string[] $principalEmailAddresses
* @param IUser[] $users
* @return void
*/
abstract public function send(VEvent $vevent,
?string $calendarDisplayName,
array $principalEmailAddresses,
array $users = []): void;
/**
* @return string
*/
protected function getFallbackLanguage():string {
if ($this->fallbackLanguage) {
return $this->fallbackLanguage;
}
$fallbackLanguage = $this->l10nFactory->findGenericLanguage();
$this->fallbackLanguage = $fallbackLanguage;
return $fallbackLanguage;
}
/**
* @param string $lang
* @return bool
*/
protected function hasL10NForLang(string $lang):bool {
return $this->l10nFactory->languageExists('dav', $lang);
}
/**
* @param string $lang
* @return IL10N
*/
protected function getL10NForLang(string $lang):IL10N {
if (isset($this->l10ns[$lang])) {
return $this->l10ns[$lang];
}
$l10n = $this->l10nFactory->get('dav', $lang);
$this->l10ns[$lang] = $l10n;
return $l10n;
}
/**
* @param VEvent $vevent
* @return string
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
return (string) $vevent->STATUS;
}
// Doesn't say so in the standard,
// but we consider events without a status
// to be confirmed
return 'CONFIRMED';
}
/**
* @param VEvent $vevent
* @return bool
*/
protected function isEventTentative(VEvent $vevent):bool {
return $this->getStatusOfEvent($vevent) === 'TENTATIVE';
}
/**
* @param VEvent $vevent
* @return Property\ICalendar\DateTime
*/
protected function getDTEndFromEvent(VEvent $vevent):Property\ICalendar\DateTime {
if (isset($vevent->DTEND)) {
return $vevent->DTEND;
}
if (isset($vevent->DURATION)) {
$isFloating = $vevent->DTSTART->isFloating();
/** @var Property\ICalendar\DateTime $end */
$end = clone $vevent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
$end->setDateTime($endDateTime, $isFloating);
return $end;
}
if (!$vevent->DTSTART->hasTime()) {
$isFloating = $vevent->DTSTART->isFloating();
/** @var Property\ICalendar\DateTime $end */
$end = clone $vevent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->modify('+1 day');
$end->setDateTime($endDateTime, $isFloating);
return $end;
}
return clone $vevent->DTSTART;
}
protected function getCalendarDisplayNameFallback(string $lang): string {
return $this->getL10NForLang($lang)->t('Untitled calendar');
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
/**
* Class AudioProvider
*
* This class only extends PushProvider at the moment. It does not provide true
* audio-alarms yet, but it's better than no alarm at all right now.
*
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
class AudioProvider extends PushProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'AUDIO';
}
@@ -0,0 +1,465 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Richard Steinmetz <richard@steinmetz.cloud>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use DateTime;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\Headers\AutoSubmitted;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property;
/**
* Class EmailProvider
*
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
class EmailProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'EMAIL';
private IMailer $mailer;
public function __construct(IConfig $config,
IMailer $mailer,
LoggerInterface $logger,
L10NFactory $l10nFactory,
IURLGenerator $urlGenerator) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
$this->mailer = $mailer;
}
/**
* Send out notification via email
*
* @param VEvent $vevent
* @param string|null $calendarDisplayName
* @param string[] $principalEmailAddresses
* @param array $users
* @throws \Exception
*/
public function send(VEvent $vevent,
?string $calendarDisplayName,
array $principalEmailAddresses,
array $users = []):void {
$fallbackLanguage = $this->getFallbackLanguage();
$organizerEmailAddress = null;
if (isset($vevent->ORGANIZER)) {
$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
}
$emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
$emailAddressesOfAttendees = [];
if (count($principalEmailAddresses) === 0
|| ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true))
) {
$emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
}
// Quote from php.net:
// If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
// => if there are duplicate email addresses, it will always take the system value
$emailAddresses = array_merge(
$emailAddressesOfAttendees,
$emailAddressesOfSharees
);
$sortedByLanguage = $this->sortEMailAddressesByLanguage($emailAddresses, $fallbackLanguage);
$organizer = $this->getOrganizerEMailAndNameFromEvent($vevent);
foreach ($sortedByLanguage as $lang => $emailAddresses) {
if (!$this->hasL10NForLang($lang)) {
$lang = $fallbackLanguage;
}
$l10n = $this->getL10NForLang($lang);
$fromEMail = \OCP\Util::getDefaultEmailAddress('reminders-noreply');
$template = $this->mailer->createEMailTemplate('dav.calendarReminder');
$template->addHeader();
$this->addSubjectAndHeading($template, $l10n, $vevent);
$this->addBulletList($template, $l10n, $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($lang), $vevent);
$template->addFooter();
foreach ($emailAddresses as $emailAddress) {
if (!$this->mailer->validateMailAddress($emailAddress)) {
$this->logger->error('Email address {address} for reminder notification is incorrect', ['app' => 'dav', 'address' => $emailAddress]);
continue;
}
$message = $this->mailer->createMessage();
$message->setFrom([$fromEMail]);
if ($organizer) {
$message->setReplyTo($organizer);
}
$message->setTo([$emailAddress]);
$message->useTemplate($template);
$message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
try {
$failed = $this->mailer->send($message);
if ($failed) {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
}
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
}
}
}
}
/**
* @param IEMailTemplate $template
* @param IL10N $l10n
* @param VEvent $vevent
*/
private function addSubjectAndHeading(IEMailTemplate $template, IL10N $l10n, VEvent $vevent):void {
$template->setSubject('Notification: ' . $this->getTitleFromVEvent($vevent, $l10n));
$template->addHeading($this->getTitleFromVEvent($vevent, $l10n));
}
/**
* @param IEMailTemplate $template
* @param IL10N $l10n
* @param string $calendarDisplayName
* @param array $eventData
*/
private function addBulletList(IEMailTemplate $template,
IL10N $l10n,
string $calendarDisplayName,
VEvent $vevent):void {
$template->addBodyListItem($calendarDisplayName, $l10n->t('Calendar:'),
$this->getAbsoluteImagePath('actions/info.png'));
$template->addBodyListItem($this->generateDateString($l10n, $vevent), $l10n->t('Date:'),
$this->getAbsoluteImagePath('places/calendar.png'));
if (isset($vevent->LOCATION)) {
$template->addBodyListItem((string) $vevent->LOCATION, $l10n->t('Where:'),
$this->getAbsoluteImagePath('actions/address.png'));
}
if (isset($vevent->DESCRIPTION)) {
$template->addBodyListItem((string) $vevent->DESCRIPTION, $l10n->t('Description:'),
$this->getAbsoluteImagePath('actions/more.png'));
}
}
private function getAbsoluteImagePath(string $path):string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath('core', $path)
);
}
/**
* @param VEvent $vevent
* @return array|null
*/
private function getOrganizerEMailAndNameFromEvent(VEvent $vevent):?array {
if (!$vevent->ORGANIZER) {
return null;
}
$organizer = $vevent->ORGANIZER;
if (strcasecmp($organizer->getValue(), 'mailto:') !== 0) {
return null;
}
$organizerEMail = substr($organizer->getValue(), 7);
if (!$this->mailer->validateMailAddress($organizerEMail)) {
return null;
}
$name = $organizer->offsetGet('CN');
if ($name instanceof Parameter) {
return [$organizerEMail => $name];
}
return [$organizerEMail];
}
/**
* @param array<string, array{LANG?: string}> $emails
* @return array<string, string[]>
*/
private function sortEMailAddressesByLanguage(array $emails,
string $defaultLanguage):array {
$sortedByLanguage = [];
foreach ($emails as $emailAddress => $parameters) {
if (isset($parameters['LANG'])) {
$lang = $parameters['LANG'];
} else {
$lang = $defaultLanguage;
}
if (!isset($sortedByLanguage[$lang])) {
$sortedByLanguage[$lang] = [];
}
$sortedByLanguage[$lang][] = $emailAddress;
}
return $sortedByLanguage;
}
/**
* @param VEvent $vevent
* @return array<string, array{LANG?: string}>
*/
private function getAllEMailAddressesFromEvent(VEvent $vevent):array {
$emailAddresses = [];
if (isset($vevent->ATTENDEE)) {
foreach ($vevent->ATTENDEE as $attendee) {
if (!($attendee instanceof VObject\Property)) {
continue;
}
$cuType = $this->getCUTypeOfAttendee($attendee);
if (\in_array($cuType, ['RESOURCE', 'ROOM', 'UNKNOWN'])) {
// Don't send emails to things
continue;
}
$partstat = $this->getPartstatOfAttendee($attendee);
if ($partstat === 'DECLINED') {
// Don't send out emails to people who declined
continue;
}
if ($partstat === 'DELEGATED') {
$delegates = $attendee->offsetGet('DELEGATED-TO');
if (!($delegates instanceof VObject\Parameter)) {
continue;
}
$emailAddressesOfDelegates = $delegates->getParts();
foreach ($emailAddressesOfDelegates as $addressesOfDelegate) {
if (strcasecmp($addressesOfDelegate, 'mailto:') === 0) {
$delegateEmail = substr($addressesOfDelegate, 7);
if ($this->mailer->validateMailAddress($delegateEmail)) {
$emailAddresses[$delegateEmail] = [];
}
}
}
continue;
}
$emailAddressOfAttendee = $this->getEMailAddressOfAttendee($attendee);
if ($emailAddressOfAttendee !== null) {
$properties = [];
$langProp = $attendee->offsetGet('LANG');
if ($langProp instanceof VObject\Parameter && $langProp->getValue() !== null) {
$properties['LANG'] = $langProp->getValue();
}
$emailAddresses[$emailAddressOfAttendee] = $properties;
}
}
}
if (isset($vevent->ORGANIZER) && $this->hasAttendeeMailURI($vevent->ORGANIZER)) {
$organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
if ($organizerEmailAddress !== null) {
$emailAddresses[$organizerEmailAddress] = [];
}
}
return $emailAddresses;
}
private function getCUTypeOfAttendee(VObject\Property $attendee):string {
$cuType = $attendee->offsetGet('CUTYPE');
if ($cuType instanceof VObject\Parameter) {
return strtoupper($cuType->getValue());
}
return 'INDIVIDUAL';
}
private function getPartstatOfAttendee(VObject\Property $attendee):string {
$partstat = $attendee->offsetGet('PARTSTAT');
if ($partstat instanceof VObject\Parameter) {
return strtoupper($partstat->getValue());
}
return 'NEEDS-ACTION';
}
private function hasAttendeeMailURI(VObject\Property $attendee): bool {
return stripos($attendee->getValue(), 'mailto:') === 0;
}
private function getEMailAddressOfAttendee(VObject\Property $attendee): ?string {
if (!$this->hasAttendeeMailURI($attendee)) {
return null;
}
$attendeeEMail = substr($attendee->getValue(), 7);
if (!$this->mailer->validateMailAddress($attendeeEMail)) {
return null;
}
return $attendeeEMail;
}
/**
* @param IUser[] $users
* @return array<string, array{LANG?: string}>
*/
private function getEMailAddressesOfAllUsersWithWriteAccessToCalendar(array $users):array {
$emailAddresses = [];
foreach ($users as $user) {
$emailAddress = $user->getEMailAddress();
if ($emailAddress) {
$lang = $this->l10nFactory->getUserLanguage($user);
if ($lang) {
$emailAddresses[$emailAddress] = [
'LANG' => $lang,
];
} else {
$emailAddresses[$emailAddress] = [];
}
}
}
return $emailAddresses;
}
/**
* @throws \Exception
*/
private function generateDateString(IL10N $l10n, VEvent $vevent): string {
$isAllDay = $vevent->DTSTART instanceof Property\ICalendar\Date;
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
/** @var \DateTimeImmutable $dtstartDt */
$dtstartDt = $vevent->DTSTART->getDateTime();
/** @var \DateTimeImmutable $dtendDt */
$dtendDt = $this->getDTEndFromEvent($vevent)->getDateTime();
$diff = $dtstartDt->diff($dtendDt);
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
if ($isAllDay) {
// One day event
if ($diff->days === 1) {
return $this->getDateString($l10n, $dtstartDt);
}
return implode(' - ', [
$this->getDateString($l10n, $dtstartDt),
$this->getDateString($l10n, $dtendDt),
]);
}
$startTimezone = $endTimezone = null;
if (!$vevent->DTSTART->isFloating()) {
$startTimezone = $vevent->DTSTART->getDateTime()->getTimezone()->getName();
$endTimezone = $this->getDTEndFromEvent($vevent)->getDateTime()->getTimezone()->getName();
}
$localeStart = implode(', ', [
$this->getWeekDayName($l10n, $dtstartDt),
$this->getDateTimeString($l10n, $dtstartDt)
]);
// always show full date with timezone if timezones are different
if ($startTimezone !== $endTimezone) {
$localeEnd = implode(', ', [
$this->getWeekDayName($l10n, $dtendDt),
$this->getDateTimeString($l10n, $dtendDt)
]);
return $localeStart
. ' (' . $startTimezone . ') '
. ' - '
. $localeEnd
. ' (' . $endTimezone . ')';
}
// Show only the time if the day is the same
$localeEnd = $this->isDayEqual($dtstartDt, $dtendDt)
? $this->getTimeString($l10n, $dtendDt)
: implode(', ', [
$this->getWeekDayName($l10n, $dtendDt),
$this->getDateTimeString($l10n, $dtendDt)
]);
return $localeStart
. ' - '
. $localeEnd
. ' (' . $startTimezone . ')';
}
private function isDayEqual(DateTime $dtStart,
DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
private function getWeekDayName(IL10N $l10n, DateTime $dt):string {
return (string)$l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
private function getDateString(IL10N $l10n, DateTime $dt):string {
return (string)$l10n->l('date', $dt, ['width' => 'medium']);
}
private function getDateTimeString(IL10N $l10n, DateTime $dt):string {
return (string)$l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
private function getTimeString(IL10N $l10n, DateTime $dt):string {
return (string)$l10n->l('time', $dt, ['width' => 'short']);
}
private function getTitleFromVEvent(VEvent $vevent, IL10N $l10n):string {
if (isset($vevent->SUMMARY)) {
return (string)$vevent->SUMMARY;
}
return $l10n->t('Untitled event');
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Thomas Citharel <tcit@tcit.fr>
*
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
class ProviderNotAvailableException extends \Exception {
/**
* ProviderNotAvailableException constructor.
*
* @since 16.0.0
*
* @param string $type ReminderType
*/
public function __construct(string $type) {
parent::__construct("No notification provider for type $type available");
}
}
@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder\NotificationProvider;
use OCA\DAV\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Property;
/**
* Class PushProvider
*
* @package OCA\DAV\CalDAV\Reminder\NotificationProvider
*/
class PushProvider extends AbstractProvider {
/** @var string */
public const NOTIFICATION_TYPE = 'DISPLAY';
/** @var IManager */
private $manager;
/** @var ITimeFactory */
private $timeFactory;
public function __construct(IConfig $config,
IManager $manager,
LoggerInterface $logger,
L10NFactory $l10nFactory,
IURLGenerator $urlGenerator,
ITimeFactory $timeFactory) {
parent::__construct($logger, $l10nFactory, $urlGenerator, $config);
$this->manager = $manager;
$this->timeFactory = $timeFactory;
}
/**
* Send push notification to all users.
*
* @param VEvent $vevent
* @param string|null $calendarDisplayName
* @param string[] $principalEmailAddresses
* @param IUser[] $users
* @throws \Exception
*/
public function send(VEvent $vevent,
?string $calendarDisplayName,
array $principalEmailAddresses,
array $users = []):void {
if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'yes') !== 'yes') {
return;
}
$eventDetails = $this->extractEventDetails($vevent);
$eventUUID = (string) $vevent->UID;
if (!$eventUUID) {
return;
};
$eventUUIDHash = hash('sha256', $eventUUID, false);
foreach ($users as $user) {
$eventDetails['calendar_displayname'] = $calendarDisplayName ?? $this->getCalendarDisplayNameFallback($this->l10nFactory->getUserLanguage($user));
/** @var INotification $notification */
$notification = $this->manager->createNotification();
$notification->setApp(Application::APP_ID)
->setUser($user->getUID())
->setDateTime($this->timeFactory->getDateTime())
->setObject(Application::APP_ID, $eventUUIDHash)
->setSubject('calendar_reminder', [
'title' => $eventDetails['title'],
'start_atom' => $eventDetails['start_atom']
])
->setMessage('calendar_reminder', $eventDetails);
$this->manager->notify($notification);
}
}
/**
* @throws \Exception
*/
protected function extractEventDetails(VEvent $vevent):array {
/** @var Property\ICalendar\DateTime $start */
$start = $vevent->DTSTART;
$end = $this->getDTEndFromEvent($vevent);
return [
'title' => isset($vevent->SUMMARY)
? ((string) $vevent->SUMMARY)
: null,
'description' => isset($vevent->DESCRIPTION)
? ((string) $vevent->DESCRIPTION)
: null,
'location' => isset($vevent->LOCATION)
? ((string) $vevent->LOCATION)
: null,
'all_day' => $start instanceof Property\ICalendar\Date,
'start_atom' => $start->getDateTime()->format(\DateTimeInterface::ATOM),
'start_is_floating' => $start->isFloating(),
'start_timezone' => $start->getDateTime()->getTimezone()->getName(),
'end_atom' => $end->getDateTime()->format(\DateTimeInterface::ATOM),
'end_is_floating' => $end->isFloating(),
'end_timezone' => $end->getDateTime()->getTimezone()->getName(),
];
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
/**
* Class NotificationProviderManager
*
* @package OCA\DAV\CalDAV\Reminder
*/
class NotificationProviderManager {
/** @var INotificationProvider[] */
private $providers = [];
/**
* Checks whether a provider for a given ACTION exists
*
* @param string $type
* @return bool
*/
public function hasProvider(string $type):bool {
return (\in_array($type, ReminderService::REMINDER_TYPES, true)
&& isset($this->providers[$type]));
}
/**
* Get provider for a given ACTION
*
* @param string $type
* @return INotificationProvider
* @throws NotificationProvider\ProviderNotAvailableException
* @throws NotificationTypeDoesNotExistException
*/
public function getProvider(string $type):INotificationProvider {
if (in_array($type, ReminderService::REMINDER_TYPES, true)) {
if (isset($this->providers[$type])) {
return $this->providers[$type];
}
throw new NotificationProvider\ProviderNotAvailableException($type);
}
throw new NotificationTypeDoesNotExistException($type);
}
/**
* Registers a new provider
*
* @param string $providerClassName
* @throws \OCP\AppFramework\QueryException
*/
public function registerProvider(string $providerClassName):void {
$provider = \OC::$server->query($providerClassName);
if (!$provider instanceof INotificationProvider) {
throw new \InvalidArgumentException('Invalid notification provider registered');
}
$this->providers[$provider::NOTIFICATION_TYPE] = $provider;
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
*
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
class NotificationTypeDoesNotExistException extends \Exception {
/**
* NotificationTypeDoesNotExistException constructor.
*
* @since 16.0.0
*
* @param string $type ReminderType
*/
public function __construct(string $type) {
parent::__construct("Type $type is not an accepted type of notification");
}
}
@@ -0,0 +1,341 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
use DateTime;
use OCA\DAV\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
/**
* Class Notifier
*
* @package OCA\DAV\CalDAV\Reminder
*/
class Notifier implements INotifier {
/** @var IFactory */
private $l10nFactory;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IL10N */
private $l10n;
/** @var ITimeFactory */
private $timeFactory;
/**
* Notifier constructor.
*
* @param IFactory $factory
* @param IURLGenerator $urlGenerator
* @param ITimeFactory $timeFactory
*/
public function __construct(IFactory $factory,
IURLGenerator $urlGenerator,
ITimeFactory $timeFactory) {
$this->l10nFactory = $factory;
$this->urlGenerator = $urlGenerator;
$this->timeFactory = $timeFactory;
}
/**
* Identifier of the notifier, only use [a-z0-9_]
*
* @return string
* @since 17.0.0
*/
public function getID():string {
return Application::APP_ID;
}
/**
* Human readable name describing the notifier
*
* @return string
* @since 17.0.0
*/
public function getName():string {
return $this->l10nFactory->get('dav')->t('Calendar');
}
/**
* Prepare sending the notification
*
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
* @throws \Exception
*/
public function prepare(INotification $notification,
string $languageCode):INotification {
if ($notification->getApp() !== Application::APP_ID) {
throw new \InvalidArgumentException('Notification not from this app');
}
// Read the language from the notification
$this->l10n = $this->l10nFactory->get('dav', $languageCode);
// Handle notifier subjects
switch ($notification->getSubject()) {
case 'calendar_reminder':
return $this->prepareReminderNotification($notification);
default:
throw new \InvalidArgumentException('Unknown subject');
}
}
/**
* @param INotification $notification
* @return INotification
*/
private function prepareReminderNotification(INotification $notification):INotification {
$imagePath = $this->urlGenerator->imagePath('core', 'places/calendar.svg');
$iconUrl = $this->urlGenerator->getAbsoluteURL($imagePath);
$notification->setIcon($iconUrl);
$this->prepareNotificationSubject($notification);
$this->prepareNotificationMessage($notification);
return $notification;
}
/**
* Sets the notification subject based on the parameters set in PushProvider
*
* @param INotification $notification
*/
private function prepareNotificationSubject(INotification $notification): void {
$parameters = $notification->getSubjectParameters();
$startTime = \DateTime::createFromFormat(\DateTimeInterface::ATOM, $parameters['start_atom']);
$now = $this->timeFactory->getDateTime();
$title = $this->getTitleFromParameters($parameters);
$diff = $startTime->diff($now);
if ($diff === false) {
return;
}
$components = [];
if ($diff->y) {
$components[] = $this->l10n->n('%n year', '%n years', $diff->y);
}
if ($diff->m) {
$components[] = $this->l10n->n('%n month', '%n months', $diff->m);
}
if ($diff->d) {
$components[] = $this->l10n->n('%n day', '%n days', $diff->d);
}
if ($diff->h) {
$components[] = $this->l10n->n('%n hour', '%n hours', $diff->h);
}
if ($diff->i) {
$components[] = $this->l10n->n('%n minute', '%n minutes', $diff->i);
}
if (count($components) > 0 && !$this->hasPhpDatetimeDiffBug()) {
// Limiting to the first three components to prevent
// the string from getting too long
$firstThreeComponents = array_slice($components, 0, 2);
$diffLabel = implode(', ', $firstThreeComponents);
if ($diff->invert) {
$title = $this->l10n->t('%s (in %s)', [$title, $diffLabel]);
} else {
$title = $this->l10n->t('%s (%s ago)', [$title, $diffLabel]);
}
}
$notification->setParsedSubject($title);
}
/**
* @see https://github.com/nextcloud/server/issues/41615
* @see https://github.com/php/php-src/issues/9699
*/
private function hasPhpDatetimeDiffBug(): bool {
$d1 = DateTime::createFromFormat(\DateTimeInterface::ATOM, '2023-11-22T11:52:00+01:00');
$d2 = new DateTime('2023-11-22T10:52:03', new \DateTimeZone('UTC'));
// The difference is 3 seconds, not -1year+11months+…
return $d1->diff($d2)->y < 0;
}
/**
* Sets the notification message based on the parameters set in PushProvider
*
* @param INotification $notification
*/
private function prepareNotificationMessage(INotification $notification): void {
$parameters = $notification->getMessageParameters();
$description = [
$this->l10n->t('Calendar: %s', $parameters['calendar_displayname']),
$this->l10n->t('Date: %s', $this->generateDateString($parameters)),
];
if ($parameters['description']) {
$description[] = $this->l10n->t('Description: %s', $parameters['description']);
}
if ($parameters['location']) {
$description[] = $this->l10n->t('Where: %s', $parameters['location']);
}
$message = implode("\r\n", $description);
$notification->setParsedMessage($message);
}
/**
* @param array $parameters
* @return string
*/
private function getTitleFromParameters(array $parameters):string {
return $parameters['title'] ?? $this->l10n->t('Untitled event');
}
/**
* @param array $parameters
* @return string
* @throws \Exception
*/
private function generateDateString(array $parameters):string {
$startDateTime = DateTime::createFromFormat(\DateTimeInterface::ATOM, $parameters['start_atom']);
$endDateTime = DateTime::createFromFormat(\DateTimeInterface::ATOM, $parameters['end_atom']);
// If the event has already ended, dismiss the notification
if ($endDateTime < $this->timeFactory->getDateTime()) {
throw new AlreadyProcessedException();
}
$isAllDay = $parameters['all_day'];
$diff = $startDateTime->diff($endDateTime);
if ($isAllDay) {
// One day event
if ($diff->days === 1) {
return $this->getDateString($startDateTime);
}
return implode(' - ', [
$this->getDateString($startDateTime),
$this->getDateString($endDateTime),
]);
}
$startTimezone = $endTimezone = null;
if (!$parameters['start_is_floating']) {
$startTimezone = $parameters['start_timezone'];
$endTimezone = $parameters['end_timezone'];
}
$localeStart = implode(', ', [
$this->getWeekDayName($startDateTime),
$this->getDateTimeString($startDateTime)
]);
// always show full date with timezone if timezones are different
if ($startTimezone !== $endTimezone) {
$localeEnd = implode(', ', [
$this->getWeekDayName($endDateTime),
$this->getDateTimeString($endDateTime)
]);
return $localeStart
. ' (' . $startTimezone . ') '
. ' - '
. $localeEnd
. ' (' . $endTimezone . ')';
}
// Show only the time if the day is the same
$localeEnd = $this->isDayEqual($startDateTime, $endDateTime)
? $this->getTimeString($endDateTime)
: implode(', ', [
$this->getWeekDayName($endDateTime),
$this->getDateTimeString($endDateTime)
]);
return $localeStart
. ' - '
. $localeEnd
. ' (' . $startTimezone . ')';
}
/**
* @param DateTime $dtStart
* @param DateTime $dtEnd
* @return bool
*/
private function isDayEqual(DateTime $dtStart,
DateTime $dtEnd):bool {
return $dtStart->format('Y-m-d') === $dtEnd->format('Y-m-d');
}
/**
* @param DateTime $dt
* @return string
*/
private function getWeekDayName(DateTime $dt):string {
return (string)$this->l10n->l('weekdayName', $dt, ['width' => 'abbreviated']);
}
/**
* @param DateTime $dt
* @return string
*/
private function getDateString(DateTime $dt):string {
return (string)$this->l10n->l('date', $dt, ['width' => 'medium']);
}
/**
* @param DateTime $dt
* @return string
*/
private function getDateTimeString(DateTime $dt):string {
return (string)$this->l10n->l('datetime', $dt, ['width' => 'medium|short']);
}
/**
* @param DateTime $dt
* @return string
*/
private function getTimeString(DateTime $dt):string {
return (string)$this->l10n->l('time', $dt, ['width' => 'short']);
}
}
@@ -0,0 +1,886 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Thomas Citharel
* @copyright Copyright (c) 2019, Georg Ehrke
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Reminder;
use DateTimeImmutable;
use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Connector\Sabre\Principal;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Sabre\VObject;
use Sabre\VObject\Component\VAlarm;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\InvalidDataException;
use Sabre\VObject\ParseException;
use Sabre\VObject\Recur\EventIterator;
use Sabre\VObject\Recur\MaxInstancesExceededException;
use Sabre\VObject\Recur\NoInstancesException;
use function count;
use function strcasecmp;
class ReminderService {
/** @var Backend */
private $backend;
/** @var NotificationProviderManager */
private $notificationProviderManager;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var CalDavBackend */
private $caldavBackend;
/** @var ITimeFactory */
private $timeFactory;
/** @var IConfig */
private $config;
/** @var LoggerInterface */
private $logger;
/** @var Principal */
private $principalConnector;
public const REMINDER_TYPE_EMAIL = 'EMAIL';
public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
public const REMINDER_TYPE_AUDIO = 'AUDIO';
/**
* @var String[]
*
* Official RFC5545 reminder types
*/
public const REMINDER_TYPES = [
self::REMINDER_TYPE_EMAIL,
self::REMINDER_TYPE_DISPLAY,
self::REMINDER_TYPE_AUDIO
];
public function __construct(Backend $backend,
NotificationProviderManager $notificationProviderManager,
IUserManager $userManager,
IGroupManager $groupManager,
CalDavBackend $caldavBackend,
ITimeFactory $timeFactory,
IConfig $config,
LoggerInterface $logger,
Principal $principalConnector) {
$this->backend = $backend;
$this->notificationProviderManager = $notificationProviderManager;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->caldavBackend = $caldavBackend;
$this->timeFactory = $timeFactory;
$this->config = $config;
$this->logger = $logger;
$this->principalConnector = $principalConnector;
}
/**
* Process reminders to activate
*
* @throws NotificationProvider\ProviderNotAvailableException
* @throws NotificationTypeDoesNotExistException
*/
public function processReminders() :void {
$reminders = $this->backend->getRemindersToProcess();
$this->logger->debug('{numReminders} reminders to process', [
'numReminders' => count($reminders),
]);
foreach ($reminders as $reminder) {
$calendarData = is_resource($reminder['calendardata'])
? stream_get_contents($reminder['calendardata'])
: $reminder['calendardata'];
if (!$calendarData) {
continue;
}
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
$this->logger->debug('Reminder {id} does not belong to a valid calendar', [
'id' => $reminder['id'],
]);
$this->backend->removeReminder($reminder['id']);
continue;
}
try {
$vevent = $this->getVEventByRecurrenceId($vcalendar, $reminder['recurrence_id'], $reminder['is_recurrence_exception']);
} catch (MaxInstancesExceededException $e) {
$this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
$this->backend->removeReminder($reminder['id']);
continue;
}
if (!$vevent) {
$this->logger->debug('Reminder {id} does not belong to a valid event', [
'id' => $reminder['id'],
]);
$this->backend->removeReminder($reminder['id']);
continue;
}
if ($this->wasEventCancelled($vevent)) {
$this->logger->debug('Reminder {id} belongs to a cancelled event', [
'id' => $reminder['id'],
]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
if (!$this->notificationProviderManager->hasProvider($reminder['type'])) {
$this->logger->debug('Reminder {id} does not belong to a valid notification provider', [
'id' => $reminder['id'],
]);
$this->deleteOrProcessNext($reminder, $vevent);
continue;
}
if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') {
$users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']);
} else {
$users = [];
}
$user = $this->getUserFromPrincipalURI($reminder['principaluri']);
if ($user) {
$users[] = $user;
}
$userPrincipalEmailAddresses = [];
$userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
if ($userPrincipal) {
$userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
}
$this->logger->debug('Reminder {id} will be sent to {numUsers} users', [
'id' => $reminder['id'],
'numUsers' => count($users),
]);
$notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
$notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
$this->deleteOrProcessNext($reminder, $vevent);
}
}
/**
* @param array $objectData
* @throws VObject\InvalidDataException
*/
public function onCalendarObjectCreate(array $objectData):void {
// We only support VEvents for now
if (strcasecmp($objectData['component'], 'vevent') !== 0) {
return;
}
$calendarData = is_resource($objectData['calendardata'])
? stream_get_contents($objectData['calendardata'])
: $objectData['calendardata'];
if (!$calendarData) {
return;
}
$vcalendar = $this->parseCalendarData($calendarData);
if (!$vcalendar) {
return;
}
$calendarTimeZone = $this->getCalendarTimeZone((int) $objectData['calendarid']);
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return;
}
$uid = (string) $vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
$isRecurring = $masterItem ? $this->isRecurring($masterItem) : false;
foreach ($recurrenceExceptions as $recurrenceException) {
$eventHash = $this->getEventHash($recurrenceException);
if (!isset($recurrenceException->VALARM)) {
continue;
}
foreach ($recurrenceException->VALARM as $valarm) {
/** @var VAlarm $valarm */
$alarmHash = $this->getAlarmHash($valarm);
$triggerTime = $valarm->getEffectiveTriggerTime();
$diff = $now->diff($triggerTime);
if ($diff->invert === 1) {
continue;
}
$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone,
$eventHash, $alarmHash, true, true);
$this->writeRemindersToDatabase($alarms);
}
}
if ($masterItem) {
$processedAlarms = [];
$masterAlarms = [];
$masterHash = $this->getEventHash($masterItem);
if (!isset($masterItem->VALARM)) {
return;
}
foreach ($masterItem->VALARM as $valarm) {
$masterAlarms[] = $this->getAlarmHash($valarm);
}
try {
$iterator = new EventIterator($vevents, $uid);
} catch (NoInstancesException $e) {
// This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output
// entirely.
return;
} catch (MaxInstancesExceededException $e) {
// The event has more than 3500 recurring-instances
// so we can ignore it
return;
}
while ($iterator->valid() && count($processedAlarms) < count($masterAlarms)) {
$event = $iterator->getEventObject();
// Recurrence-exceptions are handled separately, so just ignore them here
if (\in_array($event, $recurrenceExceptions, true)) {
$iterator->next();
continue;
}
foreach ($event->VALARM as $valarm) {
/** @var VAlarm $valarm */
$alarmHash = $this->getAlarmHash($valarm);
if (\in_array($alarmHash, $processedAlarms, true)) {
continue;
}
if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
// Action allows x-name, we don't insert reminders
// into the database if they are not standard
$processedAlarms[] = $alarmHash;
continue;
}
try {
$triggerTime = $valarm->getEffectiveTriggerTime();
/**
* @psalm-suppress DocblockTypeContradiction
* https://github.com/vimeo/psalm/issues/9244
*/
if ($triggerTime->getTimezone() === false || $triggerTime->getTimezone()->getName() === 'UTC') {
$triggerTime = new DateTimeImmutable(
$triggerTime->format('Y-m-d H:i:s'),
$calendarTimeZone
);
}
} catch (InvalidDataException $e) {
continue;
}
// If effective trigger time is in the past
// just skip and generate for next event
$diff = $now->diff($triggerTime);
if ($diff->invert === 1) {
// If an absolute alarm is in the past,
// just add it to processedAlarms, so
// we don't extend till eternity
if (!$this->isAlarmRelative($valarm)) {
$processedAlarms[] = $alarmHash;
}
continue;
}
$alarms = $this->getRemindersForVAlarm($valarm, $objectData, $calendarTimeZone, $masterHash, $alarmHash, $isRecurring, false);
$this->writeRemindersToDatabase($alarms);
$processedAlarms[] = $alarmHash;
}
$iterator->next();
}
}
}
/**
* @param array $objectData
* @throws VObject\InvalidDataException
*/
public function onCalendarObjectEdit(array $objectData):void {
// TODO - this can be vastly improved
// - get cached reminders
// - ...
$this->onCalendarObjectDelete($objectData);
$this->onCalendarObjectCreate($objectData);
}
/**
* @param array $objectData
* @throws VObject\InvalidDataException
*/
public function onCalendarObjectDelete(array $objectData):void {
// We only support VEvents for now
if (strcasecmp($objectData['component'], 'vevent') !== 0) {
return;
}
$this->backend->cleanRemindersForEvent((int) $objectData['id']);
}
/**
* @param VAlarm $valarm
* @param array $objectData
* @param DateTimeZone $calendarTimeZone
* @param string|null $eventHash
* @param string|null $alarmHash
* @param bool $isRecurring
* @param bool $isRecurrenceException
* @return array
*/
private function getRemindersForVAlarm(VAlarm $valarm,
array $objectData,
DateTimeZone $calendarTimeZone,
string $eventHash = null,
string $alarmHash = null,
bool $isRecurring = false,
bool $isRecurrenceException = false):array {
if ($eventHash === null) {
$eventHash = $this->getEventHash($valarm->parent);
}
if ($alarmHash === null) {
$alarmHash = $this->getAlarmHash($valarm);
}
$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($valarm->parent);
$isRelative = $this->isAlarmRelative($valarm);
/** @var DateTimeImmutable $notificationDate */
$notificationDate = $valarm->getEffectiveTriggerTime();
/**
* @psalm-suppress DocblockTypeContradiction
* https://github.com/vimeo/psalm/issues/9244
*/
if ($notificationDate->getTimezone() === false || $notificationDate->getTimezone()->getName() === 'UTC') {
$notificationDate = new DateTimeImmutable(
$notificationDate->format('Y-m-d H:i:s'),
$calendarTimeZone
);
}
$clonedNotificationDate = new \DateTime('now', $notificationDate->getTimezone());
$clonedNotificationDate->setTimestamp($notificationDate->getTimestamp());
$alarms = [];
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
'uid' => (string) $valarm->parent->UID,
'is_recurring' => $isRecurring,
'recurrence_id' => $recurrenceId,
'is_recurrence_exception' => $isRecurrenceException,
'event_hash' => $eventHash,
'alarm_hash' => $alarmHash,
'type' => (string) $valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $notificationDate->getTimestamp(),
'is_repeat_based' => false,
];
$repeat = isset($valarm->REPEAT) ? (int) $valarm->REPEAT->getValue() : 0;
for ($i = 0; $i < $repeat; $i++) {
if ($valarm->DURATION === null) {
continue;
}
$clonedNotificationDate->add($valarm->DURATION->getDateInterval());
$alarms[] = [
'calendar_id' => $objectData['calendarid'],
'object_id' => $objectData['id'],
'uid' => (string) $valarm->parent->UID,
'is_recurring' => $isRecurring,
'recurrence_id' => $recurrenceId,
'is_recurrence_exception' => $isRecurrenceException,
'event_hash' => $eventHash,
'alarm_hash' => $alarmHash,
'type' => (string) $valarm->ACTION,
'is_relative' => $isRelative,
'notification_date' => $clonedNotificationDate->getTimestamp(),
'is_repeat_based' => true,
];
}
return $alarms;
}
/**
* @param array $reminders
*/
private function writeRemindersToDatabase(array $reminders): void {
foreach ($reminders as $reminder) {
$this->backend->insertReminder(
(int) $reminder['calendar_id'],
(int) $reminder['object_id'],
$reminder['uid'],
$reminder['is_recurring'],
(int) $reminder['recurrence_id'],
$reminder['is_recurrence_exception'],
$reminder['event_hash'],
$reminder['alarm_hash'],
$reminder['type'],
$reminder['is_relative'],
(int) $reminder['notification_date'],
$reminder['is_repeat_based']
);
}
}
/**
* @param array $reminder
* @param VEvent $vevent
*/
private function deleteOrProcessNext(array $reminder,
VObject\Component\VEvent $vevent):void {
if ($reminder['is_repeat_based'] ||
!$reminder['is_recurring'] ||
!$reminder['is_relative'] ||
$reminder['is_recurrence_exception']) {
$this->backend->removeReminder($reminder['id']);
return;
}
$vevents = $this->getAllVEventsFromVCalendar($vevent->parent);
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$now = $this->timeFactory->getDateTime();
$calendarTimeZone = $this->getCalendarTimeZone((int) $reminder['calendar_id']);
try {
$iterator = new EventIterator($vevents, $reminder['uid']);
} catch (NoInstancesException $e) {
// This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output
// entirely.
return;
}
try {
while ($iterator->valid()) {
$event = $iterator->getEventObject();
// Recurrence-exceptions are handled separately, so just ignore them here
if (\in_array($event, $recurrenceExceptions, true)) {
$iterator->next();
continue;
}
$recurrenceId = $this->getEffectiveRecurrenceIdOfVEvent($event);
if ($reminder['recurrence_id'] >= $recurrenceId) {
$iterator->next();
continue;
}
foreach ($event->VALARM as $valarm) {
/** @var VAlarm $valarm */
$alarmHash = $this->getAlarmHash($valarm);
if ($alarmHash !== $reminder['alarm_hash']) {
continue;
}
$triggerTime = $valarm->getEffectiveTriggerTime();
// If effective trigger time is in the past
// just skip and generate for next event
$diff = $now->diff($triggerTime);
if ($diff->invert === 1) {
continue;
}
$this->backend->removeReminder($reminder['id']);
$alarms = $this->getRemindersForVAlarm($valarm, [
'calendarid' => $reminder['calendar_id'],
'id' => $reminder['object_id'],
], $calendarTimeZone, $reminder['event_hash'], $alarmHash, true, false);
$this->writeRemindersToDatabase($alarms);
// Abort generating reminders after creating one successfully
return;
}
$iterator->next();
}
} catch (MaxInstancesExceededException $e) {
// Using debug logger as this isn't really an error
$this->logger->debug('Recurrence with too many instances detected, skipping VEVENT', ['exception' => $e]);
}
$this->backend->removeReminder($reminder['id']);
}
/**
* @param int $calendarId
* @return IUser[]
*/
private function getAllUsersWithWriteAccessToCalendar(int $calendarId):array {
$shares = $this->caldavBackend->getShares($calendarId);
$users = [];
$userIds = [];
$groups = [];
foreach ($shares as $share) {
// Only consider writable shares
if ($share['readOnly']) {
continue;
}
$principal = explode('/', $share['{http://owncloud.org/ns}principal']);
if ($principal[1] === 'users') {
$user = $this->userManager->get($principal[2]);
if ($user) {
$users[] = $user;
$userIds[] = $principal[2];
}
} elseif ($principal[1] === 'groups') {
$groups[] = $principal[2];
}
}
foreach ($groups as $gid) {
$group = $this->groupManager->get($gid);
if ($group instanceof IGroup) {
foreach ($group->getUsers() as $user) {
if (!\in_array($user->getUID(), $userIds, true)) {
$users[] = $user;
$userIds[] = $user->getUID();
}
}
}
}
return $users;
}
/**
* Gets a hash of the event.
* If the hash changes, we have to update all relative alarms.
*
* @param VEvent $vevent
* @return string
*/
private function getEventHash(VEvent $vevent):string {
$properties = [
(string) $vevent->DTSTART->serialize(),
];
if ($vevent->DTEND) {
$properties[] = (string) $vevent->DTEND->serialize();
}
if ($vevent->DURATION) {
$properties[] = (string) $vevent->DURATION->serialize();
}
if ($vevent->{'RECURRENCE-ID'}) {
$properties[] = (string) $vevent->{'RECURRENCE-ID'}->serialize();
}
if ($vevent->RRULE) {
$properties[] = (string) $vevent->RRULE->serialize();
}
if ($vevent->EXDATE) {
$properties[] = (string) $vevent->EXDATE->serialize();
}
if ($vevent->RDATE) {
$properties[] = (string) $vevent->RDATE->serialize();
}
return md5(implode('::', $properties));
}
/**
* Gets a hash of the alarm.
* If the hash changes, we have to update oc_dav_reminders.
*
* @param VAlarm $valarm
* @return string
*/
private function getAlarmHash(VAlarm $valarm):string {
$properties = [
(string) $valarm->ACTION->serialize(),
(string) $valarm->TRIGGER->serialize(),
];
if ($valarm->DURATION) {
$properties[] = (string) $valarm->DURATION->serialize();
}
if ($valarm->REPEAT) {
$properties[] = (string) $valarm->REPEAT->serialize();
}
return md5(implode('::', $properties));
}
/**
* @param VObject\Component\VCalendar $vcalendar
* @param int $recurrenceId
* @param bool $isRecurrenceException
* @return VEvent|null
*/
private function getVEventByRecurrenceId(VObject\Component\VCalendar $vcalendar,
int $recurrenceId,
bool $isRecurrenceException):?VEvent {
$vevents = $this->getAllVEventsFromVCalendar($vcalendar);
if (count($vevents) === 0) {
return null;
}
$uid = (string) $vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
// Handle recurrence-exceptions first, because recurrence-expansion is expensive
if ($isRecurrenceException) {
foreach ($recurrenceExceptions as $recurrenceException) {
if ($this->getEffectiveRecurrenceIdOfVEvent($recurrenceException) === $recurrenceId) {
return $recurrenceException;
}
}
return null;
}
if ($masterItem) {
try {
$iterator = new EventIterator($vevents, $uid);
} catch (NoInstancesException $e) {
// This event is recurring, but it doesn't have a single
// instance. We are skipping this event from the output
// entirely.
return null;
}
while ($iterator->valid()) {
$event = $iterator->getEventObject();
// Recurrence-exceptions are handled separately, so just ignore them here
if (\in_array($event, $recurrenceExceptions, true)) {
$iterator->next();
continue;
}
if ($this->getEffectiveRecurrenceIdOfVEvent($event) === $recurrenceId) {
return $event;
}
$iterator->next();
}
}
return null;
}
/**
* @param VEvent $vevent
* @return string
*/
private function getStatusOfEvent(VEvent $vevent):string {
if ($vevent->STATUS) {
return (string) $vevent->STATUS;
}
// Doesn't say so in the standard,
// but we consider events without a status
// to be confirmed
return 'CONFIRMED';
}
/**
* @param VObject\Component\VEvent $vevent
* @return bool
*/
private function wasEventCancelled(VObject\Component\VEvent $vevent):bool {
return $this->getStatusOfEvent($vevent) === 'CANCELLED';
}
/**
* @param string $calendarData
* @return VObject\Component\VCalendar|null
*/
private function parseCalendarData(string $calendarData):?VObject\Component\VCalendar {
try {
return VObject\Reader::read($calendarData,
VObject\Reader::OPTION_FORGIVING);
} catch (ParseException $ex) {
return null;
}
}
/**
* @param string $principalUri
* @return IUser|null
*/
private function getUserFromPrincipalURI(string $principalUri):?IUser {
if (!$principalUri) {
return null;
}
if (stripos($principalUri, 'principals/users/') !== 0) {
return null;
}
$userId = substr($principalUri, 17);
return $this->userManager->get($userId);
}
/**
* @param VObject\Component\VCalendar $vcalendar
* @return VObject\Component\VEvent[]
*/
private function getAllVEventsFromVCalendar(VObject\Component\VCalendar $vcalendar):array {
$vevents = [];
foreach ($vcalendar->children() as $child) {
if (!($child instanceof VObject\Component)) {
continue;
}
if ($child->name !== 'VEVENT') {
continue;
}
// Ignore invalid events with no DTSTART
if ($child->DTSTART === null) {
continue;
}
$vevents[] = $child;
}
return $vevents;
}
/**
* @param array $vevents
* @return VObject\Component\VEvent[]
*/
private function getRecurrenceExceptionFromListOfVEvents(array $vevents):array {
return array_values(array_filter($vevents, function (VEvent $vevent) {
return $vevent->{'RECURRENCE-ID'} !== null;
}));
}
/**
* @param array $vevents
* @return VEvent|null
*/
private function getMasterItemFromListOfVEvents(array $vevents):?VEvent {
$elements = array_values(array_filter($vevents, function (VEvent $vevent) {
return $vevent->{'RECURRENCE-ID'} === null;
}));
if (count($elements) === 0) {
return null;
}
if (count($elements) > 1) {
throw new \TypeError('Multiple master objects');
}
return $elements[0];
}
/**
* @param VAlarm $valarm
* @return bool
*/
private function isAlarmRelative(VAlarm $valarm):bool {
$trigger = $valarm->TRIGGER;
return $trigger instanceof VObject\Property\ICalendar\Duration;
}
/**
* @param VEvent $vevent
* @return int
*/
private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
if (isset($vevent->{'RECURRENCE-ID'})) {
return $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp();
}
return $vevent->DTSTART->getDateTime()->getTimestamp();
}
/**
* @param VEvent $vevent
* @return bool
*/
private function isRecurring(VEvent $vevent):bool {
return isset($vevent->RRULE) || isset($vevent->RDATE);
}
/**
* @param int $calendarid
*
* @return DateTimeZone
*/
private function getCalendarTimeZone(int $calendarid): DateTimeZone {
$calendarInfo = $this->caldavBackend->getCalendarById($calendarid);
$tzProp = '{urn:ietf:params:xml:ns:caldav}calendar-timezone';
if (!isset($calendarInfo[$tzProp])) {
// Defaulting to UTC
return new DateTimeZone('UTC');
}
// This property contains a VCALENDAR with a single VTIMEZONE
/** @var string $timezoneProp */
$timezoneProp = $calendarInfo[$tzProp];
/** @var VObject\Component\VCalendar $vtimezoneObj */
$vtimezoneObj = VObject\Reader::read($timezoneProp);
/** @var VObject\Component\VTimeZone $vtimezone */
$vtimezone = $vtimezoneObj->VTIMEZONE;
return $vtimezone->getTimeZone();
}
}
@@ -0,0 +1,538 @@
<?php
/**
* @copyright 2019, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCA\DAV\Traits\PrincipalProxyTrait;
use OCP\Calendar\Resource\IResourceMetadata;
use OCP\Calendar\Room\IRoomMetadata;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use Sabre\DAV\PropPatch;
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
use function array_intersect;
use function array_map;
use function array_merge;
use function array_unique;
use function array_values;
abstract class AbstractPrincipalBackend implements BackendInterface {
/** @var IDBConnection */
private $db;
/** @var IUserSession */
private $userSession;
/** @var IGroupManager */
private $groupManager;
private LoggerInterface $logger;
/** @var ProxyMapper */
private $proxyMapper;
/** @var string */
private $principalPrefix;
/** @var string */
private $dbTableName;
/** @var string */
private $dbMetaDataTableName;
/** @var string */
private $dbForeignKeyName;
/** @var string */
private $cuType;
public function __construct(IDBConnection $dbConnection,
IUserSession $userSession,
IGroupManager $groupManager,
LoggerInterface $logger,
ProxyMapper $proxyMapper,
string $principalPrefix,
string $dbPrefix,
string $cuType) {
$this->db = $dbConnection;
$this->userSession = $userSession;
$this->groupManager = $groupManager;
$this->logger = $logger;
$this->proxyMapper = $proxyMapper;
$this->principalPrefix = $principalPrefix;
$this->dbTableName = 'calendar_' . $dbPrefix . 's';
$this->dbMetaDataTableName = $this->dbTableName . '_md';
$this->dbForeignKeyName = $dbPrefix . '_id';
$this->cuType = $cuType;
}
use PrincipalProxyTrait;
/**
* Returns a list of principals based on a prefix.
*
* This prefix will often contain something like 'principals'. You are only
* expected to return principals that are in this base path.
*
* You are expected to return at least a 'uri' for every user, you can
* return any additional properties if you wish so. Common properties are:
* {DAV:}displayname
*
* @param string $prefixPath
* @return string[]
*/
public function getPrincipalsByPrefix($prefixPath): array {
$principals = [];
if ($prefixPath === $this->principalPrefix) {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
->from($this->dbTableName);
$stmt = $query->execute();
$metaDataQuery = $this->db->getQueryBuilder();
$metaDataQuery->select([$this->dbForeignKeyName, 'key', 'value'])
->from($this->dbMetaDataTableName);
$metaDataStmt = $metaDataQuery->execute();
$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
$metaDataById = [];
foreach ($metaDataRows as $metaDataRow) {
if (!isset($metaDataById[$metaDataRow[$this->dbForeignKeyName]])) {
$metaDataById[$metaDataRow[$this->dbForeignKeyName]] = [];
}
$metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] =
$metaDataRow['value'];
}
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$id = $row['id'];
if (isset($metaDataById[$id])) {
$principals[] = $this->rowToPrincipal($row, $metaDataById[$id]);
} else {
$principals[] = $this->rowToPrincipal($row);
}
}
$stmt->closeCursor();
}
return $principals;
}
/**
* Returns a specific principal, specified by its path.
* The returned structure should be the exact same as from
* getPrincipalsByPrefix.
*
* @param string $prefixPath
*
* @return array
*/
public function getPrincipalByPath($path) {
if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
[$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
->from($this->dbTableName)
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
$stmt = $query->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$metaDataQuery = $this->db->getQueryBuilder();
$metaDataQuery->select(['key', 'value'])
->from($this->dbMetaDataTableName)
->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
$metaDataStmt = $metaDataQuery->execute();
$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
$metadata = [];
foreach ($metaDataRows as $metaDataRow) {
$metadata[$metaDataRow['key']] = $metaDataRow['value'];
}
return $this->rowToPrincipal($row, $metadata);
}
/**
* @param int $id
* @return string[]|null
*/
public function getPrincipalById($id): ?array {
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
->from($this->dbTableName)
->where($query->expr()->eq('id', $query->createNamedParameter($id)));
$stmt = $query->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
$metaDataQuery = $this->db->getQueryBuilder();
$metaDataQuery->select(['key', 'value'])
->from($this->dbMetaDataTableName)
->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
$metaDataStmt = $metaDataQuery->execute();
$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
$metadata = [];
foreach ($metaDataRows as $metaDataRow) {
$metadata[$metaDataRow['key']] = $metaDataRow['value'];
}
return $this->rowToPrincipal($row, $metadata);
}
/**
* @param string $path
* @param PropPatch $propPatch
* @return int
*/
public function updatePrincipal($path, PropPatch $propPatch): int {
return 0;
}
/**
* @param string $prefixPath
* @param string $test
*
* @return array
*/
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
$results = [];
if (\count($searchProperties) === 0) {
return [];
}
if ($prefixPath !== $this->principalPrefix) {
return [];
}
$user = $this->userSession->getUser();
if (!$user) {
return [];
}
$usersGroups = $this->groupManager->getUserGroupIds($user);
foreach ($searchProperties as $prop => $value) {
switch ($prop) {
case '{http://sabredav.org/ns}email-address':
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
->from($this->dbTableName)
->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
$stmt = $query->execute();
$principals = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
continue;
}
$principals[] = $this->rowToPrincipal($row)['uri'];
}
$results[] = $principals;
$stmt->closeCursor();
break;
case '{DAV:}displayname':
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
->from($this->dbTableName)
->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
$stmt = $query->execute();
$principals = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
continue;
}
$principals[] = $this->rowToPrincipal($row)['uri'];
}
$results[] = $principals;
$stmt->closeCursor();
break;
case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
// If you add support for more search properties that qualify as a user-address,
// please also add them to the array below
$results[] = $this->searchPrincipals($this->principalPrefix, [
'{http://sabredav.org/ns}email-address' => $value,
], 'anyof');
break;
case IRoomMetadata::FEATURES:
$results[] = $this->searchPrincipalsByRoomFeature($prop, $value);
break;
case IRoomMetadata::CAPACITY:
case IResourceMetadata::VEHICLE_SEATING_CAPACITY:
$results[] = $this->searchPrincipalsByCapacity($prop, $value);
break;
default:
$results[] = $this->searchPrincipalsByMetadataKey($prop, $value, $usersGroups);
break;
}
}
// results is an array of arrays, so this is not the first search result
// but the results of the first searchProperty
if (count($results) === 1) {
return $results[0];
}
switch ($test) {
case 'anyof':
return array_values(array_unique(array_merge(...$results)));
case 'allof':
default:
return array_values(array_intersect(...$results));
}
}
/**
* @param string $key
* @return IQueryBuilder
*/
private function getMetadataQuery(string $key): IQueryBuilder {
$query = $this->db->getQueryBuilder();
$query->select([$this->dbForeignKeyName])
->from($this->dbMetaDataTableName)
->where($query->expr()->eq('key', $query->createNamedParameter($key)));
return $query;
}
/**
* Searches principals based on their metadata keys.
* This allows to search for all principals with a specific key.
* e.g.:
* '{http://nextcloud.com/ns}room-building-address' => 'ABC Street 123, ...'
*
* @param string $key
* @param string $value
* @param string[] $usersGroups
* @return string[]
*/
private function searchPrincipalsByMetadataKey(string $key, string $value, array $usersGroups = []): array {
$query = $this->getMetadataQuery($key);
$query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
return $this->getRows($query, $usersGroups);
}
/**
* Searches principals based on room features
* e.g.:
* '{http://nextcloud.com/ns}room-features' => 'TV,PROJECTOR'
*
* @param string $key
* @param string $value
* @param string[] $usersGroups
* @return string[]
*/
private function searchPrincipalsByRoomFeature(string $key, string $value, array $usersGroups = []): array {
$query = $this->getMetadataQuery($key);
foreach (explode(',', $value) as $v) {
$query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($v) . '%')));
}
return $this->getRows($query, $usersGroups);
}
/**
* Searches principals based on room seating capacity or vehicle capacity
* e.g.:
* '{http://nextcloud.com/ns}room-seating-capacity' => '100'
*
* @param string $key
* @param string $value
* @param string[] $usersGroups
* @return string[]
*/
private function searchPrincipalsByCapacity(string $key, string $value, array $usersGroups = []): array {
$query = $this->getMetadataQuery($key);
$query->andWhere($query->expr()->gte('value', $query->createNamedParameter($value)));
return $this->getRows($query, $usersGroups);
}
/**
* @param IQueryBuilder $query
* @param string[] $usersGroups
* @return string[]
*/
private function getRows(IQueryBuilder $query, array $usersGroups): array {
try {
$stmt = $query->executeQuery();
} catch (Exception $e) {
$this->logger->error("Could not search resources: " . $e->getMessage(), ['exception' => $e]);
}
$rows = [];
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$principalRow = $this->getPrincipalById($row[$this->dbForeignKeyName]);
if (!$principalRow) {
continue;
}
$rows[] = $principalRow;
}
$stmt->closeCursor();
$filteredRows = array_filter($rows, function ($row) use ($usersGroups) {
return $this->isAllowedToAccessResource($row, $usersGroups);
});
return array_map(static function ($row): string {
return $row['uri'];
}, $filteredRows);
}
/**
* @param string $uri
* @param string $principalPrefix
* @return null|string
* @throws Exception
*/
public function findByUri($uri, $principalPrefix): ?string {
$user = $this->userSession->getUser();
if (!$user) {
return null;
}
$usersGroups = $this->groupManager->getUserGroupIds($user);
if (str_starts_with($uri, 'mailto:')) {
$email = substr($uri, 7);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
->from($this->dbTableName)
->where($query->expr()->eq('email', $query->createNamedParameter($email)));
$stmt = $query->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
return null;
}
return $this->rowToPrincipal($row)['uri'];
}
if (str_starts_with($uri, 'principal:')) {
$path = substr($uri, 10);
if (!str_starts_with($path, $this->principalPrefix)) {
return null;
}
[, $name] = \Sabre\Uri\split($path);
[$backendId, $resourceId] = explode('-', $name, 2);
$query = $this->db->getQueryBuilder();
$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
->from($this->dbTableName)
->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
$stmt = $query->execute();
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
if (!$row) {
return null;
}
if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
return null;
}
return $this->rowToPrincipal($row)['uri'];
}
return null;
}
/**
* convert database row to principal
*
* @param string[] $row
* @param string[] $metadata
* @return string[]
*/
private function rowToPrincipal(array $row, array $metadata = []): array {
return array_merge([
'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'],
'{DAV:}displayname' => $row['displayname'],
'{http://sabredav.org/ns}email-address' => $row['email'],
'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType,
], $metadata);
}
/**
* @param array $row
* @param array $userGroups
* @return bool
*/
private function isAllowedToAccessResource(array $row, array $userGroups): bool {
if (!isset($row['group_restrictions']) ||
$row['group_restrictions'] === null ||
$row['group_restrictions'] === '') {
return true;
}
// group restrictions contains something, but not parsable, deny access and log warning
$json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR);
if (!\is_array($json)) {
$this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
return false;
}
// empty array => no group restrictions
if (empty($json)) {
return true;
}
return !empty(array_intersect($json, $userGroups));
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
/**
* Class ResourcePrincipalBackend
*
* @package OCA\DAV\CalDAV\ResourceBooking
*/
class ResourcePrincipalBackend extends AbstractPrincipalBackend {
/**
* ResourcePrincipalBackend constructor.
*/
public function __construct(IDBConnection $dbConnection,
IUserSession $userSession,
IGroupManager $groupManager,
LoggerInterface $logger,
ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-resources', 'resource', 'RESOURCE');
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @copyright 2018, Georg Ehrke <oc.list@georgehrke.com>
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\ResourceBooking;
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
/**
* Class RoomPrincipalBackend
*
* @package OCA\DAV\CalDAV\ResourceBooking
*/
class RoomPrincipalBackend extends AbstractPrincipalBackend {
/**
* RoomPrincipalBackend constructor.
*/
public function __construct(IDBConnection $dbConnection,
IUserSession $userSession,
IGroupManager $groupManager,
LoggerInterface $logger,
ProxyMapper $proxyMapper) {
parent::__construct($dbConnection, $userSession, $groupManager, $logger,
$proxyMapper, 'principals/calendar-rooms', 'room', 'ROOM');
}
}
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use function max;
class RetentionService {
public const RETENTION_CONFIG_KEY = 'calendarRetentionObligation';
private const DEFAULT_RETENTION_SECONDS = 30 * 24 * 60 * 60;
/** @var IConfig */
private $config;
/** @var ITimeFactory */
private $time;
/** @var CalDavBackend */
private $calDavBackend;
public function __construct(IConfig $config,
ITimeFactory $time,
CalDavBackend $calDavBackend) {
$this->config = $config;
$this->time = $time;
$this->calDavBackend = $calDavBackend;
}
public function getDuration(): int {
return max(
(int) $this->config->getAppValue(
Application::APP_ID,
self::RETENTION_CONFIG_KEY,
(string) self::DEFAULT_RETENTION_SECONDS
),
0 // Just making sure we don't delete things in the future when a negative number is passed
);
}
public function cleanUp(): void {
$retentionTime = $this->getDuration();
$now = $this->time->getTime();
$calendars = $this->calDavBackend->getDeletedCalendars($now - $retentionTime);
foreach ($calendars as $calendar) {
$this->calDavBackend->deleteCalendar($calendar['id'], true);
}
$objects = $this->calDavBackend->getDeletedCalendarObjects($now - $retentionTime);
foreach ($objects as $object) {
$this->calDavBackend->deleteCalendarObject(
$object['calendarid'],
$object['uri'],
$object['calendartype'],
true
);
}
}
}
@@ -0,0 +1,340 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
* @copyright Copyright (c) 2017, Georg Ehrke
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @copyright 2022 Anna Larch <anna.larch@gmx.net>
*
* @author brad2014 <brad2014@users.noreply.github.com>
* @author Brad Rubenstein <brad@wbr.tech>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Leon Klingele <leon@struktur.de>
* @author Nick Sweeting <git@sweeting.me>
* @author rakekniven <mark.ziegler@rakekniven.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Anna Larch <anna.larch@gmx.net>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\DAV\CalDAV\Schedule;
use OCA\DAV\CalDAV\CalendarObject;
use OCA\DAV\CalDAV\EventComparisonService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IUserManager;
use OCP\Mail\IMailer;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Schedule\IMipPlugin as SabreIMipPlugin;
use Sabre\DAV;
use Sabre\DAV\INode;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
use Sabre\VObject\Reader;
/**
* iMIP handler.
*
* This class is responsible for sending out iMIP messages. iMIP is the
* email-based transport for iTIP. iTIP deals with scheduling operations for
* iCalendar objects.
*
* If you want to customize the email that gets sent out, you can do so by
* extending this class and overriding the sendMessage method.
*
* @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class IMipPlugin extends SabreIMipPlugin {
private ?string $userId;
private IConfig $config;
private IMailer $mailer;
private LoggerInterface $logger;
private ITimeFactory $timeFactory;
private Defaults $defaults;
private IUserManager $userManager;
private ?VCalendar $vCalendar = null;
private IMipService $imipService;
public const MAX_DATE = '2038-01-01';
public const METHOD_REQUEST = 'request';
public const METHOD_REPLY = 'reply';
public const METHOD_CANCEL = 'cancel';
public const IMIP_INDENT = 15; // Enough for the length of all body bullet items, in all languages
private EventComparisonService $eventComparisonService;
public function __construct(IConfig $config,
IMailer $mailer,
LoggerInterface $logger,
ITimeFactory $timeFactory,
Defaults $defaults,
IUserManager $userManager,
$userId,
IMipService $imipService,
EventComparisonService $eventComparisonService) {
parent::__construct('');
$this->userId = $userId;
$this->config = $config;
$this->mailer = $mailer;
$this->logger = $logger;
$this->timeFactory = $timeFactory;
$this->defaults = $defaults;
$this->userManager = $userManager;
$this->imipService = $imipService;
$this->eventComparisonService = $eventComparisonService;
}
public function initialize(DAV\Server $server): void {
parent::initialize($server);
$server->on('beforeWriteContent', [$this, 'beforeWriteContent'], 10);
}
/**
* Check quota before writing content
*
* @param string $uri target file URI
* @param INode $node Sabre Node
* @param resource $data data
* @param bool $modified modified
*/
public function beforeWriteContent($uri, INode $node, $data, $modified): void {
if(!$node instanceof CalendarObject) {
return;
}
/** @var VCalendar $vCalendar */
$vCalendar = Reader::read($node->get());
$this->setVCalendar($vCalendar);
}
/**
* Event handler for the 'schedule' event.
*
* @param Message $iTipMessage
* @return void
*/
public function schedule(Message $iTipMessage) {
// Not sending any emails if the system considers the update
// insignificant.
if (!$iTipMessage->significantChange) {
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
}
return;
}
if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto'
|| parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto') {
return;
}
// don't send out mails for events that already took place
$lastOccurrence = $this->imipService->getLastOccurrence($iTipMessage->message);
$currentTime = $this->timeFactory->getTime();
if ($lastOccurrence < $currentTime) {
return;
}
// Strip off mailto:
$recipient = substr($iTipMessage->recipient, 7);
if (!$this->mailer->validateMailAddress($recipient)) {
// Nothing to send if the recipient doesn't have a valid email address
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
$recipientName = $iTipMessage->recipientName ? (string)$iTipMessage->recipientName : null;
$newEvents = $iTipMessage->message;
$oldEvents = $this->getVCalendar();
$modified = $this->eventComparisonService->findModified($newEvents, $oldEvents);
/** @var VEvent $vEvent */
$vEvent = array_pop($modified['new']);
/** @var VEvent $oldVevent */
$oldVevent = !empty($modified['old']) && is_array($modified['old']) ? array_pop($modified['old']) : null;
$isModified = isset($oldVevent);
// No changed events after all - this shouldn't happen if there is significant change yet here we are
// The scheduling status is debatable
if(empty($vEvent)) {
$this->logger->warning('iTip message said the change was significant but comparison did not detect any updated VEvents');
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
return;
}
// we (should) have one event component left
// as the ITip\Broker creates one iTip message per change
// and triggers the "schedule" event once per message
// we also might not have an old event as this could be a new
// invitation, or a new recurrence exception
$attendee = $this->imipService->getCurrentAttendee($iTipMessage);
if($attendee === null) {
$uid = $vEvent->UID ?? 'no UID found';
$this->logger->debug('Could not find recipient ' . $recipient . ' as attendee for event with UID ' . $uid);
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
// Don't send emails to things
if($this->imipService->isRoomOrResource($attendee)) {
$this->logger->debug('No invitation sent as recipient is room or resource', [
'attendee' => $recipient,
]);
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
return;
}
$this->imipService->setL10n($attendee);
// Build the sender name.
// Due to a bug in sabre, the senderName property for an iTIP message
// can actually also be a VObject Property
/** @var Parameter|string|null $senderName */
$senderName = $iTipMessage->senderName ?: null;
if($senderName instanceof Parameter) {
$senderName = $senderName->getValue() ?? null;
}
// Try to get the sender name from the current user id if available.
if ($this->userId !== null && ($senderName === null || empty(trim($senderName)))) {
$senderName = $this->userManager->getDisplayName($this->userId);
}
$sender = substr($iTipMessage->sender, 7);
$replyingAttendee = null;
switch (strtolower($iTipMessage->method)) {
case self::METHOD_REPLY:
$method = self::METHOD_REPLY;
$data = $this->imipService->buildBodyData($vEvent, $oldVevent);
$replyingAttendee = $this->imipService->getReplyingAttendee($iTipMessage);
break;
case self::METHOD_CANCEL:
$method = self::METHOD_CANCEL;
$data = $this->imipService->buildCancelledBodyData($vEvent);
break;
default:
$method = self::METHOD_REQUEST;
$data = $this->imipService->buildBodyData($vEvent, $oldVevent);
break;
}
$data['attendee_name'] = ($recipientName ?: $recipient);
$data['invitee_name'] = ($senderName ?: $sender);
$fromEMail = Util::getDefaultEmailAddress('invitations-noreply');
$fromName = $this->imipService->getFrom($senderName, $this->defaults->getName());
$message = $this->mailer->createMessage()
->setFrom([$fromEMail => $fromName]);
if ($recipientName !== null) {
$message->setTo([$recipient => $recipientName]);
} else {
$message->setTo([$recipient]);
}
if ($senderName !== null) {
$message->setReplyTo([$sender => $senderName]);
} else {
$message->setReplyTo([$sender]);
}
$template = $this->mailer->createEMailTemplate('dav.calendarInvite.' . $method, $data);
$template->addHeader();
$this->imipService->addSubjectAndHeading($template, $method, $data['invitee_name'], $data['meeting_title'], $isModified, $replyingAttendee);
$this->imipService->addBulletList($template, $vEvent, $data);
// Only add response buttons to invitation requests: Fix Issue #11230
if (strcasecmp($method, self::METHOD_REQUEST) === 0 && $this->imipService->getAttendeeRsvpOrReqForParticipant($attendee)) {
/*
** Only offer invitation accept/reject buttons, which link back to the
** nextcloud server, to recipients who can access the nextcloud server via
** their internet/intranet. Issue #12156
**
** The app setting is stored in the appconfig database table.
**
** For nextcloud servers accessible to the public internet, the default
** "invitation_link_recipients" value "yes" (all recipients) is appropriate.
**
** When the nextcloud server is restricted behind a firewall, accessible
** only via an internal network or via vpn, you can set "dav.invitation_link_recipients"
** to the email address or email domain, or comma separated list of addresses or domains,
** of recipients who can access the server.
**
** To always deliver URLs, set invitation_link_recipients to "yes".
** To suppress URLs entirely, set invitation_link_recipients to boolean "no".
*/
$recipientDomain = substr(strrchr($recipient, '@'), 1);
$invitationLinkRecipients = explode(',', preg_replace('/\s+/', '', strtolower($this->config->getAppValue('dav', 'invitation_link_recipients', 'yes'))));
if (strcmp('yes', $invitationLinkRecipients[0]) === 0
|| in_array(strtolower($recipient), $invitationLinkRecipients)
|| in_array(strtolower($recipientDomain), $invitationLinkRecipients)) {
$token = $this->imipService->createInvitationToken($iTipMessage, $vEvent, $lastOccurrence);
$this->imipService->addResponseButtons($template, $token);
$this->imipService->addMoreOptionsButton($template, $token);
}
}
$template->addFooter();
$message->useTemplate($template);
$itip_msg = $iTipMessage->message->serialize();
$message->attachInline(
$itip_msg,
'event.ics',
'text/calendar; method=' . $iTipMessage->method,
);
try {
$failed = $this->mailer->send($message);
$iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
if (!empty($failed)) {
$this->logger->error('Unable to deliver message to {failed}', ['app' => 'dav', 'failed' => implode(', ', $failed)]);
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage(), ['app' => 'dav', 'exception' => $ex]);
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
}
}
/**
* @return ?VCalendar
*/
public function getVCalendar(): ?VCalendar {
return $this->vCalendar;
}
/**
* @param ?VCalendar $vCalendar
*/
public function setVCalendar(?VCalendar $vCalendar): void {
$this->vCalendar = $vCalendar;
}
}
@@ -0,0 +1,691 @@
<?php
declare(strict_types=1);
/*
* DAV App
*
* @copyright 2022 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\DAV\CalDAV\Schedule;
use OC\URLGenerator;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\L10N\IFactory as L10NFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Security\ISecureRandom;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property;
use Sabre\VObject\Recur\EventIterator;
class IMipService {
private URLGenerator $urlGenerator;
private IConfig $config;
private IDBConnection $db;
private ISecureRandom $random;
private L10NFactory $l10nFactory;
private IL10N $l10n;
/** @var string[] */
private const STRING_DIFF = [
'meeting_title' => 'SUMMARY',
'meeting_description' => 'DESCRIPTION',
'meeting_url' => 'URL',
'meeting_location' => 'LOCATION'
];
public function __construct(URLGenerator $urlGenerator,
IConfig $config,
IDBConnection $db,
ISecureRandom $random,
L10NFactory $l10nFactory) {
$this->urlGenerator = $urlGenerator;
$this->config = $config;
$this->db = $db;
$this->random = $random;
$this->l10nFactory = $l10nFactory;
$default = $this->l10nFactory->findGenericLanguage();
$this->l10n = $this->l10nFactory->get('dav', $default);
}
/**
* @param string|null $senderName
* @param string $default
* @return string
*/
public function getFrom(?string $senderName, string $default): string {
if ($senderName === null) {
return $default;
}
return $this->l10n->t('%1$s via %2$s', [$senderName, $default]);
}
public static function readPropertyWithDefault(VEvent $vevent, string $property, string $default) {
if (isset($vevent->$property)) {
$value = $vevent->$property->getValue();
if (!empty($value)) {
return $value;
}
}
return $default;
}
private function generateDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
$strikethrough = "<span style='text-decoration: line-through'>%s</span><br />%s";
if (!isset($vevent->$property)) {
return $default;
}
$newstring = $vevent->$property->getValue();
if(isset($oldVEvent->$property) && $oldVEvent->$property->getValue() !== $newstring) {
$oldstring = $oldVEvent->$property->getValue();
return sprintf($strikethrough, $oldstring, $newstring);
}
return $newstring;
}
/**
* Like generateDiffString() but linkifies the property values if they are urls.
*/
private function generateLinkifiedDiffString(VEvent $vevent, VEvent $oldVEvent, string $property, string $default): ?string {
if (!isset($vevent->$property)) {
return $default;
}
/** @var string|null $newString */
$newString = $vevent->$property->getValue();
$oldString = isset($oldVEvent->$property) ? $oldVEvent->$property->getValue() : null;
if ($oldString !== $newString) {
return sprintf(
"<span style='text-decoration: line-through'>%s</span><br />%s",
$this->linkify($oldString) ?? $oldString ?? '',
$this->linkify($newString) ?? $newString ?? ''
);
}
return $this->linkify($newString) ?? $newString;
}
/**
* Convert a given url to a html link element or return null otherwise.
*/
private function linkify(?string $url): ?string {
if ($url === null) {
return null;
}
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
return null;
}
return sprintf('<a href="%1$s">%1$s</a>', htmlspecialchars($url));
}
/**
* @param VEvent $vEvent
* @param VEvent|null $oldVEvent
* @return array
*/
public function buildBodyData(VEvent $vEvent, ?VEvent $oldVEvent): array {
$defaultVal = '';
$data = [];
$data['meeting_when'] = $this->generateWhenString($vEvent);
foreach(self::STRING_DIFF as $key => $property) {
$data[$key] = self::readPropertyWithDefault($vEvent, $property, $defaultVal);
}
$data['meeting_url_html'] = self::readPropertyWithDefault($vEvent, 'URL', $defaultVal);
if (($locationHtml = $this->linkify($data['meeting_location'])) !== null) {
$data['meeting_location_html'] = $locationHtml;
}
if(!empty($oldVEvent)) {
$oldMeetingWhen = $this->generateWhenString($oldVEvent);
$data['meeting_title_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'SUMMARY', $data['meeting_title']);
$data['meeting_description_html'] = $this->generateDiffString($vEvent, $oldVEvent, 'DESCRIPTION', $data['meeting_description']);
$data['meeting_location_html'] = $this->generateLinkifiedDiffString($vEvent, $oldVEvent, 'LOCATION', $data['meeting_location']);
$oldUrl = self::readPropertyWithDefault($oldVEvent, 'URL', $defaultVal);
$data['meeting_url_html'] = !empty($oldUrl) && $oldUrl !== $data['meeting_url'] ? sprintf('<a href="%1$s">%1$s</a>', $oldUrl) : $data['meeting_url'];
$data['meeting_when_html'] =
($oldMeetingWhen !== $data['meeting_when'] && $oldMeetingWhen !== null)
? sprintf("<span style='text-decoration: line-through'>%s</span><br />%s", $oldMeetingWhen, $data['meeting_when'])
: $data['meeting_when'];
}
return $data;
}
/**
* @param IL10N $this->l10n
* @param VEvent $vevent
* @return false|int|string
*/
public function generateWhenString(VEvent $vevent) {
/** @var Property\ICalendar\DateTime $dtstart */
$dtstart = $vevent->DTSTART;
if (isset($vevent->DTEND)) {
/** @var Property\ICalendar\DateTime $dtend */
$dtend = $vevent->DTEND;
} elseif (isset($vevent->DURATION)) {
$isFloating = $dtstart->isFloating();
$dtend = clone $dtstart;
$endDateTime = $dtend->getDateTime();
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
$dtend->setDateTime($endDateTime, $isFloating);
} elseif (!$dtstart->hasTime()) {
$isFloating = $dtstart->isFloating();
$dtend = clone $dtstart;
$endDateTime = $dtend->getDateTime();
$endDateTime = $endDateTime->modify('+1 day');
$dtend->setDateTime($endDateTime, $isFloating);
} else {
$dtend = clone $dtstart;
}
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtstart */
/** @var \DateTimeImmutable $dtstartDt */
$dtstartDt = $dtstart->getDateTime();
/** @var Property\ICalendar\Date | Property\ICalendar\DateTime $dtend */
/** @var \DateTimeImmutable $dtendDt */
$dtendDt = $dtend->getDateTime();
$diff = $dtstartDt->diff($dtendDt);
$dtstartDt = new \DateTime($dtstartDt->format(\DateTimeInterface::ATOM));
$dtendDt = new \DateTime($dtendDt->format(\DateTimeInterface::ATOM));
if ($dtstart instanceof Property\ICalendar\Date) {
// One day event
if ($diff->days === 1) {
return $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
}
// DTEND is exclusive, so if the ics data says 2020-01-01 to 2020-01-05,
// the email should show 2020-01-01 to 2020-01-04.
$dtendDt->modify('-1 day');
//event that spans over multiple days
$localeStart = $this->l10n->l('date', $dtstartDt, ['width' => 'medium']);
$localeEnd = $this->l10n->l('date', $dtendDt, ['width' => 'medium']);
return $localeStart . ' - ' . $localeEnd;
}
/** @var Property\ICalendar\DateTime $dtstart */
/** @var Property\ICalendar\DateTime $dtend */
$isFloating = $dtstart->isFloating();
$startTimezone = $endTimezone = null;
if (!$isFloating) {
$prop = $dtstart->offsetGet('TZID');
if ($prop instanceof Parameter) {
$startTimezone = $prop->getValue();
}
$prop = $dtend->offsetGet('TZID');
if ($prop instanceof Parameter) {
$endTimezone = $prop->getValue();
}
}
$localeStart = $this->l10n->l('weekdayName', $dtstartDt, ['width' => 'abbreviated']) . ', ' .
$this->l10n->l('datetime', $dtstartDt, ['width' => 'medium|short']);
// always show full date with timezone if timezones are different
if ($startTimezone !== $endTimezone) {
$localeEnd = $this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
return $localeStart . ' (' . $startTimezone . ') - ' .
$localeEnd . ' (' . $endTimezone . ')';
}
// show only end time if date is the same
if ($dtstartDt->format('Y-m-d') === $dtendDt->format('Y-m-d')) {
$localeEnd = $this->l10n->l('time', $dtendDt, ['width' => 'short']);
} else {
$localeEnd = $this->l10n->l('weekdayName', $dtendDt, ['width' => 'abbreviated']) . ', ' .
$this->l10n->l('datetime', $dtendDt, ['width' => 'medium|short']);
}
return $localeStart . ' - ' . $localeEnd . ' (' . $startTimezone . ')';
}
/**
* @param VEvent $vEvent
* @return array
*/
public function buildCancelledBodyData(VEvent $vEvent): array {
$defaultVal = '';
$strikethrough = "<span style='text-decoration: line-through'>%s</span>";
$newMeetingWhen = $this->generateWhenString($vEvent);
$newSummary = isset($vEvent->SUMMARY) && (string)$vEvent->SUMMARY !== '' ? (string)$vEvent->SUMMARY : $this->l10n->t('Untitled event');
;
$newDescription = isset($vEvent->DESCRIPTION) && (string)$vEvent->DESCRIPTION !== '' ? (string)$vEvent->DESCRIPTION : $defaultVal;
$newUrl = isset($vEvent->URL) && (string)$vEvent->URL !== '' ? sprintf('<a href="%1$s">%1$s</a>', $vEvent->URL) : $defaultVal;
$newLocation = isset($vEvent->LOCATION) && (string)$vEvent->LOCATION !== '' ? (string)$vEvent->LOCATION : $defaultVal;
$newLocationHtml = $this->linkify($newLocation) ?? $newLocation;
$data = [];
$data['meeting_when_html'] = $newMeetingWhen === '' ?: sprintf($strikethrough, $newMeetingWhen);
$data['meeting_when'] = $newMeetingWhen;
$data['meeting_title_html'] = sprintf($strikethrough, $newSummary);
$data['meeting_title'] = $newSummary !== '' ? $newSummary: $this->l10n->t('Untitled event');
$data['meeting_description_html'] = $newDescription !== '' ? sprintf($strikethrough, $newDescription) : '';
$data['meeting_description'] = $newDescription;
$data['meeting_url_html'] = $newUrl !== '' ? sprintf($strikethrough, $newUrl) : '';
$data['meeting_url'] = isset($vEvent->URL) ? (string)$vEvent->URL : '';
$data['meeting_location_html'] = $newLocationHtml !== '' ? sprintf($strikethrough, $newLocationHtml) : '';
$data['meeting_location'] = $newLocation;
return $data;
}
/**
* Check if event took place in the past
*
* @param VCalendar $vObject
* @return int
*/
public function getLastOccurrence(VCalendar $vObject) {
/** @var VEvent $component */
$component = $vObject->VEVENT;
if (isset($component->RRULE)) {
$it = new EventIterator($vObject, (string)$component->UID);
$maxDate = new \DateTime(IMipPlugin::MAX_DATE);
if ($it->isInfinite()) {
return $maxDate->getTimestamp();
}
$end = $it->getDtEnd();
while ($it->valid() && $end < $maxDate) {
$end = $it->getDtEnd();
$it->next();
}
return $end->getTimestamp();
}
/** @var Property\ICalendar\DateTime $dtStart */
$dtStart = $component->DTSTART;
if (isset($component->DTEND)) {
/** @var Property\ICalendar\DateTime $dtEnd */
$dtEnd = $component->DTEND;
return $dtEnd->getDateTime()->getTimeStamp();
}
if(isset($component->DURATION)) {
/** @var \DateTime $endDate */
$endDate = clone $dtStart->getDateTime();
// $component->DTEND->getDateTime() returns DateTimeImmutable
$endDate = $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
return $endDate->getTimestamp();
}
if(!$dtStart->hasTime()) {
/** @var \DateTime $endDate */
// $component->DTSTART->getDateTime() returns DateTimeImmutable
$endDate = clone $dtStart->getDateTime();
$endDate = $endDate->modify('+1 day');
return $endDate->getTimestamp();
}
// No computation of end time possible - return start date
return $dtStart->getDateTime()->getTimeStamp();
}
/**
* @param Property|null $attendee
*/
public function setL10n(?Property $attendee = null) {
if($attendee === null) {
return;
}
$lang = $attendee->offsetGet('LANGUAGE');
if ($lang instanceof Parameter) {
$lang = $lang->getValue();
$this->l10n = $this->l10nFactory->get('dav', $lang);
}
}
/**
* @param Property|null $attendee
* @return bool
*/
public function getAttendeeRsvpOrReqForParticipant(?Property $attendee = null) {
if($attendee === null) {
return false;
}
$rsvp = $attendee->offsetGet('RSVP');
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
return true;
}
$role = $attendee->offsetGet('ROLE');
// @see https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16
// Attendees without a role are assumed required and should receive an invitation link even if they have no RSVP set
if ($role === null
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'REQ-PARTICIPANT') === 0))
|| (($role instanceof Parameter) && (strcasecmp($role->getValue(), 'OPT-PARTICIPANT') === 0))
) {
return true;
}
// RFC 5545 3.2.17: default RSVP is false
return false;
}
/**
* @param IEMailTemplate $template
* @param string $method
* @param string $sender
* @param string $summary
* @param string|null $partstat
* @param bool $isModified
*/
public function addSubjectAndHeading(IEMailTemplate $template,
string $method, string $sender, string $summary, bool $isModified, ?Property $replyingAttendee = null): void {
if ($method === IMipPlugin::METHOD_CANCEL) {
// TRANSLATORS Subject for email, when an invitation is cancelled. Ex: "Cancelled: {{Event Name}}"
$template->setSubject($this->l10n->t('Cancelled: %1$s', [$summary]));
$template->addHeading($this->l10n->t('"%1$s" has been canceled', [$summary]));
} elseif ($method === IMipPlugin::METHOD_REPLY) {
// TRANSLATORS Subject for email, when an invitation is replied to. Ex: "Re: {{Event Name}}"
$template->setSubject($this->l10n->t('Re: %1$s', [$summary]));
// Build the strings
$partstat = (isset($replyingAttendee)) ? $replyingAttendee->offsetGet('PARTSTAT') : null;
$partstat = ($partstat instanceof Parameter) ? $partstat->getValue() : null;
switch ($partstat) {
case 'ACCEPTED':
$template->addHeading($this->l10n->t('%1$s has accepted your invitation', [$sender]));
break;
case 'TENTATIVE':
$template->addHeading($this->l10n->t('%1$s has tentatively accepted your invitation', [$sender]));
break;
case 'DECLINED':
$template->addHeading($this->l10n->t('%1$s has declined your invitation', [$sender]));
break;
case null:
default:
$template->addHeading($this->l10n->t('%1$s has responded to your invitation', [$sender]));
break;
}
} elseif ($method === IMipPlugin::METHOD_REQUEST && $isModified) {
// TRANSLATORS Subject for email, when an invitation is updated. Ex: "Invitation updated: {{Event Name}}"
$template->setSubject($this->l10n->t('Invitation updated: %1$s', [$summary]));
$template->addHeading($this->l10n->t('%1$s updated the event "%2$s"', [$sender, $summary]));
} else {
// TRANSLATORS Subject for email, when an invitation is sent. Ex: "Invitation: {{Event Name}}"
$template->setSubject($this->l10n->t('Invitation: %1$s', [$summary]));
$template->addHeading($this->l10n->t('%1$s would like to invite you to "%2$s"', [$sender, $summary]));
}
}
/**
* @param string $path
* @return string
*/
public function getAbsoluteImagePath($path): string {
return $this->urlGenerator->getAbsoluteURL(
$this->urlGenerator->imagePath('core', $path)
);
}
/**
* addAttendees: add organizer and attendee names/emails to iMip mail.
*
* Enable with DAV setting: invitation_list_attendees (default: no)
*
* The default is 'no', which matches old behavior, and is privacy preserving.
*
* To enable including attendees in invitation emails:
* % php occ config:app:set dav invitation_list_attendees --value yes
*
* @param IEMailTemplate $template
* @param IL10N $this->l10n
* @param VEvent $vevent
* @author brad2014 on github.com
*/
public function addAttendees(IEMailTemplate $template, VEvent $vevent) {
if ($this->config->getAppValue('dav', 'invitation_list_attendees', 'no') === 'no') {
return;
}
if (isset($vevent->ORGANIZER)) {
/** @var Property | Property\ICalendar\CalAddress $organizer */
$organizer = $vevent->ORGANIZER;
$organizerEmail = substr($organizer->getNormalizedValue(), 7);
/** @var string|null $organizerName */
$organizerName = isset($organizer->CN) ? $organizer->CN->getValue() : null;
$organizerHTML = sprintf('<a href="%s">%s</a>',
htmlspecialchars($organizer->getNormalizedValue()),
htmlspecialchars($organizerName ?: $organizerEmail));
$organizerText = sprintf('%s <%s>', $organizerName, $organizerEmail);
if(isset($organizer['PARTSTAT'])) {
/** @var Parameter $partstat */
$partstat = $organizer['PARTSTAT'];
if(strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
$organizerHTML .= ' ✔︎';
$organizerText .= ' ✔︎';
}
}
$template->addBodyListItem($organizerHTML, $this->l10n->t('Organizer:'),
$this->getAbsoluteImagePath('caldav/organizer.png'),
$organizerText, '', IMipPlugin::IMIP_INDENT);
}
$attendees = $vevent->select('ATTENDEE');
if (count($attendees) === 0) {
return;
}
$attendeesHTML = [];
$attendeesText = [];
foreach ($attendees as $attendee) {
$attendeeEmail = substr($attendee->getNormalizedValue(), 7);
$attendeeName = isset($attendee['CN']) ? $attendee['CN']->getValue() : null;
$attendeeHTML = sprintf('<a href="%s">%s</a>',
htmlspecialchars($attendee->getNormalizedValue()),
htmlspecialchars($attendeeName ?: $attendeeEmail));
$attendeeText = sprintf('%s <%s>', $attendeeName, $attendeeEmail);
if (isset($attendee['PARTSTAT'])) {
/** @var Parameter $partstat */
$partstat = $attendee['PARTSTAT'];
if (strcasecmp($partstat->getValue(), 'ACCEPTED') === 0) {
$attendeeHTML .= ' ✔︎';
$attendeeText .= ' ✔︎';
}
}
$attendeesHTML[] = $attendeeHTML;
$attendeesText[] = $attendeeText;
}
$template->addBodyListItem(implode('<br/>', $attendeesHTML), $this->l10n->t('Attendees:'),
$this->getAbsoluteImagePath('caldav/attendees.png'),
implode("\n", $attendeesText), '', IMipPlugin::IMIP_INDENT);
}
/**
* @param IEMailTemplate $template
* @param VEVENT $vevent
* @param $data
*/
public function addBulletList(IEMailTemplate $template, VEvent $vevent, $data) {
$template->addBodyListItem(
$data['meeting_title_html'] ?? $data['meeting_title'], $this->l10n->t('Title:'),
$this->getAbsoluteImagePath('caldav/title.png'), $data['meeting_title'], '', IMipPlugin::IMIP_INDENT);
if ($data['meeting_when'] !== '') {
$template->addBodyListItem($data['meeting_when_html'] ?? $data['meeting_when'], $this->l10n->t('Time:'),
$this->getAbsoluteImagePath('caldav/time.png'), $data['meeting_when'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_location'] !== '') {
$template->addBodyListItem($data['meeting_location_html'] ?? $data['meeting_location'], $this->l10n->t('Location:'),
$this->getAbsoluteImagePath('caldav/location.png'), $data['meeting_location'], '', IMipPlugin::IMIP_INDENT);
}
if ($data['meeting_url'] !== '') {
$template->addBodyListItem($data['meeting_url_html'] ?? $data['meeting_url'], $this->l10n->t('Link:'),
$this->getAbsoluteImagePath('caldav/link.png'), $data['meeting_url'], '', IMipPlugin::IMIP_INDENT);
}
$this->addAttendees($template, $vevent);
/* Put description last, like an email body, since it can be arbitrarily long */
if ($data['meeting_description']) {
$template->addBodyListItem($data['meeting_description_html'] ?? $data['meeting_description'], $this->l10n->t('Description:'),
$this->getAbsoluteImagePath('caldav/description.png'), $data['meeting_description'], '', IMipPlugin::IMIP_INDENT);
}
}
/**
* @param Message $iTipMessage
* @return null|Property
*/
public function getCurrentAttendee(Message $iTipMessage): ?Property {
/** @var VEvent $vevent */
$vevent = $iTipMessage->message->VEVENT;
$attendees = $vevent->select('ATTENDEE');
foreach ($attendees as $attendee) {
/** @var Property $attendee */
if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
return $attendee;
}
}
return null;
}
/**
* @param Message $iTipMessage
* @param VEvent $vevent
* @param int $lastOccurrence
* @return string
*/
public function createInvitationToken(Message $iTipMessage, VEvent $vevent, int $lastOccurrence): string {
$token = $this->random->generate(60, ISecureRandom::CHAR_ALPHANUMERIC);
$attendee = $iTipMessage->recipient;
$organizer = $iTipMessage->sender;
$sequence = $iTipMessage->sequence;
$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ?
$vevent->{'RECURRENCE-ID'}->serialize() : null;
$uid = $vevent->{'UID'};
$query = $this->db->getQueryBuilder();
$query->insert('calendar_invitations')
->values([
'token' => $query->createNamedParameter($token),
'attendee' => $query->createNamedParameter($attendee),
'organizer' => $query->createNamedParameter($organizer),
'sequence' => $query->createNamedParameter($sequence),
'recurrenceid' => $query->createNamedParameter($recurrenceId),
'expiration' => $query->createNamedParameter($lastOccurrence),
'uid' => $query->createNamedParameter($uid)
])
->execute();
return $token;
}
/**
* Create a valid VCalendar object out of the details of
* a VEvent and its associated iTip Message
*
* We do this to filter out all unchanged VEvents
* This is especially important in iTip Messages with recurrences
* and recurrence exceptions
*
* @param Message $iTipMessage
* @param VEvent $vEvent
* @return VCalendar
*/
public function generateVCalendar(Message $iTipMessage, VEvent $vEvent): VCalendar {
$vCalendar = new VCalendar();
$vCalendar->add('METHOD', $iTipMessage->method);
foreach ($iTipMessage->message->getComponents() as $component) {
if ($component instanceof VEvent) {
continue;
}
$vCalendar->add(clone $component);
}
$vCalendar->add($vEvent);
return $vCalendar;
}
/**
* @param IEMailTemplate $template
* @param $token
*/
public function addResponseButtons(IEMailTemplate $template, $token) {
$template->addBodyButtonGroup(
$this->l10n->t('Accept'),
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.accept', [
'token' => $token,
]),
$this->l10n->t('Decline'),
$this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.decline', [
'token' => $token,
])
);
}
public function addMoreOptionsButton(IEMailTemplate $template, $token) {
$moreOptionsURL = $this->urlGenerator->linkToRouteAbsolute('dav.invitation_response.options', [
'token' => $token,
]);
$html = vsprintf('<small><a href="%s">%s</a></small>', [
$moreOptionsURL, $this->l10n->t('More options …')
]);
$text = $this->l10n->t('More options at %s', [$moreOptionsURL]);
$template->addBodyText($html, $text);
}
public function getReplyingAttendee(Message $iTipMessage): ?Property {
/** @var VEvent $vevent */
$vevent = $iTipMessage->message->VEVENT;
$attendees = $vevent->select('ATTENDEE');
foreach ($attendees as $attendee) {
/** @var Property $attendee */
if (strcasecmp($attendee->getValue(), $iTipMessage->sender) === 0) {
return $attendee;
}
}
return null;
}
public function isRoomOrResource(Property $attendee): bool {
$cuType = $attendee->offsetGet('CUTYPE');
if(!$cuType instanceof Parameter) {
return false;
}
$type = $cuType->getValue() ?? 'INDIVIDUAL';
if (\in_array(strtoupper($type), ['RESOURCE', 'ROOM', 'UNKNOWN'], true)) {
// Don't send emails to things
return true;
}
return false;
}
}
@@ -0,0 +1,699 @@
<?php
/**
* @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
* @copyright Copyright (c) 2016, Joas Schilling <coding@schilljs.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Schedule;
use DateTimeZone;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\ICalendar;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Schedule\ISchedulingObject;
use Sabre\DAV\INode;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\Xml\Property\LocalHref;
use Sabre\DAVACL\IACL;
use Sabre\DAVACL\IPrincipal;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject\Component;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\DateTimeParser;
use Sabre\VObject\FreeBusyGenerator;
use Sabre\VObject\ITip;
use Sabre\VObject\ITip\SameOrganizerForAllComponentsException;
use Sabre\VObject\Parameter;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function \Sabre\Uri\split;
class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
/**
* @var IConfig
*/
private $config;
/** @var ITip\Message[] */
private $schedulingResponses = [];
/** @var string|null */
private $pathOfCalendarObjectChange = null;
public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type';
public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL';
private LoggerInterface $logger;
/**
* @param IConfig $config
*/
public function __construct(IConfig $config, LoggerInterface $logger) {
$this->config = $config;
$this->logger = $logger;
}
/**
* Initializes the plugin
*
* @param Server $server
* @return void
*/
public function initialize(Server $server) {
parent::initialize($server);
$server->on('propFind', [$this, 'propFindDefaultCalendarUrl'], 90);
$server->on('afterWriteContent', [$this, 'dispatchSchedulingResponses']);
$server->on('afterCreateFile', [$this, 'dispatchSchedulingResponses']);
}
/**
* Allow manual setting of the object change URL
* to support public write
*
* @param string $path
*/
public function setPathOfCalendarObjectChange(string $path): void {
$this->pathOfCalendarObjectChange = $path;
}
/**
* This method handler is invoked during fetching of properties.
*
* We use this event to add calendar-auto-schedule-specific properties.
*
* @param PropFind $propFind
* @param INode $node
* @return void
*/
public function propFind(PropFind $propFind, INode $node) {
if ($node instanceof IPrincipal) {
// overwrite Sabre/Dav's implementation
$propFind->handle(self::CALENDAR_USER_TYPE, function () use ($node) {
if ($node instanceof IProperties) {
$props = $node->getProperties([self::CALENDAR_USER_TYPE]);
if (isset($props[self::CALENDAR_USER_TYPE])) {
return $props[self::CALENDAR_USER_TYPE];
}
}
return 'INDIVIDUAL';
});
}
parent::propFind($propFind, $node);
}
/**
* Returns a list of addresses that are associated with a principal.
*
* @param string $principal
* @return array
*/
protected function getAddressesForPrincipal($principal) {
$result = parent::getAddressesForPrincipal($principal);
if ($result === null) {
$result = [];
}
return $result;
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param VCalendar $vCal
* @param mixed $calendarPath
* @param mixed $modified
* @param mixed $isNew
*/
public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
// Save the first path we get as a calendar-object-change request
if (!$this->pathOfCalendarObjectChange) {
$this->pathOfCalendarObjectChange = $request->getPath();
}
try {
parent::calendarObjectChange($request, $response, $vCal, $calendarPath, $modified, $isNew);
} catch (SameOrganizerForAllComponentsException $e) {
$this->handleSameOrganizerException($e, $vCal, $calendarPath);
}
}
/**
* @inheritDoc
*/
public function beforeUnbind($path): void {
try {
parent::beforeUnbind($path);
} catch (SameOrganizerForAllComponentsException $e) {
$node = $this->server->tree->getNodeForPath($path);
if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
throw $e;
}
/** @var VCalendar $vCal */
$vCal = Reader::read($node->get());
$this->handleSameOrganizerException($e, $vCal, $path);
}
}
/**
* @inheritDoc
*/
public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
/** @var VEvent|null $vevent */
$vevent = $iTipMessage->message->VEVENT ?? null;
// Strip VALARMs from incoming VEVENT
if ($vevent && isset($vevent->VALARM)) {
$vevent->remove('VALARM');
}
parent::scheduleLocalDelivery($iTipMessage);
// We only care when the message was successfully delivered locally
// Log all possible codes returned from the parent method that mean something went wrong
// 3.7, 3.8, 5.0, 5.2
if ($iTipMessage->scheduleStatus !== '1.2;Message delivered locally') {
$this->logger->debug('Message not delivered locally with status: ' . $iTipMessage->scheduleStatus);
return;
}
// We only care about request. reply and cancel are properly handled
// by parent::scheduleLocalDelivery already
if (strcasecmp($iTipMessage->method, 'REQUEST') !== 0) {
return;
}
// If parent::scheduleLocalDelivery set scheduleStatus to 1.2,
// it means that it was successfully delivered locally.
// Meaning that the ACL plugin is loaded and that a principal
// exists for the given recipient id, no need to double check
/** @var \Sabre\DAVACL\Plugin $aclPlugin */
$aclPlugin = $this->server->getPlugin('acl');
$principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
$calendarUserType = $this->getCalendarUserTypeForPrincipal($principalUri);
if (strcasecmp($calendarUserType, 'ROOM') !== 0 && strcasecmp($calendarUserType, 'RESOURCE') !== 0) {
$this->logger->debug('Calendar user type is room or resource, not processing further');
return;
}
$attendee = $this->getCurrentAttendee($iTipMessage);
if (!$attendee) {
$this->logger->debug('No attendee set for scheduling message');
return;
}
// We only respond when a response was actually requested
$rsvp = $this->getAttendeeRSVP($attendee);
if (!$rsvp) {
$this->logger->debug('No RSVP requested for attendee ' . $attendee->getValue());
return;
}
if (!$vevent) {
$this->logger->debug('No VEVENT set to process on scheduling message');
return;
}
// We don't support autoresponses for recurrencing events for now
if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
$this->logger->debug('VEVENT is a recurring event, autoresponding not supported');
return;
}
$dtstart = $vevent->DTSTART;
$dtend = $this->getDTEndFromVEvent($vevent);
$uid = $vevent->UID->getValue();
$sequence = isset($vevent->SEQUENCE) ? $vevent->SEQUENCE->getValue() : 0;
$recurrenceId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->serialize() : '';
$message = <<<EOF
BEGIN:VCALENDAR
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VEVENT
ATTENDEE;PARTSTAT=%s:%s
ORGANIZER:%s
UID:%s
SEQUENCE:%s
REQUEST-STATUS:2.0;Success
%sEND:VEVENT
END:VCALENDAR
EOF;
if ($this->isAvailableAtTime($attendee->getValue(), $dtstart->getDateTime(), $dtend->getDateTime(), $uid)) {
$partStat = 'ACCEPTED';
} else {
$partStat = 'DECLINED';
}
$vObject = Reader::read(vsprintf($message, [
$partStat,
$iTipMessage->recipient,
$iTipMessage->sender,
$uid,
$sequence,
$recurrenceId
]));
$responseITipMessage = new ITip\Message();
$responseITipMessage->uid = $uid;
$responseITipMessage->component = 'VEVENT';
$responseITipMessage->method = 'REPLY';
$responseITipMessage->sequence = $sequence;
$responseITipMessage->sender = $iTipMessage->recipient;
$responseITipMessage->recipient = $iTipMessage->sender;
$responseITipMessage->message = $vObject;
// We can't dispatch them now already, because the organizers calendar-object
// was not yet created. Hence Sabre/DAV won't find a calendar-object, when we
// send our reply.
$this->schedulingResponses[] = $responseITipMessage;
}
/**
* @param string $uri
*/
public function dispatchSchedulingResponses(string $uri):void {
if ($uri !== $this->pathOfCalendarObjectChange) {
return;
}
foreach ($this->schedulingResponses as $schedulingResponse) {
$this->scheduleLocalDelivery($schedulingResponse);
}
}
/**
* Always use the personal calendar as target for scheduled events
*
* @param PropFind $propFind
* @param INode $node
* @return void
*/
public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) {
if ($node instanceof IPrincipal) {
$propFind->handle(self::SCHEDULE_DEFAULT_CALENDAR_URL, function () use ($node) {
/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
$caldavPlugin = $this->server->getPlugin('caldav');
$principalUrl = $node->getPrincipalUrl();
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
if (!$calendarHomePath) {
return null;
}
$isResourceOrRoom = str_starts_with($principalUrl, 'principals/calendar-resources') ||
str_starts_with($principalUrl, 'principals/calendar-rooms');
if (str_starts_with($principalUrl, 'principals/users')) {
[, $userId] = split($principalUrl);
$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
$displayName = CalDavBackend::PERSONAL_CALENDAR_NAME;
} elseif ($isResourceOrRoom) {
$uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI;
$displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME;
} else {
// How did we end up here?
// TODO - throw exception or just ignore?
return null;
}
/** @var CalendarHome $calendarHome */
$calendarHome = $this->server->tree->getNodeForPath($calendarHomePath);
$currentCalendarDeleted = false;
if (!$calendarHome->childExists($uri) || $currentCalendarDeleted = $this->isCalendarDeleted($calendarHome, $uri)) {
// If the default calendar doesn't exist
if ($isResourceOrRoom) {
// Resources or rooms can't be in the trashbin, so we're fine
$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
} else {
// And we're not handling scheduling on resource/room booking
$userCalendars = [];
/**
* If the default calendar of the user isn't set and the
* fallback doesn't match any of the user's calendar
* try to find the first "personal" calendar we can write to
* instead of creating a new one.
* A appropriate personal calendar to receive invites:
* - isn't a calendar subscription
* - user can write to it (no virtual/3rd-party calendars)
* - calendar isn't a share
*/
foreach ($calendarHome->getChildren() as $node) {
if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) {
$userCalendars[] = $node;
}
}
if (count($userCalendars) > 0) {
// Calendar backend returns calendar by calendarorder property
$uri = $userCalendars[0]->getName();
} else {
// Otherwise if we have really nothing, create a new calendar
if ($currentCalendarDeleted) {
// If the calendar exists but is deleted, we need to purge it first
// This may cause some issues in a non synchronous database setup
$calendar = $this->getCalendar($calendarHome, $uri);
if ($calendar instanceof Calendar) {
$calendar->disableTrashbin();
$calendar->delete();
}
}
$this->createCalendar($calendarHome, $principalUrl, $uri, $displayName);
}
}
}
$result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1);
if (empty($result)) {
return null;
}
return new LocalHref($result[0]['href']);
});
}
}
/**
* Returns a list of addresses that are associated with a principal.
*
* @param string $principal
* @return string|null
*/
protected function getCalendarUserTypeForPrincipal($principal):?string {
$calendarUserType = '{' . self::NS_CALDAV . '}calendar-user-type';
$properties = $this->server->getProperties(
$principal,
[$calendarUserType]
);
// If we can't find this information, we'll stop processing
if (!isset($properties[$calendarUserType])) {
return null;
}
return $properties[$calendarUserType];
}
/**
* @param ITip\Message $iTipMessage
* @return null|Property
*/
private function getCurrentAttendee(ITip\Message $iTipMessage):?Property {
/** @var VEvent $vevent */
$vevent = $iTipMessage->message->VEVENT;
$attendees = $vevent->select('ATTENDEE');
foreach ($attendees as $attendee) {
/** @var Property $attendee */
if (strcasecmp($attendee->getValue(), $iTipMessage->recipient) === 0) {
return $attendee;
}
}
return null;
}
/**
* @param Property|null $attendee
* @return bool
*/
private function getAttendeeRSVP(Property $attendee = null):bool {
if ($attendee !== null) {
$rsvp = $attendee->offsetGet('RSVP');
if (($rsvp instanceof Parameter) && (strcasecmp($rsvp->getValue(), 'TRUE') === 0)) {
return true;
}
}
// RFC 5545 3.2.17: default RSVP is false
return false;
}
/**
* @param VEvent $vevent
* @return Property\ICalendar\DateTime
*/
private function getDTEndFromVEvent(VEvent $vevent):Property\ICalendar\DateTime {
if (isset($vevent->DTEND)) {
return $vevent->DTEND;
}
if (isset($vevent->DURATION)) {
$isFloating = $vevent->DTSTART->isFloating();
/** @var Property\ICalendar\DateTime $end */
$end = clone $vevent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->add(DateTimeParser::parse($vevent->DURATION->getValue()));
$end->setDateTime($endDateTime, $isFloating);
return $end;
}
if (!$vevent->DTSTART->hasTime()) {
$isFloating = $vevent->DTSTART->isFloating();
/** @var Property\ICalendar\DateTime $end */
$end = clone $vevent->DTSTART;
$endDateTime = $end->getDateTime();
$endDateTime = $endDateTime->modify('+1 day');
$end->setDateTime($endDateTime, $isFloating);
return $end;
}
return clone $vevent->DTSTART;
}
/**
* @param string $email
* @param \DateTimeInterface $start
* @param \DateTimeInterface $end
* @param string $ignoreUID
* @return bool
*/
private function isAvailableAtTime(string $email, \DateTimeInterface $start, \DateTimeInterface $end, string $ignoreUID):bool {
// This method is heavily inspired by Sabre\CalDAV\Schedule\Plugin::scheduleLocalDelivery
// and Sabre\CalDAV\Schedule\Plugin::getFreeBusyForEmail
$aclPlugin = $this->server->getPlugin('acl');
$this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
$result = $aclPlugin->principalSearch(
['{http://sabredav.org/ns}email-address' => $this->stripOffMailTo($email)],
[
'{DAV:}principal-URL',
'{' . self::NS_CALDAV . '}calendar-home-set',
'{' . self::NS_CALDAV . '}schedule-inbox-URL',
'{http://sabredav.org/ns}email-address',
]
);
$this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
// Grabbing the calendar list
$objects = [];
$calendarTimeZone = new DateTimeZone('UTC');
$homePath = $result[0][200]['{' . self::NS_CALDAV . '}calendar-home-set']->getHref();
foreach ($this->server->tree->getNodeForPath($homePath)->getChildren() as $node) {
if (!$node instanceof ICalendar) {
continue;
}
// Getting the list of object uris within the time-range
$urls = $node->calendarQuery([
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => 'VEVENT',
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
'comp-filters' => [],
'prop-filters' => [],
],
[
'name' => 'VEVENT',
'is-not-defined' => false,
'time-range' => null,
'comp-filters' => [],
'prop-filters' => [
[
'name' => 'UID',
'is-not-defined' => false,
'time-range' => null,
'text-match' => [
'value' => $ignoreUID,
'negate-condition' => true,
'collation' => 'i;octet',
],
'param-filters' => [],
],
]
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
foreach ($urls as $url) {
$objects[] = $node->getChild($url)->get();
}
}
$inboxProps = $this->server->getProperties(
$result[0][200]['{' . self::NS_CALDAV . '}schedule-inbox-URL']->getHref(),
['{' . self::NS_CALDAV . '}calendar-availability']
);
$vcalendar = new VCalendar();
$vcalendar->METHOD = 'REPLY';
$generator = new FreeBusyGenerator();
$generator->setObjects($objects);
$generator->setTimeRange($start, $end);
$generator->setBaseObject($vcalendar);
$generator->setTimeZone($calendarTimeZone);
if (isset($inboxProps['{' . self::NS_CALDAV . '}calendar-availability'])) {
$generator->setVAvailability(
Reader::read(
$inboxProps['{' . self::NS_CALDAV . '}calendar-availability']
)
);
}
$result = $generator->getResult();
if (!isset($result->VFREEBUSY)) {
return false;
}
/** @var Component $freeBusyComponent */
$freeBusyComponent = $result->VFREEBUSY;
$freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
// If there is no Free-busy property at all, the time-range is empty and available
if (count($freeBusyProperties) === 0) {
return true;
}
// If more than one Free-Busy property was returned, it means that an event
// starts or ends inside this time-range, so it's not available and we return false
if (count($freeBusyProperties) > 1) {
return false;
}
/** @var Property $freeBusyProperty */
$freeBusyProperty = $freeBusyProperties[0];
if (!$freeBusyProperty->offsetExists('FBTYPE')) {
// If there is no FBTYPE, it means it's busy
return false;
}
$fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
if (!($fbTypeParameter instanceof Parameter)) {
return false;
}
return (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);
}
/**
* @param string $email
* @return string
*/
private function stripOffMailTo(string $email): string {
if (stripos($email, 'mailto:') === 0) {
return substr($email, 7);
}
return $email;
}
private function getCalendar(CalendarHome $calendarHome, string $uri): INode {
return $calendarHome->getChild($uri);
}
private function isCalendarDeleted(CalendarHome $calendarHome, string $uri): bool {
$calendar = $this->getCalendar($calendarHome, $uri);
return $calendar instanceof Calendar && $calendar->isDeleted();
}
private function createCalendar(CalendarHome $calendarHome, string $principalUri, string $uri, string $displayName): void {
$calendarHome->getCalDAVBackend()->createCalendar($principalUri, $uri, [
'{DAV:}displayname' => $displayName,
]);
}
/**
* Try to handle the given exception gracefully or throw it if necessary.
*
* @throws SameOrganizerForAllComponentsException If the exception should not be ignored
*/
private function handleSameOrganizerException(
SameOrganizerForAllComponentsException $e,
VCalendar $vCal,
string $calendarPath,
): void {
// This is very hacky! However, we want to allow saving events with multiple
// organizers. Those events are not RFC compliant, but sometimes imported from major
// external calendar services (e.g. Google). If the current user is not an organizer of
// the event we ignore the exception as no scheduling messages will be sent anyway.
// It would be cleaner to patch Sabre to validate organizers *after* checking if
// scheduling messages are necessary. Currently, organizers are validated first and
// afterwards the broker checks if messages should be scheduled. So the code will throw
// even if the organizers are not relevant. This is to ensure compliance with RFCs but
// a bit too strict for real world usage.
if (!isset($vCal->VEVENT)) {
throw $e;
}
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
if (!($calendarNode instanceof IACL)) {
// Should always be an instance of IACL but just to be sure
throw $e;
}
$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
foreach ($vCal->VEVENT as $vevent) {
if (in_array($vevent->ORGANIZER->getNormalizedValue(), $addresses, true)) {
// User is an organizer => throw the exception
throw $e;
}
}
}
}
@@ -0,0 +1,165 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Joas Schilling <coding@schilljs.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\Search\Xml\Request\CalendarSearchReport;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
class SearchPlugin extends ServerPlugin {
public const NS_Nextcloud = 'http://nextcloud.com/ns';
/**
* Reference to SabreDAV server object.
*
* @var \Sabre\DAV\Server
*/
protected $server;
/**
* This method should return a list of server-features.
*
* This is for example 'versioning' and is added to the DAV: header
* in an OPTIONS response.
*
* @return string[]
*/
public function getFeatures() {
// May have to be changed to be detected
return ['nc-calendar-search'];
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using Sabre\DAV\Server::getPlugin
*
* @return string
*/
public function getPluginName() {
return 'nc-calendar-search';
}
/**
* This initializes the plugin.
*
* This function is called by Sabre\DAV\Server, after
* addPlugin is called.
*
* This method should set up the required event subscriptions.
*
* @param Server $server
*/
public function initialize(Server $server) {
$this->server = $server;
$server->on('report', [$this, 'report']);
$server->xml->elementMap['{' . self::NS_Nextcloud . '}calendar-search'] =
CalendarSearchReport::class;
}
/**
* This functions handles REPORT requests specific to CalDAV
*
* @param string $reportName
* @param mixed $report
* @param mixed $path
* @return bool
*/
public function report($reportName, $report, $path) {
switch ($reportName) {
case '{' . self::NS_Nextcloud . '}calendar-search':
$this->server->transactionType = 'report-nc-calendar-search';
$this->calendarSearch($report);
return false;
}
}
/**
* Returns a list of reports this plugin supports.
*
* This will be used in the {DAV:}supported-report-set property.
* Note that you still need to subscribe to the 'report' event to actually
* implement them
*
* @param string $uri
* @return array
*/
public function getSupportedReportSet($uri) {
$node = $this->server->tree->getNodeForPath($uri);
$reports = [];
if ($node instanceof CalendarHome) {
$reports[] = '{' . self::NS_Nextcloud . '}calendar-search';
}
return $reports;
}
/**
* This function handles the calendar-query REPORT
*
* This report is used by clients to request calendar objects based on
* complex conditions.
*
* @param Xml\Request\CalendarSearchReport $report
* @return void
*/
private function calendarSearch($report) {
$node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
$depth = $this->server->getHTTPDepth(2);
// The default result is an empty array
$result = [];
// If we're dealing with the calendar home, the calendar home itself is
// responsible for the calendar-query
if ($node instanceof CalendarHome && $depth === 2) {
$nodePaths = $node->calendarSearch($report->filters, $report->limit, $report->offset);
foreach ($nodePaths as $path) {
[$properties] = $this->server->getPropertiesForPath(
$this->server->getRequestUri() . '/' . $path,
$report->properties);
$result[] = $properties;
}
}
$prefer = $this->server->getHTTPPrefer();
$this->server->httpResponse->setStatus(207);
$this->server->httpResponse->setHeader('Content-Type',
'application/xml; charset=utf-8');
$this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
$this->server->httpResponse->setBody(
$this->server->generateMultiStatus($result,
$prefer['return'] === 'minimal'));
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class CompFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return string
*/
public static function xmlDeserialize(Reader $reader) {
$att = $reader->parseAttributes();
$componentName = $att['name'];
$reader->parseInnerTree();
if (!is_string($componentName)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}comp-filter requires a valid name attribute');
}
return $componentName;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class LimitFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return int
*/
public static function xmlDeserialize(Reader $reader) {
$value = $reader->parseInnerTree();
if (!is_int($value) && !is_string($value)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}limit has illegal value');
}
return (int)$value;
}
}
@@ -0,0 +1,48 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class OffsetFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return int
*/
public static function xmlDeserialize(Reader $reader) {
$value = $reader->parseInnerTree();
if (!is_int($value) && !is_string($value)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}offset has illegal value');
}
return (int)$value;
}
}
@@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class ParamFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return string
*/
public static function xmlDeserialize(Reader $reader) {
$att = $reader->parseAttributes();
$property = $att['property'];
$parameter = $att['name'];
$reader->parseInnerTree();
if (!is_string($property)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}param-filter requires a valid property attribute');
}
if (!is_string($parameter)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}param-filter requires a valid parameter attribute');
}
return [
'property' => $property,
'parameter' => $parameter,
];
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class PropFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return string
*/
public static function xmlDeserialize(Reader $reader) {
$att = $reader->parseAttributes();
$componentName = $att['name'];
$reader->parseInnerTree();
if (!is_string($componentName)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}prop-filter requires a valid name attribute');
}
return $componentName;
}
}
@@ -0,0 +1,47 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Filter;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
class SearchTermFilter implements XmlDeserializable {
/**
* @param Reader $reader
* @throws BadRequest
* @return string
*/
public static function xmlDeserialize(Reader $reader) {
$value = $reader->parseInnerTree();
if (!is_string($value)) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}search-term has illegal value');
}
return $value;
}
}
@@ -0,0 +1,171 @@
<?php
/**
* @copyright Copyright (c) 2017 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Search\Xml\Request;
use OCA\DAV\CalDAV\Search\SearchPlugin;
use Sabre\DAV\Exception\BadRequest;
use Sabre\Xml\Reader;
use Sabre\Xml\XmlDeserializable;
/**
* CalendarSearchReport request parser.
*
* This class parses the {urn:ietf:params:xml:ns:caldav}calendar-query
* REPORT, as defined in:
*
* https:// link to standard
*/
class CalendarSearchReport implements XmlDeserializable {
/**
* An array with requested properties.
*
* @var array
*/
public $properties;
/**
* List of property/component filters.
*
* @var array
*/
public $filters;
/**
* @var int
*/
public $limit;
/**
* @var int
*/
public $offset;
/**
* The deserialize method is called during xml parsing.
*
* This method is called statically, this is because in theory this method
* may be used as a type of constructor, or factory method.
*
* Often you want to return an instance of the current class, but you are
* free to return other data as well.
*
* You are responsible for advancing the reader to the next element. Not
* doing anything will result in a never-ending loop.
*
* If you just want to skip parsing for this element altogether, you can
* just call $reader->next();
*
* $reader->parseInnerTree() will parse the entire sub-tree, and advance to
* the next element.
*
* @param Reader $reader
* @return mixed
*/
public static function xmlDeserialize(Reader $reader) {
$elems = $reader->parseInnerTree([
'{http://nextcloud.com/ns}comp-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\CompFilter',
'{http://nextcloud.com/ns}prop-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter',
'{http://nextcloud.com/ns}param-filter' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\ParamFilter',
'{http://nextcloud.com/ns}search-term' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter',
'{http://nextcloud.com/ns}limit' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\LimitFilter',
'{http://nextcloud.com/ns}offset' => 'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\OffsetFilter',
'{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue',
]);
$newProps = [
'filters' => [],
'properties' => [],
'limit' => null,
'offset' => null
];
if (!is_array($elems)) {
$elems = [];
}
foreach ($elems as $elem) {
switch ($elem['name']) {
case '{DAV:}prop':
$newProps['properties'] = array_keys($elem['value']);
break;
case '{' . SearchPlugin::NS_Nextcloud . '}filter':
foreach ($elem['value'] as $subElem) {
if ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}comp-filter') {
if (!isset($newProps['filters']['comps']) || !is_array($newProps['filters']['comps'])) {
$newProps['filters']['comps'] = [];
}
$newProps['filters']['comps'][] = $subElem['value'];
} elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}prop-filter') {
if (!isset($newProps['filters']['props']) || !is_array($newProps['filters']['props'])) {
$newProps['filters']['props'] = [];
}
$newProps['filters']['props'][] = $subElem['value'];
} elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}param-filter') {
if (!isset($newProps['filters']['params']) || !is_array($newProps['filters']['params'])) {
$newProps['filters']['params'] = [];
}
$newProps['filters']['params'][] = $subElem['value'];
} elseif ($subElem['name'] === '{' . SearchPlugin::NS_Nextcloud . '}search-term') {
$newProps['filters']['search-term'] = $subElem['value'];
}
}
break;
case '{' . SearchPlugin::NS_Nextcloud . '}limit':
$newProps['limit'] = $elem['value'];
break;
case '{' . SearchPlugin::NS_Nextcloud . '}offset':
$newProps['offset'] = $elem['value'];
break;
}
}
if (empty($newProps['filters'])) {
throw new BadRequest('The {' . SearchPlugin::NS_Nextcloud . '}filter element is required for this request');
}
$propsOrParamsDefined = (!empty($newProps['filters']['props']) || !empty($newProps['filters']['params']));
$noCompsDefined = empty($newProps['filters']['comps']);
if ($propsOrParamsDefined && $noCompsDefined) {
throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}prop-filter or {' . SearchPlugin::NS_Nextcloud . '}param-filter given without any {' . SearchPlugin::NS_Nextcloud . '}comp-filter');
}
if (!isset($newProps['filters']['search-term'])) {
throw new BadRequest('{' . SearchPlugin::NS_Nextcloud . '}search-term is required for this request');
}
if (empty($newProps['filters']['props']) && empty($newProps['filters']['params'])) {
throw new BadRequest('At least one{' . SearchPlugin::NS_Nextcloud . '}prop-filter or {' . SearchPlugin::NS_Nextcloud . '}param-filter is required for this request');
}
$obj = new self();
foreach ($newProps as $key => $value) {
$obj->$key = $value;
}
return $obj;
}
}
@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\DAV\CalDAV\Security;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OC\Security\RateLimiting\Limiter;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
use OCP\IConfig;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ServerPlugin;
use function count;
use function explode;
class RateLimitingPlugin extends ServerPlugin {
private Limiter $limiter;
private IUserManager $userManager;
private CalDavBackend $calDavBackend;
private IConfig $config;
private LoggerInterface $logger;
private ?string $userId;
public function __construct(Limiter $limiter,
IUserManager $userManager,
CalDavBackend $calDavBackend,
LoggerInterface $logger,
IConfig $config,
?string $userId) {
$this->limiter = $limiter;
$this->userManager = $userManager;
$this->calDavBackend = $calDavBackend;
$this->config = $config;
$this->logger = $logger;
$this->userId = $userId;
}
public function initialize(DAV\Server $server): void {
$server->on('beforeBind', [$this, 'beforeBind'], 1);
}
public function beforeBind(string $path): void {
if ($this->userId === null) {
// We only care about authenticated users here
return;
}
$user = $this->userManager->get($this->userId);
if ($user === null) {
// We only care about authenticated users here
return;
}
$pathParts = explode('/', $path);
if (count($pathParts) === 3 && $pathParts[0] === 'calendars') {
// Path looks like calendars/username/calendarname so a new calendar or subscription is created
try {
$this->limiter->registerUserRequest(
'caldav-create-calendar',
(int) $this->config->getAppValue('dav', 'rateLimitCalendarCreation', '10'),
(int) $this->config->getAppValue('dav', 'rateLimitPeriodCalendarCreation', '3600'),
$user
);
} catch (RateLimitExceededException $e) {
throw new TooManyRequests('Too many calendars created', 0, $e);
}
$calendarLimit = (int) $this->config->getAppValue('dav', 'maximumCalendarsSubscriptions', '30');
if ($calendarLimit === -1) {
return;
}
$numCalendars = $this->calDavBackend->getCalendarsForUserCount('principals/users/' . $user->getUID());
$numSubscriptions = $this->calDavBackend->getSubscriptionsForUserCount('principals/users/' . $user->getUID());
if (($numCalendars + $numSubscriptions) >= $calendarLimit) {
$this->logger->warning('Maximum number of calendars/subscriptions reached', [
'calendars' => $numCalendars,
'subscription' => $numSubscriptions,
'limit' => $calendarLimit,
]);
throw new Forbidden('Calendar limit reached', 0);
}
}
}
}
@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Status;
use DateTimeImmutable;
use OC\Calendar\CalendarQuery;
use OCA\DAV\CalDAV\CalendarImpl;
use OCA\UserStatus\Service\StatusService as UserStatusService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\IManager;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUser as User;
use OCP\IUserManager;
use OCP\User\IAvailabilityCoordinator;
use OCP\UserStatus\IUserStatus;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
class StatusService {
private ICache $cache;
public function __construct(private ITimeFactory $timeFactory,
private IManager $calendarManager,
private IUserManager $userManager,
private UserStatusService $userStatusService,
private IAvailabilityCoordinator $availabilityCoordinator,
private ICacheFactory $cacheFactory,
private LoggerInterface $logger) {
$this->cache = $cacheFactory->createLocal('CalendarStatusService');
}
public function processCalendarStatus(string $userId): void {
$user = $this->userManager->get($userId);
if($user === null) {
return;
}
$availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
$this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]);
return;
}
$calendarEvents = $this->cache->get($userId);
if($calendarEvents === null) {
$calendarEvents = $this->getCalendarEvents($user);
$this->cache->set($userId, $calendarEvents, 300);
}
if(empty($calendarEvents)) {
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
$this->logger->debug('No calendar events found for status check', ['user' => $userId]);
return;
}
try {
$currentStatus = $this->userStatusService->findByUserId($userId);
// Was the status set by anything other than the calendar automation?
$userStatusTimestamp = $currentStatus->getIsUserDefined() && $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY ? $currentStatus->getStatusTimestamp() : null;
} catch (DoesNotExistException) {
$userStatusTimestamp = null;
$currentStatus = null;
}
if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL
|| $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND
|| $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) {
// We don't overwrite the call status, DND status or Invisible status
$this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]);
return;
}
// Filter events to see if we have any that apply to the calendar status
$applicableEvents = array_filter($calendarEvents, static function (array $calendarEvent) use ($userStatusTimestamp): bool {
if (empty($calendarEvent['objects'])) {
return false;
}
$component = $calendarEvent['objects'][0];
if (isset($component['X-NEXTCLOUD-OUT-OF-OFFICE'])) {
return false;
}
if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
/** @var DateTimeImmutable $dateTime */
$dateTime = $component['DTSTART'][0];
if($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
return false;
}
}
// Ignore events that are transparent
if (isset($component['TRANSP']) && strcasecmp($component['TRANSP'][0], 'TRANSPARENT') === 0) {
return false;
}
return true;
});
if(empty($applicableEvents)) {
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
$this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
return;
}
// Only update the status if it's neccesary otherwise we mess up the timestamp
if($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
// One event that fulfills all status conditions is enough
// 1. Not an OOO event
// 2. Current user status (that is not a calendar status) was not set after the start of this event
// 3. Event is not set to be transparent
$count = count($applicableEvents);
$this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
$this->userStatusService->setUserStatus(
$userId,
IUserStatus::AWAY,
IUserStatus::MESSAGE_CALENDAR_BUSY,
true
);
}
}
private function getCalendarEvents(User $user): array {
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
if(empty($calendars)) {
return [];
}
$query = $this->calendarManager->newQuery('principals/users/' . $user->getUID());
foreach ($calendars as $calendarObject) {
// We can only work with a calendar if it exposes its scheduling information
if (!$calendarObject instanceof CalendarImpl) {
continue;
}
$sct = $calendarObject->getSchedulingTransparency();
if ($sct !== null && ScheduleCalendarTransp::TRANSPARENT == strtolower($sct->getValue())) {
// If a calendar is marked as 'transparent', it means we must
// ignore it for free-busy purposes.
continue;
}
$query->addSearchCalendar($calendarObject->getUri());
}
$dtStart = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime());
$dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes'));
// Only query the calendars when there's any to search
if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
// Query the next hour
$query->setTimerangeStart($dtStart);
$query->setTimerangeEnd($dtEnd);
return $this->calendarManager->searchForPrincipal($query);
}
return [];
}
}
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/*
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace OCA\DAV\CalDAV;
use OCA\DAV\Db\PropertyMapper;
use OCP\Calendar\ICalendar;
use OCP\Calendar\IManager;
use OCP\IConfig;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\Reader;
use function array_reduce;
class TimezoneService {
public function __construct(private IConfig $config,
private PropertyMapper $propertyMapper,
private IManager $calendarManager) {
}
public function getUserTimezone(string $userId): ?string {
$fromConfig = $this->config->getUserValue(
$userId,
'core',
'timezone',
);
if ($fromConfig !== '') {
return $fromConfig;
}
$availabilityPropPath = 'calendars/' . $userId . '/inbox';
$availabilityProp = '{' . Plugin::NS_CALDAV . '}calendar-availability';
$availabilities = $this->propertyMapper->findPropertyByPathAndName($userId, $availabilityPropPath, $availabilityProp);
if (!empty($availabilities)) {
$availability = $availabilities[0]->getPropertyvalue();
/** @var VCalendar $vCalendar */
$vCalendar = Reader::read($availability);
/** @var VTimeZone $vTimezone */
$vTimezone = $vCalendar->VTIMEZONE;
// Sabre has a fallback to date_default_timezone_get
return $vTimezone->getTimeZone()->getName();
}
$principal = 'principals/users/' . $userId;
$uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
$calendars = $this->calendarManager->getCalendarsForPrincipal($principal);
/** @var ?VTimeZone $personalCalendarTimezone */
$personalCalendarTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) use ($uri) {
if ($acc !== null) {
return $acc;
}
if ($calendar->getUri() === $uri && !$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
return $calendar->getSchedulingTimezone();
}
return null;
});
if ($personalCalendarTimezone !== null) {
return $personalCalendarTimezone->getTimeZone()->getName();
}
// No timezone in the personalCalendarTimezone calendar or no personalCalendarTimezone calendar
// Loop through all calendars until we find a timezone.
/** @var ?VTimeZone $firstTimezone */
$firstTimezone = array_reduce($calendars, function (?VTimeZone $acc, ICalendar $calendar) {
if ($acc !== null) {
return $acc;
}
if (!$calendar->isDeleted() && $calendar instanceof CalendarImpl) {
return $calendar->getSchedulingTimezone();
}
return null;
});
if ($firstTimezone !== null) {
return $firstTimezone->getTimeZone()->getName();
}
return null;
}
public function getDefaultTimezone(): string {
return $this->config->getSystemValueString('default_timezone', 'UTC');
}
}
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Trashbin;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\IRestorable;
use Sabre\CalDAV\ICalendarObject;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAVACL\ACLTrait;
use Sabre\DAVACL\IACL;
class DeletedCalendarObject implements IACL, ICalendarObject, IRestorable {
use ACLTrait;
/** @var string */
private $name;
/** @var mixed[] */
private $objectData;
/** @var string */
private $principalUri;
/** @var CalDavBackend */
private $calDavBackend;
public function __construct(string $name,
array $objectData,
string $principalUri,
CalDavBackend $calDavBackend) {
$this->name = $name;
$this->objectData = $objectData;
$this->calDavBackend = $calDavBackend;
$this->principalUri = $principalUri;
}
public function delete() {
$this->calDavBackend->deleteCalendarObject(
$this->objectData['calendarid'],
$this->objectData['uri'],
CalDavBackend::CALENDAR_TYPE_CALENDAR,
true
);
}
public function getName() {
return $this->name;
}
public function setName($name) {
throw new Forbidden();
}
public function getLastModified() {
return 0;
}
public function put($data) {
throw new Forbidden();
}
public function get() {
return $this->objectData['calendardata'];
}
public function getContentType() {
$mime = 'text/calendar; charset=utf-8';
if (isset($this->objectData['component']) && $this->objectData['component']) {
$mime .= '; component='.$this->objectData['component'];
}
return $mime;
}
public function getETag() {
return $this->objectData['etag'];
}
public function getSize() {
return (int) $this->objectData['size'];
}
public function restore(): void {
$this->calDavBackend->restoreCalendarObject($this->objectData);
}
public function getDeletedAt(): ?int {
return $this->objectData['deleted_at'] ? (int) $this->objectData['deleted_at'] : null;
}
public function getCalendarUri(): string {
return $this->objectData['calendaruri'];
}
public function getACL(): array {
return [
[
'privilege' => '{DAV:}read', // For queries
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}unbind', // For moving and deletion
'principal' => '{DAV:}owner',
'protected' => true,
],
];
}
public function getOwner() {
return $this->principalUri;
}
}
@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Trashbin;
use OCA\DAV\CalDAV\CalDavBackend;
use Sabre\CalDAV\ICalendarObjectContainer;
use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\Exception\NotImplemented;
use Sabre\DAVACL\ACLTrait;
use Sabre\DAVACL\IACL;
use function array_map;
use function implode;
use function preg_match;
class DeletedCalendarObjectsCollection implements ICalendarObjectContainer, IACL {
use ACLTrait;
public const NAME = 'objects';
/** @var CalDavBackend */
protected $caldavBackend;
/** @var mixed[] */
private $principalInfo;
public function __construct(CalDavBackend $caldavBackend,
array $principalInfo) {
$this->caldavBackend = $caldavBackend;
$this->principalInfo = $principalInfo;
}
/**
* @see \OCA\DAV\CalDAV\Trashbin\DeletedCalendarObjectsCollection::calendarQuery
*/
public function getChildren() {
throw new NotImplemented();
}
public function getChild($name) {
if (!preg_match("/(\d+)\\.ics/", $name, $matches)) {
throw new NotFound();
}
$data = $this->caldavBackend->getCalendarObjectById(
$this->principalInfo['uri'],
(int) $matches[1],
);
// If the object hasn't been deleted yet then we don't want to find it here
if ($data === null) {
throw new NotFound();
}
if (!isset($data['deleted_at'])) {
throw new BadRequest('The calendar object you\'re trying to restore is not marked as deleted');
}
return new DeletedCalendarObject(
$this->getRelativeObjectPath($data),
$data,
$this->principalInfo['uri'],
$this->caldavBackend
);
}
public function createFile($name, $data = null) {
throw new Forbidden();
}
public function createDirectory($name) {
throw new Forbidden();
}
public function childExists($name) {
try {
$this->getChild($name);
} catch (NotFound $e) {
return false;
}
return true;
}
public function delete() {
throw new Forbidden();
}
public function getName(): string {
return self::NAME;
}
public function setName($name) {
throw new Forbidden();
}
public function getLastModified(): int {
return 0;
}
public function calendarQuery(array $filters) {
return array_map(function (array $calendarObjectInfo) {
return $this->getRelativeObjectPath($calendarObjectInfo);
}, $this->caldavBackend->getDeletedCalendarObjectsByPrincipal($this->principalInfo['uri']));
}
private function getRelativeObjectPath(array $calendarInfo): string {
return implode(
'.',
[$calendarInfo['id'], 'ics'],
);
}
public function getOwner() {
return $this->principalInfo['uri'];
}
public function getACL(): array {
return [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}unbind',
'principal' => '{DAV:}owner',
'protected' => true,
]
];
}
}
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Trashbin;
use Closure;
use DateTimeImmutable;
use DateTimeInterface;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\RetentionService;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use function array_slice;
use function implode;
class Plugin extends ServerPlugin {
public const PROPERTY_DELETED_AT = '{http://nextcloud.com/ns}deleted-at';
public const PROPERTY_CALENDAR_URI = '{http://nextcloud.com/ns}calendar-uri';
public const PROPERTY_RETENTION_DURATION = '{http://nextcloud.com/ns}trash-bin-retention-duration';
/** @var bool */
private $disableTrashbin;
/** @var RetentionService */
private $retentionService;
/** @var Server */
private $server;
public function __construct(IRequest $request,
RetentionService $retentionService) {
$this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1';
$this->retentionService = $retentionService;
}
public function initialize(Server $server): void {
$this->server = $server;
$server->on('beforeMethod:*', [$this, 'beforeMethod']);
$server->on('propFind', Closure::fromCallable([$this, 'propFind']));
}
public function beforeMethod(RequestInterface $request, ResponseInterface $response): void {
if (!$this->disableTrashbin) {
return;
}
$path = $request->getPath();
$pathParts = explode('/', ltrim($path, '/'));
if (\count($pathParts) < 3) {
// We are looking for a path like calendars/username/calendarname
return;
}
// $calendarPath will look like calendars/username/calendarname
$calendarPath = implode(
'/',
array_slice($pathParts, 0, 3)
);
try {
$calendar = $this->server->tree->getNodeForPath($calendarPath);
if (!($calendar instanceof Calendar)) {
// This is odd
return;
}
/** @var Calendar $calendar */
$calendar->disableTrashbin();
} catch (NotFound $ex) {
return;
}
}
private function propFind(
PropFind $propFind,
INode $node): void {
if ($node instanceof DeletedCalendarObject) {
$propFind->handle(self::PROPERTY_DELETED_AT, function () use ($node) {
$ts = $node->getDeletedAt();
if ($ts === null) {
return null;
}
return (new DateTimeImmutable())
->setTimestamp($ts)
->format(DateTimeInterface::ATOM);
});
$propFind->handle(self::PROPERTY_CALENDAR_URI, function () use ($node) {
return $node->getCalendarUri();
});
}
if ($node instanceof TrashbinHome) {
$propFind->handle(self::PROPERTY_RETENTION_DURATION, function () use ($node) {
return $this->retentionService->getDuration();
});
}
}
public function getFeatures(): array {
return ['nc-calendar-trashbin'];
}
public function getPluginName(): string {
return 'nc-calendar-trashbin';
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Trashbin;
use OCA\DAV\CalDAV\IRestorable;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
use Sabre\DAV\IMoveTarget;
use Sabre\DAV\INode;
class RestoreTarget implements ICollection, IMoveTarget {
public const NAME = 'restore';
public function createFile($name, $data = null) {
throw new Forbidden();
}
public function createDirectory($name) {
throw new Forbidden();
}
public function getChild($name) {
throw new NotFound();
}
public function getChildren(): array {
return [];
}
public function childExists($name): bool {
return false;
}
public function moveInto($targetName, $sourcePath, INode $sourceNode): bool {
if ($sourceNode instanceof IRestorable) {
$sourceNode->restore();
return true;
}
return false;
}
public function delete() {
throw new Forbidden();
}
public function getName(): string {
return 'restore';
}
public function setName($name) {
throw new Forbidden();
}
public function getLastModified() {
return 0;
}
}
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\DAV\CalDAV\Trashbin;
use OCA\DAV\CalDAV\CalDavBackend;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\ICollection;
use Sabre\DAV\INode;
use Sabre\DAV\IProperties;
use Sabre\DAV\PropPatch;
use Sabre\DAV\Xml\Property\ResourceType;
use Sabre\DAVACL\ACLTrait;
use Sabre\DAVACL\IACL;
use function in_array;
use function sprintf;
class TrashbinHome implements IACL, ICollection, IProperties {
use ACLTrait;
public const NAME = 'trashbin';
/** @var CalDavBackend */
private $caldavBackend;
/** @var array */
private $principalInfo;
public function __construct(CalDavBackend $caldavBackend,
array $principalInfo) {
$this->caldavBackend = $caldavBackend;
$this->principalInfo = $principalInfo;
}
public function getOwner(): string {
return $this->principalInfo['uri'];
}
public function createFile($name, $data = null) {
throw new Forbidden('Permission denied to create files in the trashbin');
}
public function createDirectory($name) {
throw new Forbidden('Permission denied to create a directory in the trashbin');
}
public function getChild($name): INode {
switch ($name) {
case RestoreTarget::NAME:
return new RestoreTarget();
case DeletedCalendarObjectsCollection::NAME:
return new DeletedCalendarObjectsCollection(
$this->caldavBackend,
$this->principalInfo
);
}
throw new NotFound();
}
public function getChildren(): array {
return [
new RestoreTarget(),
new DeletedCalendarObjectsCollection(
$this->caldavBackend,
$this->principalInfo
),
];
}
public function childExists($name): bool {
return in_array($name, [
RestoreTarget::NAME,
DeletedCalendarObjectsCollection::NAME,
], true);
}
public function delete() {
throw new Forbidden('Permission denied to delete the trashbin');
}
public function getName(): string {
return self::NAME;
}
public function setName($name) {
throw new Forbidden('Permission denied to rename the trashbin');
}
public function getLastModified(): int {
return 0;
}
public function propPatch(PropPatch $propPatch): void {
throw new Forbidden('not implemented');
}
public function getProperties($properties): array {
return [
'{DAV:}resourcetype' => new ResourceType([
'{DAV:}collection',
sprintf('{%s}trash-bin', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
]),
];
}
}

Some files were not shown because too many files have changed in this diff Show More