mirror of
https://github.com/nextcloud/server.git
synced 2026-04-15 22:11:17 -04:00
Merge pull request #43740 from nextcloud/backport/43727/stable27
This commit is contained in:
commit
245a439d47
10 changed files with 142 additions and 16 deletions
|
|
@ -161,7 +161,7 @@ class ServerFactory {
|
|||
|
||||
// Allow view-only plugin for webdav requests
|
||||
$server->addPlugin(new ViewOnlyPlugin(
|
||||
$this->logger
|
||||
$userFolder,
|
||||
));
|
||||
|
||||
if ($this->userSession->isLoggedIn()) {
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ namespace OCA\DAV\DAV;
|
|||
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
|
||||
use OCA\DAV\Connector\Sabre\File as DavFile;
|
||||
use OCA\Files_Versions\Sabre\VersionFile;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\NotFoundException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\ServerPlugin;
|
||||
use Sabre\HTTP\RequestInterface;
|
||||
|
|
@ -36,10 +36,12 @@ use Sabre\DAV\Exception\NotFound;
|
|||
*/
|
||||
class ViewOnlyPlugin extends ServerPlugin {
|
||||
private ?Server $server = null;
|
||||
private LoggerInterface $logger;
|
||||
private ?Folder $userFolder;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
public function __construct(
|
||||
?Folder $userFolder,
|
||||
) {
|
||||
$this->userFolder = $userFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,6 +78,16 @@ class ViewOnlyPlugin extends ServerPlugin {
|
|||
$node = $davNode->getNode();
|
||||
} else if ($davNode instanceof VersionFile) {
|
||||
$node = $davNode->getVersion()->getSourceFile();
|
||||
$currentUserId = $this->userFolder?->getOwner()?->getUID();
|
||||
// The version source file is relative to the owner storage.
|
||||
// But we need the node from the current user perspective.
|
||||
if ($node->getOwner()->getUID() !== $currentUserId) {
|
||||
$nodes = $this->userFolder->getById($node->getId());
|
||||
$node = array_pop($nodes);
|
||||
if (!$node) {
|
||||
throw new NotFoundException("Version file not accessible by current user");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ class Server {
|
|||
|
||||
// Allow view-only plugin for webdav requests
|
||||
$this->server->addPlugin(new ViewOnlyPlugin(
|
||||
$logger
|
||||
\OC::$server->getUserFolder(),
|
||||
));
|
||||
|
||||
if (BrowserErrorPagePlugin::isBrowserRequest($request)) {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ use OCA\DAV\Connector\Sabre\File as DavFile;
|
|||
use OCA\Files_Versions\Versions\IVersion;
|
||||
use OCA\Files_Versions\Sabre\VersionFile;
|
||||
use OCP\Files\File;
|
||||
use OCP\Files\Folder;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
use OCP\IUser;
|
||||
use OCP\Share\IAttributes;
|
||||
use OCP\Share\IShare;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Server;
|
||||
use Sabre\DAV\Tree;
|
||||
use Test\TestCase;
|
||||
|
|
@ -43,10 +44,13 @@ class ViewOnlyPluginTest extends TestCase {
|
|||
private $tree;
|
||||
/** @var RequestInterface | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $request;
|
||||
/** @var Folder | \PHPUnit\Framework\MockObject\MockObject */
|
||||
private $userFolder;
|
||||
|
||||
public function setUp(): void {
|
||||
$this->userFolder = $this->createMock(Folder::class);
|
||||
$this->plugin = new ViewOnlyPlugin(
|
||||
$this->createMock(LoggerInterface::class)
|
||||
$this->userFolder,
|
||||
);
|
||||
$this->request = $this->createMock(RequestInterface::class);
|
||||
$this->tree = $this->createMock(Tree::class);
|
||||
|
|
@ -111,6 +115,26 @@ class ViewOnlyPluginTest extends TestCase {
|
|||
$davNode->expects($this->once())
|
||||
->method('getVersion')
|
||||
->willReturn($version);
|
||||
|
||||
$currentUser = $this->createMock(IUser::class);
|
||||
$currentUser->expects($this->once())
|
||||
->method('getUID')
|
||||
->willReturn('alice');
|
||||
$nodeInfo->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn($currentUser);
|
||||
|
||||
$nodeInfo = $this->createMock(File::class);
|
||||
$owner = $this->createMock(IUser::class);
|
||||
$owner->expects($this->once())
|
||||
->method('getUID')
|
||||
->willReturn('bob');
|
||||
$this->userFolder->expects($this->once())
|
||||
->method('getById')
|
||||
->willReturn([$nodeInfo]);
|
||||
$this->userFolder->expects($this->once())
|
||||
->method('getOwner')
|
||||
->willReturn($owner);
|
||||
} else {
|
||||
$davPath = 'files/path/to/file.odt';
|
||||
$davNode = $this->createMock(DavFile::class);
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class ServerTest extends \Test\TestCase {
|
|||
/** @var IRequest | \PHPUnit\Framework\MockObject\MockObject $r */
|
||||
$r = $this->createMock(IRequest::class);
|
||||
$r->expects($this->any())->method('getRequestUri')->willReturn($uri);
|
||||
$this->loginAsUser('admin');
|
||||
$s = new Server($r, '/');
|
||||
$this->assertNotNull($s->server);
|
||||
foreach ($plugins as $plugin) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ declare(strict_types=1);
|
|||
namespace OCA\Files_Versions\Versions;
|
||||
|
||||
use OC\Files\View;
|
||||
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
|
||||
use OCA\Files_Sharing\ISharedStorage;
|
||||
use OCA\Files_Sharing\SharedStorage;
|
||||
use OCA\Files_Versions\Db\VersionEntity;
|
||||
|
|
@ -42,23 +43,27 @@ use OCP\Files\NotFoundException;
|
|||
use OCP\Files\Storage\IStorage;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IUserSession;
|
||||
|
||||
class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend {
|
||||
private IRootFolder $rootFolder;
|
||||
private IUserManager $userManager;
|
||||
private VersionsMapper $versionsMapper;
|
||||
private IMimeTypeLoader $mimeTypeLoader;
|
||||
private IUserSession $userSession;
|
||||
|
||||
public function __construct(
|
||||
IRootFolder $rootFolder,
|
||||
IUserManager $userManager,
|
||||
VersionsMapper $versionsMapper,
|
||||
IMimeTypeLoader $mimeTypeLoader
|
||||
IMimeTypeLoader $mimeTypeLoader,
|
||||
IUserSession $userSession,
|
||||
) {
|
||||
$this->rootFolder = $rootFolder;
|
||||
$this->userManager = $userManager;
|
||||
$this->versionsMapper = $versionsMapper;
|
||||
$this->mimeTypeLoader = $mimeTypeLoader;
|
||||
$this->userSession = $userSession;
|
||||
}
|
||||
|
||||
public function useBackendForStorage(IStorage $storage): bool {
|
||||
|
|
@ -174,6 +179,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend,
|
|||
}
|
||||
|
||||
public function rollback(IVersion $version) {
|
||||
if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_UPDATE)) {
|
||||
throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.');
|
||||
}
|
||||
|
||||
return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser());
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +229,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend,
|
|||
}
|
||||
|
||||
public function setVersionLabel(IVersion $version, string $label): void {
|
||||
if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_UPDATE)) {
|
||||
throw new Forbidden('You cannot label this version because you do not have update permissions on the source file.');
|
||||
}
|
||||
|
||||
$versionEntity = $this->versionsMapper->findVersionForFileId(
|
||||
$version->getSourceFile()->getId(),
|
||||
$version->getTimestamp(),
|
||||
|
|
@ -232,6 +245,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend,
|
|||
}
|
||||
|
||||
public function deleteVersion(IVersion $version): void {
|
||||
if (!$this->currentUserHasPermissions($version, \OCP\Constants::PERMISSION_DELETE)) {
|
||||
throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.');
|
||||
}
|
||||
|
||||
Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId());
|
||||
$versionEntity = $this->versionsMapper->findVersionForFileId(
|
||||
$version->getSourceFile()->getId(),
|
||||
|
|
@ -271,4 +288,23 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend,
|
|||
public function deleteVersionsEntity(File $file): void {
|
||||
$this->versionsMapper->deleteAllVersionsForFileId($file->getId());
|
||||
}
|
||||
|
||||
private function currentUserHasPermissions(IVersion $version, int $permissions): bool {
|
||||
$sourceFile = $version->getSourceFile();
|
||||
$currentUserId = $this->userSession->getUser()?->getUID();
|
||||
|
||||
if ($currentUserId === null) {
|
||||
throw new NotFoundException("No user logged in");
|
||||
}
|
||||
|
||||
if ($sourceFile->getOwner()?->getUID() !== $currentUserId) {
|
||||
$nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId());
|
||||
$sourceFile = array_pop($nodes);
|
||||
if (!$sourceFile) {
|
||||
throw new NotFoundException("Version file not accessible by current user");
|
||||
}
|
||||
}
|
||||
|
||||
return ($sourceFile->getPermissions() & $permissions) === $permissions;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<NcActionButton v-if="enableLabeling"
|
||||
<NcActionButton v-if="enableLabeling && hasUpdatePermissions"
|
||||
:close-after-click="true"
|
||||
@click="openVersionLabelModal">
|
||||
<template #icon>
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
</template>
|
||||
{{ t('files_versions', 'Compare to current version') }}
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="!isCurrent"
|
||||
<NcActionButton v-if="!isCurrent && hasUpdatePermissions"
|
||||
:close-after-click="true"
|
||||
@click="restoreVersion">
|
||||
<template #icon>
|
||||
|
|
@ -71,7 +71,8 @@
|
|||
</template>
|
||||
{{ t('files_versions', 'Restore version') }}
|
||||
</NcActionButton>
|
||||
<NcActionLink :href="downloadURL"
|
||||
<NcActionLink v-if="isDownloadable"
|
||||
:href="downloadURL"
|
||||
:close-after-click="true"
|
||||
:download="downloadURL">
|
||||
<template #icon>
|
||||
|
|
@ -79,7 +80,7 @@
|
|||
</template>
|
||||
{{ t('files_versions', 'Download version') }}
|
||||
</NcActionLink>
|
||||
<NcActionButton v-if="!isCurrent && enableDeletion"
|
||||
<NcActionButton v-if="!isCurrent && enableDeletion && hasDeletePermissions"
|
||||
:close-after-click="true"
|
||||
@click="deleteVersion">
|
||||
<template #icon>
|
||||
|
|
@ -136,6 +137,9 @@ import { translate } from '@nextcloud/l10n'
|
|||
import { joinPaths } from '@nextcloud/paths'
|
||||
import { getRootUrl } from '@nextcloud/router'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { Permission } from '@nextcloud/files'
|
||||
|
||||
import { hasPermissions } from '../../../files_sharing/src/lib/SharePermissionsToolBox.js'
|
||||
|
||||
export default {
|
||||
name: 'Version',
|
||||
|
|
@ -260,6 +264,33 @@ export default {
|
|||
enableDeletion() {
|
||||
return this.capabilities.files.version_deletion === true
|
||||
},
|
||||
|
||||
/** @return {boolean} */
|
||||
hasDeletePermissions() {
|
||||
return hasPermissions(this.fileInfo.permissions, Permission.DELETE)
|
||||
},
|
||||
|
||||
/** @return {boolean} */
|
||||
hasUpdatePermissions() {
|
||||
return hasPermissions(this.fileInfo.permissions, Permission.UPDATE)
|
||||
},
|
||||
|
||||
/** @return {boolean} */
|
||||
isDownloadable() {
|
||||
if ((this.fileInfo.permissions & Permission.READ) === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the mount type is a share, ensure it got download permissions.
|
||||
if (this.fileInfo.mountType === 'shared') {
|
||||
const downloadAttribute = this.fileInfo.shareAttributes.find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download')
|
||||
if (downloadAttribute !== undefined && downloadAttribute.enabled === false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openVersionLabelModal() {
|
||||
|
|
|
|||
4
dist/files_versions-files_versions.js
vendored
4
dist/files_versions-files_versions.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -38,6 +38,28 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright 2022 Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @author Louis Chmn <louis@chmn.me>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
|
|
|
|||
2
dist/files_versions-files_versions.js.map
vendored
2
dist/files_versions-files_versions.js.map
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue