diff options
Diffstat (limited to 'apps/dav/lib/CalDAV/Trashbin')
-rw-r--r-- | apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php | 106 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php | 133 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Trashbin/Plugin.php | 114 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php | 65 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php | 106 |
5 files changed, 524 insertions, 0 deletions
diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php new file mode 100644 index 00000000000..d8c429f2056 --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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; + + public function __construct( + private string $name, + /** @var mixed[] */ + private array $objectData, + private string $principalUri, + private CalDavBackend $calDavBackend, + ) { + } + + 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; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php new file mode 100644 index 00000000000..f75e19689f1 --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php @@ -0,0 +1,133 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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'; + + public function __construct( + protected CalDavBackend $caldavBackend, + /** @var mixed[] */ + private array $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, + ] + ]; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/Plugin.php b/apps/dav/lib/CalDAV/Trashbin/Plugin.php new file mode 100644 index 00000000000..6f58b1f3110 --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/Plugin.php @@ -0,0 +1,114 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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 Server */ + private $server; + + public function __construct( + IRequest $request, + private RetentionService $retentionService, + ) { + $this->disableTrashbin = $request->getHeader('X-NC-CalDAV-No-Trashbin') === '1'; + } + + 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'; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php new file mode 100644 index 00000000000..6641148de2b --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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; + } +} diff --git a/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php new file mode 100644 index 00000000000..1c76bd2295d --- /dev/null +++ b/apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php @@ -0,0 +1,106 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +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'; + + public function __construct( + private CalDavBackend $caldavBackend, + private array $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), + ]), + ]; + } +} |