aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CalDAV/Trashbin
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/CalDAV/Trashbin')
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObject.php106
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php133
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/Plugin.php114
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/RestoreTarget.php65
-rw-r--r--apps/dav/lib/CalDAV/Trashbin/TrashbinHome.php106
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),
+ ]),
+ ];
+ }
+}