Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3caa1467b1 | |||
| 2c2335b8b4 | |||
| d6eade0119 | |||
| 8d1cb50048 | |||
| b4b328cf61 | |||
| e47195a334 | |||
| 4d00f49757 | |||
| 83d795dd18 | |||
| a6b9483a5f | |||
| 636345bac8 | |||
| 79d4953e64 | |||
| ca8050b94e | |||
| 47b08a07d8 | |||
| 77c070bc93 | |||
| 09c9241b30 | |||
| 711bd2bc6d | |||
| 2a81cba978 | |||
| 6df490942c | |||
| 0b8e7bb4f0 | |||
| 7e264ba58e | |||
| 64f319ab4e | |||
| ec0ed788fa | |||
| 67d1fac6f6 | |||
| 237d5156b6 | |||
| 40c39270c0 | |||
| f5b18dd7fd | |||
| f885d7292f | |||
| e4244c5fc8 |
@@ -77,6 +77,7 @@ return array(
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => $baseDir . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => $baseDir . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => $baseDir . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
|
||||
@@ -92,6 +92,7 @@ class ComposerStaticInitDAV
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarFactory' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarFactory.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarImpl' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarImpl.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarMapper' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarMapper.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederatedCalendarSyncService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederatedCalendarSyncService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\FederationSharingService' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/FederationSharingService.php',
|
||||
'OCA\\DAV\\CalDAV\\Federation\\Protocol\\CalendarFederationProtocolV1' => __DIR__ . '/..' . '/../lib/CalDAV/Federation/Protocol/CalendarFederationProtocolV1.php',
|
||||
|
||||
@@ -43,9 +43,9 @@ class CalendarProvider implements ICalendarProvider {
|
||||
});
|
||||
}
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
$iCalendars = [];
|
||||
|
||||
$additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos);
|
||||
foreach ($calendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
@@ -60,9 +60,7 @@ class CalendarProvider implements ICalendarProvider {
|
||||
);
|
||||
}
|
||||
|
||||
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars(
|
||||
$federatedCalendarInfos,
|
||||
);
|
||||
$additionalFederatedProps = $this->getAdditionalPropertiesForCalendars($federatedCalendarInfos);
|
||||
foreach ($federatedCalendarInfos as $calendarInfo) {
|
||||
$user = str_replace('principals/users/', '', $calendarInfo['principaluri']);
|
||||
$path = 'calendars/' . $user . '/' . $calendarInfo['uri'];
|
||||
|
||||
@@ -104,9 +104,10 @@ class CalendarFederationProvider implements ICloudFederationProvider {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: implement read-write sharing
|
||||
// convert access to permissions
|
||||
$permissions = match ($access) {
|
||||
DavSharingBackend::ACCESS_READ => Constants::PERMISSION_READ,
|
||||
DavSharingBackend::ACCESS_READ_WRITE => Constants::PERMISSION_READ | Constants::PERMISSION_CREATE | Constants::PERMISSION_UPDATE | Constants::PERMISSION_DELETE,
|
||||
default => throw new ProviderCouldNotAddShareException(
|
||||
"Unsupported access value: $access",
|
||||
'',
|
||||
@@ -122,20 +123,27 @@ class CalendarFederationProvider implements ICloudFederationProvider {
|
||||
$sharedWithPrincipal = 'principals/users/' . $share->getShareWith();
|
||||
|
||||
// Delete existing incoming federated share first
|
||||
$this->federatedCalendarMapper->deleteByUri($sharedWithPrincipal, $calendarUri);
|
||||
$calendar = $this->federatedCalendarMapper->findByUri($sharedWithPrincipal, $calendarUri);
|
||||
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setPrincipaluri($sharedWithPrincipal);
|
||||
$calendar->setUri($calendarUri);
|
||||
$calendar->setRemoteUrl($calendarUrl);
|
||||
$calendar->setDisplayName($displayName);
|
||||
$calendar->setColor($color);
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setSharedBy($share->getSharedBy());
|
||||
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$calendar = $this->federatedCalendarMapper->insert($calendar);
|
||||
if ($calendar === null) {
|
||||
$calendar = new FederatedCalendarEntity();
|
||||
$calendar->setPrincipaluri($sharedWithPrincipal);
|
||||
$calendar->setUri($calendarUri);
|
||||
$calendar->setRemoteUrl($calendarUrl);
|
||||
$calendar->setDisplayName($displayName);
|
||||
$calendar->setColor($color);
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setSharedBy($share->getSharedBy());
|
||||
$calendar->setSharedByDisplayName($share->getSharedByDisplayName());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$calendar = $this->federatedCalendarMapper->insert($calendar);
|
||||
} else {
|
||||
$calendar->setToken($share->getShareSecret());
|
||||
$calendar->setPermissions($permissions);
|
||||
$calendar->setComponents($components);
|
||||
$this->federatedCalendarMapper->update($calendar);
|
||||
}
|
||||
|
||||
$this->jobList->add(FederatedCalendarSyncJob::class, [
|
||||
FederatedCalendarSyncJob::ARGUMENT_ID => $calendar->getId(),
|
||||
|
||||
@@ -10,29 +10,289 @@ declare(strict_types=1);
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Calendar;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\CalDAV\Backend;
|
||||
use OCP\Constants;
|
||||
use Sabre\CalDAV\ICalendar;
|
||||
use Sabre\CalDAV\Plugin;
|
||||
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\IMultiGet;
|
||||
use Sabre\DAV\INode;
|
||||
use Sabre\DAV\IProperties;
|
||||
use Sabre\DAV\PropPatch;
|
||||
|
||||
class FederatedCalendar implements ICalendar, IProperties, IMultiGet {
|
||||
|
||||
private const CALENDAR_TYPE = CalDavBackend::CALENDAR_TYPE_FEDERATED;
|
||||
private const DAV_PROPERTY_CALENDAR_LABEL = '{DAV:}displayname';
|
||||
private const DAV_PROPERTY_CALENDAR_COLOR = '{http://apple.com/ns/ical/}calendar-color';
|
||||
|
||||
private string $principalUri;
|
||||
private string $calendarUri;
|
||||
private ?array $calendarACL = null;
|
||||
private FederatedCalendarEntity $federationInfo;
|
||||
|
||||
class FederatedCalendar extends Calendar {
|
||||
public function __construct(
|
||||
Backend\BackendInterface $caldavBackend,
|
||||
$calendarInfo,
|
||||
IL10N $l10n,
|
||||
IConfig $config,
|
||||
LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly FederatedCalendarSyncService $federatedCalendarService,
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
$calendarInfo,
|
||||
) {
|
||||
parent::__construct($caldavBackend, $calendarInfo, $l10n, $config, $logger);
|
||||
$this->principalUri = $calendarInfo['principaluri'];
|
||||
$this->calendarUri = $calendarInfo['uri'];
|
||||
$this->federationInfo = $federatedCalendarMapper->findByUri($this->principalUri, $this->calendarUri);
|
||||
}
|
||||
|
||||
public function delete() {
|
||||
$this->federatedCalendarMapper->deleteById($this->getResourceId());
|
||||
public function getResourceId(): int {
|
||||
return $this->federationInfo->getId();
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->federationInfo->getUri();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function setName($name): void {
|
||||
throw new MethodNotAllowed('Renaming federated calendars is not allowed');
|
||||
}
|
||||
|
||||
protected function getCalendarType(): int {
|
||||
return CalDavBackend::CALENDAR_TYPE_FEDERATED;
|
||||
return self::CALENDAR_TYPE;
|
||||
}
|
||||
|
||||
public function getPrincipalURI(): string {
|
||||
return $this->federationInfo->getPrincipaluri();
|
||||
}
|
||||
|
||||
public function getOwner(): ?string {
|
||||
return $this->federationInfo->getSharedByPrincipal();
|
||||
}
|
||||
|
||||
public function getGroup(): ?string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getACL(): array {
|
||||
if ($this->calendarACL !== null) {
|
||||
return $this->calendarACL;
|
||||
}
|
||||
|
||||
$permissions = $this->federationInfo->getPermissions();
|
||||
// default permission
|
||||
$acl = [
|
||||
// read object permission
|
||||
[
|
||||
'privilege' => '{DAV:}read',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
// read acl permission
|
||||
[
|
||||
'privilege' => '{DAV:}read-acl',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
// write properties permission (calendar name, color)
|
||||
[
|
||||
'privilege' => '{DAV:}write-properties',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
],
|
||||
];
|
||||
// create permission
|
||||
if ($permissions & Constants::PERMISSION_CREATE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}bind',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
// update permission
|
||||
if ($permissions & Constants::PERMISSION_UPDATE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}write-content',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
// delete permission
|
||||
if ($permissions & Constants::PERMISSION_DELETE) {
|
||||
$acl[] = [
|
||||
'privilege' => '{DAV:}unbind',
|
||||
'principal' => $this->principalUri,
|
||||
'protected' => true,
|
||||
];
|
||||
}
|
||||
|
||||
// cache the calculated ACL for later use
|
||||
$this->calendarACL = $acl;
|
||||
|
||||
return $acl;
|
||||
}
|
||||
|
||||
public function setACL(array $acl): void {
|
||||
throw new MethodNotAllowed('Changing ACLs on federated calendars is not allowed');
|
||||
}
|
||||
|
||||
public function getSupportedPrivilegeSet(): ?array {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed> properties array, with property name as key
|
||||
*/
|
||||
public function getProperties($properties): array {
|
||||
return [
|
||||
self::DAV_PROPERTY_CALENDAR_LABEL => $this->federationInfo->getDisplayName(),
|
||||
self::DAV_PROPERTY_CALENDAR_COLOR => $this->federationInfo->getColor(),
|
||||
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(explode(',', $this->federationInfo->getComponents())),
|
||||
];
|
||||
}
|
||||
|
||||
public function propPatch(PropPatch $propPatch): void {
|
||||
$mutations = $propPatch->getMutations();
|
||||
if (count($mutations) > 0) {
|
||||
// evaluate if name was changed
|
||||
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_LABEL])) {
|
||||
$this->federationInfo->setDisplayName($mutations[self::DAV_PROPERTY_CALENDAR_LABEL]);
|
||||
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_LABEL, 200);
|
||||
}
|
||||
// evaluate if color was changed
|
||||
if (isset($mutations[self::DAV_PROPERTY_CALENDAR_COLOR])) {
|
||||
$this->federationInfo->setColor($mutations[self::DAV_PROPERTY_CALENDAR_COLOR]);
|
||||
$propPatch->setResultCode(self::DAV_PROPERTY_CALENDAR_COLOR, 200);
|
||||
}
|
||||
$this->federatedCalendarMapper->update($this->federationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function getChildACL(): array {
|
||||
return $this->getACL();
|
||||
}
|
||||
|
||||
public function getLastModified(): ?int {
|
||||
return $this->federationInfo->getLastSync();
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$this->federatedCalendarMapper->deleteById($this->getResourceId());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function createDirectory($name): void {
|
||||
throw new MethodNotAllowed('Creating nested collection is not allowed');
|
||||
}
|
||||
|
||||
public function calendarQuery(array $filters): array {
|
||||
$uris = $this->caldavBackend->calendarQuery($this->federationInfo->getId(), $filters, $this->getCalendarType());
|
||||
return $uris;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function getChild($name): INode {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
|
||||
if ($obj === null) {
|
||||
throw new NotFound('Calendar object not found');
|
||||
}
|
||||
|
||||
return new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<INode>
|
||||
*/
|
||||
public function getChildren(): array {
|
||||
$objs = $this->caldavBackend->getCalendarObjects($this->federationInfo->getId(), $this->getCalendarType());
|
||||
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
$children[] = new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $paths Names of the files
|
||||
*
|
||||
* @return array<INode>
|
||||
*/
|
||||
public function getMultipleChildren(array $paths): array {
|
||||
$objs = $this->caldavBackend->getMultipleCalendarObjects($this->federationInfo->getId(), $paths, $this->getCalendarType());
|
||||
|
||||
$children = [];
|
||||
foreach ($objs as $obj) {
|
||||
$children[] = new FederatedCalendarObject($this, $obj);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function childExists($name): bool {
|
||||
$obj = $this->caldavBackend->getCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
return $obj !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
* @param resource|string $data Initial payload
|
||||
*/
|
||||
public function createFile($name, $data = null): string {
|
||||
if (is_resource($data)) {
|
||||
$data = stream_get_contents($data);
|
||||
}
|
||||
|
||||
// Create on remote server first
|
||||
$etag = $this->federatedCalendarService->createCalendarObject($this->federationInfo, $name, $data);
|
||||
|
||||
if (empty($etag)) {
|
||||
throw new \Exception('Failed to create calendar object on remote server');
|
||||
}
|
||||
|
||||
// Then store locally
|
||||
return $this->caldavBackend->createCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
* @param resource|string $data Initial payload
|
||||
*/
|
||||
public function updateFile($name, $data): string {
|
||||
if (is_resource($data)) {
|
||||
$data = stream_get_contents($data);
|
||||
}
|
||||
|
||||
// Update remote calendar first
|
||||
$etag = $this->federatedCalendarService->updateCalendarObject($this->federationInfo, $name, $data);
|
||||
|
||||
if (empty($etag)) {
|
||||
throw new \Exception('Failed to update calendar object on remote server');
|
||||
}
|
||||
|
||||
// Then update locally
|
||||
return $this->caldavBackend->updateCalendarObject($this->federationInfo->getId(), $name, $data, $this->getCalendarType());
|
||||
}
|
||||
|
||||
public function deleteFile(string $name): void {
|
||||
// Delete from remote server first
|
||||
$this->federatedCalendarService->deleteCalendarObject($this->federationInfo, $name);
|
||||
|
||||
// Then delete locally
|
||||
$this->caldavBackend->deleteCalendarObject($this->federationInfo->getId(), $name, $this->getCalendarType());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -94,8 +94,8 @@ class FederatedCalendarEntity extends Entity {
|
||||
'{' . \Sabre\CalDAV\Plugin::NS_CALENDARSERVER . '}getctag' => $this->getSyncTokenForSabre(),
|
||||
'{' . \Sabre\CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => $this->getSupportedCalendarComponentSet(),
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->getSharedByPrincipal(),
|
||||
// TODO: implement read-write sharing
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => 1
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => ($this->getPermissions() & \OCP\Constants::PERMISSION_UPDATE) === 0 ? 1 : 0,
|
||||
'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}permissions' => $this->getPermissions(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,34 +9,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\L10N\IFactory as IL10NFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarFactory {
|
||||
private readonly IL10N $l10n;
|
||||
|
||||
public function __construct(
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
private readonly IConfig $config,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
IL10NFactory $l10nFactory,
|
||||
private readonly FederatedCalendarSyncService $federatedCalendarService,
|
||||
private readonly CalDavBackend $caldavBackend,
|
||||
) {
|
||||
$this->l10n = $l10nFactory->get(Application::APP_ID);
|
||||
}
|
||||
|
||||
public function createFederatedCalendar(array $calendarInfo): FederatedCalendar {
|
||||
return new FederatedCalendar(
|
||||
$this->federatedCalendarMapper,
|
||||
$this->federatedCalendarService,
|
||||
$this->caldavBackend,
|
||||
$calendarInfo,
|
||||
$this->l10n,
|
||||
$this->config,
|
||||
$this->logger,
|
||||
$this->federatedCalendarMapper,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +51,7 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
|
||||
}
|
||||
|
||||
public function getPermissions(): int {
|
||||
// TODO: implement read-write sharing
|
||||
return Constants::PERMISSION_READ;
|
||||
return $this->calendarInfo['{http://owncloud.org/ns}permissions'] ?? Constants::PERMISSION_READ;
|
||||
}
|
||||
|
||||
public function isDeleted(): bool {
|
||||
@@ -64,7 +63,8 @@ class FederatedCalendarImpl implements ICalendar, ICalendarIsShared, ICalendarIs
|
||||
}
|
||||
|
||||
public function isWritable(): bool {
|
||||
return false;
|
||||
$permissions = $this->getPermissions();
|
||||
return ($permissions & Constants::PERMISSION_UPDATE) !== 0;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use Sabre\CalDAV\ICalendarObject;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAVACL\IACL;
|
||||
|
||||
class FederatedCalendarObject implements ICalendarObject, IACL {
|
||||
|
||||
public function __construct(
|
||||
protected FederatedCalendar $calendarObject,
|
||||
protected $objectData,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->objectData['uri'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name Name of the file
|
||||
*/
|
||||
public function setName($name) {
|
||||
throw new \Exception('Not implemented');
|
||||
}
|
||||
|
||||
public function get(): string {
|
||||
return $this->objectData['calendardata'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resource|string $data contents of the file
|
||||
*/
|
||||
public function put($data): string {
|
||||
|
||||
$etag = $this->calendarObject->updateFile($this->objectData['uri'], $data);
|
||||
$this->objectData['calendardata'] = $data;
|
||||
$this->objectData['etag'] = $etag;
|
||||
|
||||
return $etag;
|
||||
}
|
||||
|
||||
public function delete(): void {
|
||||
$this->calendarObject->deleteFile($this->objectData['uri']);
|
||||
}
|
||||
|
||||
public function getContentType(): ?string {
|
||||
$mime = 'text/calendar; charset=utf-8';
|
||||
if (isset($this->objectData['component']) && $this->objectData['component']) {
|
||||
$mime .= '; component=' . $this->objectData['component'];
|
||||
}
|
||||
|
||||
return $mime;
|
||||
}
|
||||
|
||||
public function getETag(): string {
|
||||
if (isset($this->objectData['etag'])) {
|
||||
return $this->objectData['etag'];
|
||||
} else {
|
||||
return '"' . md5($this->get()) . '"';
|
||||
}
|
||||
}
|
||||
|
||||
public function getLastModified(): int {
|
||||
return $this->objectData['lastmodified'];
|
||||
}
|
||||
|
||||
public function getSize(): int {
|
||||
if (isset($this->objectData['size'])) {
|
||||
return $this->objectData['size'];
|
||||
} else {
|
||||
return strlen($this->get());
|
||||
}
|
||||
}
|
||||
|
||||
public function getOwner(): ?string {
|
||||
return $this->calendarObject->getPrincipalURI();
|
||||
}
|
||||
|
||||
public function getGroup(): ?string {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function getACL(): array {
|
||||
return $this->calendarObject->getACL();
|
||||
}
|
||||
|
||||
public function setACL(array $acl): void {
|
||||
throw new MethodNotAllowed('Changing ACLs on federated events is not allowed');
|
||||
}
|
||||
|
||||
public function getSupportedPrivilegeSet(): ?array {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,20 +9,52 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\Service\ASyncService;
|
||||
use OCP\AppFramework\Db\TTransactional;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FederatedCalendarSyncService {
|
||||
class FederatedCalendarSyncService extends ASyncService {
|
||||
use TTransactional;
|
||||
|
||||
private const SYNC_TOKEN_PREFIX = 'http://sabre.io/ns/sync/';
|
||||
|
||||
public function __construct(
|
||||
IClientService $clientService,
|
||||
IConfig $config,
|
||||
private readonly FederatedCalendarMapper $federatedCalendarMapper,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly CalDavSyncService $syncService,
|
||||
private readonly CalDavBackend $backend,
|
||||
private readonly IDBConnection $dbConnection,
|
||||
private readonly ICloudIdManager $cloudIdManager,
|
||||
) {
|
||||
parent::__construct($clientService, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and encode credentials from a federated calendar entity.
|
||||
*
|
||||
* @return array{username: string, remoteUrl: string, token: string}
|
||||
*/
|
||||
private function getCredentials(FederatedCalendarEntity $calendar): array {
|
||||
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
|
||||
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
|
||||
|
||||
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
|
||||
// auth according to RFC 7617
|
||||
$calDavUser = base64_encode($calDavUser);
|
||||
|
||||
return [
|
||||
'username' => $calDavUser,
|
||||
'remoteUrl' => $calendar->getRemoteUrl(),
|
||||
'token' => $calendar->getToken(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,29 +63,77 @@ class FederatedCalendarSyncService {
|
||||
* @throws ClientExceptionInterface If syncing the calendar fails.
|
||||
*/
|
||||
public function syncOne(FederatedCalendarEntity $calendar): int {
|
||||
[,, $sharedWith] = explode('/', $calendar->getPrincipaluri());
|
||||
$calDavUser = $this->cloudIdManager->getCloudId($sharedWith, null)->getId();
|
||||
$remoteUrl = $calendar->getRemoteUrl();
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$syncToken = $calendar->getSyncTokenForSabre();
|
||||
|
||||
// Need to encode the cloud id as it might contain a colon which is not allowed in basic
|
||||
// auth according to RFC 7617
|
||||
$calDavUser = base64_encode($calDavUser);
|
||||
try {
|
||||
$response = $this->requestSyncReport(
|
||||
$credentials['remoteUrl'],
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$syncToken,
|
||||
);
|
||||
} catch (ClientExceptionInterface $ex) {
|
||||
if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
|
||||
// Remote server revoked access to the calendar => remove it
|
||||
$this->federatedCalendarMapper->delete($calendar);
|
||||
$this->logger->warning("Authorization failed, remove federated calendar: {$credentials['remoteUrl']}", [
|
||||
'app' => 'dav',
|
||||
]);
|
||||
return 0;
|
||||
}
|
||||
$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
|
||||
throw $ex;
|
||||
}
|
||||
|
||||
$syncResponse = $this->syncService->syncRemoteCalendar(
|
||||
$remoteUrl,
|
||||
$calDavUser,
|
||||
$calendar->getToken(),
|
||||
$syncToken,
|
||||
$calendar,
|
||||
);
|
||||
// Process changes from remote
|
||||
$downloadedEvents = 0;
|
||||
foreach ($response['response'] as $resource => $status) {
|
||||
$objectUri = basename($resource);
|
||||
if (isset($status[200])) {
|
||||
// Object created or updated
|
||||
$absoluteUrl = $this->prepareUri($credentials['remoteUrl'], $resource);
|
||||
$calendarData = $this->download($absoluteUrl, $credentials['username'], $credentials['token']);
|
||||
$this->atomic(function () use ($calendar, $objectUri, $calendarData): void {
|
||||
$existingObject = $this->backend->getCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
if (!$existingObject) {
|
||||
$this->backend->createCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
$calendarData,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
} else {
|
||||
$this->backend->updateCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
$calendarData,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED
|
||||
);
|
||||
}
|
||||
}, $this->dbConnection);
|
||||
$downloadedEvents++;
|
||||
} else {
|
||||
// Object deleted
|
||||
$this->backend->deleteCalendarObject(
|
||||
$calendar->getId(),
|
||||
$objectUri,
|
||||
CalDavBackend::CALENDAR_TYPE_FEDERATED,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$newSyncToken = $syncResponse->getSyncToken();
|
||||
$newSyncToken = $response['token'];
|
||||
|
||||
// Check sync token format and extract the actual sync token integer
|
||||
$matches = [];
|
||||
if (!preg_match('/^http:\/\/sabre\.io\/ns\/sync\/([0-9]+)$/', $newSyncToken, $matches)) {
|
||||
$this->logger->error("Failed to sync federated calendar at $remoteUrl: New sync token has unexpected format: $newSyncToken", [
|
||||
$this->logger->error("Failed to sync federated calendar at {$credentials['remoteUrl']}: New sync token has unexpected format: $newSyncToken", [
|
||||
'calendar' => $calendar->toCalendarInfo(),
|
||||
'newSyncToken' => $newSyncToken,
|
||||
]);
|
||||
@@ -67,10 +147,58 @@ class FederatedCalendarSyncService {
|
||||
$newSyncToken,
|
||||
);
|
||||
} else {
|
||||
$this->logger->debug("Sync Token for $remoteUrl unchanged from previous sync");
|
||||
$this->logger->debug("Sync Token for {$credentials['remoteUrl']} unchanged from previous sync");
|
||||
$this->federatedCalendarMapper->updateSyncTime($calendar->getId());
|
||||
}
|
||||
|
||||
return $syncResponse->getDownloadedEvents();
|
||||
return $downloadedEvents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function createCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
return $this->requestPut(
|
||||
$objectUrl,
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$data,
|
||||
'text/calendar; charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function updateCalendarObject(FederatedCalendarEntity $calendar, string $name, string $data): string {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
return $this->requestPut(
|
||||
$objectUrl,
|
||||
$credentials['username'],
|
||||
$credentials['token'],
|
||||
$data,
|
||||
'text/calendar; charset=utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a calendar object on the remote server.
|
||||
*
|
||||
* @throws ClientExceptionInterface If the remote request fails.
|
||||
*/
|
||||
public function deleteCalendarObject(FederatedCalendarEntity $calendar, string $name): void {
|
||||
$credentials = $this->getCredentials($calendar);
|
||||
$objectUrl = $this->prepareUri($credentials['remoteUrl'], $name);
|
||||
|
||||
$this->requestDelete($objectUrl, $credentials['username'], $credentials['token']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\Calendar;
|
||||
use OCA\DAV\CalDAV\CalendarHome;
|
||||
use OCA\DAV\CalDAV\CalendarObject;
|
||||
use OCA\DAV\CalDAV\DefaultCalendarValidator;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
|
||||
use OCA\DAV\CalDAV\TipBroker;
|
||||
use OCP\IConfig;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -173,8 +174,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Calendar $calendarNode */
|
||||
/** @var Calendar&ICalendar $calendarNode */
|
||||
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
|
||||
|
||||
// abort if calendar is federated
|
||||
if ($calendarNode instanceof FederatedCalendar) {
|
||||
$this->logger->debug('Not processing scheduling for federated calendar at path: ' . $calendarPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// extract addresses for owner
|
||||
$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
|
||||
// determine if request is from a sharee
|
||||
|
||||
@@ -191,4 +191,70 @@ abstract class ASyncService {
|
||||
rtrim($responseUri, '/'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push data to the remote server via HTTP PUT.
|
||||
* Used for creating or updating CalDAV/CardDAV objects.
|
||||
*
|
||||
* @param string $url The absolute URL to PUT to
|
||||
* @param string $username The username for authentication
|
||||
* @param string $token The authentication token/password
|
||||
* @param string $data The data to upload
|
||||
* @param string $contentType The Content-Type header (e.g., 'text/calendar' or 'text/vcard')
|
||||
*
|
||||
* @return string The ETag returned by the server
|
||||
*/
|
||||
protected function requestPut(
|
||||
string $url,
|
||||
string $username,
|
||||
string $token,
|
||||
string $data,
|
||||
string $contentType = 'text/calendar; charset=utf-8',
|
||||
): string {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$username, $token],
|
||||
'body' => $data,
|
||||
'headers' => [
|
||||
'Content-Type' => $contentType,
|
||||
],
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$response = $client->put($url, $options);
|
||||
|
||||
// Extract and return the ETag from the response
|
||||
$etag = $response->getHeader('ETag');
|
||||
return $etag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a resource from the remote server via HTTP DELETE.
|
||||
* Used for deleting CalDAV/CardDAV objects.
|
||||
*
|
||||
* @param string $url The absolute URL to DELETE
|
||||
* @param string $username The username for authentication
|
||||
* @param string $token The authentication token/password
|
||||
*/
|
||||
protected function requestDelete(
|
||||
string $url,
|
||||
string $username,
|
||||
string $token,
|
||||
): void {
|
||||
$client = $this->getClient();
|
||||
|
||||
$options = [
|
||||
'auth' => [$username, $token],
|
||||
'verify' => !$this->config->getSystemValue(
|
||||
'sharing.federation.allowSelfSignedCertificates',
|
||||
false,
|
||||
),
|
||||
];
|
||||
|
||||
$client->delete($url, $options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,12 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('deleteByUri')
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
);
|
||||
)
|
||||
->willReturn(null);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('insert')
|
||||
@@ -123,6 +124,68 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithExistingCalendar(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 3,
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('new-token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$existingCalendar = new FederatedCalendarEntity();
|
||||
$existingCalendar->setId(10);
|
||||
$existingCalendar->setPrincipaluri('principals/users/sharee1');
|
||||
$existingCalendar->setUri('ae4b8ab904076fff2b955ea21b1a0d92');
|
||||
$existingCalendar->setRemoteUrl('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1');
|
||||
$existingCalendar->setToken('old-token');
|
||||
$existingCalendar->setPermissions(1);
|
||||
$existingCalendar->setComponents('VEVENT');
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
)
|
||||
->willReturn($existingCalendar);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
|
||||
$this->assertEquals('new-token', $calendar->getToken());
|
||||
$this->assertEquals(1, $calendar->getPermissions());
|
||||
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
|
||||
return $calendar;
|
||||
});
|
||||
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
|
||||
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithInvalidProtocolVersion(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
@@ -270,7 +333,7 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
$this->calendarFederationProvider->shareReceived($share);
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedAccess(): void {
|
||||
public function testShareReceivedWithReadWriteAccess(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
@@ -296,6 +359,65 @@ class CalendarFederationProviderTest extends TestCase {
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('findByUri')
|
||||
->with(
|
||||
'principals/users/sharee1',
|
||||
'ae4b8ab904076fff2b955ea21b1a0d92',
|
||||
)
|
||||
->willReturn(null);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('insert')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $calendar) {
|
||||
$this->assertEquals('principals/users/sharee1', $calendar->getPrincipaluri());
|
||||
$this->assertEquals('ae4b8ab904076fff2b955ea21b1a0d92', $calendar->getUri());
|
||||
$this->assertEquals('https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1', $calendar->getRemoteUrl());
|
||||
$this->assertEquals('Calendar 1', $calendar->getDisplayName());
|
||||
$this->assertEquals('#ff0000', $calendar->getColor());
|
||||
$this->assertEquals('token', $calendar->getToken());
|
||||
$this->assertEquals('user1@nextcloud.remote', $calendar->getSharedBy());
|
||||
$this->assertEquals('User 1', $calendar->getSharedByDisplayName());
|
||||
$this->assertEquals(15, $calendar->getPermissions()); // READ | CREATE | UPDATE | DELETE
|
||||
$this->assertEquals('VEVENT,VTODO', $calendar->getComponents());
|
||||
|
||||
$calendar->setId(10);
|
||||
return $calendar;
|
||||
});
|
||||
|
||||
$this->jobList->expects(self::once())
|
||||
->method('add')
|
||||
->with(FederatedCalendarSyncJob::class, ['id' => 10]);
|
||||
|
||||
$this->assertEquals(10, $this->calendarFederationProvider->shareReceived($share));
|
||||
}
|
||||
|
||||
public function testShareReceivedWithUnsupportedAccess(): void {
|
||||
$share = $this->createMock(ICloudFederationShare::class);
|
||||
$share->method('getShareType')
|
||||
->willReturn('user');
|
||||
$share->method('getProtocol')
|
||||
->willReturn([
|
||||
'version' => 'v1',
|
||||
'url' => 'https://nextcloud.remote/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user1',
|
||||
'displayName' => 'Calendar 1',
|
||||
'color' => '#ff0000',
|
||||
'access' => 999, // Invalid access value
|
||||
'components' => 'VEVENT,VTODO',
|
||||
]);
|
||||
$share->method('getShareWith')
|
||||
->willReturn('sharee1');
|
||||
$share->method('getShareSecret')
|
||||
->willReturn('token');
|
||||
$share->method('getSharedBy')
|
||||
->willReturn('user1@nextcloud.remote');
|
||||
$share->method('getSharedByDisplayName')
|
||||
->willReturn('User 1');
|
||||
|
||||
$this->calendarFederationConfig->expects(self::once())
|
||||
->method('isFederationEnabled')
|
||||
->willReturn(true);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('insert');
|
||||
$this->jobList->expects(self::never())
|
||||
|
||||
@@ -9,13 +9,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCA\DAV\CalDAV\SyncService as CalDavSyncService;
|
||||
use OCA\DAV\CalDAV\SyncServiceResult;
|
||||
use OCP\Federation\ICloudId;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Http\Client\IClient;
|
||||
use OCP\Http\Client\IClientService;
|
||||
use OCP\Http\Client\IResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -26,21 +30,30 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private CalDavSyncService&MockObject $calDavSyncService;
|
||||
private CalDavBackend&MockObject $backend;
|
||||
private IDBConnection&MockObject $dbConnection;
|
||||
private ICloudIdManager&MockObject $cloudIdManager;
|
||||
private IClientService&MockObject $clientService;
|
||||
private IConfig&MockObject $config;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->calDavSyncService = $this->createMock(CalDavSyncService::class);
|
||||
$this->backend = $this->createMock(CalDavBackend::class);
|
||||
$this->dbConnection = $this->createMock(IDBConnection::class);
|
||||
$this->cloudIdManager = $this->createMock(ICloudIdManager::class);
|
||||
$this->clientService = $this->createMock(IClientService::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
|
||||
$this->federatedCalendarSyncService = new FederatedCalendarSyncService(
|
||||
$this->clientService,
|
||||
$this->config,
|
||||
$this->federatedCalendarMapper,
|
||||
$this->logger,
|
||||
$this->calDavSyncService,
|
||||
$this->backend,
|
||||
$this->dbConnection,
|
||||
$this->cloudIdManager,
|
||||
);
|
||||
}
|
||||
@@ -61,16 +74,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/101', 10));
|
||||
// Mock HTTP client for sync report
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/101</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('updateSyncTokenAndTime')
|
||||
@@ -78,7 +99,7 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTime');
|
||||
|
||||
$this->assertEquals(10, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
$this->assertEquals(0, $this->federatedCalendarSyncService->syncOne($calendar));
|
||||
}
|
||||
|
||||
public function testSyncOneUnchanged(): void {
|
||||
@@ -97,16 +118,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult('http://sabre.io/ns/sync/100', 0));
|
||||
// Mock HTTP client for sync report
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>http://sabre.io/ns/sync/100</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
@@ -143,16 +172,24 @@ class FederatedCalendarSyncServiceTest extends TestCase {
|
||||
->with('user1')
|
||||
->willReturn($cloudId);
|
||||
|
||||
$this->calDavSyncService->expects(self::once())
|
||||
->method('syncRemoteCalendar')
|
||||
->with(
|
||||
'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2',
|
||||
'dXNlcjFAbmV4dGNsb3VkLnRlc3Rpbmc=',
|
||||
'token',
|
||||
'http://sabre.io/ns/sync/100',
|
||||
$calendar,
|
||||
)
|
||||
->willReturn(new SyncServiceResult($syncToken, 10));
|
||||
// Mock HTTP client for sync report with unexpected token format
|
||||
$client = $this->createMock(IClient::class);
|
||||
$response = $this->createMock(IResponse::class);
|
||||
$response->method('getBody')
|
||||
->willReturn('<?xml version="1.0"?><d:multistatus xmlns:d="DAV:"><d:sync-token>' . $syncToken . '</d:sync-token></d:multistatus>');
|
||||
|
||||
$client->expects(self::once())
|
||||
->method('request')
|
||||
->with('REPORT', 'https://remote.tld/remote.php/dav/remote-calendars/abcdef123/cal1_shared_by_user2', self::anything())
|
||||
->willReturn($response);
|
||||
|
||||
$this->clientService->method('newClient')
|
||||
->willReturn($client);
|
||||
|
||||
$this->config->method('getSystemValueInt')
|
||||
->willReturn(30);
|
||||
$this->config->method('getSystemValue')
|
||||
->willReturn(false);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('updateSyncTokenAndTime');
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\DAV\Tests\unit\CalDAV\Federation;
|
||||
|
||||
use OCA\DAV\CalDAV\CalDavBackend;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendar;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarEntity;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarMapper;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarObject;
|
||||
use OCA\DAV\CalDAV\Federation\FederatedCalendarSyncService;
|
||||
use OCP\Constants;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Sabre\DAV\Exception\MethodNotAllowed;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
use Sabre\DAV\PropPatch;
|
||||
use Test\TestCase;
|
||||
|
||||
class FederatedCalendarTest extends TestCase {
|
||||
private FederatedCalendar $federatedCalendar;
|
||||
|
||||
private FederatedCalendarMapper&MockObject $federatedCalendarMapper;
|
||||
private FederatedCalendarSyncService&MockObject $federatedCalendarService;
|
||||
private CalDavBackend&MockObject $caldavBackend;
|
||||
private FederatedCalendarEntity $federationInfo;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->federatedCalendarMapper = $this->createMock(FederatedCalendarMapper::class);
|
||||
$this->federatedCalendarService = $this->createMock(FederatedCalendarSyncService::class);
|
||||
$this->caldavBackend = $this->createMock(CalDavBackend::class);
|
||||
|
||||
$this->federationInfo = new FederatedCalendarEntity();
|
||||
$this->federationInfo->setId(10);
|
||||
$this->federationInfo->setPrincipaluri('principals/users/user1');
|
||||
$this->federationInfo->setUri('calendar-uri');
|
||||
$this->federationInfo->setDisplayName('Federated Calendar');
|
||||
$this->federationInfo->setColor('#ff0000');
|
||||
$this->federationInfo->setSharedBy('user2@nextcloud.remote');
|
||||
$this->federationInfo->setSharedByDisplayName('User 2');
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
|
||||
$this->federationInfo->setLastSync(1234567890);
|
||||
|
||||
$this->federatedCalendarMapper->method('findByUri')
|
||||
->with('principals/users/user1', 'calendar-uri')
|
||||
->willReturn($this->federationInfo);
|
||||
|
||||
$calendarInfo = [
|
||||
'principaluri' => 'principals/users/user1',
|
||||
'id' => 10,
|
||||
'uri' => 'calendar-uri',
|
||||
];
|
||||
|
||||
$this->federatedCalendar = new FederatedCalendar(
|
||||
$this->federatedCalendarMapper,
|
||||
$this->federatedCalendarService,
|
||||
$this->caldavBackend,
|
||||
$calendarInfo,
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetResourceId(): void {
|
||||
$this->assertEquals(10, $this->federatedCalendar->getResourceId());
|
||||
}
|
||||
|
||||
public function testGetName(): void {
|
||||
$this->assertEquals('calendar-uri', $this->federatedCalendar->getName());
|
||||
}
|
||||
|
||||
public function testSetName(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Renaming federated calendars is not allowed');
|
||||
$this->federatedCalendar->setName('new-name');
|
||||
}
|
||||
|
||||
public function testGetPrincipalURI(): void {
|
||||
$this->assertEquals('principals/users/user1', $this->federatedCalendar->getPrincipalURI());
|
||||
}
|
||||
|
||||
public function testGetOwner(): void {
|
||||
$expected = 'principals/remote-users/' . base64_encode('user2@nextcloud.remote');
|
||||
$this->assertEquals($expected, $this->federatedCalendar->getOwner());
|
||||
}
|
||||
|
||||
public function testGetGroup(): void {
|
||||
$this->assertNull($this->federatedCalendar->getGroup());
|
||||
}
|
||||
|
||||
public function testGetACLWithReadOnlyPermissions(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(3, $acl);
|
||||
// Check basic read permissions
|
||||
$this->assertEquals('{DAV:}read', $acl[0]['privilege']);
|
||||
$this->assertTrue($acl[0]['protected']);
|
||||
$this->assertEquals('{DAV:}read-acl', $acl[1]['privilege']);
|
||||
$this->assertTrue($acl[1]['protected']);
|
||||
$this->assertEquals('{DAV:}write-properties', $acl[2]['privilege']);
|
||||
$this->assertTrue($acl[2]['protected']);
|
||||
}
|
||||
|
||||
public function testGetACLWithCreatePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that create permission is added
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}bind', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithUpdatePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_UPDATE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that update permission is added (write-content, not write-properties which is already in base ACL)
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}write-content', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithDeletePermission(): void {
|
||||
$this->federationInfo->setPermissions(Constants::PERMISSION_READ | Constants::PERMISSION_DELETE);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(4, $acl);
|
||||
// Check that delete permission is added
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}unbind', $privileges);
|
||||
}
|
||||
|
||||
public function testGetACLWithAllPermissions(): void {
|
||||
$this->federationInfo->setPermissions(
|
||||
Constants::PERMISSION_READ
|
||||
| Constants::PERMISSION_CREATE
|
||||
| Constants::PERMISSION_UPDATE
|
||||
| Constants::PERMISSION_DELETE
|
||||
);
|
||||
|
||||
$acl = $this->federatedCalendar->getACL();
|
||||
|
||||
$this->assertCount(6, $acl);
|
||||
$privileges = array_column($acl, 'privilege');
|
||||
$this->assertContains('{DAV:}read', $privileges);
|
||||
$this->assertContains('{DAV:}bind', $privileges);
|
||||
$this->assertContains('{DAV:}write-content', $privileges);
|
||||
$this->assertContains('{DAV:}write-properties', $privileges);
|
||||
$this->assertContains('{DAV:}unbind', $privileges);
|
||||
}
|
||||
|
||||
public function testSetACL(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Changing ACLs on federated calendars is not allowed');
|
||||
$this->federatedCalendar->setACL([]);
|
||||
}
|
||||
|
||||
public function testGetSupportedPrivilegeSet(): void {
|
||||
$this->assertNull($this->federatedCalendar->getSupportedPrivilegeSet());
|
||||
}
|
||||
|
||||
public function testGetProperties(): void {
|
||||
$properties = $this->federatedCalendar->getProperties([
|
||||
'{DAV:}displayname',
|
||||
'{http://apple.com/ns/ical/}calendar-color',
|
||||
]);
|
||||
|
||||
$this->assertEquals('Federated Calendar', $properties['{DAV:}displayname']);
|
||||
$this->assertEquals('#ff0000', $properties['{http://apple.com/ns/ical/}calendar-color']);
|
||||
}
|
||||
|
||||
public function testPropPatchWithDisplayName(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([
|
||||
'{DAV:}displayname' => 'New Calendar Name',
|
||||
]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $entity) {
|
||||
$this->assertEquals('New Calendar Name', $entity->getDisplayName());
|
||||
return $entity;
|
||||
});
|
||||
|
||||
$propPatch->expects(self::once())
|
||||
->method('setResultCode')
|
||||
->with('{DAV:}displayname', 200);
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testPropPatchWithColor(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([
|
||||
'{http://apple.com/ns/ical/}calendar-color' => '#00ff00',
|
||||
]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('update')
|
||||
->willReturnCallback(function (FederatedCalendarEntity $entity) {
|
||||
$this->assertEquals('#00ff00', $entity->getColor());
|
||||
return $entity;
|
||||
});
|
||||
|
||||
$propPatch->expects(self::once())
|
||||
->method('setResultCode')
|
||||
->with('{http://apple.com/ns/ical/}calendar-color', 200);
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testPropPatchWithNoMutations(): void {
|
||||
$propPatch = $this->createMock(PropPatch::class);
|
||||
$propPatch->method('getMutations')
|
||||
->willReturn([]);
|
||||
|
||||
$this->federatedCalendarMapper->expects(self::never())
|
||||
->method('update');
|
||||
|
||||
$propPatch->expects(self::never())
|
||||
->method('handle');
|
||||
|
||||
$this->federatedCalendar->propPatch($propPatch);
|
||||
}
|
||||
|
||||
public function testGetChildACL(): void {
|
||||
$this->assertEquals($this->federatedCalendar->getACL(), $this->federatedCalendar->getChildACL());
|
||||
}
|
||||
|
||||
public function testGetLastModified(): void {
|
||||
$this->assertEquals(1234567890, $this->federatedCalendar->getLastModified());
|
||||
}
|
||||
|
||||
public function testDelete(): void {
|
||||
$this->federatedCalendarMapper->expects(self::once())
|
||||
->method('deleteById')
|
||||
->with(10);
|
||||
|
||||
$this->federatedCalendar->delete();
|
||||
}
|
||||
|
||||
public function testCreateDirectory(): void {
|
||||
$this->expectException(MethodNotAllowed::class);
|
||||
$this->expectExceptionMessage('Creating nested collection is not allowed');
|
||||
$this->federatedCalendar->createDirectory('test');
|
||||
}
|
||||
|
||||
public function testCalendarQuery(): void {
|
||||
$filters = ['comp-filter' => ['name' => 'VEVENT']];
|
||||
$expectedUris = ['event1.ics', 'event2.ics'];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('calendarQuery')
|
||||
->with(10, $filters, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($expectedUris);
|
||||
|
||||
$result = $this->federatedCalendar->calendarQuery($filters);
|
||||
$this->assertEquals($expectedUris, $result);
|
||||
}
|
||||
|
||||
public function testGetChild(): void {
|
||||
$objectData = [
|
||||
'id' => 1,
|
||||
'uri' => 'event1.ics',
|
||||
'calendardata' => 'BEGIN:VCALENDAR...',
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'event1.ics', 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objectData);
|
||||
|
||||
$child = $this->federatedCalendar->getChild('event1.ics');
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $child);
|
||||
}
|
||||
|
||||
public function testGetChildNotFound(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'nonexistent.ics', 2)
|
||||
->willReturn(null);
|
||||
|
||||
$this->expectException(NotFound::class);
|
||||
$this->federatedCalendar->getChild('nonexistent.ics');
|
||||
}
|
||||
|
||||
public function testGetChildren(): void {
|
||||
$objects = [
|
||||
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObjects')
|
||||
->with(10, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objects);
|
||||
|
||||
$children = $this->federatedCalendar->getChildren();
|
||||
$this->assertCount(2, $children);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
|
||||
}
|
||||
|
||||
public function testGetMultipleChildren(): void {
|
||||
$paths = ['event1.ics', 'event2.ics'];
|
||||
$objects = [
|
||||
['id' => 1, 'uri' => 'event1.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
['id' => 2, 'uri' => 'event2.ics', 'calendardata' => 'BEGIN:VCALENDAR...'],
|
||||
];
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getMultipleCalendarObjects')
|
||||
->with(10, $paths, 2) // 2 is CALENDAR_TYPE_FEDERATED
|
||||
->willReturn($objects);
|
||||
|
||||
$children = $this->federatedCalendar->getMultipleChildren($paths);
|
||||
$this->assertCount(2, $children);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[0]);
|
||||
$this->assertInstanceOf(FederatedCalendarObject::class, $children[1]);
|
||||
}
|
||||
|
||||
public function testChildExists(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'event1.ics', 2)
|
||||
->willReturn(['id' => 1, 'uri' => 'event1.ics']);
|
||||
|
||||
$result = $this->federatedCalendar->childExists('event1.ics');
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
public function testChildNotExists(): void {
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('getCalendarObject')
|
||||
->with(10, 'nonexistent.ics', 2)
|
||||
->willReturn(null);
|
||||
|
||||
$result = $this->federatedCalendar->childExists('nonexistent.ics');
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
public function testCreateFile(): void {
|
||||
$calendarData = 'BEGIN:VCALENDAR...END:VCALENDAR';
|
||||
$remoteEtag = '"remote-etag-123"';
|
||||
$localEtag = '"local-etag-456"';
|
||||
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics', $calendarData)
|
||||
->willReturn($remoteEtag);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('createCalendarObject')
|
||||
->with(10, 'event1.ics', $calendarData, 2)
|
||||
->willReturn($localEtag);
|
||||
|
||||
$result = $this->federatedCalendar->createFile('event1.ics', $calendarData);
|
||||
$this->assertEquals($localEtag, $result);
|
||||
}
|
||||
|
||||
public function testUpdateFile(): void {
|
||||
$calendarData = 'BEGIN:VCALENDAR...UPDATED...END:VCALENDAR';
|
||||
$remoteEtag = '"remote-etag-updated"';
|
||||
$localEtag = '"local-etag-updated"';
|
||||
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('updateCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics', $calendarData)
|
||||
->willReturn($remoteEtag);
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('updateCalendarObject')
|
||||
->with(10, 'event1.ics', $calendarData, 2)
|
||||
->willReturn($localEtag);
|
||||
|
||||
$result = $this->federatedCalendar->updateFile('event1.ics', $calendarData);
|
||||
$this->assertEquals($localEtag, $result);
|
||||
}
|
||||
|
||||
public function testDeleteFile(): void {
|
||||
$this->federatedCalendarService->expects(self::once())
|
||||
->method('deleteCalendarObject')
|
||||
->with($this->federationInfo, 'event1.ics');
|
||||
|
||||
$this->caldavBackend->expects(self::once())
|
||||
->method('deleteCalendarObject')
|
||||
->with(10, 'event1.ics', 2);
|
||||
|
||||
$this->federatedCalendar->deleteFile('event1.ics');
|
||||
}
|
||||
}
|
||||
@@ -545,7 +545,7 @@ class Crypt {
|
||||
$options,
|
||||
$iv);
|
||||
|
||||
if ($plainContent) {
|
||||
if ($plainContent !== false) {
|
||||
return $plainContent;
|
||||
} else {
|
||||
throw new DecryptionFailedException('Encryption library: Decryption (symmetric) of content failed: ' . openssl_error_string());
|
||||
|
||||
@@ -95,7 +95,7 @@ class Backends extends Base {
|
||||
*/
|
||||
private function formatConfiguration(array $parameters): array {
|
||||
$configuration = array_filter($parameters, function (DefinitionParameter $parameter) {
|
||||
return $parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN);
|
||||
return !$parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN);
|
||||
});
|
||||
return array_map(function (DefinitionParameter $parameter) {
|
||||
return $parameter->getTypeName();
|
||||
|
||||
@@ -270,8 +270,8 @@ class AmazonS3 extends Common {
|
||||
$connection->deleteObjects([
|
||||
'Bucket' => $this->bucket,
|
||||
'Delete' => [
|
||||
'Quiet' => true,
|
||||
'Objects' => array_map(fn (array $object) => [
|
||||
'ETag' => $object['ETag'],
|
||||
'Key' => $object['Key'],
|
||||
], $objects['Contents'])
|
||||
]
|
||||
|
||||
@@ -233,7 +233,7 @@ OC.L10N.register(
|
||||
"Create a new share link" : "Креирајте нов линк за споделување",
|
||||
"Quick share options, the current selected is \"{selectedOption}\"" : "Опции за брзо споделување за , тековната избрана е \"{selectedOption}\"",
|
||||
"View only" : "Само за гледање",
|
||||
"Can edit" : "Може да се уредува",
|
||||
"Can edit" : "Може да уредува",
|
||||
"Custom permissions" : "Прилагодени дозволи",
|
||||
"Resharing is not allowed" : "Повторно споделување не е дозволено",
|
||||
"Name or email …" : "Име или е-пошта …",
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
"Create a new share link" : "Креирајте нов линк за споделување",
|
||||
"Quick share options, the current selected is \"{selectedOption}\"" : "Опции за брзо споделување за , тековната избрана е \"{selectedOption}\"",
|
||||
"View only" : "Само за гледање",
|
||||
"Can edit" : "Може да се уредува",
|
||||
"Can edit" : "Може да уредува",
|
||||
"Custom permissions" : "Прилагодени дозволи",
|
||||
"Resharing is not allowed" : "Повторно споделување не е дозволено",
|
||||
"Name or email …" : "Име или е-пошта …",
|
||||
|
||||
Generated
+21
-21
@@ -6730,9 +6730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/asn1.js/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/assert": {
|
||||
@@ -7084,9 +7084,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
|
||||
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
|
||||
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
@@ -8114,9 +8114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/create-ecdh/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/create-hash": {
|
||||
@@ -8690,9 +8690,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/diffie-hellman/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dijkstrajs": {
|
||||
@@ -8889,9 +8889,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/elliptic/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-mart-vue-fast": {
|
||||
@@ -13028,9 +13028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/miller-rabin/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime": {
|
||||
@@ -14750,9 +14750,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/public-encrypt/node_modules/bn.js": {
|
||||
"version": "4.12.2",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
|
||||
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
|
||||
"version": "4.12.3",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
|
||||
"integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
|
||||
@@ -21,6 +21,38 @@ $qb->selectColumnsDistinct('f');
|
||||
$qb->selectAlias('g', 'h');
|
||||
$qb->selectAlias($qb->func()->lower('i'), 'j');
|
||||
|
||||
$qb
|
||||
->setParameter('k', 'l')
|
||||
->setParameters([])
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(0)
|
||||
->delete()
|
||||
->update()
|
||||
->insert()
|
||||
->from('m')
|
||||
->join('n', 'o', 'p')
|
||||
->innerJoin('q', 'r', 's')
|
||||
->leftJoin('t', 'u', 'v')
|
||||
->rightJoin('w', 'x', 'y')
|
||||
->set('z', '1')
|
||||
->where()
|
||||
->andWhere()
|
||||
->orWhere()
|
||||
->groupBy()
|
||||
->addGroupBy()
|
||||
->setValue('2', '3')
|
||||
->values([])
|
||||
->having()
|
||||
->andHaving()
|
||||
->orHaving()
|
||||
->orderBy('4')
|
||||
->addOrderBy('5')
|
||||
->resetQueryParts()
|
||||
->resetQueryPart('6')
|
||||
->hintShardKey('7', '8')
|
||||
->runAcrossAllShards()
|
||||
->forUpdate();
|
||||
|
||||
/** @psalm-check-type-exact $result = \OCP\DB\IResult<'a'|'b'|'c'|'d'|'e'|'f'|'h'|'j'> */
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
|
||||
@@ -46,29 +46,16 @@ $CONFIG = [
|
||||
'instanceid' => '',
|
||||
|
||||
/**
|
||||
* This parameter is used to differentiate between different PHP server instances
|
||||
* that share the same ``config.php``. Its core use is in the "Snowflake" ID generation
|
||||
* system, which ensures that unique IDs created in distributed environments cannot collide.
|
||||
* This is a unique identifier for your server.
|
||||
* It is useful when your Nextcloud instance is using different PHP servers.
|
||||
* Once it's set it shouldn't be changed.
|
||||
*
|
||||
* The ``serverid`` is an integer (0–1023) that must be unique for each PHP server.
|
||||
* Value must be an integer, comprised between 0 and 1023.
|
||||
*
|
||||
* This parameter is especially important if you deploy Nextcloud across multiple servers
|
||||
* using a shared ``config.php`` file.
|
||||
* When config.php is shared between different servers, this value should be overriden with "NC_serverid=<int>" on each server.
|
||||
* Note that it must be overriden for CLI and for your webserver.
|
||||
*
|
||||
* Once it is set, it should not be changed, to avoid data inconsistencies.
|
||||
*
|
||||
* In shared configurations, override ``serverid`` by setting the environment variable
|
||||
* ``NC_serverid=<int>`` for each server (applies to both web servers and CLI commands).
|
||||
*
|
||||
* Example for CLI::
|
||||
*
|
||||
* NC_serverid=42 occ config:list system
|
||||
*
|
||||
* For simple, standalone deployments, the default fallback (based on the hostname)
|
||||
* is generally safe as long as your server's hostname does not change and you do not
|
||||
* clone or migrate the installation. However, if you plan to clone, migrate,
|
||||
* or scale your installation at any time, it is recommended to explicitly set ``serverid``
|
||||
* to a unique, fixed integer between 0 and 1023.
|
||||
* Example for CLI: NC_serverid=42 occ config:list system
|
||||
*/
|
||||
'serverid' => -1,
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ class StatusCommand extends Command implements CompletionAwareInterface {
|
||||
|
||||
$numExecutedUnavailableMigrations = count($executedUnavailableMigrations);
|
||||
$numNewMigrations = count(array_diff(array_keys($availableMigrations), $executedMigrations));
|
||||
$pending = $ms->describeMigrationStep('lastest');
|
||||
$pending = $ms->describeMigrationStep();
|
||||
|
||||
$infos = [
|
||||
'App' => $ms->getApp(),
|
||||
|
||||
@@ -43,6 +43,12 @@ class ResetPassword extends Base {
|
||||
InputOption::VALUE_NONE,
|
||||
'read password from environment variable NC_PASS/OC_PASS'
|
||||
)
|
||||
->addOption(
|
||||
'no-password',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
'Sets the password to blank'
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -76,22 +82,32 @@ class ResetPassword extends Base {
|
||||
}
|
||||
}
|
||||
|
||||
$question = new Question('Enter a new password: ');
|
||||
$question->setHidden(true);
|
||||
$password = $helper->ask($input, $output, $question);
|
||||
if ($input->getOption('no-password')) {
|
||||
$question = new ConfirmationQuestion('Are you sure you want to clear the password for ' . $username . '?');
|
||||
|
||||
if ($password === null) {
|
||||
$output->writeln('<error>Password cannot be empty!</error>');
|
||||
return 1;
|
||||
}
|
||||
if (!$helper->ask($input, $output, $question)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$question = new Question('Confirm the new password: ');
|
||||
$question->setHidden(true);
|
||||
$confirm = $helper->ask($input, $output, $question);
|
||||
$password = '';
|
||||
} else {
|
||||
$question = new Question('Enter a new password: ');
|
||||
$question->setHidden(true);
|
||||
$password = $helper->ask($input, $output, $question);
|
||||
|
||||
if ($password !== $confirm) {
|
||||
$output->writeln('<error>Passwords did not match!</error>');
|
||||
return 1;
|
||||
if ($password === null) {
|
||||
$output->writeln('<error>Password cannot be empty!</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$question = new Question('Confirm the new password: ');
|
||||
$question->setHidden(true);
|
||||
$confirm = $helper->ask($input, $output, $question);
|
||||
|
||||
if ($password !== $confirm) {
|
||||
$output->writeln('<error>Passwords did not match!</error>');
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$output->writeln('<error>Interactive input or --password-from-env is needed for entering a new password!</error>');
|
||||
|
||||
+4
-3
@@ -766,9 +766,10 @@ class OC {
|
||||
self::checkConfig();
|
||||
self::checkInstalled($systemConfig);
|
||||
|
||||
self::addSecurityHeaders();
|
||||
|
||||
self::performSameSiteCookieProtection($config);
|
||||
if (!self::$CLI) {
|
||||
self::addSecurityHeaders();
|
||||
self::performSameSiteCookieProtection($config);
|
||||
}
|
||||
|
||||
if (!defined('OC_CONSOLE')) {
|
||||
$eventLogger->start('check_server', 'Run a few configuration checks');
|
||||
|
||||
@@ -800,14 +800,24 @@ class ObjectStoreStorage extends Common implements IChunkedFileWrite {
|
||||
$this->getCache()->update($stat['fileid'], $stat);
|
||||
}
|
||||
} catch (S3MultipartUploadException|S3Exception $e) {
|
||||
$this->objectStore->abortMultipartUpload($urn, $writeToken);
|
||||
$this->logger->error(
|
||||
'Could not complete multipart upload ' . $urn . ' with uploadId ' . $writeToken,
|
||||
'Unable to complete multipart upload for "' . $urn . '" (uploadId: "' . $writeToken . '")',
|
||||
[
|
||||
'app' => 'objectstore',
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
try {
|
||||
$this->objectStore->abortMultipartUpload($urn, $writeToken);
|
||||
} catch (S3Exception $e) {
|
||||
$this->logger->error(
|
||||
'Unable to abort multipart upload for "' . $urn . '" (uploadId: "' . $writeToken . '") after completion error',
|
||||
[
|
||||
'app' => 'objectstore',
|
||||
'exception' => $e,
|
||||
]
|
||||
);
|
||||
}
|
||||
throw new GenericFileException('Could not write chunked file');
|
||||
}
|
||||
return $size;
|
||||
|
||||
@@ -145,7 +145,7 @@ class Factory implements IFactory {
|
||||
if ($lang === null) {
|
||||
return null;
|
||||
}
|
||||
$lang = preg_replace('/[^a-zA-Z0-9.;,=-]/', '', $lang);
|
||||
$lang = preg_replace('/[^a-zA-Z0-9.;,=_-]/', '', $lang);
|
||||
return str_replace('..', '', $lang);
|
||||
}
|
||||
|
||||
|
||||
@@ -199,14 +199,19 @@ class Repair implements IOutput {
|
||||
* @return list<IRepairStep>
|
||||
*/
|
||||
public static function getExpensiveRepairSteps(): array {
|
||||
return [
|
||||
$expensiveSteps = [
|
||||
Server::get(OldGroupMembershipShares::class),
|
||||
Server::get(RemoveBrokenProperties::class),
|
||||
Server::get(RepairMimeTypes::class),
|
||||
Server::get(DeleteSchedulingObjects::class),
|
||||
Server::get(RemoveObjectProperties::class),
|
||||
Server::get(CleanupShareTarget::class),
|
||||
];
|
||||
|
||||
if (class_exists(CleanupShareTarget::class)) {
|
||||
$expensiveSteps[] = Server::get(CleanupShareTarget::class);
|
||||
}
|
||||
|
||||
return $expensiveSteps;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -12,28 +14,27 @@ use OCP\Files\NotFoundException;
|
||||
use OCP\Files\NotPermittedException;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\Share\IShareHelper;
|
||||
use Override;
|
||||
|
||||
class ShareHelper implements IShareHelper {
|
||||
public function __construct(
|
||||
private IManager $shareManager,
|
||||
private readonly IManager $shareManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return array [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
*/
|
||||
public function getPathsForAccessList(Node $node) {
|
||||
#[Override]
|
||||
public function getPathsForAccessList(Node $node): array {
|
||||
$result = [
|
||||
'users' => [],
|
||||
'remotes' => [],
|
||||
];
|
||||
|
||||
$accessList = $this->shareManager->getAccessList($node, true, true);
|
||||
if (!empty($accessList['users'])) {
|
||||
if (isset($accessList['users']) && $accessList['users'] !== []) {
|
||||
$result['users'] = $this->getPathsForUsers($node, $accessList['users']);
|
||||
}
|
||||
if (!empty($accessList['remote'])) {
|
||||
|
||||
if (isset($accessList['remote']) && $accessList['remote'] !== []) {
|
||||
$result['remotes'] = $this->getPathsForRemotes($node, $accessList['remote']);
|
||||
}
|
||||
|
||||
@@ -60,20 +61,20 @@ class ShareHelper implements IShareHelper {
|
||||
* 'test3' => '/cat',
|
||||
* ],
|
||||
*
|
||||
* @param Node $node
|
||||
* @param array[] $users
|
||||
* @return array
|
||||
* @param non-empty-array<string, array{node_id: int, node_path: string}> $users
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function getPathsForUsers(Node $node, array $users) {
|
||||
/** @var array[] $byId */
|
||||
protected function getPathsForUsers(Node $node, array $users): array {
|
||||
/** @var array<int, array<string, string>> $byId */
|
||||
$byId = [];
|
||||
/** @var array[] $results */
|
||||
/** @var array<string, string> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($users as $uid => $info) {
|
||||
if (!isset($byId[$info['node_id']])) {
|
||||
$byId[$info['node_id']] = [];
|
||||
}
|
||||
|
||||
$byId[$info['node_id']][$uid] = $info['node_path'];
|
||||
}
|
||||
|
||||
@@ -82,15 +83,14 @@ class ShareHelper implements IShareHelper {
|
||||
foreach ($byId[$node->getId()] as $uid => $path) {
|
||||
$results[$uid] = $path;
|
||||
}
|
||||
|
||||
unset($byId[$node->getId()]);
|
||||
}
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
} catch (NotFoundException|InvalidPathException) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
if (empty($byId)) {
|
||||
if ($byId === []) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
@@ -98,22 +98,18 @@ class ShareHelper implements IShareHelper {
|
||||
$appendix = '/' . $node->getName();
|
||||
while (!empty($byId)) {
|
||||
try {
|
||||
/** @var Node $item */
|
||||
$item = $item->getParent();
|
||||
|
||||
if (!empty($byId[$item->getId()])) {
|
||||
if ($byId[$item->getId()] !== []) {
|
||||
foreach ($byId[$item->getId()] as $uid => $path) {
|
||||
$results[$uid] = $path . $appendix;
|
||||
}
|
||||
|
||||
unset($byId[$item->getId()]);
|
||||
}
|
||||
|
||||
$appendix = '/' . $item->getName() . $appendix;
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
return $results;
|
||||
} catch (NotPermittedException $e) {
|
||||
} catch (NotFoundException|InvalidPathException|NotPermittedException) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -141,27 +137,27 @@ class ShareHelper implements IShareHelper {
|
||||
* 'test3' => ['token' => 't3', 'node_path' => '/SixTeen/TwentyThree/FortyTwo'],
|
||||
* ],
|
||||
*
|
||||
* @param Node $node
|
||||
* @param array[] $remotes
|
||||
* @return array
|
||||
* @param non-empty-array<string, array{node_id: int, token: string}> $remotes
|
||||
* @return array<string, array{token: string, node_path: string}>
|
||||
*/
|
||||
protected function getPathsForRemotes(Node $node, array $remotes) {
|
||||
/** @var array[] $byId */
|
||||
protected function getPathsForRemotes(Node $node, array $remotes): array {
|
||||
/** @var array<int, array<string, string>> $byId */
|
||||
$byId = [];
|
||||
/** @var array[] $results */
|
||||
/** @var array<string, array{token: string, node_path: string}> $results */
|
||||
$results = [];
|
||||
|
||||
foreach ($remotes as $cloudId => $info) {
|
||||
if (!isset($byId[$info['node_id']])) {
|
||||
$byId[$info['node_id']] = [];
|
||||
}
|
||||
|
||||
$byId[$info['node_id']][$cloudId] = $info['token'];
|
||||
}
|
||||
|
||||
$item = $node;
|
||||
while (!empty($byId)) {
|
||||
try {
|
||||
if (!empty($byId[$item->getId()])) {
|
||||
if ($byId[$item->getId()] !== []) {
|
||||
$path = $this->getMountedPath($item);
|
||||
foreach ($byId[$item->getId()] as $uid => $token) {
|
||||
$results[$uid] = [
|
||||
@@ -169,16 +165,12 @@ class ShareHelper implements IShareHelper {
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
unset($byId[$item->getId()]);
|
||||
}
|
||||
|
||||
/** @var Node $item */
|
||||
$item = $item->getParent();
|
||||
} catch (NotFoundException $e) {
|
||||
return $results;
|
||||
} catch (InvalidPathException $e) {
|
||||
return $results;
|
||||
} catch (NotPermittedException $e) {
|
||||
} catch (NotFoundException|InvalidPathException|NotPermittedException) {
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -186,11 +178,7 @@ class ShareHelper implements IShareHelper {
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return string
|
||||
*/
|
||||
protected function getMountedPath(Node $node) {
|
||||
protected function getMountedPath(Node $node): string {
|
||||
$path = $node->getPath();
|
||||
$sections = explode('/', $path, 4);
|
||||
return '/' . $sections[3];
|
||||
|
||||
@@ -37,7 +37,9 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS ...$columns The columns to select. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @return $this
|
||||
* @since 34.0.0
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
public function selectColumns(string ...$columns): self;
|
||||
|
||||
@@ -52,7 +54,9 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS ...$columns The columns to select distinct. They are not allowed to contain table names or aliases, or asterisks. Use {@see self::selectAlias()} for that.
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @return $this
|
||||
* @since 34.0.0
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
public function selectColumnsDistinct(string ...$columns): self;
|
||||
|
||||
@@ -69,8 +73,244 @@ interface ITypedQueryBuilder extends IQueryBuilder {
|
||||
* @template NewS of string
|
||||
* @param NewS $alias
|
||||
* @psalm-this-out self<S|NewS>
|
||||
* @psalm-suppress LessSpecificImplementedReturnType
|
||||
* @return $this
|
||||
* @note Psalm has a bug that prevents inferring the correct type in chained calls: https://github.com/vimeo/psalm/issues/8803. Convert the chained calls to standalone calls or switch to PHPStan, which suffered the same bug in the past, but fixed it in 2.1.5: https://github.com/phpstan/phpstan/issues/8439
|
||||
*/
|
||||
#[Override]
|
||||
public function selectAlias($select, $alias): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setParameter($key, $value, $type = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function setParameters(array $params, array $types = []);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setFirstResult($firstResult);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setMaxResults($maxResults);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function delete($delete = null, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function update($update = null, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function insert($insert = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function from($from, $alias = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function join($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function innerJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function leftJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function rightJoin($fromAlias, $join, $alias, $condition = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function set($key, $value);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function where(...$predicates);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function andWhere(...$where);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orWhere(...$where);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function groupBy(...$groupBys);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function addGroupBy(...$groupBy);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function setValue($column, $value);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function values(array $values);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function having(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function andHaving(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orHaving(...$having);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function orderBy($sort, $order = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function addOrderBy($sort, $order = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function resetQueryParts($queryPartNames = null);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
* @psalm-suppress MissingParamType
|
||||
*/
|
||||
#[Override]
|
||||
public function resetQueryPart($queryPartName);
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function hintShardKey(string $column, mixed $value, bool $overwrite = false): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function runAcrossAllShards(): self;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return $this
|
||||
*/
|
||||
#[Override]
|
||||
public function forUpdate(ConflictResolutionMode $conflictResolutionMode = ConflictResolutionMode::Ordinary): self;
|
||||
}
|
||||
|
||||
@@ -258,9 +258,9 @@ interface IManager {
|
||||
* @return ($currentAccess is true
|
||||
* ? array{
|
||||
* users?: array<string, array{node_id: int, node_path: string}>,
|
||||
* remote?: array<string, array{node_id: int, node_path: string}>,
|
||||
* remote?: array<string, array{node_id: int, token: string}>,
|
||||
* public?: bool,
|
||||
* mail?: array<string, array{node_id: int, node_path: string}>
|
||||
* mail?: array<string, array{node_id: int, token: string}>
|
||||
* }
|
||||
* : array{users?: list<string>, remote?: bool, public?: bool, mail?: list<string>})
|
||||
* @since 12.0.0
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCP\Share;
|
||||
|
||||
use OCP\AppFramework\Attribute\Consumable;
|
||||
use OCP\Files\Node;
|
||||
|
||||
/**
|
||||
* Interface IShareHelper
|
||||
*
|
||||
* @since 12
|
||||
* @since 12.0.0
|
||||
*/
|
||||
#[Consumable(since: '12.0.0')]
|
||||
interface IShareHelper {
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return array [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
* @return array{users: array<string, string>, remotes: array<string, array{token: string, node_path: string}>} [ users => [Mapping $uid => $pathForUser], remotes => [Mapping $cloudId => $pathToMountRoot]]
|
||||
* @since 12
|
||||
*/
|
||||
public function getPathsForAccessList(Node $node);
|
||||
public function getPathsForAccessList(Node $node): array;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<file name="build/psalm/ITypedQueryBuilderTest.php"/>
|
||||
<file name="lib/private/DB/QueryBuilder/TypedQueryBuilder.php"/>
|
||||
<file name="lib/public/DB/QueryBuilder/ITypedQueryBuilder.php"/>
|
||||
<file name="lib/private/Share20/ShareHelper.php"/>
|
||||
<file name="lib/public/Share/IShareHelper.php"/>
|
||||
<ignoreFiles>
|
||||
<directory name="apps/**/composer"/>
|
||||
<directory name="apps/**/tests"/>
|
||||
|
||||
@@ -95,6 +95,7 @@ class FactoryTest extends TestCase {
|
||||
return [
|
||||
'null shortcut' => [null, null],
|
||||
'default language' => ['de', 'de'],
|
||||
'regional language' => ['de_DE', 'de_DE'],
|
||||
'malicious language' => ['de/../fr', 'defr'],
|
||||
'request language' => ['kab;q=0.8,ka;q=0.7,de;q=0.6', 'kab;q=0.8,ka;q=0.7,de;q=0.6'],
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user