diff --git a/apps/dav/lib/Meta/MetaFile.php b/apps/dav/lib/Meta/MetaFile.php
index ba65afa4c9ee..6ddf64840c9d 100644
--- a/apps/dav/lib/Meta/MetaFile.php
+++ b/apps/dav/lib/Meta/MetaFile.php
@@ -25,6 +25,7 @@
use OCA\DAV\Files\ICopySource;
use OCA\DAV\Files\IProvidesAdditionalHeaders;
use OCA\DAV\Files\IFileNode;
+use OCP\Files\IProvidesVersionAuthor;
use OCP\Files\Node;
use Sabre\DAV\File;
@@ -126,4 +127,27 @@ public function getContentDispositionFileName() {
public function getNode() {
return $this->file;
}
+
+ /**
+ * @return string
+ */
+ public function getVersionAuthor() : string {
+ if ($this->file instanceof IProvidesVersionAuthor) {
+ return $this->file->getEditedBy();
+ }
+ return '';
+ }
+
+ /**
+ * @return string
+ */
+ public function getVersionAuthorName() : string {
+ if ($this->file instanceof IProvidesVersionAuthor) {
+ $uid = $this->file->getEditedBy();
+ $manager = \OC::$server->getUserManager();
+ $user = $manager->get($uid);
+ return $user !== null ? $user->getDisplayName() : '';
+ }
+ return '';
+ }
}
diff --git a/apps/dav/lib/Meta/MetaPlugin.php b/apps/dav/lib/Meta/MetaPlugin.php
index 8965f0fb9ce4..759af31fe94b 100644
--- a/apps/dav/lib/Meta/MetaPlugin.php
+++ b/apps/dav/lib/Meta/MetaPlugin.php
@@ -33,6 +33,8 @@ class MetaPlugin extends ServerPlugin {
public const NS_OWNCLOUD = 'http://owncloud.org/ns';
public const PATH_FOR_FILEID_PROPERTYNAME = '{http://owncloud.org/ns}meta-path-for-user';
+ public const VERSION_EDITED_BY_PROPERTYNAME = '{http://owncloud.org/ns}meta-version-edited-by';
+ public const VERSION_EDITED_BY_PROPERTYNAME_NAME = '{http://owncloud.org/ns}meta-version-edited-by-name';
/**
* Reference to main server object
*
@@ -97,6 +99,13 @@ public function handleGetProperties(PropFind $propFind, INode $node) {
$file = \current($files);
return $baseFolder->getRelativePath($file->getPath());
});
+ } elseif ($node instanceof MetaFile) {
+ $propFind->handle(self::VERSION_EDITED_BY_PROPERTYNAME, function () use ($node) {
+ return $node->getVersionAuthor();
+ });
+ $propFind->handle(self::VERSION_EDITED_BY_PROPERTYNAME_NAME, function () use ($node) {
+ return $node->getVersionAuthorName();
+ });
}
}
}
diff --git a/apps/files_trashbin/lib/Trashbin.php b/apps/files_trashbin/lib/Trashbin.php
index 2a0f1ebb4d1c..acb8a8580cd2 100644
--- a/apps/files_trashbin/lib/Trashbin.php
+++ b/apps/files_trashbin/lib/Trashbin.php
@@ -400,19 +400,35 @@ private static function retainVersions($filename, $owner, $ownerPath, $timestamp
$rootView = new View('/');
if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
+ $metadataFileExists = $rootView->file_exists($owner . '/files_versions/' . $ownerPath . '.json');
+
if ($owner !== $user || $forceCopy) {
self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . \basename($ownerPath) . '.d' . $timestamp, $rootView);
+ if ($metadataFileExists) {
+ self::copy_recursive($owner . '/files_versions/' . $ownerPath . '.json', $owner . '/files_trashbin/versions/' . \basename($ownerPath) . '.json' . '.d' . $timestamp, $rootView);
+ }
}
if (!$forceCopy) {
self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
+ if ($metadataFileExists) {
+ self::move($rootView, $owner . '/files_versions/' . $ownerPath . '.json', $user . '/files_trashbin/versions/' . $filename . '.json' . '.d' . $timestamp);
+ }
}
} elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
foreach ($versions as $v) {
+ $metaVersionExists = $rootView->file_exists($owner . '/files_versions' . $v['path'] . '.v' . $v['version'] . '.json');
+
if ($owner !== $user || $forceCopy) {
self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
+ if ($metaVersionExists) {
+ self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'] . '.json', $owner . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.json' . '.d' . $timestamp);
+ }
}
if (!$forceCopy) {
self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
+ if ($metaVersionExists) {
+ self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'] . '.json', $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.json' . '.d' . $timestamp);
+ }
}
}
}
diff --git a/apps/files_versions/js/versioncollection.js b/apps/files_versions/js/versioncollection.js
index 7ff13a849dc4..906750afac21 100644
--- a/apps/files_versions/js/versioncollection.js
+++ b/apps/files_versions/js/versioncollection.js
@@ -11,12 +11,29 @@
/* global moment */
(function() {
+
+ _.extend(OC.Files.Client, {
+ PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
+ PROPERTY_VERSION_EDITED_BY: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by',
+ PROPERTY_VERSION_EDITED_BY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}meta-version-edited-by-name',
+ });
+
/**
* @memberof OCA.Versions
*/
var VersionCollection = OC.Backbone.Collection.extend({
sync: OC.Backbone.davSync,
+ davProperties: {
+ 'meta-version-edited-by': OC.Files.Client.PROPERTY_VERSION_EDITED_BY,
+ 'meta-version-edited-by-name': OC.Files.Client.PROPERTY_VERSION_EDITED_BY_NAME,
+ 'id': OC.Files.Client.PROPERTY_FILEID,
+ 'getlastmodified': OC.Files.Client.PROPERTY_GETLASTMODIFIED,
+ 'getcontentlength': OC.Files.Client.PROPERTY_GETCONTENTLENGTH,
+ 'resourcetype': OC.Files.Client.PROPERTY_RESOURCETYPE,
+ 'getcontenttype': OC.Files.Client.PROPERTY_GETCONTENTTYPE,
+ },
+
model: OCA.Versions.VersionModel,
/**
@@ -46,9 +63,12 @@
id: revision,
name: revision,
fullPath: fullPath,
- timestamp: moment(new Date(version['{DAV:}getlastmodified'])).format('X'),
- size: version['{DAV:}getcontentlength'],
- mimetype: version['{DAV:}getcontenttype'],
+ timestamp: moment(new Date(version.getlastmodified)).format('X'),
+ relativeTimestamp: moment(new Date(version.getlastmodified)).fromNow(),
+ size: version.getcontentlength,
+ mimetype: version.getcontenttype,
+ editedBy: version['meta-version-edited-by'],
+ editedByName: version['meta-version-edited-by-name'],
fileId: fileId
};
});
diff --git a/apps/files_versions/js/versionstabview.js b/apps/files_versions/js/versionstabview.js
index 6c721d6dd073..55788f88d81a 100644
--- a/apps/files_versions/js/versionstabview.js
+++ b/apps/files_versions/js/versionstabview.js
@@ -39,6 +39,7 @@
'{{#hasDetails}}' +
'
' +
'{{humanReadableSize}}' +
+ '{{editedByName}}' +
'
' +
'{{/hasDetails}}' +
'' +
@@ -213,7 +214,9 @@
revertIconUrl: OC.imagePath('core', 'actions/history'),
previewUrl: getPreviewUrl(version),
revertLabel: t('files_versions', 'Restore'),
- canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0
+ canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0,
+ editedBy: version.has('editedBy'),
+ editedByName: version.has('editedByName')
}, version.attributes);
},
diff --git a/apps/files_versions/lib/Hooks.php b/apps/files_versions/lib/Hooks.php
index 081a901ca0f4..6843e73e43fe 100644
--- a/apps/files_versions/lib/Hooks.php
+++ b/apps/files_versions/lib/Hooks.php
@@ -36,6 +36,7 @@ class Hooks {
public static function connectHooks() {
// Listen to write signals
\OCP\Util::connectHook('OC_Filesystem', 'write', 'OCA\Files_Versions\Hooks', 'write_hook');
+
// Listen to delete and rename signals
\OCP\Util::connectHook('OC_Filesystem', 'post_delete', 'OCA\Files_Versions\Hooks', 'remove_hook');
\OCP\Util::connectHook('OC_Filesystem', 'delete', 'OCA\Files_Versions\Hooks', 'pre_remove_hook');
diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php
index 06166b748c2d..7e4c73169a8d 100644
--- a/apps/files_versions/lib/Storage.php
+++ b/apps/files_versions/lib/Storage.php
@@ -43,6 +43,8 @@
use OC\Files\Filesystem;
use OC\Files\View;
+use OC\Share\Constants;
+use OCA\DAV\Meta\MetaPlugin;
use OCA\Files_Versions\AppInfo\Application;
use OCA\Files_Versions\Command\Expire;
use OCP\Files\NotFoundException;
@@ -192,13 +194,26 @@ public static function store($filename) {
// store a new version of a file
$mtime = $users_view->filemtime('files/' . $filename);
$sourceFileInfo = $users_view->getFileInfo("files/$filename");
- if ($users_view->copy("files/$filename", "files_versions/$filename.v$mtime")) {
+
+ $versionFileName = "files_versions/$filename.v$mtime";
+ if ($users_view->copy("files/$filename", $versionFileName)) {
// call getFileInfo to enforce a file cache entry for the new version
- $users_view->getFileInfo("files_versions/$filename.v$mtime");
+ $users_view->getFileInfo($versionFileName);
// update checksum of the version
- $users_view->putFileInfo("files_versions/$filename.v$mtime", [
+ $users_view->putFileInfo($versionFileName, [
'checksum' => $sourceFileInfo->getChecksum(),
]);
+
+ $config = \OC::$server->getConfig();
+ if ($config->getSystemValue('file_storage.save_version_author', false) === true) {
+ $user = \OC::$server->getUserSession()->getUser();
+ if ($user !== null && !$users_view->file_exists($versionFileName . '.json')) {
+ $metaDataKey = MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME;
+ $metadata = [$metaDataKey => $user->getUID()];
+ $metadataJsonObject = \json_encode($metadata);
+ $users_view->file_put_contents($versionFileName . '.json', $metadataJsonObject);
+ }
+ }
}
}
}
@@ -254,6 +269,9 @@ public static function delete($path) {
];
\OC_Hook::emit('\OCP\Versions', 'preDelete', $hookData);
self::deleteVersion($view, $filename . '.v' . $v['version']);
+ if ($view->file_exists($path . ".json")) {
+ $view->unlink($path . ".json");
+ }
\OC_Hook::emit('\OCP\Versions', 'delete', $hookData);
}
}
@@ -310,6 +328,14 @@ public static function renameOrCopy($sourcePath, $targetPath, $operation) {
'/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'],
'/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version']
);
+ // move each version json file that holds the name of the user that've made an edit
+ $sourceMetaDataFile = '/' . $sourceOwner . '/files_versions/' . $sourcePath . '.v' . $v['version'] . '.json';
+ if ($rootView->file_exists($sourceMetaDataFile)) {
+ $rootView->$operation(
+ $sourceMetaDataFile,
+ '/' . $targetOwner . '/files_versions/' . $targetPath . '.v' . $v['version'] . '.json'
+ );
+ }
}
}
@@ -328,13 +354,21 @@ public static function restoreVersion($uid, $filename, $fileToRestore, $revision
return false;
}
+ $metaDataEnabled = \OC::$server->getConfig()->getSystemValue('file_storage.save_version_author', false);
$versionCreated = false;
- //first create a new version
+ //first create a new version and metadata if enabled
$version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename);
if (!$users_view->file_exists($version)) {
- $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
+ $users_view->copy('files'.$filename, $version);
$versionCreated = true;
+
+ if ($metaDataEnabled === true) {
+ $metaTargetPath = $version . '.json';
+ $metaDataKey = MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME;
+ $metadataJsonObject = \json_encode([$metaDataKey => $uid]);
+ $users_view->file_put_contents($metaTargetPath, $metadataJsonObject);
+ }
}
// Restore encrypted version of the old file for the newly restored file
@@ -356,11 +390,20 @@ public static function restoreVersion($uid, $filename, $fileToRestore, $revision
if (self::copyFileContents($users_view, $fileToRestore, 'files' . $filename)) {
$users_view->touch("/files$filename", $revision);
Storage::scheduleExpire($uid, $filename);
+
+ if ($metaDataEnabled && $users_view->file_exists($fileToRestore . '.json')) {
+ $users_view->unlink($fileToRestore . '.json');
+ list($storage, $internalPath) = $users_view->resolvePath($fileToRestore . '.json');
+ $cache = $storage->getCache($internalPath);
+ $cache->remove($internalPath);
+ }
+
\OC_Hook::emit('\OCP\Versions', 'rollback', [
'path' => $filename,
'user' => $uid,
'revision' => $revision,
]);
+
return true;
} elseif ($versionCreated) {
self::deleteVersion($users_view, $version);
@@ -456,6 +499,16 @@ public static function getVersions($uid, $filename) {
$versions[$key]['etag'] = $view->getETag($dir . '/' . $entryName);
$versions[$key]['storage_location'] = "$dir/$entryName";
$versions[$key]['owner'] = $uid;
+
+ $jsonMetadataFile = $dir . '/' . $entryName . '.json';
+ if ($view->file_exists($jsonMetadataFile)) {
+ $metaDataFileContents = $view->file_get_contents($jsonMetadataFile);
+ if ($decoded = \json_decode($metaDataFileContents, true)) {
+ if (isset($decoded[MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME])) {
+ $versions[$key]['edited_by'] = $decoded[MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME];
+ }
+ }
+ }
}
}
}
@@ -561,6 +614,7 @@ protected static function getExpireList($time, $versions, $quotaExceeded = false
/**
* get list of files we want to expire
+ *
* @param array $versions list of versions
* @param integer $time
* @return array containing the list of to deleted versions and the size of them
diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php
index 80a298c45f5d..c59f97349e52 100644
--- a/apps/files_versions/tests/VersioningTest.php
+++ b/apps/files_versions/tests/VersioningTest.php
@@ -37,7 +37,11 @@
use OC\Files\ObjectStore\ObjectStoreStorage;
use OC\Files\Storage\Temporary;
+use OC\Share\Constants;
+use OCA\DAV\Meta\MetaPlugin;
use OCP\Files\Storage;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
/**
@@ -55,6 +59,8 @@ class VersioningTest extends TestCase {
private $user2;
/** @var string */
private $versionsRootOfUser1;
+ /** @var IConfig|MockObject */
+ private $mockConfig;
/**
* @var \OC\Files\View
@@ -71,23 +77,7 @@ public static function setUpBeforeClass(): void {
protected function setUp(): void {
parent::setUp();
- $config = \OC::$server->getConfig();
- $mockConfig = $this->createMock('\OCP\IConfig');
- $mockConfig->expects($this->any())
- ->method('getSystemValue')
- ->will($this->returnCallback(function ($key, $default) use ($config) {
- if ($key === 'filesystem_check_changes') {
- return \OC\Files\Cache\Watcher::CHECK_ONCE;
- } else {
- return $config->getSystemValue($key, $default);
- }
- }));
- $this->overwriteService('AllConfig', $mockConfig);
-
- // clear hooks
- \OC_Hook::clear();
- \OC::registerShareHooks();
- \OCA\Files_Versions\Hooks::connectHooks();
+ \OC::$server->getEncryptionManager()->setupStorage();
// Generate random usernames for better isolation
$testId = \uniqid();
@@ -120,10 +110,12 @@ protected function tearDown(): void {
$user = \OC::$server->getUserManager()->get($this->user1);
if ($user !== null) {
+ $this->logout();
$user->delete();
}
$user = \OC::$server->getUserManager()->get($this->user2);
if ($user !== null) {
+ $this->logout();
$user->delete();
}
@@ -132,7 +124,46 @@ protected function tearDown(): void {
parent::tearDown();
}
- public function testMoveFileIntoSharedFolderAsRecipient() {
+ /**
+ * Enables versioning metadata for unit-testing. Each test in this suite
+ * is executed once with and without versioning metadata enabled.
+ */
+ private function overwriteConfig($saveVersionAuthor) {
+ $config = \OC::$server->getConfig();
+ $this->mockConfig = $this->createMock('\OCP\IConfig');
+ $this->mockConfig->expects($this->any())
+ ->method('getSystemValue')
+ ->will($this->returnCallback(function ($key, $default) use ($config, $saveVersionAuthor) {
+ if ($key === 'filesystem_check_changes') {
+ return \OC\Files\Cache\Watcher::CHECK_ONCE;
+ } elseif ($key === 'file_storage.save_version_author') {
+ return $saveVersionAuthor;
+ } else {
+ return $config->getSystemValue($key, $default);
+ }
+ }));
+
+ $this->overwriteService('AllConfig', $this->mockConfig);
+
+ // clear hooks
+ \OC_Hook::clear();
+ \OC::registerShareHooks();
+ \OCA\Files_Versions\Hooks::connectHooks();
+ }
+
+ public function metaDataEnabledProvider(): array {
+ return [
+ 'metaDisabled' => [false],
+ 'metaEnabled' => [true],
+ ];
+ }
+
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testMoveFileIntoSharedFolderAsRecipient(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::mkdir('folder1');
$fileInfo = \OC\Files\Filesystem::getFileInfo('folder1');
@@ -158,16 +189,29 @@ public function testMoveFileIntoSharedFolderAsRecipient() {
// create some versions
$v1 = $versionsFolder2 . '/test.txt.v' . $t1;
$v2 = $versionsFolder2 . '/test.txt.v' . $t2;
-
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ // one metadata file for each version
+ $m1 = $versionsFolder2 . '/test.txt.v' . $t1 . '.json';
+ $m2 = $versionsFolder2 . '/test.txt.v' . $t2 . '.json';
+
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ }
+
// move file into the shared folder as recipient
\OC\Files\Filesystem::rename('/test.txt', '/folder1/test.txt');
$this->assertFalse($this->rootView->file_exists($v1));
$this->assertFalse($this->rootView->file_exists($v2));
+ if ($metaDataEnabled) {
+ $this->assertFalse($this->rootView->file_exists($m1));
+ $this->assertFalse($this->rootView->file_exists($m2));
+ }
+
self::loginHelper($this->user1);
$versionsFolder1 = '/' . $this->user1 . '/files_versions';
@@ -178,6 +222,14 @@ public function testMoveFileIntoSharedFolderAsRecipient() {
$this->assertTrue($this->rootView->file_exists($v1Renamed));
$this->assertTrue($this->rootView->file_exists($v2Renamed));
+ if ($metaDataEnabled) {
+ $m1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1 . '.json';
+ $m2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2 . '.json';
+
+ $this->assertTrue($this->rootView->file_exists($m1Renamed));
+ $this->assertTrue($this->rootView->file_exists($m2Renamed));
+ }
+
\OC::$server->getShareManager()->deleteShare($share);
}
@@ -187,7 +239,6 @@ public function testMoveFileIntoSharedFolderAsRecipient() {
* @dataProvider versionsProvider
*/
public function testGetExpireList($versions, $sizeOfAllDeletedFiles) {
-
// last interval end at 2592000
$startTime = 5000000;
@@ -327,8 +378,12 @@ public function versionsProvider() {
];
}
- public function testRename() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testRename(bool $metaDataEnabled) {
\OC\Files\Filesystem::file_put_contents("test.txt", "test file");
+ $this->overwriteConfig($metaDataEnabled);
$t1 = \time();
// second version is two weeks older, this way we make sure that no
@@ -338,12 +393,22 @@ public function testRename() {
// create some versions
$v1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1;
$v2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2;
+ $m1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . '.json';
+ $m2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1 . '.json';
+
$v1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1;
$v2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2;
+ $m1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . '.json';
+ $m2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1 . '.json';
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ }
+
// execute rename hook of versions app
\OC\Files\Filesystem::rename("test.txt", "test2.txt");
@@ -354,9 +419,22 @@ public function testRename() {
$this->assertTrue($this->rootView->file_exists($v1Renamed));
$this->assertTrue($this->rootView->file_exists($v2Renamed));
+
+ if ($metaDataEnabled) {
+ $this->assertFalse($this->rootView->file_exists($m1));
+ $this->assertFalse($this->rootView->file_exists($m2));
+
+ $this->assertTrue($this->rootView->file_exists($m1Renamed));
+ $this->assertTrue($this->rootView->file_exists($m2Renamed));
+ }
}
- public function testRenameInSharedFolder() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testRenameInSharedFolder(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::mkdir('folder1');
\OC\Files\Filesystem::mkdir('folder1/folder2');
\OC\Files\Filesystem::file_put_contents("folder1/test.txt", "test file");
@@ -370,12 +448,22 @@ public function testRenameInSharedFolder() {
// create some versions
$v1 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t1;
$v2 = $this->versionsRootOfUser1 . '/folder1/test.txt.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
+
$v1Renamed = $this->versionsRootOfUser1 . '/folder1/folder2/test.txt.v' . $t1;
$v2Renamed = $this->versionsRootOfUser1 . '/folder1/folder2/test.txt.v' . $t2;
+ $m1Renamed = $v1Renamed . '.json';
+ $m2Renamed = $v2Renamed . '.json';
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
$node = \OC::$server->getUserFolder($this->user1)->get('folder1');
$share = \OC::$server->getShareManager()->newShare();
$share->setNode($node)
@@ -402,10 +490,23 @@ public function testRenameInSharedFolder() {
$this->assertTrue($this->rootView->file_exists($v1Renamed));
$this->assertTrue($this->rootView->file_exists($v2Renamed));
+ if ($metaDataEnabled) {
+ $this->assertFalse($this->rootView->file_exists($m1));
+ $this->assertFalse($this->rootView->file_exists($m2));
+
+ $this->assertTrue($this->rootView->file_exists($m1Renamed));
+ $this->assertTrue($this->rootView->file_exists($m2Renamed));
+ }
+
\OC::$server->getShareManager()->deleteShare($share);
}
- public function testMoveFolder() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testMoveFolder(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::mkdir('folder1');
\OC\Files\Filesystem::mkdir('folder2');
\OC\Files\Filesystem::file_put_contents('folder1/test.txt', 'test file');
@@ -422,9 +523,19 @@ public function testMoveFolder() {
$v1Renamed = $this->versionsRootOfUser1 . '/folder2/folder1/test.txt.v' . $t1;
$v2Renamed = $this->versionsRootOfUser1 . '/folder2/folder1/test.txt.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
+ $m1Renamed = $v1Renamed . '.json';
+ $m2Renamed = $v2Renamed . '.json';
+
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
// execute rename hook of versions app
\OC\Files\Filesystem::rename('folder1', 'folder2/folder1');
@@ -435,9 +546,22 @@ public function testMoveFolder() {
$this->assertTrue($this->rootView->file_exists($v1Renamed));
$this->assertTrue($this->rootView->file_exists($v2Renamed));
+
+ if ($metaDataEnabled) {
+ $this->assertFalse($this->rootView->file_exists($m1));
+ $this->assertFalse($this->rootView->file_exists($m2));
+
+ $this->assertTrue($this->rootView->file_exists($m1Renamed));
+ $this->assertTrue($this->rootView->file_exists($m2Renamed));
+ }
}
- public function testMoveFolderIntoSharedFolderAsRecipient() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testMoveFolderIntoSharedFolderAsRecipient(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::mkdir('folder1');
$node = \OC::$server->getUserFolder($this->user1)->get('folder1');
@@ -462,9 +586,17 @@ public function testMoveFolderIntoSharedFolderAsRecipient() {
$this->rootView->mkdir($versionsFolder2);
$this->rootView->mkdir($versionsFolder2 . '/folder2');
// create some versions
+
$v1 = $versionsFolder2 . '/folder2/test.txt.v' . $t1;
$v2 = $versionsFolder2 . '/folder2/test.txt.v' . $t2;
+ if ($metaDataEnabled) {
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user2]));
+ }
+
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
@@ -480,14 +612,26 @@ public function testMoveFolderIntoSharedFolderAsRecipient() {
$v1Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t1;
$v2Renamed = $versionsFolder1 . '/folder1/folder2/test.txt.v' . $t2;
+ $m1Renamed = $v1Renamed . '.json';
+ $m2Renamed = $v2Renamed . '.json';
$this->assertTrue($this->rootView->file_exists($v1Renamed));
$this->assertTrue($this->rootView->file_exists($v2Renamed));
+ if ($metaDataEnabled) {
+ $this->assertTrue($this->rootView->file_exists($m1Renamed));
+ $this->assertTrue($this->rootView->file_exists($m2Renamed));
+ }
+
\OC::$server->getShareManager()->deleteShare($share);
}
- public function testRenameSharedFile() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testRenameSharedFile(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::file_put_contents("test.txt", "test file");
$t1 = \time();
@@ -495,17 +639,26 @@ public function testRenameSharedFile() {
// version will be expired
$t2 = $t1 - 60 * 60 * 24 * 14;
- $this->rootView->mkdir(self::USERS_VERSIONS_ROOT);
+ $this->rootView->mkdir($this->versionsRootOfUser1);
// create some versions
- $v1 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t1;
- $v2 = self::USERS_VERSIONS_ROOT . '/test.txt.v' . $t2;
+ $v1 = $this->versionsRootOfUser1 . '/test.txt.v' . $t1;
+ $v2 = $this->versionsRootOfUser1 . '/test.txt.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
// the renamed versions should not exist! Because we only moved the mount point!
- $v1Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t1;
- $v2Renamed = self::USERS_VERSIONS_ROOT . '/test2.txt.v' . $t2;
+ $v1Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1;
+ $v2Renamed = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2;
+ $m1Renamed = $v1Renamed . '.json';
+ $m2Renamed = $v2Renamed . '.json';
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
$node = \OC::$server->getUserFolder($this->user1)->get('test.txt');
$share = \OC::$server->getShareManager()->newShare();
$share->setNode($node)
@@ -532,10 +685,23 @@ public function testRenameSharedFile() {
$this->assertFalse($this->rootView->file_exists($v1Renamed));
$this->assertFalse($this->rootView->file_exists($v2Renamed));
+ if ($metaDataEnabled) {
+ $this->assertTrue($this->rootView->file_exists($m1));
+ $this->assertTrue($this->rootView->file_exists($m2));
+
+ $this->assertFalse($this->rootView->file_exists($m1Renamed));
+ $this->assertFalse($this->rootView->file_exists($m2Renamed));
+ }
+
\OC::$server->getShareManager()->deleteShare($share);
}
- public function testCopy() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testCopy(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::file_put_contents("test.txt", "test file");
$t1 = \time();
@@ -549,9 +715,19 @@ public function testCopy() {
$v1Copied = $this->versionsRootOfUser1 . '/test2.txt.v' . $t1;
$v2Copied = $this->versionsRootOfUser1 . '/test2.txt.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
+ $m1Copied = $v1Copied . '.json';
+ $m2Copied = $v2Copied . '.json';
+
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
// execute copy hook of versions app
\OC\Files\Filesystem::copy("test.txt", "test2.txt");
@@ -562,14 +738,27 @@ public function testCopy() {
$this->assertTrue($this->rootView->file_exists($v1Copied));
$this->assertTrue($this->rootView->file_exists($v2Copied));
+
+ if ($metaDataEnabled) {
+ $this->assertTrue($this->rootView->file_exists($m1));
+ $this->assertTrue($this->rootView->file_exists($m2));
+
+ $this->assertTrue($this->rootView->file_exists($m1Copied));
+ $this->assertTrue($this->rootView->file_exists($m2Copied));
+ }
}
public function getVersionsProvider() {
return [
- ['/test.txt'],
- ['/subfolder/test.txt'],
- ['/subfolder/0'],
- ['/0'],
+ ['/test.txt', false],
+ ['/subfolder/test.txt', false],
+ ['/subfolder/0', false],
+ ['/0', false],
+
+ ['/test.txt', true],
+ ['/subfolder/test.txt',true],
+ ['/subfolder/0', true],
+ ['/0', true],
];
}
@@ -580,7 +769,9 @@ public function getVersionsProvider() {
* @dataProvider getVersionsProvider
* @param string $filepath
*/
- public function testGetVersions($filepath) {
+ public function testGetVersions(string $filepath, bool $enableMetadata) {
+ $this->overwriteConfig($enableMetadata);
+
$t1 = \time();
// second version is two weeks older, this way we make sure that no
// version will be expired
@@ -592,12 +783,19 @@ public function testGetVersions($filepath) {
// create some versions
$v1 = $this->versionsRootOfUser1 . $filepath . '.v' . $t1;
$v2 = $this->versionsRootOfUser1 . $filepath . '.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
$this->rootView->mkdir($this->versionsRootOfUser1 . $parent);
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($enableMetadata) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
// execute copy hook of versions app
$versions = \OCA\Files_Versions\Storage::getVersions($this->user1, $filepath);
@@ -608,6 +806,11 @@ public function testGetVersions($filepath) {
$this->assertSame($fileName, $version['name']);
}
+ if ($enableMetadata) {
+ $this->assertArrayHasKey('edited_by', array_shift($versions));
+ $this->assertArrayHasKey('edited_by', array_shift($versions));
+ }
+
//cleanup
$this->rootView->deleteAll($this->versionsRootOfUser1 . $parent);
}
@@ -615,8 +818,12 @@ public function testGetVersions($filepath) {
/**
* test if we find all versions and if the versions array contain
* the correct 'path' and 'name'
+ *
+ * @dataProvider metaDataEnabledProvider
*/
- public function testGetVersionsEmptyFile() {
+ public function testGetVersionsEmptyFile(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
// execute copy hook of versions app
$versions = \OCA\Files_Versions\Storage::getVersions($this->user1, '');
$this->assertCount(0, $versions);
@@ -625,7 +832,12 @@ public function testGetVersionsEmptyFile() {
$this->assertCount(0, $versions);
}
- public function testExpireNonexistingFile() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testExpireNonexistingFile(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
$this->logout();
// needed to have a FS setup (the background job does this)
\OC_Util::setupFS($this->user1);
@@ -634,9 +846,11 @@ public function testExpireNonexistingFile() {
}
/**
+ * @dataProvider metaDataEnabledProvider
*/
- public function testExpireNonexistingUser() {
+ public function testExpireNonexistingUser(bool $metaDataEnabled) {
$this->expectException(\OC\User\NoUserException::class);
+ $this->overwriteConfig($metaDataEnabled);
$this->logout();
// needed to have a FS setup (the background job does this)
@@ -646,16 +860,26 @@ public function testExpireNonexistingUser() {
$this->assertFalse(\OCA\Files_Versions\Storage::expire('test.txt', 'unexist'));
}
- public function testRestoreSameStorage() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testRestoreSameStorage(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
\OC\Files\Filesystem::mkdir('sub');
- $this->doTestRestore();
+ $this->doTestRestore($metaDataEnabled);
}
- public function testRestoreCrossStorage() {
+ /**
+ * @dataProvider metaDataEnabledProvider
+ */
+ public function testRestoreCrossStorage(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
$storage2 = new Temporary([]);
\OC\Files\Filesystem::mount($storage2, [], $this->user1 . '/files/sub');
- $this->doTestRestore();
+ $this->doTestRestore($metaDataEnabled);
}
/**
@@ -687,7 +911,7 @@ function ($p) use (&$params) {
);
}
- private function doTestRestore() {
+ private function doTestRestore(bool $metaDataEnabled) {
$filePath = $this->user1 . '/files/sub/test.txt';
$this->rootView->file_put_contents($filePath, 'test file');
@@ -702,10 +926,18 @@ private function doTestRestore() {
$v1 = $this->versionsRootOfUser1 . '/sub/test.txt.v' . $t1;
$v2 = $this->versionsRootOfUser1 . '/sub/test.txt.v' . $t2;
+ $m1 = $v1 . '.json';
+ $m2 = $v2 . '.json';
+
$this->rootView->mkdir($this->versionsRootOfUser1 . '/sub');
$this->rootView->file_put_contents($v1, 'version1');
$this->rootView->file_put_contents($v2, 'version2');
+ if ($metaDataEnabled) {
+ $this->rootView->file_put_contents($m1, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ $this->rootView->file_put_contents($m2, \json_encode([MetaPlugin::VERSION_EDITED_BY_PROPERTYNAME => $this->user1]));
+ }
+
$oldVersions = \OCA\Files_Versions\Storage::getVersions(
$this->user1,
'/sub/test.txt'
@@ -745,7 +977,7 @@ private function doTestRestore() {
$this->assertEquals(
$info2['mtime'],
$t2,
- 'Restored file has mtime from version'
+ 'Restored file must have mtime from version'
);
$newVersions = \OCA\Files_Versions\Storage::getVersions(
@@ -755,8 +987,9 @@ private function doTestRestore() {
$this->assertTrue(
$this->rootView->file_exists($this->versionsRootOfUser1 . '/sub/test.txt.v' . $t0),
- 'A version file was created for the file before restoration'
+ 'A version file must be created for the file before restoration'
);
+
$this->assertTrue(
$this->rootView->file_exists($v1),
'Untouched version file is still there'
@@ -766,6 +999,23 @@ private function doTestRestore() {
'Restored version file gone from files_version folder'
);
+ if ($metaDataEnabled) {
+ $this->assertTrue(
+ $this->rootView->file_exists($this->versionsRootOfUser1 . '/sub/test.txt.v' . $t0 . '.json'),
+ 'A version metadata-file must be created for the file before restoration'
+ );
+
+ $this->assertTrue(
+ $this->rootView->file_exists($m1),
+ 'Untouched metadata-file is still there'
+ );
+
+ $this->assertFalse(
+ $this->rootView->file_exists($m2),
+ 'Restored metadata file must be gone from files_version folder'
+ );
+ }
+
$this->assertCount(2, $newVersions, 'Additional version created');
$this->assertArrayHasKey(
@@ -787,8 +1037,11 @@ private function doTestRestore() {
/**
* Test whether versions are created when overwriting as owner
+ *
+ * @dataProvider metaDataEnabledProvider
*/
- public function testStoreVersionAsOwner() {
+ public function testStoreVersionAsOwner(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
$this->loginAsUser($this->user1);
$this->createAndCheckVersions(
@@ -799,8 +1052,11 @@ public function testStoreVersionAsOwner() {
/**
* Test whether versions are created when overwriting as share recipient
+ * @dataProvider metaDataEnabledProvider
*/
- public function testStoreVersionAsRecipient() {
+ public function testStoreVersionAsRecipient(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
$this->loginAsUser($this->user1);
\OC\Files\Filesystem::mkdir('folder');
@@ -831,8 +1087,12 @@ public function testStoreVersionAsRecipient() {
* When uploading through a public link or publicwebdav, no user
* is logged in. File modification must still be able to find
* the owner and create versions.
+ *
+ * @dataProvider metaDataEnabledProvider
*/
- public function testStoreVersionAsAnonymous() {
+ public function testStoreVersionAsAnonymous(bool $metaDataEnabled) {
+ $this->overwriteConfig($metaDataEnabled);
+
$this->logout();
// note: public link upload does this,
diff --git a/changelog/unreleased/39126 b/changelog/unreleased/39126
new file mode 100644
index 000000000000..7d6ae872674f
--- /dev/null
+++ b/changelog/unreleased/39126
@@ -0,0 +1,8 @@
+Enhancement: Save and display the author of a file version
+
+The author attribute will be saved and shown in the version list grid for each new file version.
+This will allow the users to see who performed the changes on a specific file and when.
+Also, the author attribute will retain on renaming, copying, and deletion/restoration of the file.
+
+https://github.com/owncloud/enterprise/issues/4518
+https://github.com/owncloud/core/pull/39126
diff --git a/config/config.sample.php b/config/config.sample.php
index 347ca4e6c132..235c117f8665 100644
--- a/config/config.sample.php
+++ b/config/config.sample.php
@@ -1265,6 +1265,11 @@
*/
'sharing.showPublicLinkQuickAction' => false,
+/**
+ * Save and display version of uploaded and edited files.
+ */
+'file_storage.save_version_author' => false,
+
/**
* All other configuration options
*/
diff --git a/lib/private/Files/Meta/MetaFileVersionNode.php b/lib/private/Files/Meta/MetaFileVersionNode.php
index d26df26431de..00ee300adbcc 100644
--- a/lib/private/Files/Meta/MetaFileVersionNode.php
+++ b/lib/private/Files/Meta/MetaFileVersionNode.php
@@ -28,6 +28,7 @@
use OCP\Files\IProvidesAdditionalHeaders;
use OC\Preview;
use OCA\Files_Sharing\SharedStorage;
+use OCP\Files\IProvidesVersionAuthor;
use OCP\Files\IRootFolder;
use OCP\Files\IPreviewNode;
use OCP\Files\Storage\IVersionedStorage;
@@ -40,7 +41,7 @@
*
* @package OC\Files\Meta
*/
-class MetaFileVersionNode extends AbstractFile implements IPreviewNode, IProvidesAdditionalHeaders {
+class MetaFileVersionNode extends AbstractFile implements IPreviewNode, IProvidesAdditionalHeaders, IProvidesVersionAuthor {
/** @var string */
private $versionId;
@@ -79,6 +80,13 @@ public function __construct(
$this->root = $root;
}
+ /**
+ * @return string
+ */
+ public function getEditedBy() : string {
+ return $this->versionInfo['edited_by'] ?? '';
+ }
+
/**
* @inheritdoc
*/
diff --git a/lib/public/Files/IProvidesVersionAuthor.php b/lib/public/Files/IProvidesVersionAuthor.php
new file mode 100644
index 000000000000..d1b4706e628a
--- /dev/null
+++ b/lib/public/Files/IProvidesVersionAuthor.php
@@ -0,0 +1,40 @@
+
+ *
+ * @copyright Copyright (c) 2021, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OCP\Files;
+
+/**
+ * Interface IProvidesVersionAuthor
+ * This interface provides version author retrieval for file version
+ *
+ * @package OCP\Files
+ * @since 10.9.0
+ */
+interface IProvidesVersionAuthor {
+ /**
+ * Returns the username of author which made this edit. Returns
+ * empty string if this is the initial version @see IProvidesVersionAuthor::getCreatedBy()
+ *
+ * @return string
+ * @since 10.9.0
+ */
+ public function getEditedBy() : string;
+}