feat(files_sharing): allow not deleting expiring shares

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2024-09-19 19:17:08 +02:00 committed by skjnldsv
parent cd3dc1719b
commit 06069e56ee
14 changed files with 394 additions and 33 deletions

View file

@ -139,6 +139,14 @@ return [
'url' => '/api/v1/deletedshares/{id}',
'verb' => 'POST',
],
/*
* Expired Shares
*/
[
'name' => 'ExpiredShareAPI#index',
'url' => '/api/v1/expiredshares',
'verb' => 'GET',
],
/*
* OCS Sharee API
*/

View file

@ -29,6 +29,7 @@ return array(
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExpiredShareAPIController' => $baseDir . '/../lib/Controller/ExpiredShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php',
'OCA\\Files_Sharing\\Controller\\PublicPreviewController' => $baseDir . '/../lib/Controller/PublicPreviewController.php',
'OCA\\Files_Sharing\\Controller\\RemoteController' => $baseDir . '/../lib/Controller/RemoteController.php',

View file

@ -44,6 +44,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExpiredShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/ExpiredShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php',
'OCA\\Files_Sharing\\Controller\\PublicPreviewController' => __DIR__ . '/..' . '/../lib/Controller/PublicPreviewController.php',
'OCA\\Files_Sharing\\Controller\\RemoteController' => __DIR__ . '/..' . '/../lib/Controller/RemoteController.php',

View file

@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files_Sharing\Controller;
use OCA\Files_Sharing\ResponseDefinitions;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\QueryException;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\IGroupManager;
use OCP\IRequest;
use OCP\IServerContainer;
use OCP\IUserManager;
use OCP\Share\IManager as ShareManager;
use OCP\Share\IShare;
/**
* @psalm-import-type Files_SharingDeletedShare from ResponseDefinitions
*/
class ExpiredShareAPIController extends OCSController {
/** @var ShareManager */
private $shareManager;
/** @var string */
private $userId;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IRootFolder */
private $rootFolder;
/** @var IAppManager */
private $appManager;
/** @var IServerContainer */
private $serverContainer;
public function __construct(string $appName,
IRequest $request,
ShareManager $shareManager,
string $UserId,
IUserManager $userManager,
IGroupManager $groupManager,
IRootFolder $rootFolder,
IAppManager $appManager,
IServerContainer $serverContainer) {
parent::__construct($appName, $request);
$this->shareManager = $shareManager;
$this->userId = $UserId;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->rootFolder = $rootFolder;
$this->appManager = $appManager;
$this->serverContainer = $serverContainer;
}
/**
* @suppress PhanUndeclaredClassMethod
*
* @return Files_SharingDeletedShare
*/
private function formatShare(IShare $share): array {
$result = [
'id' => $share->getFullId(),
'share_type' => $share->getShareType(),
'uid_owner' => $share->getSharedBy(),
'displayname_owner' => $this->userManager->get($share->getSharedBy())->getDisplayName(),
'permissions' => 0,
'stime' => $share->getShareTime()->getTimestamp(),
'parent' => null,
'expiration' => null,
'token' => null,
'uid_file_owner' => $share->getShareOwner(),
'displayname_file_owner' => $this->userManager->get($share->getShareOwner())->getDisplayName(),
'path' => $share->getTarget(),
];
$userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
$node = $userFolder->getFirstNodeById($share->getNodeId());
if (!$node) {
// fallback to guessing the path
$node = $userFolder->get($share->getTarget());
if ($node === null || $share->getTarget() === '') {
throw new NotFoundException();
}
}
$result['path'] = $userFolder->getRelativePath($node->getPath());
if ($node instanceof \OCP\Files\Folder) {
$result['item_type'] = 'folder';
} else {
$result['item_type'] = 'file';
}
$result['mimetype'] = $node->getMimetype();
$result['storage_id'] = $node->getStorage()->getId();
$result['storage'] = $node->getStorage()->getCache()->getNumericStorageId();
$result['item_source'] = $node->getId();
$result['file_source'] = $node->getId();
$result['file_parent'] = $node->getParent()->getId();
$result['file_target'] = $share->getTarget();
$result['item_size'] = $node->getSize();
$result['item_mtime'] = $node->getMTime();
$expiration = $share->getExpirationDate();
if ($expiration !== null) {
$result['expiration'] = $expiration->format('Y-m-d 00:00:00');
}
if ($share->getShareType() === IShare::TYPE_GROUP) {
$group = $this->groupManager->get($share->getSharedWith());
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = $group !== null ? $group->getDisplayName() : $share->getSharedWith();
} elseif ($share->getShareType() === IShare::TYPE_ROOM) {
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = '';
try {
$result = array_merge($result, $this->getRoomShareHelper()->formatShare($share));
} catch (QueryException $e) {
}
} elseif ($share->getShareType() === IShare::TYPE_DECK) {
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = '';
try {
$result = array_merge($result, $this->getDeckShareHelper()->formatShare($share));
} catch (QueryException $e) {
}
} elseif ($share->getShareType() === IShare::TYPE_SCIENCEMESH) {
$result['share_with'] = $share->getSharedWith();
$result['share_with_displayname'] = '';
try {
$result = array_merge($result, $this->getSciencemeshShareHelper()->formatShare($share));
} catch (QueryException $e) {
}
}
return $result;
}
/**
* Get a list of all expired shares
*
* @return DataResponse<Http::STATUS_OK, Files_SharingDeletedShare[], array{}>
*
* 200: Deleted shares returned
*/
#[NoAdminRequired]
public function index(): DataResponse {
$groupShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_GROUP, null, -1, 0);
$roomShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_ROOM, null, -1, 0);
$deckShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_DECK, null, -1, 0);
$sciencemeshShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_SCIENCEMESH, null, -1, 0);
$linkShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_LINK, null, -1, 0);
$userShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_USER, null, -1, 0);
$emailsShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_EMAIL, null, -1, 0);
$circlesShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_CIRCLE, null, -1, 0);
$remoteShares = $this->shareManager->getExpiredShares($this->userId, IShare::TYPE_REMOTE, null, -1, 0);
$shares = array_merge($groupShares, $roomShares, $deckShares, $sciencemeshShares, $linkShares, $userShares, $emailsShares, $circlesShares, $remoteShares);
$shares = array_map(function (IShare $share) {
return $this->formatShare($share);
}, $shares);
return new DataResponse($shares);
}
/**
* Returns the helper of DeletedShareAPIController for room shares.
*
* If the Talk application is not enabled or the helper is not available
* a QueryException is thrown instead.
*
* @return \OCA\Talk\Share\Helper\DeletedShareAPIController
* @throws QueryException
*/
private function getRoomShareHelper() {
if (!$this->appManager->isEnabledForUser('spreed')) {
throw new QueryException();
}
return $this->serverContainer->get('\OCA\Talk\Share\Helper\DeletedShareAPIController');
}
/**
* Returns the helper of DeletedShareAPIHelper for deck shares.
*
* If the Deck application is not enabled or the helper is not available
* a QueryException is thrown instead.
*
* @return \OCA\Deck\Sharing\ShareAPIHelper
* @throws QueryException
*/
private function getDeckShareHelper() {
if (!$this->appManager->isEnabledForUser('deck')) {
throw new QueryException();
}
return $this->serverContainer->get('\OCA\Deck\Sharing\ShareAPIHelper');
}
/**
* Returns the helper of DeletedShareAPIHelper for sciencemesh shares.
*
* If the sciencemesh application is not enabled or the helper is not available
* a QueryException is thrown instead.
*
* @return \OCA\Deck\Sharing\ShareAPIHelper
* @throws QueryException
*/
private function getSciencemeshShareHelper() {
if (!$this->appManager->isEnabledForUser('sciencemesh')) {
throw new QueryException();
}
return $this->serverContainer->get('\OCA\ScienceMesh\Sharing\ShareAPIHelper');
}
}

