Compare commits

..

30 Commits

Author SHA1 Message Date
Carl Schwan 3caa1467b1 refactor: Improve log message
Co-authored-by: Josh <josh.t.richards@gmail.com>
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
2026-03-02 13:19:08 +01:00
Robin Appelman 2c2335b8b4 fix: improve logging around failed chunked object store uploads
Signed-off-by: Robin Appelman <robin@icewind.nl>
2026-02-27 14:50:49 +01:00
Kent Delante d6eade0119 Merge pull request #58582 from nextcloud/leftybournes/fix/files_external_delete_objects
fix: pass only object key to deleteObjects call
2026-02-27 11:28:19 +08:00
Kent Delante 8d1cb50048 fix: pass only object key to deleteObjects call
Some S3-compatible object storage hosts don't like the ETag being included in
the request and return a MalformedXML response. In the AWS API documentation,
only the object key is required so just pass that in.

Signed-off-by: Kent Delante <kent.delante@proton.me>
2026-02-27 10:54:23 +08:00
Nextcloud bot b4b328cf61 fix(l10n): Update translations from Transifex
Signed-off-by: Nextcloud bot <bot@nextcloud.com>
2026-02-27 00:19:48 +00:00
Carl Schwan e47195a334 Merge pull request #58597 from nextcloud/bugfix/58594/files_sharing-disabled
fix(repair): Fix repair steps when files_sharing is disabled
2026-02-27 00:35:03 +01:00
github-actions[bot] 4d00f49757 Merge pull request #58556 from nextcloud/dependabot/npm_and_yarn/build/frontend-legacy/multi-5543462fab
chore(deps): Bump bn.js in /build/frontend-legacy
2026-02-26 21:58:10 +01:00
Andy Scherzinger 83d795dd18 Merge pull request #58595 from nextcloud/fix/db-occ-pending-migrations-typo
fix(db): pending migrations in `occ migrations:status`
2026-02-26 21:56:27 +01:00
Joas Schilling a6b9483a5f fix(repair): Fix repair steps when files_sharing is disabled
Signed-off-by: Joas Schilling <coding@schilljs.com>
2026-02-26 21:51:13 +01:00
Josh 636345bac8 fix(db): pending migrations in occ migrations:status
Fixes #58569

Signed-off-by: Josh <josh.t.richards@gmail.com>
2026-02-26 12:38:00 -05:00
Benjamin Gaussorgues 79d4953e64 Merge pull request #58535 from nextcloud/fix/do-not-send-headers-on-cli 2026-02-26 16:20:52 +01:00
Sebastian Krupinski ca8050b94e Merge pull request #58008 from nextcloud/feat/calendar-federation-readwrite
feat: calendar read and write federation
2026-02-26 09:01:25 -05:00
Carl Schwan 47b08a07d8 Merge pull request #58586 from nextcloud/carl/sharehelper-typing
refactor(typing): Correct typing of IShareHelper
2026-02-26 14:21:53 +01:00
Kate 77c070bc93 Merge pull request #58559 from nextcloud/fix/itypedquerybuilder/chained-calls 2026-02-26 13:44:40 +01:00
Kent Delante 09c9241b30 Merge pull request #58042 from nextcloud/feat/clear_password
feat(occ): allow admins to clear account passwords
2026-02-26 20:29:26 +08:00
Kent Delante 711bd2bc6d feat(occ): allow admins to clear account passwords
Signed-off-by: Kent Delante <kent.delante@proton.me>
2026-02-26 19:36:22 +08:00
Carl Schwan 2a81cba978 refactor(typing): Correct typing of IShareHelper
Signed-off-by: Carl Schwan <carlschwan@kde.org>
2026-02-26 12:22:44 +01:00
Andy Scherzinger 6df490942c Merge pull request #58525 from nextcloud/fix/fix-decryption-failure-false-positive
fix(encryption): Improve type strictness on decryption check
2026-02-26 12:10:01 +01:00
Andy Scherzinger 0b8e7bb4f0 Merge pull request #58205 from nextcloud/bug-show-configuration-options-for-again
fix: show configuration options for external storage backends
2026-02-26 11:56:41 +01:00
Salvatore Martire 7e264ba58e Merge pull request #58571 from nextcloud/fix/regional-languages
fix(L10N): stop stripping _ from language codes
2026-02-25 16:54:18 +01:00
SebastianKrupinski 64f319ab4e feat: calendar federation write
Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
2026-02-25 10:35:27 -05:00
Salvatore Martire ec0ed788fa fix(L10N): stop stripping _ from language codes
Stripping the underscore breaks support for all languages like de_AT,
de_DE and so on...

Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
2026-02-25 15:28:34 +01:00
Daniel Kesselberg 67d1fac6f6 fix: show configuration options for external storage backends
The occ files_external_backends command is supposed to list available backends along with their configuration options.

For the SMB backend, only the timeout option is currently shown, while options like host, share, root, and domain are missing. This makes configuring external storage via occ complicated.

Since the timeout option is marked as hidden but still shown, while the other options are not, the logic needs to be inverted so that all relevant configuration options are displayed correctly.

Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
2026-02-25 15:24:20 +01:00
Kate af98eed523 Merge pull request #58548 from nextcloud/artonge/feat/ai_pr_template 2026-02-25 11:52:12 +01:00
Louis Chmn bfac9e7023 feat: Add AI checkbox to pull request template
Request committers to be transparent with their usage of AI.

