Merge pull request #3304 from nextcloud/backport/3298/stable1.4
[stable1.4] Delete file shares through attachments API
This commit is contained in:
@@ -959,6 +959,7 @@ For now only `deck_file` is supported as an attachment type.
|
|||||||
|
|
||||||
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
|
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
|
||||||
|
|
||||||
|
|
||||||
#### Request parameters
|
#### Request parameters
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
| Parameter | Type | Description |
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ use OCA\Deck\InvalidAttachmentType;
|
|||||||
use OCA\Deck\NoPermissionException;
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCA\Deck\NotFoundException;
|
use OCA\Deck\NotFoundException;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
|
use OCP\AppFramework\Db\IMapperException;
|
||||||
use OCP\AppFramework\Http\Response;
|
use OCP\AppFramework\Http\Response;
|
||||||
use OCP\ICache;
|
use OCP\ICache;
|
||||||
use OCP\ICacheFactory;
|
use OCP\ICacheFactory;
|
||||||
@@ -320,14 +321,10 @@ class AttachmentService {
|
|||||||
* Either mark an attachment as deleted for later removal or just remove it depending
|
* Either mark an attachment as deleted for later removal or just remove it depending
|
||||||
* on the IAttachmentService implementation
|
* on the IAttachmentService implementation
|
||||||
*
|
*
|
||||||
* @param $attachmentId
|
* @throws NoPermissionException
|
||||||
* @return \OCP\AppFramework\Db\Entity
|
* @throws NotFoundException
|
||||||
* @throws \OCA\Deck\NoPermissionException
|
|
||||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
|
||||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
|
||||||
* @throws BadRequestException
|
|
||||||
*/
|
*/
|
||||||
public function delete($cardId, $attachmentId, $type = 'deck_file') {
|
public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
|
||||||
try {
|
try {
|
||||||
$service = $this->getService($type);
|
$service = $this->getService($type);
|
||||||
} catch (InvalidAttachmentType $e) {
|
} catch (InvalidAttachmentType $e) {
|
||||||
@@ -340,40 +337,32 @@ class AttachmentService {
|
|||||||
$attachment->setType($type);
|
$attachment->setType($type);
|
||||||
$attachment->setCardId($cardId);
|
$attachment->setCardId($cardId);
|
||||||
$service->extendData($attachment);
|
$service->extendData($attachment);
|
||||||
$service->delete($attachment);
|
} else {
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
try {
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$attachment = $this->attachmentMapper->find($attachmentId);
|
||||||
return $attachment;
|
} catch (IMapperException $e) {
|
||||||
|
throw new NoPermissionException('Permission denied');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$attachment = $this->attachmentMapper->find($attachmentId);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new NoPermissionException('Permission denied');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
||||||
$this->cache->clear('card-' . $attachment->getCardId());
|
|
||||||
|
|
||||||
if ($service->allowUndo()) {
|
if ($service->allowUndo()) {
|
||||||
$service->markAsDeleted($attachment);
|
$service->markAsDeleted($attachment);
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$attachment = $this->attachmentMapper->update($attachment);
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
} else {
|
||||||
return $this->attachmentMapper->update($attachment);
|
$service->delete($attachment);
|
||||||
|
if (!$service instanceof ICustomAttachmentService) {
|
||||||
|
$attachment = $this->attachmentMapper->delete($attachment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$service->delete($attachment);
|
|
||||||
|
|
||||||
$attachment = $this->attachmentMapper->delete($attachment);
|
$this->cache->clear('card-' . $attachment->getCardId());
|
||||||
$this->changeHelper->cardChanged($attachment->getCardId());
|
$this->changeHelper->cardChanged($attachment->getCardId());
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
||||||
return $attachment;
|
return $attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function restore($cardId, $attachmentId, $type = 'deck_file') {
|
public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
|
||||||
if (is_numeric($attachmentId) === false) {
|
|
||||||
throw new BadRequestException('attachment id must be a number');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$attachment = $this->attachmentMapper->find($attachmentId);
|
$attachment = $this->attachmentMapper->find($attachmentId);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
|
|
||||||
namespace OCA\Deck\Service;
|
namespace OCA\Deck\Service;
|
||||||
|
|
||||||
|
use OCA\Deck\Db\Acl;
|
||||||
use OCA\Deck\Db\Attachment;
|
use OCA\Deck\Db\Attachment;
|
||||||
|
use OCA\Deck\Db\CardMapper;
|
||||||
|
use OCA\Deck\NoPermissionException;
|
||||||
use OCA\Deck\Sharing\DeckShareProvider;
|
use OCA\Deck\Sharing\DeckShareProvider;
|
||||||
use OCA\Deck\StatusException;
|
use OCA\Deck\StatusException;
|
||||||
use OCP\AppFramework\Http\StreamResponse;
|
use OCP\AppFramework\Http\StreamResponse;
|
||||||
@@ -38,6 +41,7 @@ use OCP\IRequest;
|
|||||||
use OCP\Share\Exceptions\ShareNotFound;
|
use OCP\Share\Exceptions\ShareNotFound;
|
||||||
use OCP\Share\IManager;
|
use OCP\Share\IManager;
|
||||||
use OCP\Share\IShare;
|
use OCP\Share\IShare;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||||
private $request;
|
private $request;
|
||||||
@@ -48,8 +52,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
private $configService;
|
private $configService;
|
||||||
private $l10n;
|
private $l10n;
|
||||||
private $preview;
|
private $preview;
|
||||||
private $permissionService;
|
|
||||||
private $mimeTypeDetector;
|
private $mimeTypeDetector;
|
||||||
|
private $permissionService;
|
||||||
|
private $cardMapper;
|
||||||
|
private $logger;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
IRequest $request,
|
IRequest $request,
|
||||||
@@ -59,8 +65,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
ConfigService $configService,
|
ConfigService $configService,
|
||||||
DeckShareProvider $shareProvider,
|
DeckShareProvider $shareProvider,
|
||||||
IPreview $preview,
|
IPreview $preview,
|
||||||
PermissionService $permissionService,
|
|
||||||
IMimeTypeDetector $mimeTypeDetector,
|
IMimeTypeDetector $mimeTypeDetector,
|
||||||
|
PermissionService $permissionService,
|
||||||
|
CardMapper $cardMapper,
|
||||||
|
LoggerInterface $logger,
|
||||||
string $userId = null
|
string $userId = null
|
||||||
) {
|
) {
|
||||||
$this->request = $request;
|
$this->request = $request;
|
||||||
@@ -72,15 +80,20 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$this->userId = $userId;
|
$this->userId = $userId;
|
||||||
$this->preview = $preview;
|
$this->preview = $preview;
|
||||||
$this->mimeTypeDetector = $mimeTypeDetector;
|
$this->mimeTypeDetector = $mimeTypeDetector;
|
||||||
|
$this->permissionService = $permissionService;
|
||||||
|
$this->cardMapper = $cardMapper;
|
||||||
|
$this->logger = $logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function listAttachments(int $cardId): array {
|
public function listAttachments(int $cardId): array {
|
||||||
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
|
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
|
||||||
$shares = array_filter($shares, function ($share) {
|
return array_filter(array_map(function (IShare $share) use ($cardId) {
|
||||||
return $share->getPermissions() > 0;
|
try {
|
||||||
});
|
$file = $share->getNode();
|
||||||
return array_map(function (IShare $share) use ($cardId) {
|
} catch (NotFoundException $e) {
|
||||||
$file = $share->getNode();
|
$this->logger->debug('Unable to find node for share with ID ' . $share->getId());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
$attachment = new Attachment();
|
$attachment = new Attachment();
|
||||||
$attachment->setType('file');
|
$attachment->setType('file');
|
||||||
$attachment->setId((int)$share->getId());
|
$attachment->setId((int)$share->getId());
|
||||||
@@ -89,9 +102,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$attachment->setData($file->getName());
|
$attachment->setData($file->getName());
|
||||||
$attachment->setLastModified($file->getMTime());
|
$attachment->setLastModified($file->getMTime());
|
||||||
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
|
$attachment->setCreatedAt($share->getShareTime()->getTimestamp());
|
||||||
$attachment->setDeletedAt(0);
|
$attachment->setDeletedAt($share->getPermissions() === 0 ? $share->getShareTime()->getTimestamp() : 0);
|
||||||
return $attachment;
|
return $attachment;
|
||||||
}, $shares);
|
}, $shares));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getAttachmentCount(int $cardId): int {
|
public function getAttachmentCount(int $cardId): int {
|
||||||
@@ -144,6 +157,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function display(Attachment $attachment) {
|
public function display(Attachment $attachment) {
|
||||||
|
// Problem: Folders
|
||||||
/** @psalm-suppress InvalidCatch */
|
/** @psalm-suppress InvalidCatch */
|
||||||
try {
|
try {
|
||||||
$share = $this->shareProvider->getShareById($attachment->getId());
|
$share = $this->shareProvider->getShareById($attachment->getId());
|
||||||
@@ -165,6 +179,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$file = $this->getUploadedFile();
|
$file = $this->getUploadedFile();
|
||||||
$fileName = $file['name'];
|
$fileName = $file['name'];
|
||||||
|
|
||||||
|
// get shares for current card
|
||||||
|
// check if similar filename already exists
|
||||||
|
|
||||||
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
$userFolder = $this->rootFolder->getUserFolder($this->userId);
|
||||||
try {
|
try {
|
||||||
$folder = $userFolder->get($this->configService->getAttachmentFolder());
|
$folder = $userFolder->get($this->configService->getAttachmentFolder());
|
||||||
@@ -245,12 +262,16 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
|||||||
$file = $share->getNode();
|
$file = $share->getNode();
|
||||||
$attachment->setData($file->getName());
|
$attachment->setData($file->getName());
|
||||||
|
|
||||||
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) {
|
// Deleting a Nextcloud file attachment will remove the share to the card, keeping the source file untouched
|
||||||
$file->delete();
|
// Opt-out of individual shares per user is no longer performed within deck but can still be done through the files app
|
||||||
|
$canEdit = $this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
|
||||||
|
$isFileOwner = $file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId;
|
||||||
|
if ($isFileOwner || $canEdit) {
|
||||||
|
$this->shareManager->deleteShare($share);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shareManager->deleteFromSelf($share, $this->userId);
|
throw new NoPermissionException('No permission to remove the attachment from the card');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function allowUndo() {
|
public function allowUndo() {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
|
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
|
||||||
<div class="button-group">
|
<div class="button-group" v-if="!isReadOnly">
|
||||||
<button class="icon-upload" @click="uploadNewFile()">
|
<button class="icon-upload" @click="uploadNewFile()">
|
||||||
{{ t('deck', 'Upload new files') }}
|
{{ t('deck', 'Upload new files') }}
|
||||||
</button>
|
</button>
|
||||||
@@ -49,18 +49,25 @@
|
|||||||
</li>
|
</li>
|
||||||
<li v-for="attachment in attachments"
|
<li v-for="attachment in attachments"
|
||||||
:key="attachment.id"
|
:key="attachment.id"
|
||||||
class="attachment">
|
class="attachment"
|
||||||
|
:class="{ 'attachment--deleted': attachment.deletedAt > 0 }">
|
||||||
<a class="fileicon"
|
<a class="fileicon"
|
||||||
|
:href="internalLink(attachment)"
|
||||||
:style="mimetypeForAttachment(attachment)"
|
:style="mimetypeForAttachment(attachment)"
|
||||||
@click.prevent="showViewer(attachment)" />
|
@click.prevent="showViewer(attachment)" />
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<a @click.prevent="showViewer(attachment)">
|
<a :href="internalLink(attachment)" @click.prevent="showViewer(attachment)">
|
||||||
<div class="filename">
|
<div class="filename">
|
||||||
<span class="basename">{{ attachment.data }}</span>
|
<span class="basename">{{ attachment.data }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
<div v-if="attachment.deletedAt === 0">
|
||||||
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
|
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
||||||
<span class="filedate">{{ attachment.createdBy }}</span>
|
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
|
||||||
|
<span class="filedate">{{ attachment.createdBy }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<span class="attachment--info">{{ t('deck', 'Pending share') }}</span>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<Actions v-if="selectable">
|
<Actions v-if="selectable">
|
||||||
@@ -68,12 +75,12 @@
|
|||||||
{{ t('deck', 'Add this attachment') }}
|
{{ t('deck', 'Add this attachment') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Actions>
|
</Actions>
|
||||||
<Actions v-if="removable" :force-menu="true">
|
<Actions v-if="removable && !isReadOnly" :force-menu="true">
|
||||||
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
|
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
|
||||||
{{ t('deck', 'Show in Files') }}
|
{{ t('deck', 'Show in Files') }}
|
||||||
</ActionLink>
|
</ActionLink>
|
||||||
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)">
|
<ActionButton v-if="attachment.extendedData.fileid && !isReadOnly" icon="icon-delete" @click="unshareAttachment(attachment)">
|
||||||
{{ t('deck', 'Unshare file') }}
|
{{ t('deck', 'Remove attachment') }}
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
||||||
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
|
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('deleteAttachment', attachment)">
|
||||||
@@ -143,6 +150,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
attachments() {
|
attachments() {
|
||||||
|
// FIXME sort propertly by last modified / deleted at
|
||||||
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
|
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
|
||||||
},
|
},
|
||||||
mimetypeForAttachment() {
|
mimetypeForAttachment() {
|
||||||
@@ -320,9 +328,10 @@ export default {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.attachment--info,
|
||||||
.filesize, .filedate {
|
.filesize, .filedate {
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
color: darkgray;
|
color: var(--color-text-maxcontrast);
|
||||||
}
|
}
|
||||||
.app-popover-menu-utils {
|
.app-popover-menu-utils {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -193,6 +193,9 @@
|
|||||||
<code>$cardId</code>
|
<code>$cardId</code>
|
||||||
</InvalidScalarArgument>
|
</InvalidScalarArgument>
|
||||||
</file>
|
</file>
|
||||||
|
<file src="lib/Service/AttachmentService.php">
|
||||||
|
<InvalidCatch occurrences="1"/>
|
||||||
|
</file>
|
||||||
<file src="lib/Service/BoardService.php">
|
<file src="lib/Service/BoardService.php">
|
||||||
<TooManyArguments occurrences="2">
|
<TooManyArguments occurrences="2">
|
||||||
<code>findAll</code>
|
<code>findAll</code>
|
||||||
|
|||||||
Reference in New Issue
Block a user