View file

@ -8,25 +8,24 @@ namespace OCA\Files_Sharing;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IAppConfig;
use OCP\IDBConnection;
use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
/**
* Delete all shares that are expired
*/
class ExpireSharesJob extends TimedJob {
/** @var IManager */
private $shareManager;
/** @var IDBConnection */
private $db;
public function __construct(ITimeFactory $time, IManager $shareManager, IDBConnection $db) {
$this->shareManager = $shareManager;
$this->db = $db;
public function __construct(
ITimeFactory $time,
private IManager $shareManager,
private IDBConnection $db,
private IAppConfig $config,
private LoggerInterface $logger) {
parent::__construct($time);
@ -42,13 +41,16 @@ class ExpireSharesJob extends TimedJob {
* @param array $argument unused argument
*/
public function run($argument) {
//Current time
if ($this->config->getValueString('core', 'shareapi_delete_on_expire', 'yes') === 'no') {
$this->logger->info('Share deletion on expiration is disabled');
return;
}
// Current time
$now = new \DateTime();
$now = $now->format('Y-m-d H:i:s');
/*
* Expire file link shares only (for now)
*/
// Expire file link shares only (for now)
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'share_type')
->from('share')

View file

@ -14,6 +14,7 @@ import LinkSvg from '@mdi/svg/svg/link.svg?raw'
import CircleSvg from '../../../../core/img/apps/circles.svg?raw'
import { action as sidebarAction } from '../../../files/src/actions/sidebarAction'
import { expiredSharesViewId } from '../files_views/shares'
import { generateAvatarSvg } from '../utils/AccountIcon'
import './sharingStatusAction.scss'
@ -24,10 +25,14 @@ const isExternal = (node: Node) => {
export const action = new FileAction({
id: 'sharing-status',
displayName(nodes: Node[]) {
displayName(nodes: Node[], view: View) {
const node = nodes[0]
const shareTypes = Object.values(node?.attributes?.['share-types'] || {}).flat() as number[]
if (view.id === expiredSharesViewId) {
return t('files_sharing', 'Expired')
}
if (shareTypes.length > 0
|| (node.owner !== getCurrentUser()?.uid || isExternal(node))) {
return t('files_sharing', 'Shared')

View file

@ -3,8 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { translate as t } from '@nextcloud/l10n'
import { View, getNavigation } from '@nextcloud/files'
import { Column, View, getNavigation } from '@nextcloud/files'
import { ShareType } from '@nextcloud/sharing'
import moment from '@nextcloud/moment'
import AccountArrowLeftSvg from '@mdi/svg/svg/account-arrow-left.svg?raw'
import AccountClockSvg from '@mdi/svg/svg/account-clock.svg?raw'
import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
import AccountPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
@ -22,6 +25,7 @@ export const sharingByLinksViewId = 'sharinglinks'
export const deletedSharesViewId = 'deletedshares'
export const pendingSharesViewId = 'pendingshares'
export const fileRequestViewId = 'filerequest'
export const expiredSharesViewId = 'expiredshares'
export default () => {
const Navigation = getNavigation()
@ -89,7 +93,7 @@ export default () => {
columns: [],
getContents: () => getContents(false, true, false, false, [ShareType.Link]),
getContents: () => getContents(false, true, false, false, false, [ShareType.Link]),
}))
Navigation.register(new View({
@ -106,7 +110,7 @@ export default () => {
columns: [],
getContents: () => getContents(false, true, false, false, [ShareType.Link, ShareType.Email])
getContents: () => getContents(false, true, false, false, false, [ShareType.Link, ShareType.Email])
.then(({ folder, contents }) => {
return {
folder,
@ -132,6 +136,46 @@ export default () => {
getContents: () => getContents(false, false, false, true),
}))
Navigation.register(new View({
id: expiredSharesViewId,
name: t('files_sharing', 'Expired shares'),
caption: t('files_sharing', 'List of shares that expired.'),
emptyTitle: t('files_sharing', 'No expired shares'),
emptyCaption: t('files_sharing', 'Shares that have expired will show up here'),
icon: AccountClockSvg,
order: 6,
parent: sharesViewId,
columns: [
new Column({
id: 'expired',
title: t('files_sharing', 'Expired'),
render(node) {
const expirationTime = node.attributes?.expiration
const span = document.createElement('span')
if (expirationTime) {
span.title = moment.unix(expirationTime).format('LLL')
span.textContent = moment.unix(expirationTime).fromNow()
return span
}
// Unknown expiration time
span.textContent = t('files_sharing', 'A long time ago')
return span
},
sort(nodeA, nodeB) {
const expirationTimeA = nodeA.attributes?.expiration || 0
const expirationTimeB = nodeB.attributes?.expiration || 0
return expirationTimeB - expirationTimeA
},
}),
],
getContents: () => getContents(false, false, false, false, true),
}))
Navigation.register(new View({
id: pendingSharesViewId,
name: t('files_sharing', 'Pending shares'),
@ -140,8 +184,8 @@ export default () => {
emptyTitle: t('files_sharing', 'No pending shares'),
emptyCaption: t('files_sharing', 'Shares you have received but not approved will show up here'),
icon: AccountClockSvg,
order: 6,
icon: AccountArrowLeftSvg,
order: 7,
parent: sharesViewId,
columns: [],

View file

@ -59,11 +59,16 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu
const source = `${davRemoteURL}${davRootPath}/${path.replace(/^\/+/, '')}`
let mtime = ocsEntry.item_mtime ? new Date((ocsEntry.item_mtime) * 1000) : undefined
// Prefer share time if more recent than item mtime
if (ocsEntry?.stime > (ocsEntry?.item_mtime || 0)) {
mtime = new Date((ocsEntry.stime) * 1000)
}
const expiration = ocsEntry?.expiration
? new Date(ocsEntry?.expiration)?.getTime() / 1000
: undefined
return new Node({
id: fileid,
source,
@ -75,6 +80,7 @@ const ocsEntryToNode = async function(ocsEntry: any): Promise<Folder | File | nu
root: davRootPath,
attributes: {
...ocsEntry,
expiration,
'has-preview': hasPreview,
// Also check the sharingStatusAction.ts code
'owner-id': ocsEntry?.uid_owner,
@ -149,6 +155,16 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> {
})
}
const getExpiredShares = function(): AxiosPromise<OCSResponse<any>> {
const url = generateOcsUrl('apps/files_sharing/api/v1/expiredshares')
return axios.get(url, {
headers,
params: {
include_tags: true,
},
})
}
/**
* Check if a file request is enabled
* @param attributes the share attributes json-encoded array
@ -180,7 +196,7 @@ const groupBy = function(nodes: (Folder | File)[], key: string) {
}, {})) as (Folder | File)[][]
}
export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, expiredShares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
const promises = [] as AxiosPromise<OCSResponse<any>>[]
if (sharedWithYou) {
@ -195,6 +211,9 @@ export const getContents = async (sharedWithYou = true, sharedWithOthers = true,
if (deletedshares) {
promises.push(getDeletedShares())
}
if (expiredShares) {
promises.push(getExpiredShares())
}
const responses = await Promise.all(promises)
const data = responses.map((response) => response.data.ocs.data).flat()

View file

@ -9,8 +9,10 @@ namespace OCA\Files_Sharing\Tests;
use OCA\Files_Sharing\ExpireSharesJob;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Constants;
use OCP\IAppConfig;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
/**
* Class ExpireSharesJobTest
@ -49,7 +51,14 @@ class ExpireSharesJobTest extends \Test\TestCase {
\OC::registerShareHooks(\OC::$server->getSystemConfig());
$this->job = new ExpireSharesJob(\OC::$server->get(ITimeFactory::class), \OC::$server->get(IManager::class), $this->connection);
$this->job = new ExpireSharesJob(
\OC::$server->get(ITimeFactory::class),
\OC::$server->get(IManager::class),
$this->connection,
\OC::$server->get(IAppConfig::class),
\OC::$server->get(LoggerInterface::class),
);
}
protected function tearDown(): void {

View file

@ -71,6 +71,7 @@ class Sharing implements IDelegatedSettings {
'defaultRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_default_remote_expire_date'),
'remoteExpireAfterNDays' => $this->config->getAppValue('core', 'shareapi_remote_expire_after_n_days', '7'),
'enforceRemoteExpireDate' => $this->getHumanBooleanConfig('core', 'shareapi_enforce_remote_expire_date'),
'deleteOnExpire' => $this->getHumanBooleanConfig('core', 'shareapi_delete_on_expire', true),
];
$this->initialState->provideInitialState('sharingAppEnabled', $this->appManager->isEnabledForUser('files_sharing'));

View file

@ -141,6 +141,12 @@
:placeholder="t('settings', 'Expire shares after x days')"
:value.sync="settings.expireAfterNDays" />
</fieldset>
<NcCheckboxRadioSwitch type="switch"
aria-controls="settings-sharing-api-expiration-delete"
:checked.sync="settings.deleteOnExpire">
{{ t('settings', 'Delete shares on expiration') }}
</NcCheckboxRadioSwitch>
</div>
<div v-show="settings.enabled" id="settings-sharing-privary-related" class="sharing__section">
@ -240,6 +246,7 @@ interface IShareSettings {
defaultRemoteExpireDate: boolean
remoteExpireAfterNDays: string
enforceRemoteExpireDate: boolean
deleteOnExpire: boolean
}
export default defineComponent({

View file

@ -1143,7 +1143,7 @@ class Manager implements IManager {
/**
* @inheritdoc
*/
public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0) {
public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0, $expired = false) {
if ($path !== null &&
!($path instanceof \OCP\Files\File) &&
!($path instanceof \OCP\Files\Folder)) {
@ -1169,7 +1169,7 @@ class Manager implements IManager {
$added = 0;
foreach ($shares as $share) {
try {
$this->checkShare($share);
$this->checkShare($share, $expired);
} catch (ShareNotFound $e) {
// Ignore since this basically means the share is deleted
continue;
@ -1216,7 +1216,7 @@ class Manager implements IManager {
/**
* @inheritdoc
*/
public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0, $expired = false) {
try {
$provider = $this->factory->getProviderForType($shareType);
} catch (ProviderException $e) {
@ -1228,7 +1228,7 @@ class Manager implements IManager {
// remove all shares which are already expired
foreach ($shares as $key => $share) {
try {
$this->checkShare($share);
$this->checkShare($share, $expired);
} catch (ShareNotFound $e) {
unset($shares[$key]);
}
@ -1259,7 +1259,18 @@ class Manager implements IManager {
/**
* @inheritdoc
*/
public function getShareById($id, $recipient = null) {
public function getExpiredShares($userId, $shareType, ?Node $path = null, $limit = 50, $offset = 0) {
$shares = $this->getSharesBy($userId, $shareType, $path, false, $limit, $offset, true);
return array_filter($shares, function (IShare $share) {
return $share->isExpired();
});
}
/**
* @inheritdoc
*/
public function getShareById($id, $recipient = null, $expired = false) {
if ($id === null) {
throw new ShareNotFound();
}
@ -1274,7 +1285,7 @@ class Manager implements IManager {
$share = $provider->getShareById($id, $recipient);
$this->checkShare($share);
$this->checkShare($share, $expired);
return $share;
}
@ -1300,7 +1311,7 @@ class Manager implements IManager {
*
* @throws ShareNotFound
*/
public function getShareByToken($token) {
public function getShareByToken($token, $expired = false) {
// tokens cannot be valid local user names
if ($this->userManager->userExists($token)) {
throw new ShareNotFound();
@ -1358,7 +1369,7 @@ class Manager implements IManager {
throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
}
$this->checkShare($share);
$this->checkShare($share, $expired);
/*
* Reduce the permissions for link or email shares if public upload is not enabled
@ -1376,11 +1387,14 @@ class Manager implements IManager {
*
* @throws ShareNotFound
*/
protected function checkShare(IShare $share): void {
if ($share->isExpired()) {
$this->deleteShare($share);
throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
protected function checkShare(IShare $share, $allowExpired = false): void {
if ($share->isExpired() && !$allowExpired) {
if ($this->config->getAppValue('core', 'shareapi_delete_on_expire', 'yes') !== 'no') {
$this->deleteShare($share);
}
throw new ShareNotFound($this->l->t('The requested share has expired'));
}
if ($this->config->getAppValue('files_sharing', 'hide_disabled_user_shares', 'no') === 'yes') {
$uids = array_unique([$share->getShareOwner(),$share->getSharedBy()]);
foreach ($uids as $uid) {

View file

@ -160,6 +160,20 @@ interface IManager {
*/
public function getDeletedSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0);
/**
* Get expired shares created by $user.
* Filter by $node if provided
*
* @param string $userId
* @param int $shareType
* @param Node|null $path
* @param int $limit The maximum number of shares returned, -1 for all
* @param int $offset
* @return IShare[]
* @since 31.0.0
*/
public function getExpiredShares($userId, $shareType, ?Node $path = null, $limit = 50, $offset = 0);
/**
* Retrieve a share by the share id.
* If the recipient is set make sure to retrieve the file for that user.

View file

@ -2720,6 +2720,7 @@ class ManagerTest extends \Test\TestCase {
* deleted (as they are evaluated). but share 8 should still be there.
*/
public function testGetSharesByExpiredLinkShares(): void {
/** @var IManager|\PHPUnit\Framework\MockObject\MockObject */
$manager = $this->createManagerMock()
->setMethods(['deleteShare'])
->getMock();