Signed-off-by: Louis <louis@chmn.me>
Signed-off-by: Louis Chmn <louis@chmn.me>
2026-02-25 10:41:38 +01:00
provokateurin 237d5156b6 fix(ITypedQueryBuilder): Add correct return type and add note about Psalm bug
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-25 09:52:48 +01:00
provokateurin 40c39270c0 fix((ITypedQueryBuilder): Fix chained calls of non-select methods
Signed-off-by: provokateurin <kate@provokateurin.de>
2026-02-25 09:51:59 +01:00
dependabot[bot] f5b18dd7fd chore(deps): Bump bn.js in /build/frontend-legacy
Bumps  and [bn.js](https://github.com/indutny/bn.js). These dependencies needed to be updated together.

Updates `bn.js` from 5.2.2 to 5.2.3
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v5.2.2...v5.2.3)

Updates `bn.js` from 4.12.2 to 4.12.3
- [Release notes](https://github.com/indutny/bn.js/releases)
- [Changelog](https://github.com/indutny/bn.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/indutny/bn.js/compare/v5.2.2...v5.2.3)

---
updated-dependencies:
- dependency-name: bn.js
  dependency-version: 5.2.3
  dependency-type: indirect
- dependency-name: bn.js
  dependency-version: 4.12.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-24 21:02:56 +00:00
Côme Chilliet f885d7292f fix(occ): Do not attempt to send headers on CLI
This avoids errors like 'Cannot modify header information - headers already sent',
 when using --debug-log with occ.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-24 10:22:13 +01:00
Côme Chilliet e4244c5fc8 fix(encryption): Improve type strictness on decryption check
Otherwise decrypting a falsy value like '0' would be seen as a
 decryption failure.

Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
2026-02-23 14:49:03 +01:00
56 changed files with 2271 additions and 1029 deletions
+4
View File
@@ -23,3 +23,7 @@
- [ ] [Backports requested](https://github.com/nextcloud/backportbot/#usage) where applicable (ex: critical bugfixes)
- [ ] [Labels added](https://github.com/nextcloud/server/labels) where applicable (ex: bug/enhancement, `3. to review`, feature component)
- [ ] [Milestone added](https://github.com/nextcloud/server/milestones) for target branch/version (ex: 32.x for `stable32`)
## AI (if applicable)
- [ ] The content of this PR was partly or fully generated using AI
@@ -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',
+2 -4
View File
@@ -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']);
}
}
+9 -1
View File
@@ -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
+24 -21
View File
@@ -15,7 +15,6 @@ use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\ISharedStorage;
use OCP\IUserSession;
use OCP\PaginationParameters;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Sabre\DAV\Exception\NotFound;
@@ -80,9 +79,10 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
/**
* @return list<IShare>
* @param Node $node
* @return IShare[]
*/
private function getShare(Node $node, PaginationParameters $paginationParameters): array {
private function getShare(Node $node): array {
$result = [];
$requestedShareTypes = [
IShare::TYPE_USER,
@@ -95,23 +95,26 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
IShare::TYPE_DECK,
];
$result[] = $this->shareManager->getAllSharesBy(
$this->userId,
$node,
$paginationParameters,
false,
);
// Also check for shares where the user is the recipient
try {
$result[] = $this->shareManager->getAllSharedWith(
foreach ($requestedShareTypes as $requestedShareType) {
$result[] = $this->shareManager->getSharesBy(
$this->userId,
$requestedShareTypes,
$requestedShareType,
$node,
$paginationParameters,
false,
-1
);
} catch (BackendError $e) {
// ignore
// Also check for shares where the user is the recipient
try {
$result[] = $this->shareManager->getSharedWith(
$this->userId,
$requestedShareType,
$node,
-1
);
} catch (BackendError $e) {
// ignore
}
}
return array_merge(...$result);
@@ -152,7 +155,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
return [];
}
$shares = $this->getShare($node, new PaginationParameters(limit: null));
$shares = $this->getShare($node);
$this->cachedShares[$sabreNode->getId()] = $shares;
return $shares;
}
@@ -232,9 +235,9 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
return true;
}
$targetShares = $this->getShare($targetNode->getNode(), new PaginationParameters(limit: null));
$targetShares = $this->getShare($targetNode->getNode());
if (empty($targetShares)) {
// Target is not a share so no re-sharing in-progress
// Target is not a share so no re-sharing inprogress
return true;
}
@@ -250,7 +253,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin {
}
}
// if the share recipient is allowed to delete from the share, they are allowed to move the file out of the share
// if the share recipient is allow to delete from the share, they are allowed to move the file out of the share
// the user moving the file out of the share to their home storage would give them share permissions and allow moving into the share
//
// since the 2-step move is allowed, we also allow both steps at once
+66
View File
@@ -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');
}
}
@@ -87,11 +87,12 @@ class SharesPluginTest extends \Test\TestCase {
});
$this->shareManager->expects($this->any())
->method('getAllSharedWith')
->method('getSharedWith')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->equalTo($node),
$this->equalTo(-1)
)
->willReturn([]);
@@ -182,11 +183,12 @@ class SharesPluginTest extends \Test\TestCase {
});
$this->shareManager->expects($this->any())
->method('getAllSharedWith')
->method('getSharedWith')
->with(
$this->equalTo('user1'),
$this->anything(),
$this->equalTo($node),
$this->equalTo(-1)
)
->willReturn([]);
+1 -1
View File
@@ -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());
@@ -24,38 +24,42 @@ use OCP\IL10N;
use OCP\IUserManager;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\ICreateShareProvider;
use OCP\Share\IShare;
use OCP\Share\IShareProvider;
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
use Override;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Exception\LogicException;
/**
* Class FederatedShareProvider
*
* @package OCA\FederatedFileSharing
*/
class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder, ICreateShareProvider {
private string $externalShareTable = 'share_external';
class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAllSharesInFolder {
public const SHARE_TYPE_REMOTE = 6;
/** @var list<IShare::TYPE_*> list of supported share types */
private array $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE];
/** @var string */
private $externalShareTable = 'share_external';
/** @var array list of supported share types */
private $supportedShareType = [IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE, IShare::TYPE_CIRCLE];
/**
* DefaultShareProvider constructor.
*/
public function __construct(
private readonly IDBConnection $dbConnection,
private readonly AddressHandler $addressHandler,
private readonly Notifications $notifications,
private readonly TokenHandler $tokenHandler,
private readonly IL10N $l,
private readonly IRootFolder $rootFolder,
private readonly IConfig $config,
private readonly IUserManager $userManager,
private readonly ICloudIdManager $cloudIdManager,
private readonly \OCP\GlobalScale\IConfig $gsConfig,
private readonly ICloudFederationProviderManager $cloudFederationProviderManager,
private readonly LoggerInterface $logger,
private IDBConnection $dbConnection,
private AddressHandler $addressHandler,
private Notifications $notifications,
private TokenHandler $tokenHandler,
private IL10N $l,
private IRootFolder $rootFolder,
private IConfig $config,
private IUserManager $userManager,
private ICloudIdManager $cloudIdManager,
private \OCP\GlobalScale\IConfig $gsConfig,
private ICloudFederationProviderManager $cloudFederationProviderManager,
private LoggerInterface $logger,
) {
}
@@ -64,16 +68,6 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
return 'ocFederatedSharing';
}
#[Override]
public function getShareTypes(): array {
return $this->supportedShareType;
}
#[Override]
public function getTokenShareTypes(): array {
return $this->supportedShareType;
}
/**
* Share a path
*
@@ -166,7 +160,7 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
}
$data = $this->getRawShare($shareId);
return $this->createShare($data);
return $this->createShareObject($data);
}
/**
@@ -430,7 +424,7 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
$children[] = $this->createShare($data);
$children[] = $this->createShareObject($data);
}
$cursor->closeCursor();
@@ -577,25 +571,104 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[$data['fileid']][] = $this->createShare($data);
$shares[$data['fileid']][] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
#[Override]
/**
* @inheritdoc
*/
public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
throw new LogicException('Is no longer used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share');
$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter($shareType)));
/**
* Reshares for this user are shares where they are the owner.
*/
if ($reshares === false) {
//Special case for old shares created via the web UI
$or1 = $qb->expr()->andX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->isNull('uid_initiator')
);
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
$or1
)
);
} else {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
)
);
}
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
if ($limit !== -1) {
$qb->setMaxResults($limit);
}
$qb->setFirstResult($offset);
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
#[Override]
/**
* @inheritdoc
*/
public function getShareById($id, $recipientId = null) {
throw new LogicException('Is no longer used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($this->supportedShareType, IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
$data = $cursor->fetchAssociative();
$cursor->closeCursor();
if ($data === false) {
throw new ShareNotFound('Can not find share with ID: ' . $id);
}
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound();
}
return $share;
}
#[Override]
public function getSharesByPath(Node $path): array {
/**
* Get shares for a given path
*
* @param Node $path
* @return IShare[]
*/
public function getSharesByPath(Node $path) {
$qb = $this->dbConnection->getQueryBuilder();
// get federated user shares
@@ -607,15 +680,17 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShare($data);
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
#[Override]
public function getSharedWith(string $userId, int $shareType, ?Node $node, int $limit, int $offset): array {
/**
* @inheritdoc
*/
public function getSharedWith($userId, $shareType, $node, $limit, $offset) {
/** @var IShare[] $shares */
$shares = [];
@@ -644,7 +719,7 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShare($data);
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
@@ -668,7 +743,13 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
return $this->createShare($data);
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
return $share;
}
/**
@@ -696,8 +777,15 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
return $data;
}
#[Override]
public function createShare(array $data): IShare {
/**
* Create a share object from an database row
*
* @param array $data
* @return IShare
* @throws InvalidShare
* @throws ShareNotFound
*/
private function createShareObject($data): IShare {
$share = new Share($this->rootFolder, $this->userManager);
$share->setId((string)$data['id'])
->setShareType((int)$data['share_type'])
@@ -950,8 +1038,25 @@ class FederatedShareProvider implements IShareProvider, IShareProviderSupportsAl
return ['remote' => $remote];
}
#[Override]
public function getAllShares(): iterable {
throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_REMOTE_GROUP, IShare::TYPE_REMOTE], IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
continue;
} catch (ShareNotFound $e) {
continue;
}
yield $share;
}
$cursor->closeCursor();
}
}
@@ -31,7 +31,6 @@ use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\PaginationParameters;
use OCP\Server;
use OCP\Share\Events\ShareTransferredEvent;
use OCP\Share\IManager as IShareManager;
@@ -411,19 +410,16 @@ class OwnershipTransferService {
$progress = new ProgressBar($output);
$normalizedPath = Filesystem::normalizePath($path);
$maxShareId = null;
$offset = 0;
while (true) {
/** @var list<IShare> $sharePage */
$sharePage = $this->shareManager->getAllSharedWith($sourceUid, [IShare::TYPE_USER], null, new PaginationParameters(limit: 50, maxId: $maxShareId));
$sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
$progress->advance(count($sharePage));
if (empty($sharePage)) {
break;
}
$maxShareId = end($sharePage)->getId();
if ($path !== null && $path !== "$sourceUid/files") {
$sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath): bool {
$sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) {
try {
return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/');
} catch (Exception) {
@@ -435,6 +431,8 @@ class OwnershipTransferService {
foreach ($sharePage as $share) {
$shares[$share->getNodeId()] = $share;
}
$offset += 50;
}
+1 -1
View File
@@ -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'])
]
+1 -1
View File
@@ -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 …" : "Име или е-пошта …",
+1 -1
View File
@@ -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 …" : "Име или е-пошта …",
@@ -155,7 +155,7 @@ class Application extends App implements IBootstrap {
// notifications api to accept incoming user shares
$dispatcher->addListener(ShareCreatedEvent::class, function (ShareCreatedEvent $event): void {
/** @var Listener $listener */
$listener = $this->getContainer()->get(Listener::class);
$listener = $this->getContainer()->query(Listener::class);
$listener->shareNotification($event);
});
$dispatcher->addListener(IGroup::class . '::postAddUser', function ($event): void {
@@ -163,7 +163,7 @@ class Application extends App implements IBootstrap {
return;
}
/** @var Listener $listener */
$listener = $this->getContainer()->get(Listener::class);
$listener = $this->getContainer()->query(Listener::class);
$listener->userAddedToGroup($event);
});
}
@@ -23,7 +23,6 @@ use OCP\Files\NotFoundException;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\PaginationParameters;
use OCP\Server;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
@@ -136,9 +135,13 @@ class DeletedShareAPIController extends OCSController {
*/
#[NoAdminRequired]
public function index(): DataResponse {
$shares = $this->shareManager->getAllDeletedSharedWith($this->userId, [IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK], null, new PaginationParameters(limit: null));
$groupShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_GROUP, null, -1, 0);
$teamShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_CIRCLE, null, -1, 0);
$roomShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_ROOM, null, -1, 0);
$deckShares = $this->shareManager->getDeletedSharedWith($this->userId, IShare::TYPE_DECK, null, -1, 0);
$shares = array_map(fn (IShare $share): array => $this->formatShare($share), $shares);
$shares = array_merge($groupShares, $teamShares, $roomShares, $deckShares);
$shares = array_values(array_map(fn (IShare $share): array => $this->formatShare($share), $shares));
return new DataResponse($shares);
}
@@ -57,7 +57,6 @@ use OCP\Lock\ILockingProvider;
use OCP\Lock\LockedException;
use OCP\Mail\IEmailValidator;
use OCP\Mail\IMailer;
use OCP\PaginationParameters;
use OCP\Server;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
@@ -846,11 +845,20 @@ class ShareAPIController extends OCSController {
* @return list<Files_SharingShare>
*/
private function getSharedWithMe($node, bool $includeTags): array {
$shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK];
$shares = $this->shareManager->getAllSharedWith($this->userId, $shareTypes, $node, new PaginationParameters(limit: null), ignoreWithSelf: true);
$userShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_USER, $node, -1, 0);
$groupShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_GROUP, $node, -1, 0);
$circleShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_CIRCLE, $node, -1, 0);
$roomShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_ROOM, $node, -1, 0);
$deckShares = $this->shareManager->getSharedWith($this->userId, IShare::TYPE_DECK, $node, -1, 0);
$shares = array_merge($userShares, $groupShares, $circleShares, $roomShares, $deckShares);
$filteredShares = array_filter($shares, function (IShare $share) {
return $share->getShareOwner() !== $this->userId && $share->getSharedBy() !== $this->userId;
});
$formatted = [];
foreach ($shares as $share) {
foreach ($filteredShares as $share) {
if ($this->canAccessShare($share)) {
try {
$formatted[] = $this->formatShare($share);
@@ -879,30 +887,38 @@ class ShareAPIController extends OCSController {
throw new OCSBadRequestException($this->l->t('Not a directory'));
}
$nodes = $folder->getDirectoryListing();
/** @var IShare[] $shares */
$shares = array_reduce($nodes, function ($carry, $node) {
$carry = array_merge($carry, $this->getAllShares($node, true));
return $carry;
}, []);
// filter out duplicate shares
$known = [];
$formatted = $miniFormatted = [];
$resharingRight = false;
$known = [];
foreach ($folder->getDirectoryListing() as $node) {
foreach ($this->getAllShares($node, true) as $share) {
if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
continue;
}
foreach ($shares as $share) {
if (in_array($share->getId(), $known) || $share->getSharedWith() === $this->userId) {
continue;
}
try {
$format = $this->formatShare($share);
try {
$format = $this->formatShare($share);
$known[] = $share->getId();
$formatted[] = $format;
if ($share->getSharedBy() === $this->userId) {
$miniFormatted[] = $format;
}
if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
$resharingRight = true;
}
} catch (\Exception $e) {
//Ignore this share
$known[] = $share->getId();
$formatted[] = $format;
if ($share->getSharedBy() === $this->userId) {
$miniFormatted[] = $format;
}
if (!$resharingRight && $this->shareProviderResharingRights($this->userId, $share, $folder)) {
$resharingRight = true;
}
} catch (\Exception $e) {
//Ignore this share
}
}
@@ -1408,15 +1424,17 @@ class ShareAPIController extends OCSController {
IShare::TYPE_GROUP
];
$shares = $this->shareManager->getAllSharedWith($this->userId, $shareTypes, null, new PaginationParameters(limit: null));
foreach ($shareTypes as $shareType) {
$shares = $this->shareManager->getSharedWith($this->userId, $shareType, null, -1, 0);
foreach ($shares as $share) {
if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
$pendingShares[] = $share;
foreach ($shares as $share) {
if ($share->getStatus() === IShare::STATUS_PENDING || $share->getStatus() === IShare::STATUS_REJECTED) {
$pendingShares[] = $share;
}
}
}
$result = array_values(array_filter(array_map(function (IShare $share): ?array {
$result = array_values(array_filter(array_map(function (IShare $share) {
$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
$node = $userFolder->getFirstNodeById($share->getNodeId());
if (!$node) {
@@ -1435,7 +1453,7 @@ class ShareAPIController extends OCSController {
} catch (NotFoundException $e) {
return null;
}
}, $pendingShares), function (?array $entry): bool {
}, $pendingShares), function ($entry) {
return $entry !== null;
}));
@@ -1957,10 +1975,39 @@ class ShareAPIController extends OCSController {
*
* @param Node|null $path
* @param boolean $reshares
* @return list<IShare>
* @return IShare[]
*/
private function getAllShares(?Node $path = null, bool $reshares = false): array {
return $this->shareManager->getAllSharesBy($this->userId, $path, new PaginationParameters(limit: null), $reshares);
private function getAllShares(?Node $path = null, bool $reshares = false) {
// Get all shares
$userShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_USER, $path, $reshares, -1, 0);
$groupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_GROUP, $path, $reshares, -1, 0);
$linkShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_LINK, $path, $reshares, -1, 0);
// EMAIL SHARES
$mailShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_EMAIL, $path, $reshares, -1, 0);
// TEAM SHARES
$circleShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_CIRCLE, $path, $reshares, -1, 0);
// TALK SHARES
$roomShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_ROOM, $path, $reshares, -1, 0);
// DECK SHARES
$deckShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_DECK, $path, $reshares, -1, 0);
// FEDERATION
if ($this->shareManager->outgoingServer2ServerSharesAllowed()) {
$federatedShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE, $path, $reshares, -1, 0);
} else {
$federatedShares = [];
}
if ($this->shareManager->outgoingServer2ServerGroupSharesAllowed()) {
$federatedGroupShares = $this->shareManager->getSharesBy($this->userId, IShare::TYPE_REMOTE_GROUP, $path, $reshares, -1, 0);
} else {
$federatedGroupShares = [];
}
return array_merge($userShares, $groupShares, $linkShares, $mailShares, $circleShares, $roomShares, $deckShares, $federatedShares, $federatedGroupShares);
}
@@ -13,7 +13,6 @@ use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Group\Events\UserAddedEvent;
use OCP\IConfig;
use OCP\PaginationParameters;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -40,7 +39,7 @@ class UserAddedToGroupListener implements IEventListener {
}
// Get all group shares this user has access to now to filter later
$shares = $this->shareManager->getAllSharedWith($user->getUID(), [IShare::TYPE_GROUP], null, new PaginationParameters());
$shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1);
foreach ($shares as $share) {
// If this is not the new group we can skip it
+7 -3
View File
@@ -21,7 +21,6 @@ use OCP\Files\Storage\IStorageFactory;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IUser;
use OCP\PaginationParameters;
use OCP\Share\IAttributes;
use OCP\Share\IManager;
use OCP\Share\IShare;
@@ -53,10 +52,15 @@ class MountProvider implements IMountProvider, IAuthoritativeMountProvider, IPar
*/
public function getSuperSharesForUser(IUser $user, array $excludeShares = []): array {
$userId = $user->getUID();
$shareTypes = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK];
$shares = $this->mergeIterables(
$this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1),
$this->shareManager->getSharedWith($userId, IShare::TYPE_GROUP, null, -1),
$this->shareManager->getSharedWith($userId, IShare::TYPE_CIRCLE, null, -1),
$this->shareManager->getSharedWith($userId, IShare::TYPE_ROOM, null, -1),
$this->shareManager->getSharedWith($userId, IShare::TYPE_DECK, null, -1),
);
$excludeShareIds = array_map(fn (IShare $share) => $share->getFullId(), $excludeShares);
$shares = $this->shareManager->getAllSharedWith($userId, $shareTypes, null, new PaginationParameters(limit: null), ignoreWithSelf: true);
$shares = $this->filterShares($shares, $userId, $excludeShareIds);
return $this->buildSuperShares($shares, $user);
}
@@ -13,7 +13,6 @@ use OCP\IGroupManager;
use OCP\IUser;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
use OCP\PaginationParameters;
use OCP\Share\Events\ShareCreatedEvent;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
@@ -61,17 +60,12 @@ class Listener {
/** @var IUser $user */
$user = $event->getArgument('user');
$sinceShareId = null;
$paginationParameters = new PaginationParameters(
limit: 50,
maxId: null,
);
$offset = 0;
while (true) {
$shares = $this->shareManager->getAllSharedWith($user->getUID(), [IShare::TYPE_GROUP], null, $paginationParameters);
$shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, 50, $offset);
if (empty($shares)) {
break;
}
$paginationParameters->maxId = end($shares)->getId();
foreach ($shares as $share) {
if ($share->getSharedWith() !== $group->getGID()) {
@@ -88,6 +82,7 @@ class Listener {
->setUser($user->getUID());
$this->notificationManager->notify($notification);
}
$offset += 50;
}
}
@@ -18,7 +18,6 @@ use OCP\Files\Mount\IMountManager;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDateTimeZone;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUserManager;
use OCP\IUserSession;
@@ -93,7 +92,6 @@ class CapabilitiesTest extends \Test\TestCase {
$this->createMock(ShareDisableChecker::class),
$this->createMock(IDateTimeZone::class),
$appConfig,
$this->createMock(IDBConnection::class),
);
$cap = new Capabilities($config, $appConfig, $shareManager, $appManager);
@@ -16,11 +16,9 @@ use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Share\IManager;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
#[Group(name: 'DB')]
class SharingTest extends TestCase {
private Sharing $admin;
+169 -42
View File
@@ -38,9 +38,7 @@ use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use OCP\Share\IShareProviderWithNotification;
use OCP\Util;
use Override;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Exception\LogicException;
/**
* Class ShareByMail
@@ -48,42 +46,41 @@ use Symfony\Component\Console\Exception\LogicException;
* @package OCA\ShareByMail
*/
class ShareByMailProvider extends DefaultShareProvider implements IShareProviderWithNotification {
public function __construct(
private readonly IConfig $config,
private readonly IDBConnection $dbConnection,
private readonly ISecureRandom $secureRandom,
private readonly IUserManager $userManager,
private readonly IRootFolder $rootFolder,
private readonly IL10N $l,
private readonly LoggerInterface $logger,
private readonly IMailer $mailer,
private readonly IURLGenerator $urlGenerator,
private readonly IManager $activityManager,
private readonly SettingsManager $settingsManager,
private readonly Defaults $defaults,
private readonly IHasher $hasher,
private readonly IEventDispatcher $eventDispatcher,
private readonly IShareManager $shareManager,
private readonly IEmailValidator $emailValidator,
) {
}
#[Override]
/**
* Return the identifier of this provider.
*
* @return string Containing only [a-zA-Z0-9]
*/
public function identifier(): string {
return 'ocMailShare';
}
#[Override]
public function getShareTypes(): array {
return [IShare::TYPE_EMAIL];
public function __construct(
private IConfig $config,
private IDBConnection $dbConnection,
private ISecureRandom $secureRandom,
private IUserManager $userManager,
private IRootFolder $rootFolder,
private IL10N $l,
private LoggerInterface $logger,
private IMailer $mailer,
private IURLGenerator $urlGenerator,
private IManager $activityManager,
private SettingsManager $settingsManager,
private Defaults $defaults,
private IHasher $hasher,
private IEventDispatcher $eventDispatcher,
private IShareManager $shareManager,
private IEmailValidator $emailValidator,
) {
}
#[Override]
public function getTokenShareTypes(): array {
return [IShare::TYPE_EMAIL];
}
#[Override]
/**
* Share a path
*
* @throws ShareNotFound
* @throws \Exception
*/
public function create(IShare $share): IShare {
$shareWith = $share->getSharedWith();
// Check if file is not already shared with the given email,
@@ -803,17 +800,97 @@ class ShareByMailProvider extends DefaultShareProvider implements IShareProvider
throw new GenericShareException('not implemented');
}
#[Override]
/**
* @inheritdoc
*/
public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset): array {
throw new LogicException('Is no longer used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share');
$qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
/**
* Reshares for this user are shares where they are the owner.
*/
if ($reshares === false) {
//Special case for old shares created via the web UI
$or1 = $qb->expr()->andX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->isNull('uid_initiator')
);
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
$or1
)
);
} elseif ($node === null) {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
)
);
}
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
if ($limit !== -1) {
$qb->setMaxResults($limit);
}
$qb->setFirstResult($offset);
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$shares[] = $this->createShareObject($data);
}
$cursor->closeCursor();
return $shares;
}
#[Override]
/**
* @inheritdoc
*/
public function getShareById($id, $recipientId = null): IShare {
throw new LogicException('Is no longer used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)));
$cursor = $qb->executeQuery();
$data = $cursor->fetchAssociative();
$cursor->closeCursor();
if ($data === false) {
throw new ShareNotFound();
}
$data['id'] = (string)$data['id'];
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound();
}
return $share;
}
#[Override]
/**
* Get shares for a given path
*
* @return IShare[]
*/
public function getSharesByPath(Node $path): array {
$qb = $this->dbConnection->getQueryBuilder();
@@ -833,7 +910,9 @@ class ShareByMailProvider extends DefaultShareProvider implements IShareProvider
return $shares;
}
#[Override]
/**
* @inheritdoc
*/
public function getSharedWith($userId, $shareType, $node, $limit, $offset): array {
/** @var IShare[] $shares */
$shares = [];
@@ -871,9 +950,35 @@ class ShareByMailProvider extends DefaultShareProvider implements IShareProvider
return $shares;
}
#[Override]
public function getShareByToken(string $token): never {
throw new LogicException('Is no longer used');
/**
* Get a share by token
*
* @throws ShareNotFound
*/
public function getShareByToken($token): IShare {
$qb = $this->dbConnection->getQueryBuilder();
$cursor = $qb->select('*')
->from('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
->executeQuery();
$data = $cursor->fetchAssociative();
if ($data === false) {
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
$data['id'] = (string)$data['id'];
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
throw new ShareNotFound('Share not found', $this->l->t('Could not find share'));
}
return $share;
}
/**
@@ -1100,7 +1205,29 @@ class ShareByMailProvider extends DefaultShareProvider implements IShareProvider
}
public function getAllShares(): iterable {
throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used');
$qb = $this->dbConnection->getQueryBuilder();
$qb->select('*')
->from('share')
->where(
$qb->expr()->orX(
$qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))
)
);
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
try {
$share = $this->createShareObject($data);
} catch (InvalidShare $e) {
continue;
} catch (ShareNotFound $e) {
continue;
}
yield $share;
}
$cursor->closeCursor();
}
/**
+21 -21
View File
@@ -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": {
+32
View File
@@ -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();
+1 -1
View File
@@ -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(),
+29 -13
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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);
}
+7 -2
View File
@@ -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;
}
/**
+116 -46
View File
@@ -37,7 +37,6 @@ use OCP\Mail\IMailer;
use OCP\Server;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IAttributes;
use OCP\Share\ICreateShareProvider;
use OCP\Share\IManager;
use OCP\Share\IPartialShareProvider;
use OCP\Share\IShare;
@@ -46,7 +45,6 @@ use OCP\Share\IShareProviderSupportsAccept;
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
use OCP\Share\IShareProviderWithNotification;
use OCP\Util;
use Override;
use Psr\Log\LoggerInterface;
use function str_starts_with;
use function strlen;
@@ -61,41 +59,41 @@ class DefaultShareProvider implements
IShareProviderSupportsAccept,
IShareProviderSupportsAllSharesInFolder,
IShareProviderGetUsers,
IPartialShareProvider,
ICreateShareProvider {
IPartialShareProvider {
public function __construct(
private readonly IDBConnection $dbConn,
private readonly IUserManager $userManager,
private readonly IGroupManager $groupManager,
private readonly IRootFolder $rootFolder,
private readonly IMailer $mailer,
private readonly Defaults $defaults,
private readonly IFactory $l10nFactory,
private readonly IURLGenerator $urlGenerator,
private readonly ITimeFactory $timeFactory,
private readonly LoggerInterface $logger,
private readonly IManager $shareManager,
private readonly IConfig $config,
private IDBConnection $dbConn,
private IUserManager $userManager,
private IGroupManager $groupManager,
private IRootFolder $rootFolder,
private IMailer $mailer,
private Defaults $defaults,
private IFactory $l10nFactory,
private IURLGenerator $urlGenerator,
private ITimeFactory $timeFactory,
private LoggerInterface $logger,
private IManager $shareManager,
private IConfig $config,
) {
}
#[Override]
public function identifier(): string {
/**
* Return the identifier of this provider.
*
* @return string Containing only [a-zA-Z0-9]
*/
public function identifier() {
return 'ocinternal';
}
#[Override]
public function getShareTypes(): array {
return [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK];
}
#[Override]
public function getTokenShareTypes(): array {
return [IShare::TYPE_LINK];
}
#[Override]
public function create(IShare $share): IShare {
/**
* Share a path
*
* @param IShare $share
* @return IShare The share object
* @throws ShareNotFound
* @throws \Exception
*/
public function create(IShare $share) {
$qb = $this->dbConn->getQueryBuilder();
$qb->insert('share');
@@ -205,8 +203,16 @@ class DefaultShareProvider implements
return $share;
}
#[Override]
public function update(IShare $share): IShare {
/**
* Update a share
*
* @param IShare $share
* @return IShare The share object
* @throws ShareNotFound
* @throws InvalidPathException
* @throws NotFoundException
*/
public function update(IShare $share) {
$originalShare = $this->getShareById($share->getId());
$shareAttributes = $this->formatShareAttributes($share->getAttributes());
@@ -303,7 +309,14 @@ class DefaultShareProvider implements
return $share;
}
#[Override]
/**
* Accept a share.
*
* @param IShare $share
* @param string $recipient
* @return IShare The share object
* @since 9.0.0
*/
public function acceptShare(IShare $share, string $recipient): IShare {
if ($share->getShareType() === IShare::TYPE_GROUP) {
$group = $this->groupManager->get($share->getSharedWith());
@@ -387,7 +400,11 @@ class DefaultShareProvider implements
return $children;
}
#[Override]
/**
* Delete a share
*
* @param IShare $share
*/
public function delete(IShare $share) {
$qb = $this->dbConn->getQueryBuilder();
$qb->delete('share')
@@ -679,7 +696,9 @@ class DefaultShareProvider implements
return $shares;
}
#[Override]
/**
* @inheritdoc
*/
public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) {
$qb = $this->dbConn->getQueryBuilder();
$qb->select('*')
@@ -725,8 +744,10 @@ class DefaultShareProvider implements
return $shares;
}
#[Override]
public function getShareById(string $id, $recipientId = null): IShare {
/**
* @inheritdoc
*/
public function getShareById($id, $recipientId = null) {
$qb = $this->dbConn->getQueryBuilder();
$qb->select('*')
@@ -769,9 +790,10 @@ class DefaultShareProvider implements
/**
* Get shares for a given path
*
* @return list<IShare>
* @param Node $path
* @return IShare[]
*/
public function getSharesByPath(Node $path): array {
public function getSharesByPath(Node $path) {
$qb = $this->dbConn->getQueryBuilder();
$cursor = $qb->select('*')
@@ -1022,13 +1044,46 @@ class DefaultShareProvider implements
return $shares;
}
#[Override]
public function getShareByToken(string $token): never {
throw new \LogicException('Should no longer be called directly, instead use IManager::getShareByToken');
/**
* Get a share by token
*
* @param string $token
* @return IShare
* @throws ShareNotFound
*/
public function getShareByToken($token) {
$qb = $this->dbConn->getQueryBuilder();
$cursor = $qb->select('*')
->from('share')
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_LINK)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
->executeQuery();
$data = $cursor->fetch();
if ($data === false) {
throw new ShareNotFound();
}
try {
$share = $this->createShare($data);
} catch (InvalidShare $e) {
throw new ShareNotFound();
}
return $share;
}
#[Override]
public function createShare(array $data): IShare {
/**
* Create a share object from a database row
*
* @param mixed[] $data
* @return IShare
* @throws InvalidShare
*/
private function createShare($data) {
$share = new Share($this->rootFolder, $this->userManager);
$share->setId($data['id'])
->setShareType((int)$data['share_type'])
@@ -1664,9 +1719,24 @@ class DefaultShareProvider implements
}
}
#[Override]
public function getAllShares(): iterable {
throw new \LogicException('getAllShare in DefaultShareProvider should no longer be used');
$qb = $this->dbConn->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK], IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
while ($data = $cursor->fetch()) {
try {
$share = $this->createShare($data);
} catch (InvalidShare $e) {
continue;
}
yield $share;
}
$cursor->closeCursor();
}
/**
+45 -348
View File
@@ -19,7 +19,6 @@ use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\SharedStorage;
use OCA\ShareByMail\ShareByMailProvider;
use OCP\Constants;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
@@ -33,14 +32,12 @@ use OCP\HintException;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDateTimeZone;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\PaginationParameters;
use OCP\Security\Events\ValidatePasswordPolicyEvent;
use OCP\Security\IHasher;
use OCP\Security\ISecureRandom;
@@ -56,7 +53,6 @@ use OCP\Share\Exceptions\AlreadySharedException;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\Exceptions\ShareTokenException;
use OCP\Share\ICreateShareProvider;
use OCP\Share\IManager;
use OCP\Share\IPartialShareProvider;
use OCP\Share\IProviderFactory;
@@ -94,7 +90,6 @@ class Manager implements IManager {
private ShareDisableChecker $shareDisableChecker,
private IDateTimeZone $dateTimeZone,
private IAppConfig $appConfig,
private IDBConnection $connection,
) {
$this->l = $this->l10nFactory->get('lib');
// The constructor of LegacyHooks registers the listeners of share events
@@ -1044,50 +1039,14 @@ class Manager implements IManager {
IShare::TYPE_EMAIL,
];
// Figure out which users has some shares with which providers
$qb = $this->connection->getQueryBuilder();
$qb->select('uid_initiator', 'share_type')
->from('share')
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere(
$qb->expr()->orX(
$qb->expr()->in('uid_initiator', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)),
// Special case for old shares created via the web UI
$qb->expr()->andX(
$qb->expr()->in('uid_owner', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)),
$qb->expr()->isNull('uid_initiator')
)
)
);
if (!$node instanceof Folder) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId(), IQueryBuilder::PARAM_INT)));
}
$qb->orderBy('id');
$cursor = $qb->executeQuery();
$rawShares = [];
while ($data = $cursor->fetch()) {
if (!isset($rawShares[$data['uid_initiator']])) {
$rawShares[$data['uid_initiator']] = [];
}
if (!in_array($data['share_type'], $rawShares[$data['uid_initiator']], true)) {
$rawShares[$data['uid_initiator']][] = $data['share_type'];
}
}
$cursor->closeCursor();
foreach ($rawShares as $userId => $shareTypes) {
foreach ($userIds as $userId) {
foreach ($shareTypes as $shareType) {
try {
$provider = $this->factory->getProviderForType($shareType);
} catch (ProviderException) {
} catch (ProviderException $e) {
continue;
}
if ($node instanceof Folder) {
/* We need to get all shares by this user to get subshares */
$shares = $provider->getSharesBy($userId, $shareType, null, false, -1, 0);
@@ -1230,95 +1189,11 @@ class Manager implements IManager {
return $shares;
}
#[Override]
public function getAllSharesBy(string $userId, ?Node $node = null, PaginationParameters $paginationParameters, bool $reshares = false, bool $onlyValid = true): array {
if ($node !== null && !$node instanceof File && !$node instanceof Folder) {
throw new \InvalidArgumentException($this->l->t('Invalid path'));
}
// Get all shares from the providers supporting createShare
$providers = $this->factory->getAllProviders();
$createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider);
$shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getTokenShareTypes(), $createShareProviders)));
if ($node?->getMountPoint() instanceof IShareOwnerlessMount) {
return $this->getSharesByPath($node, $shareTypes, $paginationParameters);
}
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('share')
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)));
/**
* Reshares for this user are shares where they are the owner.
*/
if ($reshares === false) {
//Special case for old shares created via the web UI
$or1 = $qb->expr()->andX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->isNull('uid_initiator')
);
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)),
$or1
)
);
} elseif ($node === null) {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
$qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
)
);
}
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
$paginationParameters->fillQuery($qb, 'id');
$cursor = $qb->executeQuery();
while ($data = $cursor->fetchAssociative()) {
$provider = $this->factory->getProviderForType((int)$data['share_type']);
if ($provider instanceof ICreateShareProvider) {
throw new \LogicException('Share type ' . $data['share_type'] . " doesn't have a corresponding ICreateShareProvider.");
}
$share = $provider->createShare($data);
try {
$this->checkShare($share, $added);
} catch (ShareNotFound $e) {
// Ignore since this basically means the share is deleted
continue;
}
}
$cursor->closeCursor();
// Get all the other shares from the providers not supporting createShare
$shares = [];
foreach ([IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_ROOM, IShare::TYPE_DECK] as $shareType) {
if (!in_array($shareType, $shareTypes)) {
$shares = array_merge($shares, $this->getSharesBy($userId, $shareType, $node, $reshares, -1, 0));
}
}
// FEDERATION
if ($this->outgoingServer2ServerSharesAllowed() && !in_array(IShare::TYPE_REMOTE, $shareTypes)) {
$shares = array_merge($shares, $this->getSharesBy($userId, IShare::TYPE_REMOTE, $node, $reshares, -1, 0));
}
if ($this->outgoingServer2ServerGroupSharesAllowed() && !in_array(IShare::TYPE_REMOTE_GROUP, $shareTypes)) {
$shares = array_merge($shares, $this->getSharesBy($userId, IShare::TYPE_REMOTE_GROUP, $node, $reshares, -1, 0));
}
return $shares;
}
#[Override]
public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array {
if ($path !== null && !($path instanceof File) && !($path instanceof Folder)) {
if ($path !== null
&& !($path instanceof File)
&& !($path instanceof Folder)) {
throw new \InvalidArgumentException($this->l->t('Invalid path'));
}
@@ -1391,70 +1266,8 @@ class Manager implements IManager {
}
}
return $shares2;
}
$shares = $shares2;
#[Override]
public function getAllSharedWith(string $userId, array $shareTypes, ?Node $node, PaginationParameters $paginationParameters, bool $ignoreWithSelf = false): array {
$shareTypes = [];
$noCreateShareProvider = [];
foreach ($this->factory->getAllProviders() as $provider) {
if ($provider instanceof ICreateShareProvider) {
$shareTypes = array_merge($provider->getShareTypes(), $shareTypes);
} else {
$noCreateShareProvider[] = $provider;
}
}
$shareTypes = array_unique($shareTypes);
// Get shares directly with this user
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('share');
$qb->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)));
$qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)));
if ($node !== null) {
$qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
}
if ($ignoreWithSelf) {
$qb->andWhere($qb->expr()->neq('uid_owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
$qb->andWhere($qb->expr()->neq('uid_initiator', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
}
$paginationParameters->fillQuery($qb, 'id');
$result = $qb->executeQuery();
$shares = [];
foreach ($result->fetchAssociative() as $data) {
try {
$provider = $this->factory->getProviderForType($data['share_type']);
} catch (ProviderException $e) {
continue;
}
if (!$provider instanceof ICreateShareProvider) {
throw new \LogicException($this->l->t('Invalid provider'));
}
$shares[] = $provider->createShare($data);
}
// Legacy for providers what don't support ICreateShareProvider
foreach ($noCreateShareProvider as $shareType) {
// TODO fix pagination
$unverifiedShares = $provider->getSharedWith($userId, $shareType, $node, 0, 0);
// remove all shares which are already expired
foreach ($unverifiedShares as $key => $share) {
try {
$this->checkShare($share);
} catch (ShareNotFound $e) {
unset($shares[$key]);
}
$shares[] = $share;
}
}
return $shares;
}
@@ -1480,7 +1293,9 @@ class Manager implements IManager {
return $shares;
}
#[Override]
/**
* @inheritDoc
*/
public function getSharedWithByPath(string $userId, int $shareType, string $path, bool $forChildren, int $limit = 50, int $offset = 0): iterable {
try {
$provider = $this->factory->getProviderForType($shareType);
@@ -1520,7 +1335,6 @@ class Manager implements IManager {
#[Override]
public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array {
// TODO: Remove, it is no longer used.
$shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
// Only get shares deleted shares and where the owner still exists
@@ -1529,16 +1343,11 @@ class Manager implements IManager {
}
#[Override]
public function getAllDeletedSharedWith(string $userId, array $shareTypes, ?Node $node = null, PaginationParameters $paginationParameters): array {
$shares = $this->getAllSharedWith($userId, $shareTypes, $node, $paginationParameters);
public function getShareById($id, $recipient = null, bool $onlyValid = true): IShare {
if ($id === null) {
throw new ShareNotFound();
}
// Only get shares deleted shares and where the owner still exists
return array_filter($shares, fn (IShare $share): bool => $share->getPermissions() === 0
&& $this->userManager->userExists($share->getShareOwner()));
}
#[Override]
public function getShareById(string $id, ?string $recipient = null, bool $onlyValid = true): IShare {
[$providerId, $id] = $this->splitFullId($id);
try {
@@ -1547,31 +1356,6 @@ class Manager implements IManager {
throw new ShareNotFound();
}
if ($provider instanceof ICreateShareProvider) {
$qb = $this->connection->getQueryBuilder();
$qb->select('*')
->from('share')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id)))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($provider->getShareTypes(), IQueryBuilder::PARAM_INT_ARRAY)));
$cursor = $qb->executeQuery();
$data = $cursor->fetchAssociative();
$cursor->closeCursor();
if ($data === false) {
throw new ShareNotFound('Can not find share with ID: ' . $id);
}
$share = $provider->createShare($data);
if ($onlyValid) {
$this->checkShare($share);
}
return $share;
}
$share = $provider->getShareById($id, $recipient);
if ($onlyValid) {
@@ -1581,87 +1365,24 @@ class Manager implements IManager {
return $share;
}
/**
* @param list<IShare::TYPE_*> $shareTypes
* @return list<IShare>
*/
private function getSharesByPath(Node $path, array $shareTypes, PaginationParameters $paginationParameters): array {
$qb = $this->connection->getQueryBuilder();
$qb = $qb->select('*')
->from('share')
->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
->andWhere($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)));
$paginationParameters->fillQuery($qb, 'id');
$cursor = $qb->executeQuery();
$shares = [];
while ($data = $cursor->fetchAssociative()) {
$provider = $this->factory->getProvider((int)$data['share_type']);
if ($provider instanceof ICreateShareProvider) {
$shares[] = $provider->createShare($data);
} else {
$shares[] = $provider->getShareById((int)$data['id']);
}
}
$cursor->closeCursor();
return $shares;
}
/**
* @return array{?IShare, list<IShare::TYPE_*>}
*/
private function getShareByTokenOptimized(string $token): array {
$providers = $this->factory->getAllProviders();
// Get all shares from the providers supporting createShare
$createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider);
$shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getTokenShareTypes(), $createShareProviders)));
if (!$this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) {
$shareTypes = array_filter($shareTypes, fn (int $shareType): bool => $shareType !== IShare::TYPE_LINK);
}
$qb = $this->connection->getQueryBuilder();
$result = $qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes)))
->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
->andWhere($qb->expr()->in('item_type', $qb->createNamedParameter(['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY)))
->executeQuery();
$data = $result->fetch();
if ($data === false) {
return [null, $shareTypes];
}
$provider = $this->factory->getProviderForType((int)$data['share_type']);
if (!$provider instanceof ICreateShareProvider) {
throw new \LogicException('Share type ' . $data['share_type'] . " doesn't have a corresponding ICreateShareProvider.");
}
$data['id'] = (string)$data['id'];
$share = $provider->createShare($data);
return [$share, $shareTypes];
}
#[Override]
public function getShareByToken(string $token): IShare {
// tokens cannot be valid local usernames
if ($this->userManager->userExists($token)) {
throw new ShareNotFound();
}
[$share, $testedShareTypes] = $this->getShareByTokenOptimized($token);
if ($share !== null) {
return $share;
}
if (!in_array(IShare::TYPE_LINK, $testedShareTypes) && $this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) {
try {
$share = null;
try {
if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes') {
$provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
$share = $provider->getShareByToken($token);
} catch (ProviderException|ShareNotFound) {
}
} catch (ProviderException|ShareNotFound) {
}
// If it is not a link share try to fetch a federated share by token
if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes)) {
if ($share === null) {
try {
$provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
$share = $provider->getShareByToken($token);
@@ -1670,7 +1391,7 @@ class Manager implements IManager {
}
// If it is not a link share try to fetch a mail share by token
if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
try {
$provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
$share = $provider->getShareByToken($token);
@@ -1678,7 +1399,7 @@ class Manager implements IManager {
}
}
if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
try {
$provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
$share = $provider->getShareByToken($token);
@@ -1686,7 +1407,7 @@ class Manager implements IManager {
}
}
if ($share === null && !in_array(IShare::TYPE_REMOTE, $testedShareTypes) && $this->shareProviderExists(IShare::TYPE_ROOM)) {
if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
try {
$provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
$share = $provider->getShareByToken($token);
@@ -1917,21 +1638,21 @@ class Manager implements IManager {
#[Override]
public function shareApiEnabled(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_enabled', true);
return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
}
#[Override]
public function shareApiAllowLinks(?IUser $user = null): bool {
if (!$this->appConfig->getValueBool('core', 'shareapi_allow_links', true)) {
if ($this->config->getAppValue('core', 'shareapi_allow_links', 'yes') !== 'yes') {
return false;
}
$user = $user ?? $this->userSession->getUser();
if ($user) {
$excludedGroups = $this->appConfig->getValueArray('core', 'shareapi_allow_links_exclude_groups');
$excludedGroups = json_decode($this->config->getAppValue('core', 'shareapi_allow_links_exclude_groups', '[]'));
if ($excludedGroups) {
$userGroups = $this->groupManager->getUserGroupIds($user);
return !array_intersect($excludedGroups, $userGroups);
return !(bool)array_intersect($excludedGroups, $userGroups);
}
}
@@ -1950,12 +1671,13 @@ class Manager implements IManager {
#[Override]
public function shareApiLinkEnforcePassword(bool $checkGroupMembership = true): bool {
$excludedGroups = $this->appConfig->getValueArray('core', 'shareapi_enforce_links_password_excluded_groups');
if ($excludedGroups !== [] && $checkGroupMembership) {
$excludedGroups = $this->config->getAppValue('core', 'shareapi_enforce_links_password_excluded_groups', '');
if ($excludedGroups !== '' && $checkGroupMembership) {
$excludedGroups = json_decode($excludedGroups);
$user = $this->userSession->getUser();
if ($user) {
$userGroups = $this->groupManager->getUserGroupIds($user);
if (array_intersect($excludedGroups, $userGroups)) {
if ((bool)array_intersect($excludedGroups, $userGroups)) {
return false;
}
}
@@ -1976,49 +1698,49 @@ class Manager implements IManager {
#[Override]
public function shareApiLinkDefaultExpireDays(): int {
return $this->appConfig->getValueInt('core', 'shareapi_expire_after_n_days', 7);
return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
}
#[Override]
public function shareApiInternalDefaultExpireDate(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_default_internal_expire_date');
return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
}
#[Override]
public function shareApiRemoteDefaultExpireDate(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_default_remote_expire_date');
return $this->config->getAppValue('core', 'shareapi_default_remote_expire_date', 'no') === 'yes';
}
#[Override]
public function shareApiInternalDefaultExpireDateEnforced(): bool {
return $this->shareApiInternalDefaultExpireDate()
&& $this->appConfig->getValueBool('core', 'shareapi_enforce_internal_expire_date');
&& $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
}
#[Override]
public function shareApiRemoteDefaultExpireDateEnforced(): bool {
return $this->shareApiRemoteDefaultExpireDate()
&& $this->appConfig->getValueBool('core', 'shareapi_enforce_remote_expire_date');
&& $this->config->getAppValue('core', 'shareapi_enforce_remote_expire_date', 'no') === 'yes';
}
#[Override]
public function shareApiInternalDefaultExpireDays(): int {
return $this->appConfig->getValueInt('core', 'shareapi_internal_expire_after_n_days', 7);
return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
}
#[Override]
public function shareApiRemoteDefaultExpireDays(): int {
return $this->appConfig->getValueInt('core', 'shareapi_remote_expire_after_n_days', 7);
return (int)$this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7');
}
#[Override]
public function shareApiLinkAllowPublicUpload(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_allow_public_upload', true);
return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
}
#[Override]
public function shareWithGroupMembersOnly(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_only_share_with_group_members');
return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
}
#[Override]
@@ -2032,29 +1754,29 @@ class Manager implements IManager {
#[Override]
public function allowGroupSharing(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_allow_group_sharing', true);
return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
}
#[Override]
public function allowEnumeration(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_allow_share_dialog_user_enumeration', true);
return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
}
#[Override]
public function limitEnumerationToGroups(): bool {
return $this->allowEnumeration()
&& $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_to_group');
&& $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
}
#[Override]
public function limitEnumerationToPhone(): bool {
return $this->allowEnumeration()
&& $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_to_phone');
&& $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
}
#[Override]
public function allowEnumerationFullMatch(): bool {
return $this->appConfig->getValueBool('core', 'shareapi_restrict_user_enumeration_full_match', true);
return $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_full_match', 'yes') === 'yes';
}
#[Override]
@@ -2157,32 +1879,7 @@ class Manager implements IManager {
public function getAllShares(): iterable {
$providers = $this->factory->getAllProviders();
// Get all shares from the providers supporting createShare
$createShareProviders = array_filter($providers, fn (IShareProvider $provider) => $provider instanceof ICreateShareProvider);
$shareTypes = array_unique(array_merge(...array_map(fn (ICreateShareProvider $provider): array => $provider->getShareTypes(), $createShareProviders)));
$qb = $this->connection->getQueryBuilder();
$result = $qb->select('*')
->from('share')
->where($qb->expr()->in('share_type', $qb->createNamedParameter($shareTypes, IQueryBuilder::PARAM_INT_ARRAY)))
->executeQuery();
/** @var array<IShare::TYPE*, IShareProvider> $providers */
$providers = [];
foreach ($result->iterateAssociative() as $row) {
if (!isset($providers[$row['share_type']])) {
$providers[$row['share_type']] = $this->factory->getProviderForType($row['share_type']);
}
$provider = $providers[$row['share_type']];
if (!$provider instanceof ICreateShareProvider) {
throw new \LogicException('Share type ' . $row['share_type'] . " doesn't have a corresponding ICreateShareProvider.");
}
yield $provider->createShare($row);
}
// Get all shares from the other providers
$noCreateShareProviders = array_filter($providers, fn (IShareProvider $provider) => !$provider instanceof ICreateShareProvider);
foreach ($noCreateShareProviders as $provider) {
foreach ($providers as $provider) {
yield from $provider->getAllShares();
}
}
+31 -43
View File
@@ -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;
}
-73
View File
@@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP;
use OCP\AppFramework\Attribute\Consumable;
use OCP\DB\QueryBuilder\IQueryBuilder;
/**
* Since 34.0.0
* @see https://docs.joinmastodon.org/api/guidelines/#pagination
*/
#[Consumable(since: '34.0.0')]
class PaginationParameters {
/**
* Pagination parameters.
*
* You can use $minId with $maxId and $maxId with $sinceId.
*
* @param ?int $limit The maximum number of results to return. Set to null to
* return all results.
* @param ?non-empty-string $maxId All results returned will be lesser than this ID. In effect, sets an upper bound on results.
* @param ?non-empty-string $minId Returns results immediately greater than this ID. In effect, sets a cursor at this ID and paginates forward.
* @param ?non-empty-string $sinceId All results returned will be greater than this ID. In effect, sets a lower bound on results.
*/
public function __construct(
public ?int $limit = 100,
public ?string $maxId = null,
public ?string $minId = null,
public ?string $sinceId = null,
) {
if ($minId !== null && $sinceId !== null) {
throw new \InvalidArgumentException("minId and sinceId can't be defined togeter");
}
}
/**
* Add pagination condition to the query builder based on the configured pagination settings.
*/
public function fillQuery(IQueryBuilder $qb, string $idColumn): IQueryBuilder {
// This logic is inspired by
// https://github.com/mastodon/mastodon/blob/main/app/models/concerns/paginable.rb#L7
if ($this->minId !== null) {
// paginate by min id (last entries added in the table last)
$qb->andWhere($qb->expr()->gt($idColumn, $this->minId));
if ($this->maxId !== null) {
$qb->andWhere($qb->expr()->lt($idColumn, $this->maxId));
}
$qb->orderBy($idColumn, 'ASC');
$qb->setMaxResults($this->limit);
return $qb;
} else {
// paginate by max id (last entries added in the table first)
if ($this->maxId !== null) {
$qb->andWhere($qb->expr()->lt($idColumn, $this->maxId));
}
if ($this->sinceId !== null) {
$qb->andWhere($qb->expr()->gt($idColumn, $this->sinceId));
}
$qb->orderBy($idColumn, 'DESC');
$qb->setMaxResults($this->limit);
return $qb;
}
}
}
-42
View File
@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
* SPDX-FileContributor: Carl Schwan
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\Share;
/**
* Interface IShareProviderSupportsAccept
*
* This interface allows to define IShareProvider that can list users for share with the getUsersForShare method,
* which is available since Nextcloud 17.
*
* @since 33.0.0
*/
interface ICreateShareProvider extends IShareProvider {
/**
* Fill a share with additional information from the raw row of the database.
*
* @param array<string, mixed> $data
* @return IShare $share
* @since 34.0.0
*/
public function createShare(array $data): IShare;
/**
* @return list<IShare::TYPE_*>
* @since 34.0.0
*/
public function getShareTypes(): array;
/**
* @return list<IShare::TYPE_*>
* @since 34.0.0
*/
public function getTokenShareTypes(): array;
}
+4 -39
View File
@@ -12,7 +12,6 @@ use OCP\Files\Folder;
use OCP\Files\Node;
use OCP\IUser;
use OCP\PaginationParameters;
use OCP\Share\Exceptions\GenericShareException;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\Exceptions\ShareTokenException;
@@ -116,23 +115,11 @@ interface IManager {
* @param int $limit The maximum number of returned results, -1 for all results
* @param int $offset
* @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned
* @return list<IShare>
* @return IShare[]
* @since 9.0.0
*/
public function getSharesBy(string $userId, int $shareType, ?Node $path = null, bool $reshares = false, int $limit = 50, int $offset = 0, bool $onlyValid = true): array;
/**
* Get all shares shared by (initiated) by the provided user.
*
* @param string $userId
* @param Node|null $path
* @param bool $reshares
* @param bool $onlyValid Only returns valid shares, invalid shares will be deleted automatically and are not returned
* @return list<IShare>
* @since 34.0.0
*/
public function getAllSharesBy(string $userId, ?Node $node, PaginationParameters $paginationParameters, bool $reshares = false, bool $onlyValid = true): array;
/**
* Get shares shared with $user.
* Filter by $node if provided
@@ -144,19 +131,9 @@ interface IManager {
* @param int $offset
* @return IShare[]
* @since 9.0.0
* @deprecated 34.0.0 Use getAllSharedWith instead, it's more efficient
*/
public function getSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array;
/**
* @param string $userId
* @param list<IShare::TYPE_*> $shareTypes
* @param Node|null $node
* @return list<IShare>
* @since 34.0.0
*/
public function getAllSharedWith(string $userId, array $shareTypes, ?Node $node, PaginationParameters $paginationParameters, bool $ignoreWithSelf = false): array;
/**
* Get shares shared with a $user filtering by $path.
*
@@ -175,23 +152,11 @@ interface IManager {
*
* @param IShare::TYPE_* $shareType
* @param int $limit The maximum number of shares returned, -1 for all
* @return list<IShare>
* @return IShare[]
* @since 14.0.0
* @deprecated 34.0.0 Use getAllDeletedSharedWith instead
*/
public function getDeletedSharedWith(string $userId, int $shareType, ?Node $node = null, int $limit = 50, int $offset = 0): array;
/**
* Get all the deleted shares shared with $user.
*
* Additionally filter by $node if provided
*
* @param list<IShare::TYPE_*> $shareTypes
* @return list<IShare>
* @since 34.0.0
*/
public function getAllDeletedSharedWith(string $userId, array $shareTypes, ?Node $node = null, PaginationParameters $paginationParameters): array;
/**
* Retrieve a share by the share id.
* If the recipient is set make sure to retrieve the file for that user.
@@ -293,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
+7 -4
View File
@@ -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;
}
+3 -4
View File
@@ -129,7 +129,7 @@ interface IShareProvider {
* Get shares for a given path
*
* @param Node $path
* @return list<\OCP\Share\IShare>
* @return \OCP\Share\IShare[]
* @since 9.0.0
*/
public function getSharesByPath(Node $path);
@@ -138,14 +138,14 @@ interface IShareProvider {
* Get shared with the given user
*
* @param string $userId get shares where this user is the recipient
* @param IShare::TYPE_* $shareType
* @param int $shareType
* @param Node|null $node
* @param int $limit The max number of entries returned, -1 for all
* @param int $offset
* @return \OCP\Share\IShare[]
* @since 9.0.0
*/
public function getSharedWith(string $userId, int $shareType, ?Node $node, int $limit, int $offset);
public function getSharedWith($userId, $shareType, $node, $limit, $offset);
/**
* Get a share by token
@@ -206,7 +206,6 @@ interface IShareProvider {
*
* @return iterable<IShare>
* @since 18.0.0
* @deprecated 34.0.0 This is no longer needed when implementing ICreateShareProvider
*/
public function getAllShares(): iterable;
+2
View File
@@ -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"/>
+1
View File
@@ -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'],
];
+4 -2
View File
@@ -9,7 +9,6 @@
namespace Test\Share;
use OC\Share20\Manager;
use OCP\PaginationParameters;
use OCP\Server;
use OCP\Share\IShare;
use OCP\Share_Backend;
@@ -40,7 +39,10 @@ class Backend implements Share_Backend {
$shareManager = Server::get(Manager::class);
$shares = $shareManager->getAllSharedWith($shareWith, [IShare::TYPE_USER, IShare::TYPE_GROUP], null, new PaginationParameters(limit: null));
$shares = array_merge(
$shareManager->getSharedWith($shareWith, IShare::TYPE_USER),
$shareManager->getSharedWith($shareWith, IShare::TYPE_GROUP),
);
$knownTargets = [];
foreach ($shares as $share) {
+9 -5
View File
@@ -9,6 +9,7 @@ namespace Test\Share20;
use OC\EventDispatcher\EventDispatcher;
use OC\Share20\LegacyHooks;
use OC\Share20\Manager;
use OCP\Constants;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Cache\ICacheEntry;
@@ -23,7 +24,6 @@ use OCP\Share\Events\ShareDeletedFromSelfEvent;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
use OCP\Util;
use PHPUnit\Framework\Attributes\Group;
use Psr\Log\LoggerInterface;
use Test\TestCase;
@@ -40,11 +40,15 @@ class Dummy {
}
}
#[Group(name: 'DB')]
class LegacyHooksTest extends TestCase {
private LegacyHooks $hooks;
private IEventDispatcher $eventDispatcher;
private IShareManager $manager;
/** @var LegacyHooks */
private $hooks;
/** @var IEventDispatcher */
private $eventDispatcher;
/** @var Manager */
private $manager;
protected function setUp(): void {
parent::setUp();
+1 -91
View File
@@ -18,9 +18,6 @@ use OC\Share20\Manager;
use OC\Share20\Share;
use OC\Share20\ShareDisableChecker;
use OCP\Constants;
use OCP\DB\IResult;
use OCP\DB\QueryBuilder\IExpressionBuilder;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\File;
@@ -36,7 +33,6 @@ use OCP\HintException;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IDateTimeZone;
use OCP\IDBConnection;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IL10N;
@@ -63,7 +59,6 @@ use OCP\Share\IShareProvider;
use OCP\Share\IShareProviderSupportsAllSharesInFolder;
use OCP\Util;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@@ -80,7 +75,7 @@ class DummyShareManagerListener {
*
* @package Test\Share20
*/
#[Group(name: 'DB')]
#[\PHPUnit\Framework\Attributes\Group('DB')]
class ManagerTest extends \Test\TestCase {
protected Manager $manager;
protected LoggerInterface&MockObject $logger;
@@ -102,7 +97,6 @@ class ManagerTest extends \Test\TestCase {
private DateTimeZone $timezone;
protected IDateTimeZone&MockObject $dateTimeZone;
protected IAppConfig&MockObject $appConfig;
protected IDBConnection&MockObject $connection;
protected function setUp(): void {
$this->logger = $this->createMock(LoggerInterface::class);
@@ -116,7 +110,6 @@ class ManagerTest extends \Test\TestCase {
$this->dispatcher = $this->createMock(IEventDispatcher::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->knownUserService = $this->createMock(KnownUserService::class);
$this->connection = $this->createMock(IDBConnection::class);
$this->shareDisabledChecker = new ShareDisableChecker($this->config, $this->userManager, $this->groupManager);
$this->dateTimeZone = $this->createMock(IDateTimeZone::class);
@@ -164,7 +157,6 @@ class ManagerTest extends \Test\TestCase {
$this->shareDisabledChecker,
$this->dateTimeZone,
$this->appConfig,
$this->connection,
);
}
@@ -190,7 +182,6 @@ class ManagerTest extends \Test\TestCase {
$this->shareDisabledChecker,
$this->dateTimeZone,
$this->appConfig,
$this->connection,
]);
}
@@ -495,26 +486,6 @@ class ManagerTest extends \Test\TestCase {
$manager->expects($this->exactly(1))->method('updateShare')->with($reShare)->willReturn($reShare);
$qb = $this->createMock(IQueryBuilder::class);
$result = $this->createMock(IResult::class);
$qb->method('select')
->willReturn($qb);
$qb->method('from')
->willReturn($qb);
$qb->method('andWhere')
->willReturn($qb);
$qb->method('expr')
->willReturn($this->createMock(IExpressionBuilder::class));
$qb->method('executeQuery')
->willReturn($result);
$this->connection->method('getQueryBuilder')
->willReturn($qb);
$result->method('fetch')
->willReturnOnConsecutiveCalls(
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@@ -575,26 +546,6 @@ class ManagerTest extends \Test\TestCase {
return $expected;
});
$qb = $this->createMock(IQueryBuilder::class);
$result = $this->createMock(IResult::class);
$qb->method('select')
->willReturn($qb);
$qb->method('from')
->willReturn($qb);
$qb->method('andWhere')
->willReturn($qb);
$qb->method('expr')
->willReturn($this->createMock(IExpressionBuilder::class));
$qb->method('executeQuery')
->willReturn($result);
$this->connection->method('getQueryBuilder')
->willReturn($qb);
$result->method('fetch')
->willReturnOnConsecutiveCalls(
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@@ -623,26 +574,6 @@ class ManagerTest extends \Test\TestCase {
/* No share is promoted because generalCreateChecks does not throw */
$manager->expects($this->never())->method('updateShare');
$qb = $this->createMock(IQueryBuilder::class);
$result = $this->createMock(IResult::class);
$qb->method('select')
->willReturn($qb);
$qb->method('from')
->willReturn($qb);
$qb->method('andWhere')
->willReturn($qb);
$qb->method('expr')
->willReturn($this->createMock(IExpressionBuilder::class));
$qb->method('executeQuery')
->willReturn($result);
$this->connection->method('getQueryBuilder')
->willReturn($qb);
$result->method('fetch')
->willReturnOnConsecutiveCalls(
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}
@@ -710,27 +641,6 @@ class ManagerTest extends \Test\TestCase {
return $expected;
});
$qb = $this->createMock(IQueryBuilder::class);
$result = $this->createMock(IResult::class);
$qb->method('select')
->willReturn($qb);
$qb->method('from')
->willReturn($qb);
$qb->method('andWhere')
->willReturn($qb);
$qb->method('expr')
->willReturn($this->createMock(IExpressionBuilder::class));
$qb->method('executeQuery')
->willReturn($result);
$this->connection->method('getQueryBuilder')
->willReturn($qb);
$result->method('fetch')
->willReturnOnConsecutiveCalls(
['uid_initiator' => 'userB', 'share_type' => IShare::TYPE_USER],
['uid_initiator' => 'userC', 'share_type' => IShare::TYPE_USER],
false,
);
self::invokePrivate($manager, 'promoteReshares', [$share]);
}