nextcloud/apps/dav/lib/CalDAV/Federation/FederationSharingService.php
Côme Chilliet 88fc8c94ce
fix(dav): Fix PHP warnings triggered by tests in dav application
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2025-09-27 13:05:33 +02:00

180 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\CalDAV\Federation;
use OCA\DAV\CalDAV\Federation\Protocol\CalendarFederationProtocolV1;
use OCA\DAV\DAV\RemoteUserPrincipalBackend;
use OCA\DAV\DAV\Sharing\IShareable;
use OCA\DAV\DAV\Sharing\SharingMapper;
use OCP\AppFramework\Http;
use OCP\Federation\ICloudFederationFactory;
use OCP\Federation\ICloudFederationProviderManager;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\OCM\Exceptions\OCMProviderException;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\Calendar;
// TODO: Convert this to an abstract service like the addressbook/calendar sharing services once we
// support addressbook federation as well.
class FederationSharingService {
public function __construct(
private readonly ICloudFederationProviderManager $federationManager,
private readonly ICloudFederationFactory $federationFactory,
private readonly IUserManager $userManager,
private readonly IURLGenerator $url,
private readonly LoggerInterface $logger,
private readonly ISecureRandom $random,
private readonly SharingMapper $sharingMapper,
) {
}
/**
* Decode a (base64) encoded remote user principal and return the remote user's cloud id. Will
* return null if the given principal is not belonging to a remote user (or has an invalid
* format).
*
* The remote user/cloud id needs to be encoded as it might contain slashes.
*/
private function decodeRemoteUserPrincipal(string $principal): ?string {
// Expected format: principals/remote-users/abcdef123
if (!str_starts_with($principal, 'principals/remote-users/')) {
return null;
}
$encodedId = substr($principal, strlen('principals/remote-users/'));
$decodedId = base64_decode($encodedId);
if (!is_string($decodedId)) {
return null;
}
return $decodedId;
}
/**
* Send a calendar share to a remote instance and create a federated share locally if it is
* accepted.
*
* @param IShareable $shareable The calendar to be shared.
* @param string $principal The principal to share with (should be a remote user principal).
* @param int $access The access level. The remote serve might reject it.
*/
public function shareWith(IShareable $shareable, string $principal, int $access): void {
$baseError = 'Failed to create federated calendar share: ';
// 1. Validate share data
$shareWith = $this->decodeRemoteUserPrincipal($principal);
if ($shareWith === null) {
$this->logger->error($baseError . 'Principal of sharee is not belonging to a remote user', [
'shareable' => $shareable->getName(),
'encodedShareWith' => $principal,
]);
return;
}
[,, $ownerUid] = explode('/', $shareable->getOwner());
$owner = $this->userManager->get($ownerUid);
if ($owner === null) {
$this->logger->error($baseError . 'Shareable is not owned by a user on this server', [
'shareable' => $shareable->getName(),
'shareWith' => $shareWith,
]);
return;
}
// Need a calendar instance to extract properties for the protocol
$calendar = $shareable;
if (!($calendar instanceof Calendar)) {
$this->logger->error($baseError . 'Shareable is not a calendar', [
'shareable' => $shareable->getName(),
'owner' => $owner,
'shareWith' => $shareWith,
]);
return;
}
$getProp = static fn (string $prop) => $calendar->getProperties([$prop])[$prop] ?? null;
$displayName = $getProp('{DAV:}displayname') ?? '';
$token = $this->random->generate(32);
$share = $this->federationFactory->getCloudFederationShare(
$shareWith,
$shareable->getName(),
$displayName,
CalendarFederationProvider::PROVIDER_ID,
// Resharing is not possible so the owner is always the sharer
$owner->getCloudId(),
$owner->getDisplayName(),
$owner->getCloudId(),
$owner->getDisplayName(),
$token,
CalendarFederationProvider::USER_SHARE_TYPE,
CalendarFederationProvider::CALENDAR_RESOURCE,
);
// 2. Send share to federated instance
$shareWithEncoded = base64_encode($shareWith);
$relativeCalendarUrl = "remote-calendars/$shareWithEncoded/" . $calendar->getName() . '_shared_by_' . $ownerUid;
$calendarUrl = $this->url->linkTo('', 'remote.php') . "/dav/$relativeCalendarUrl";
$calendarUrl = $this->url->getAbsoluteURL($calendarUrl);
$protocol = new CalendarFederationProtocolV1();
$protocol->setUrl($calendarUrl);
$protocol->setDisplayName($displayName);
$protocol->setColor($getProp('{http://apple.com/ns/ical/}calendar-color'));
$protocol->setAccess($access);
$protocol->setComponents(implode(',', $getProp(
'{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set')?->getValue() ?? [],
));
$share->setProtocol([
// Preserve original protocol contents
...$share->getProtocol(),
...$protocol->toProtocol(),
]);
try {
$response = $this->federationManager->sendCloudShare($share);
} catch (OCMProviderException $e) {
$this->logger->error($baseError . $e->getMessage(), [
'exception' => $e,
'owner' => $owner->getUID(),
'calendar' => $shareable->getName(),
'shareWith' => $shareWith,
]);
return;
}
if ($response->getStatusCode() !== Http::STATUS_CREATED) {
$this->logger->error($baseError . 'Server replied with code ' . $response->getStatusCode(), [
'responseBody' => $response->getBody(),
'owner' => $owner->getUID(),
'calendar' => $shareable->getName(),
'shareWith' => $shareWith,
]);
return;
}
// 3. Create a local DAV share to track the token for authentication
$shareWithPrincipalUri = RemoteUserPrincipalBackend::PRINCIPAL_PREFIX . '/' . $shareWithEncoded;
$this->sharingMapper->deleteShare(
$shareable->getResourceId(),
'calendar',
$shareWithPrincipalUri,
);
$this->sharingMapper->shareWithToken(
$shareable->getResourceId(),
'calendar',
$access,
$shareWithPrincipalUri,
$token,
);
}
}