summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/encryption/appinfo/application.php15
-rw-r--r--apps/encryption/lib/crypto/decryptall.php143
-rw-r--r--apps/encryption/lib/crypto/encryption.php39
-rw-r--r--apps/encryption/lib/session.php60
-rw-r--r--apps/encryption/tests/lib/SessionTest.php55
-rw-r--r--apps/encryption/tests/lib/crypto/decryptalltest.php125
-rw-r--r--apps/encryption/tests/lib/crypto/encryptionTest.php78
-rw-r--r--apps/files/css/detailsView.css13
-rw-r--r--apps/files/css/files.css33
-rw-r--r--apps/files/js/fileinfomodel.js8
-rw-r--r--apps/files/js/filelist.js19
-rw-r--r--apps/files/js/mainfileinfodetailview.js108
-rw-r--r--apps/files_sharing/appinfo/app.php15
-rw-r--r--apps/files_sharing/css/sharetabview.css72
-rw-r--r--apps/files_sharing/js/share.js96
-rw-r--r--apps/files_sharing/js/sharetabview.js52
-rw-r--r--apps/files_sharing/lib/notifier.php2
-rw-r--r--apps/files_sharing/tests/js/shareSpec.js125
-rw-r--r--apps/files_versions/appinfo/application.php11
-rw-r--r--apps/files_versions/lib/expiration.php185
-rw-r--r--apps/files_versions/lib/storage.php60
-rw-r--r--apps/files_versions/tests/expirationtest.php204
22 files changed, 1300 insertions, 218 deletions
diff --git a/apps/encryption/appinfo/application.php b/apps/encryption/appinfo/application.php
index 75107b2723c..515a408fa2c 100644
--- a/apps/encryption/appinfo/application.php
+++ b/apps/encryption/appinfo/application.php
@@ -30,6 +30,7 @@ use OCA\Encryption\Controller\RecoveryController;
use OCA\Encryption\Controller\SettingsController;
use OCA\Encryption\Controller\StatusController;
use OCA\Encryption\Crypto\Crypt;
+use OCA\Encryption\Crypto\DecryptAll;
use OCA\Encryption\Crypto\EncryptAll;
use OCA\Encryption\Crypto\Encryption;
use OCA\Encryption\HookManager;
@@ -113,7 +114,9 @@ class Application extends \OCP\AppFramework\App {
$container->query('Crypt'),
$container->query('KeyManager'),
$container->query('Util'),
+ $container->query('Session'),
$container->query('EncryptAll'),
+ $container->query('DecryptAll'),
$container->getServer()->getLogger(),
$container->getServer()->getL10N($container->getAppName())
);
@@ -242,6 +245,18 @@ class Application extends \OCP\AppFramework\App {
}
);
+ $container->registerService('DecryptAll',
+ function (IAppContainer $c) {
+ return new DecryptAll(
+ $c->query('Util'),
+ $c->query('KeyManager'),
+ $c->query('Crypt'),
+ $c->query('Session'),
+ new QuestionHelper()
+ );
+ }
+ );
+
}
public function registerSettings() {
diff --git a/apps/encryption/lib/crypto/decryptall.php b/apps/encryption/lib/crypto/decryptall.php
new file mode 100644
index 00000000000..599cd82aa4d
--- /dev/null
+++ b/apps/encryption/lib/crypto/decryptall.php
@@ -0,0 +1,143 @@
+<?php
+/**
+ * @author Björn Schießle <schiessle@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+
+namespace OCA\Encryption\Crypto;
+
+
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Session;
+use OCA\Encryption\Util;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use Symfony\Component\Console\Question\Question;
+
+class DecryptAll {
+
+ /** @var Util */
+ protected $util;
+
+ /** @var QuestionHelper */
+ protected $questionHelper;
+
+ /** @var Crypt */
+ protected $crypt;
+
+ /** @var KeyManager */
+ protected $keyManager;
+
+ /** @var Session */
+ protected $session;
+
+ /**
+ * @param Util $util
+ * @param KeyManager $keyManager
+ * @param Crypt $crypt
+ * @param Session $session
+ * @param QuestionHelper $questionHelper
+ */
+ public function __construct(
+ Util $util,
+ KeyManager $keyManager,
+ Crypt $crypt,
+ Session $session,
+ QuestionHelper $questionHelper
+ ) {
+ $this->util = $util;
+ $this->keyManager = $keyManager;
+ $this->crypt = $crypt;
+ $this->session = $session;
+ $this->questionHelper = $questionHelper;
+ }
+
+ /**
+ * prepare encryption module to decrypt all files
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @param $user
+ * @return bool
+ */
+ public function prepare(InputInterface $input, OutputInterface $output, $user) {
+
+ $question = new Question('Please enter the recovery key password: ');
+ $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
+
+ if (!empty($user)) {
+ $questionUseLoginPassword = new ConfirmationQuestion(
+ 'Do you want to use the users login password to decrypt all files? (y/n) ',
+ false
+ );
+ $useLoginPassword = $this->questionHelper->ask($input, $output, $questionUseLoginPassword);
+ if ($useLoginPassword) {
+ $question = new Question('Please enter the users login password: ');
+ } else if ($this->util->isRecoveryEnabledForUser($user) === false) {
+ $output->writeln('No recovery key available for user ' . $user);
+ return false;
+ } else {
+ $user = $recoveryKeyId;
+ }
+ } else {
+ $user = $recoveryKeyId;
+ }
+
+ $question->setHidden(true);
+ $question->setHiddenFallback(false);
+ $password = $this->questionHelper->ask($input, $output, $question);
+ $privateKey = $this->getPrivateKey($user, $password);
+ if ($privateKey !== false) {
+ $this->updateSession($user, $privateKey);
+ return true;
+ } else {
+ $output->writeln('Could not decrypt private key, maybe you entered the wrong password?');
+ }
+
+
+ return false;
+ }
+
+ /**
+ * get the private key which will be used to decrypt all files
+ *
+ * @param string $user
+ * @param string $password
+ * @return bool|string
+ * @throws \OCA\Encryption\Exceptions\PrivateKeyMissingException
+ */
+ protected function getPrivateKey($user, $password) {
+ $recoveryKeyId = $this->keyManager->getRecoveryKeyId();
+ if ($user === $recoveryKeyId) {
+ $recoveryKey = $this->keyManager->getSystemPrivateKey($recoveryKeyId);
+ $privateKey = $this->crypt->decryptPrivateKey($recoveryKey, $password);
+ } else {
+ $userKey = $this->keyManager->getPrivateKey($user);
+ $privateKey = $this->crypt->decryptPrivateKey($userKey, $password, $user);
+ }
+
+ return $privateKey;
+ }
+
+ protected function updateSession($user, $privateKey) {
+ $this->session->prepareDecryptAll($user, $privateKey);
+ }
+}
diff --git a/apps/encryption/lib/crypto/encryption.php b/apps/encryption/lib/crypto/encryption.php
index d2925e1b6be..fde4a2c4a9c 100644
--- a/apps/encryption/lib/crypto/encryption.php
+++ b/apps/encryption/lib/crypto/encryption.php
@@ -30,6 +30,7 @@ namespace OCA\Encryption\Crypto;
use OC\Encryption\Exceptions\DecryptionFailedException;
use OCA\Encryption\Exceptions\PublicKeyMissingException;
+use OCA\Encryption\Session;
use OCA\Encryption\Util;
use OCP\Encryption\IEncryptionModule;
use OCA\Encryption\KeyManager;
@@ -75,6 +76,9 @@ class Encryption implements IEncryptionModule {
/** @var Util */
private $util;
+ /** @var Session */
+ private $session;
+
/** @var ILogger */
private $logger;
@@ -87,25 +91,34 @@ class Encryption implements IEncryptionModule {
/** @var bool */
private $useMasterPassword;
+ /** @var DecryptAll */
+ private $decryptAll;
+
/**
*
* @param Crypt $crypt
* @param KeyManager $keyManager
* @param Util $util
+ * @param Session $session
* @param EncryptAll $encryptAll
+ * @param DecryptAll $decryptAll
* @param ILogger $logger
* @param IL10N $il10n
*/
public function __construct(Crypt $crypt,
KeyManager $keyManager,
Util $util,
+ Session $session,
EncryptAll $encryptAll,
+ DecryptAll $decryptAll,
ILogger $logger,
IL10N $il10n) {
$this->crypt = $crypt;
$this->keyManager = $keyManager;
$this->util = $util;
+ $this->session = $session;
$this->encryptAll = $encryptAll;
+ $this->decryptAll = $decryptAll;
$this->logger = $logger;
$this->l = $il10n;
$this->useMasterPassword = $util->isMasterKeyEnabled();
@@ -150,7 +163,15 @@ class Encryption implements IEncryptionModule {
$this->isWriteOperation = false;
$this->writeCache = '';
- $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
+ if ($this->session->decryptAllModeActivated()) {
+ $encryptedFileKey = $this->keyManager->getEncryptedFileKey($this->path);
+ $shareKey = $this->keyManager->getShareKey($this->path, $this->session->getDecryptAllUid());
+ $this->fileKey = $this->crypt->multiKeyDecrypt($encryptedFileKey,
+ $shareKey,
+ $this->session->getDecryptAllKey());
+ } else {
+ $this->fileKey = $this->keyManager->getFileKey($this->path, $this->user);
+ }
if (
$mode === 'w'
@@ -421,13 +442,25 @@ class Encryption implements IEncryptionModule {
*
* @param InputInterface $input
* @param OutputInterface $output write some status information to the terminal during encryption
- * @return bool
*/
public function encryptAll(InputInterface $input, OutputInterface $output) {
- return $this->encryptAll->encryptAll($input, $output);
+ $this->encryptAll->encryptAll($input, $output);
}
/**
+ * prepare module to perform decrypt all operation
+ *
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @param string $user
+ * @return bool
+ */
+ public function prepareDecryptAll(InputInterface $input, OutputInterface $output, $user = '') {
+ return $this->decryptAll->prepare($input, $output, $user);
+ }
+
+
+ /**
* @param string $path
* @return string
*/
diff --git a/apps/encryption/lib/session.php b/apps/encryption/lib/session.php
index c3759c3fc56..1d0c3711487 100644
--- a/apps/encryption/lib/session.php
+++ b/apps/encryption/lib/session.php
@@ -25,6 +25,7 @@
namespace OCA\Encryption;
+use OCA\Encryption\Exceptions\PrivateKeyMissingException;
use \OCP\ISession;
class Session {
@@ -106,6 +107,61 @@ class Session {
$this->session->set('privateKey', $key);
}
+ /**
+ * store data needed for the decrypt all operation in the session
+ *
+ * @param string $user
+ * @param string $key
+ */
+ public function prepareDecryptAll($user, $key) {
+ $this->session->set('decryptAll', true);
+ $this->session->set('decryptAllKey', $key);
+ $this->session->set('decryptAllUid', $user);
+ }
+
+ /**
+ * check if we are in decrypt all mode
+ *
+ * @return bool
+ */
+ public function decryptAllModeActivated() {
+ $decryptAll = $this->session->get('decryptAll');
+ return ($decryptAll === true);
+ }
+
+ /**
+ * get uid used for decrypt all operation
+ *
+ * @return string
+ * @throws \Exception
+ */
+ public function getDecryptAllUid() {
+ $uid = $this->session->get('decryptAllUid');
+ if (is_null($uid) && $this->decryptAllModeActivated()) {
+ throw new \Exception('No uid found while in decrypt all mode');
+ } elseif (is_null($uid)) {
+ throw new \Exception('Please activate decrypt all mode first');
+ }
+
+ return $uid;
+ }
+
+ /**
+ * get private key for decrypt all operation
+ *
+ * @return string
+ * @throws PrivateKeyMissingException
+ */
+ public function getDecryptAllKey() {
+ $privateKey = $this->session->get('decryptAllKey');
+ if (is_null($privateKey) && $this->decryptAllModeActivated()) {
+ throw new PrivateKeyMissingException('No private key found while in decrypt all mode');
+ } elseif (is_null($privateKey)) {
+ throw new PrivateKeyMissingException('Please activate decrypt all mode first');
+ }
+
+ return $privateKey;
+ }
/**
* remove keys from session
@@ -114,7 +170,9 @@ class Session {
$this->session->remove('publicSharePrivateKey');
$this->session->remove('privateKey');
$this->session->remove('encryptionInitialized');
-
+ $this->session->remove('decryptAll');
+ $this->session->remove('decryptAllKey');
+ $this->session->remove('decryptAllUid');
}
}
diff --git a/apps/encryption/tests/lib/SessionTest.php b/apps/encryption/tests/lib/SessionTest.php
index e036c439939..0fa48666d70 100644
--- a/apps/encryption/tests/lib/SessionTest.php
+++ b/apps/encryption/tests/lib/SessionTest.php
@@ -56,6 +56,7 @@ class SessionTest extends TestCase {
* @depends testSetAndGetPrivateKey
*/
public function testIsPrivateKeySet() {
+ $this->instance->setPrivateKey('dummyPrivateKey');
$this->assertTrue($this->instance->isPrivateKeySet());
unset(self::$tempStorage['privateKey']);
@@ -65,6 +66,51 @@ class SessionTest extends TestCase {
self::$tempStorage['privateKey'] = 'dummyPrivateKey';
}
+ public function testDecryptAllModeActivated() {
+ $this->instance->prepareDecryptAll('user1', 'usersKey');
+ $this->assertTrue($this->instance->decryptAllModeActivated());
+ $this->assertSame('user1', $this->instance->getDecryptAllUid());
+ $this->assertSame('usersKey', $this->instance->getDecryptAllKey());
+ }
+
+ public function testDecryptAllModeDeactivated() {
+ $this->assertFalse($this->instance->decryptAllModeActivated());
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectExceptionMessage 'Please activate decrypt all mode first'
+ */
+ public function testGetDecryptAllUidException() {
+ $this->instance->getDecryptAllUid();
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectExceptionMessage 'No uid found while in decrypt all mode'
+ */
+ public function testGetDecryptAllUidException2() {
+ $this->instance->prepareDecryptAll(null, 'key');
+ $this->instance->getDecryptAllUid();
+ }
+
+ /**
+ * @expectedException \OCA\Encryption\Exceptions\PrivateKeyMissingException
+ * @expectExceptionMessage 'Please activate decrypt all mode first'
+ */
+ public function testGetDecryptAllKeyException() {
+ $this->instance->getDecryptAllKey();
+ }
+
+ /**
+ * @expectedException \OCA\Encryption\Exceptions\PrivateKeyMissingException
+ * @expectExceptionMessage 'No key found while in decrypt all mode'
+ */
+ public function testGetDecryptAllKeyException2() {
+ $this->instance->prepareDecryptAll('user', null);
+ $this->instance->getDecryptAllKey();
+ }
+
/**
*
*/
@@ -112,6 +158,10 @@ class SessionTest extends TestCase {
*
*/
public function testClearWillRemoveValues() {
+ $this->instance->setPrivateKey('privateKey');
+ $this->instance->setStatus('initStatus');
+ $this->instance->prepareDecryptAll('user', 'key');
+ $this->assertNotEmpty(self::$tempStorage);
$this->instance->clear();
$this->assertEmpty(self::$tempStorage);
}
@@ -138,4 +188,9 @@ class SessionTest extends TestCase {
$this->instance = new Session($this->sessionMock);
}
+
+ protected function tearDown() {
+ self::$tempStorage = [];
+ parent::tearDown();
+ }
}
diff --git a/apps/encryption/tests/lib/crypto/decryptalltest.php b/apps/encryption/tests/lib/crypto/decryptalltest.php
new file mode 100644
index 00000000000..d6a52fe97c0
--- /dev/null
+++ b/apps/encryption/tests/lib/crypto/decryptalltest.php
@@ -0,0 +1,125 @@
+<?php
+/**
+ * @author Björn Schießle <schiessle@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+
+namespace OCA\Encryption\Tests\lib\Crypto;
+
+
+use OCA\Encryption\Crypto\Crypt;
+use OCA\Encryption\Crypto\DecryptAll;
+use OCA\Encryption\KeyManager;
+use OCA\Encryption\Session;
+use OCA\Encryption\Util;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Test\TestCase;
+
+class DecryptAllTest extends TestCase {
+
+ /** @var DecryptAll */
+ protected $instance;
+
+ /** @var Util | \PHPUnit_Framework_MockObject_MockObject */
+ protected $util;
+
+ /** @var KeyManager | \PHPUnit_Framework_MockObject_MockObject */
+ protected $keyManager;
+
+ /** @var Crypt | \PHPUnit_Framework_MockObject_MockObject */
+ protected $crypt;
+
+ /** @var Session | \PHPUnit_Framework_MockObject_MockObject */
+ protected $session;
+
+ /** @var QuestionHelper | \PHPUnit_Framework_MockObject_MockObject */
+ protected $questionHelper;
+
+ public function setUp() {
+ parent::setUp();
+
+ $this->util = $this->getMockBuilder('OCA\Encryption\Util')
+ ->disableOriginalConstructor()->getMock();
+ $this->keyManager = $this->getMockBuilder('OCA\Encryption\KeyManager')
+ ->disableOriginalConstructor()->getMock();
+ $this->crypt = $this->getMockBuilder('OCA\Encryption\Crypto\Crypt')
+ ->disableOriginalConstructor()->getMock();
+ $this->session = $this->getMockBuilder('OCA\Encryption\Session')
+ ->disableOriginalConstructor()->getMock();
+ $this->questionHelper = $this->getMockBuilder('Symfony\Component\Console\Helper\QuestionHelper')
+ ->disableOriginalConstructor()->getMock();
+
+ $this->instance = new DecryptAll(
+ $this->util,
+ $this->keyManager,
+ $this->crypt,
+ $this->session,
+ $this->questionHelper
+ );
+ }
+
+ public function testUpdateSession() {
+ $this->session->expects($this->once())->method('prepareDecryptAll')
+ ->with('user1', 'key1');
+
+ $this->invokePrivate($this->instance, 'updateSession', ['user1', 'key1']);
+ }
+
+ /**
+ * @dataProvider dataTestGetPrivateKey
+ *
+ * @param string $user
+ * @param string $recoveryKeyId
+ */
+ public function testGetPrivateKey($user, $recoveryKeyId) {
+ $password = 'passwd';
+ $recoveryKey = 'recoveryKey';
+ $userKey = 'userKey';
+ $unencryptedKey = 'unencryptedKey';
+
+ $this->keyManager->expects($this->any())->method('getRecoveryKeyId')
+ ->willReturn($recoveryKeyId);
+
+ if ($user === $recoveryKeyId) {
+ $this->keyManager->expects($this->once())->method('getSystemPrivateKey')
+ ->with($recoveryKeyId)->willReturn($recoveryKey);
+ $this->keyManager->expects($this->never())->method('getPrivateKey');
+ $this->crypt->expects($this->once())->method('decryptPrivateKey')
+ ->with($recoveryKey, $password)->willReturn($unencryptedKey);
+ } else {
+ $this->keyManager->expects($this->never())->method('getSystemPrivateKey');
+ $this->keyManager->expects($this->once())->method('getPrivateKey')
+ ->with($user)->willReturn($userKey);
+ $this->crypt->expects($this->once())->method('decryptPrivateKey')
+ ->with($userKey, $password, $user)->willReturn($unencryptedKey);
+ }
+
+ $this->assertSame($unencryptedKey,
+ $this->invokePrivate($this->instance, 'getPrivateKey', [$user, $password])
+ );
+ }
+
+ public function dataTestGetPrivateKey() {
+ return [
+ ['user1', 'recoveryKey'],
+ ['recoveryKeyId', 'recoveryKeyId']
+ ];
+ }
+
+}
diff --git a/apps/encryption/tests/lib/crypto/encryptionTest.php b/apps/encryption/tests/lib/crypto/encryptionTest.php
index f58aa5d3ccb..9e0cb2f09d1 100644
--- a/apps/encryption/tests/lib/crypto/encryptionTest.php
+++ b/apps/encryption/tests/lib/crypto/encryptionTest.php
@@ -40,6 +40,12 @@ class EncryptionTest extends TestCase {
private $encryptAllMock;
/** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $decryptAllMock;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $sessionMock;
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
private $cryptMock;
/** @var \PHPUnit_Framework_MockObject_MockObject */
@@ -63,9 +69,15 @@ class EncryptionTest extends TestCase {
$this->keyManagerMock = $this->getMockBuilder('OCA\Encryption\KeyManager')
->disableOriginalConstructor()
->getMock();
+ $this->sessionMock = $this->getMockBuilder('OCA\Encryption\Session')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->encryptAllMock = $this->getMockBuilder('OCA\Encryption\Crypto\EncryptAll')
->disableOriginalConstructor()
->getMock();
+ $this->decryptAllMock = $this->getMockBuilder('OCA\Encryption\Crypto\DecryptAll')
+ ->disableOriginalConstructor()
+ ->getMock();
$this->loggerMock = $this->getMockBuilder('OCP\ILogger')
->disableOriginalConstructor()
->getMock();
@@ -81,7 +93,9 @@ class EncryptionTest extends TestCase {
$this->cryptMock,
$this->keyManagerMock,
$this->utilMock,
+ $this->sessionMock,
$this->encryptAllMock,
+ $this->decryptAllMock,
$this->loggerMock,
$this->l10nMock
);
@@ -170,6 +184,16 @@ class EncryptionTest extends TestCase {
*/
public function testBegin($mode, $header, $legacyCipher, $defaultCipher, $fileKey, $expected) {
+ $this->sessionMock->expects($this->once())
+ ->method('decryptAllModeActivated')
+ ->willReturn(false);
+
+ $this->sessionMock->expects($this->never())->method('getDecryptAllUid');
+ $this->sessionMock->expects($this->never())->method('getDecryptAllKey');
+ $this->keyManagerMock->expects($this->never())->method('getEncryptedFileKey');
+ $this->keyManagerMock->expects($this->never())->method('getShareKey');
+ $this->cryptMock->expects($this->never())->method('multiKeyDecrypt');
+
$this->cryptMock->expects($this->any())
->method('getCipher')
->willReturn($defaultCipher);
@@ -209,6 +233,49 @@ class EncryptionTest extends TestCase {
);
}
+
+ /**
+ * test begin() if decryptAll mode was activated
+ */
+ public function testBeginDecryptAll() {
+
+ $path = '/user/files/foo.txt';
+ $recoveryKeyId = 'recoveryKeyId';
+ $recoveryShareKey = 'recoveryShareKey';
+ $decryptAllKey = 'decryptAllKey';
+ $fileKey = 'fileKey';
+
+ $this->sessionMock->expects($this->once())
+ ->method('decryptAllModeActivated')
+ ->willReturn(true);
+ $this->sessionMock->expects($this->once())
+ ->method('getDecryptAllUid')
+ ->willReturn($recoveryKeyId);
+ $this->sessionMock->expects($this->once())
+ ->method('getDecryptAllKey')
+ ->willReturn($decryptAllKey);
+
+ $this->keyManagerMock->expects($this->once())
+ ->method('getEncryptedFileKey')
+ ->willReturn('encryptedFileKey');
+ $this->keyManagerMock->expects($this->once())
+ ->method('getShareKey')
+ ->with($path, $recoveryKeyId)
+ ->willReturn($recoveryShareKey);
+ $this->cryptMock->expects($this->once())
+ ->method('multiKeyDecrypt')
+ ->with('encryptedFileKey', $recoveryShareKey, $decryptAllKey)
+ ->willReturn($fileKey);
+
+ $this->keyManagerMock->expects($this->never())->method('getFileKey');
+
+ $this->instance->begin($path, 'user', 'r', [], []);
+
+ $this->assertSame($fileKey,
+ $this->invokePrivate($this->instance, 'fileKey')
+ );
+ }
+
/**
* @dataProvider dataTestUpdate
*
@@ -273,4 +340,15 @@ class EncryptionTest extends TestCase {
public function testDecrypt() {
$this->instance->decrypt('abc');
}
+
+ public function testPrepareDecryptAll() {
+ $input = $this->getMock('Symfony\Component\Console\Input\InputInterface');
+ $output = $this->getMock('Symfony\Component\Console\Output\OutputInterface');
+
+ $this->decryptAllMock->expects($this->once())->method('prepare')
+ ->with($input, $output, 'user');
+
+ $this->instance->prepareDecryptAll($input, $output, 'user');
+ }
+
}
diff --git a/apps/files/css/detailsView.css b/apps/files/css/detailsView.css
index 8eded7acda1..ea9d48b470c 100644
--- a/apps/files/css/detailsView.css
+++ b/apps/files/css/detailsView.css
@@ -25,18 +25,26 @@
margin-top: -15px;
}
+#app-sidebar .thumbnailContainer.image.portrait {
+ margin: 0; /* if we dont fit the image anyway we give it back the margin */
+}
+
#app-sidebar .image .thumbnail {
width:100%;
display:block;
height: 250px;
background-repeat: no-repeat;
- background-position: 50% top;
+ background-position: center;
background-size: 100%;
float: none;
margin: 0;
}
#app-sidebar .image.portrait .thumbnail {
+ background-position: 50% top;
+}
+
+#app-sidebar .image.portrait .thumbnail {
background-size: contain;
}
@@ -64,10 +72,13 @@
#app-sidebar .fileName h3 {
max-width: 300px;
float:left;
+ padding: 5px 0;
+ margin: -5px 0;
}
#app-sidebar .file-details {
margin-top: 3px;
+ margin-bottom: 15px;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
opacity: .5;
float:left;
diff --git a/apps/files/css/files.css b/apps/files/css/files.css
index 05033dc2fed..df23f415129 100644
--- a/apps/files/css/files.css
+++ b/apps/files/css/files.css
@@ -110,10 +110,6 @@
cursor: pointer;
}
-#app-navigation .nav-files a {
- display: inline-block;
-}
-
#app-navigation .nav-files a.new.hidden {
display: none;
}
@@ -420,25 +416,20 @@ table td.filename .uploadtext {
}
/* File checkboxes */
-#fileList tr td.filename>.selectCheckBox {
- -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
- filter: alpha(opacity=0);
+#fileList tr td.filename>.selectCheckBox + label:before {
opacity: 0;
- float: left;
- top: 0;
- margin: 32px 0 4px 32px; /* bigger clickable area doesn’t work in FF width:2.8em; height:2.4em;*/
+ position: absolute;
+ bottom: 4px;
+ right: 0;
+ z-index: 10;
}
+
/* Show checkbox when hovering, checked, or selected */
-#fileList tr:hover td.filename>.selectCheckBox,
-#fileList tr:focus td.filename>.selectCheckBox,
-#fileList tr td.filename>.selectCheckBox:checked,
-#fileList tr.selected td.filename>.selectCheckBox {
+#fileList tr:hover td.filename>.selectCheckBox + label:before,
+#fileList tr:focus td.filename>.selectCheckBox + label:before,
+#fileList tr td.filename>.selectCheckBox:checked + label:before,
+#fileList tr.selected td.filename>.selectCheckBox + label:before {
opacity: 1;
-}
-.lte9 #fileList tr:hover td.filename>.selectCheckBox,
-.lte9 #fileList tr:focus td.filename>.selectCheckBox,
-.lte9 #fileList tr td.filename>.selectCheckBox[checked=checked],
-.lte9 #fileList tr.selected td.filename>.selectCheckBox {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
filter: alpha(opacity=100);
}
@@ -446,6 +437,7 @@ table td.filename .uploadtext {
/* Use label to have bigger clickable size for checkbox */
#fileList tr td.filename>.selectCheckBox + label,
.select-all + label {
+ background-position: 30px 30px;
height: 50px;
position: absolute;
width: 50px;
@@ -460,7 +452,7 @@ table td.filename .uploadtext {
.select-all + label {
top: 0;
}
-.select-all {
+.select-all + label:before {
position: absolute;
top: 18px;
left: 18px;
@@ -737,6 +729,7 @@ table.dragshadow td.size {
width: 140px;
margin-left: -56px;
margin-top: 25px;
+ z-index: 1001;
}
.newFileMenu .menuitem {
diff --git a/apps/files/js/fileinfomodel.js b/apps/files/js/fileinfomodel.js
index 22b1ca9ff0c..de1b143a160 100644
--- a/apps/files/js/fileinfomodel.js
+++ b/apps/files/js/fileinfomodel.js
@@ -57,7 +57,13 @@
* @return {boolean} true if this is an image, false otherwise
*/
isImage: function() {
- return this.has('mimetype') ? this.get('mimetype').substr(0, 6) === 'image/' : false;
+ if (!this.has('mimetype')) {
+ return false;
+ }
+ return this.get('mimetype').substr(0, 6) === 'image/'
+ || this.get('mimetype') === 'application/postscript'
+ || this.get('mimetype') === 'application/illustrator'
+ || this.get('mimetype') === 'application/x-photoshop';
},
/**
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 3e4cdaf8be3..2af5be73d96 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -10,7 +10,7 @@
(function() {
- var TEMPLATE_ADDBUTTON = '<a href="#" class="button new" title="{{addText}}"><img src="{{iconUrl}}"></img></a>';
+ var TEMPLATE_ADDBUTTON = '<a href="#" class="button new"><img src="{{iconUrl}}" alt="{{addText}}"></img></a>';
/**
* @class OCA.Files.FileList
@@ -347,7 +347,7 @@
// and contain existing models that can be used.
// This method would in the future simply retrieve the matching model from the collection.
var model = new OCA.Files.FileInfoModel(this.elementToFile($tr));
- if (!model.has('path')) {
+ if (!model.get('path')) {
model.set('path', this.getCurrentDirectory(), {silent: true});
}
@@ -369,6 +369,21 @@
},
/**
+ * Displays the details view for the given file and
+ * selects the given tab
+ *
+ * @param {string} fileName file name for which to show details
+ * @param {string} [tabId] optional tab id to select
+ */
+ showDetailsView: function(fileName, tabId) {
+ this._updateDetailsView(fileName);
+ if (tabId) {
+ this._detailsView.selectTab(tabId);
+ }
+ OC.Apps.showAppSidebar();
+ },
+
+ /**
* Update the details view to display the given file
*
* @param {string} fileName file name from the current list
diff --git a/apps/files/js/mainfileinfodetailview.js b/apps/files/js/mainfileinfodetailview.js
index efdbb5e2ad1..830f074f3f1 100644
--- a/apps/files/js/mainfileinfodetailview.js
+++ b/apps/files/js/mainfileinfodetailview.js
@@ -124,52 +124,10 @@
// TODO: we really need OC.Previews
var $iconDiv = this.$el.find('.thumbnail');
- $iconDiv.addClass('icon-loading icon-32');
- $container = this.$el.find('.thumbnailContainer');
+ var $container = this.$el.find('.thumbnailContainer');
if (!this.model.isDirectory()) {
- this._fileList.lazyLoadPreview({
- path: this.model.getFullPath(),
- mime: this.model.get('mimetype'),
- etag: this.model.get('etag'),
- y: this.model.isImage() ? 250: 75,
- x: this.model.isImage() ? 99999 /* only limit on y */ : 75,
- a: this.model.isImage() ? 1 : null,
- callback: function(previewUrl, img) {
- $iconDiv.previewImg = previewUrl;
- if (img) {
- $iconDiv.removeClass('icon-loading icon-32');
- if(img.height > img.width) {
- $container.addClass('portrait');
- }
- }
- if (this.model.isImage() && img) {
- $iconDiv.parent().addClass('image');
- var targetHeight = img.height / window.devicePixelRatio;
- if (targetHeight <= 75) {
- $container.removeClass('image'); // small enough to fit in normaly
- targetHeight = 75;
- }
- } else {
- targetHeight = 75;
- }
-
- // only set background when we have an actual preview
- // when we dont have a preview we show the mime icon in the error handler
- if (img) {
- $iconDiv.css({
- 'background-image': 'url("' + previewUrl + '")',
- 'height': targetHeight
- });
- }
- }.bind(this),
- error: function() {
- $iconDiv.removeClass('icon-loading icon-32');
- this.$el.find('.thumbnailContainer').removeClass('image'); //fall back to regular view
- $iconDiv.css({
- 'background-image': 'url("' + $iconDiv.previewImg + '")'
- });
- }.bind(this)
- });
+ $iconDiv.addClass('icon-loading icon-32');
+ this.loadPreview(this.model.getFullPath(), this.model.get('mimetype'), this.model.get('etag'), $iconDiv, $container, this.model.isImage());
} else {
// TODO: special icons / shared / external
$iconDiv.css('background-image', 'url("' + OC.MimeType.getIconUrl('dir') + '")');
@@ -179,6 +137,66 @@
this.$el.empty();
}
this.delegateEvents();
+ },
+
+ loadPreview: function(path, mime, etag, $iconDiv, $container, isImage) {
+ var maxImageHeight = ($container.parent().width() + 50) / (16/9); // 30px for negative margin
+ var smallPreviewSize = 75;
+
+ var isLandscape = function(img) {
+ return img.width > (img.height * 1.2);
+ };
+
+ var getTargetHeight = function(img) {
+ if(isImage) {
+ var targetHeight = img.height / window.devicePixelRatio;
+ if (targetHeight <= smallPreviewSize) {
+ targetHeight = smallPreviewSize;
+ }
+ return targetHeight;
+ }else{
+ return smallPreviewSize;
+ }
+ };
+
+ this._fileList.lazyLoadPreview({
+ path: path,
+ mime: mime,
+ etag: etag,
+ y: isImage ? maxImageHeight : smallPreviewSize,
+ x: isImage ? 99999 /* only limit on y */ : smallPreviewSize,
+ a: isImage ? 1 : null,
+ callback: function (previewUrl, img) {
+ $iconDiv.previewImg = previewUrl;
+
+ // as long as we only have the mimetype icon, we only save it in case there is no preview
+ if (!img) {
+ return;
+ }
+ $iconDiv.removeClass('icon-loading icon-32');
+ var targetHeight = getTargetHeight(img);
+ if (this.model.isImage() && targetHeight > smallPreviewSize) {
+ if (!isLandscape(img)) {
+ $container.addClass('portrait');
+ }
+ $container.addClass('image');
+ }
+
+ // only set background when we have an actual preview
+ // when we dont have a preview we show the mime icon in the error handler
+ $iconDiv.css({
+ 'background-image': 'url("' + previewUrl + '")',
+ 'height': targetHeight
+ });
+ }.bind(this),
+ error: function () {
+ $iconDiv.removeClass('icon-loading icon-32');
+ this.$el.find('.thumbnailContainer').removeClass('image'); //fall back to regular view
+ $iconDiv.css({
+ 'background-image': 'url("' + $iconDiv.previewImg + '")'
+ });
+ }.bind(this)
+ });
}
});
diff --git a/apps/files_sharing/appinfo/app.php b/apps/files_sharing/appinfo/app.php
index 20f1b046d35..8d919d1466f 100644
--- a/apps/files_sharing/appinfo/app.php
+++ b/apps/files_sharing/appinfo/app.php
@@ -54,9 +54,18 @@ $application->setupPropagation();
\OCP\Share::registerBackend('file', 'OC_Share_Backend_File');
\OCP\Share::registerBackend('folder', 'OC_Share_Backend_Folder', 'file');
-\OCP\Util::addScript('files_sharing', 'share');
-\OCP\Util::addScript('files_sharing', 'external');
-\OCP\Util::addStyle('files_sharing', 'sharetabview');
+$eventDispatcher = \OC::$server->getEventDispatcher();
+$eventDispatcher->addListener(
+ 'OCA\Files::loadAdditionalScripts',
+ function() {
+ \OCP\Util::addScript('files_sharing', 'share');
+ \OCP\Util::addScript('files_sharing', 'sharetabview');
+ \OCP\Util::addScript('files_sharing', 'external');
+ \OCP\Util::addStyle('files_sharing', 'sharetabview');
+ }
+);
+
+// \OCP\Util::addStyle('files_sharing', 'sharetabview');
\OC::$server->getActivityManager()->registerExtension(function() {
return new \OCA\Files_Sharing\Activity(
diff --git a/apps/files_sharing/css/sharetabview.css b/apps/files_sharing/css/sharetabview.css
index 42c9bee7173..0cc812e917c 100644
--- a/apps/files_sharing/css/sharetabview.css
+++ b/apps/files_sharing/css/sharetabview.css
@@ -1,3 +1,75 @@
.app-files .shareTabView {
min-height: 100px;
}
+
+.shareTabView .oneline { white-space: nowrap; }
+
+.shareTabView .shareWithLoading {
+ padding-left: 10px;
+ position: relative;
+ right: 30px;
+ top: 2px;
+}
+
+.shareTabView .shareWithRemoteInfo {
+ padding: 11px 0 11px 10px
+}
+
+.shareTabView label {
+ font-weight:400;
+ white-space: nowrap;
+}
+
+.shareTabView input[type="checkbox"] {
+ margin:0 3px 0 8px;
+ vertical-align: middle;
+}
+
+.shareTabView input[type="text"], .shareTabView input[type="password"] {
+ width: 91%;
+ margin-left: 7px;
+}
+
+.shareTabView form {
+ font-size: 100%;
+ margin-left: 0;
+ margin-right: 0;
+}
+
+#shareWithList {
+ list-style-type:none;
+ padding:8px;
+}
+
+#shareWithList li {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ font-weight: bold;
+ line-height: 21px;
+ white-space: normal;
+}
+
+#shareWithList .unshare img, #shareWithList .showCruds img {
+ vertical-align:text-bottom; /* properly align icons */
+}
+
+#shareWithList label input[type=checkbox]{
+ margin-left: 0;
+ position: relative;
+}
+#shareWithList .username{
+ padding-right: 8px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 254px;
+ display: inline-block;
+ overflow: hidden;
+ vertical-align: middle;
+}
+#shareWithList li label{
+ margin-right: 8px;
+}
+
+.shareTabView .icon-loading-small {
+ margin-left: -30px;
+}
diff --git a/apps/files_sharing/js/share.js b/apps/files_sharing/js/share.js
index c124d390d04..5290dfbb7d1 100644
--- a/apps/files_sharing/js/share.js
+++ b/apps/files_sharing/js/share.js
@@ -79,7 +79,9 @@
$files = fileList.$fileList.find('tr');
}
_.each($files, function(file) {
- OCA.Sharing.Util.updateFileActionIcon($(file));
+ var $tr = $(file);
+ var shareStatus = OC.Share.statuses[$tr.data('id')];
+ OCA.Sharing.Util._updateFileActionIcon($tr, !!shareStatus, shareStatus && shareStatus.link);
});
}
@@ -104,71 +106,59 @@
permissions: OC.PERMISSION_SHARE,
icon: OC.imagePath('core', 'actions/share'),
type: OCA.Files.FileActions.TYPE_INLINE,
- actionHandler: function(filename, context) {
- var $tr = context.$file;
- var itemType = 'file';
- if ($tr.data('type') === 'dir') {
- itemType = 'folder';
- }
- var possiblePermissions = $tr.data('share-permissions');
- if (_.isUndefined(possiblePermissions)) {
- possiblePermissions = $tr.data('permissions');
- }
-
- var appendTo = $tr.find('td.filename');
- // Check if drop down is already visible for a different file
- if (OC.Share.droppedDown) {
- if ($tr.attr('data-id') !== $('#dropdown').attr('data-item-source')) {
- OC.Share.hideDropDown(function () {
- $tr.addClass('mouseOver');
- OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
- });
- } else {
- OC.Share.hideDropDown();
- }
- } else {
- $tr.addClass('mouseOver');
- OC.Share.showDropDown(itemType, $tr.data('id'), appendTo, true, possiblePermissions, filename);
- }
- $('#dropdown').on('sharesChanged', function(ev) {
- // files app current cannot show recipients on load, so we don't update the
- // icon when changed for consistency
- if (context.fileList.$el.closest('#app-content-files').length) {
- return;
- }
- var recipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_USER], 'share_with_displayname');
- var groupRecipients = _.pluck(ev.shares[OC.Share.SHARE_TYPE_GROUP], 'share_with_displayname');
- recipients = recipients.concat(groupRecipients);
- // note: we only update the data attribute because updateIcon()
- // is called automatically after this event
- if (recipients.length) {
- $tr.attr('data-share-recipients', OCA.Sharing.Util.formatRecipients(recipients));
- }
- else {
- $tr.removeAttr('data-share-recipients');
- }
- });
+ actionHandler: function(fileName) {
+ fileList.showDetailsView(fileName, 'shareTabView');
}
});
- OC.addScript('files_sharing', 'sharetabview').done(function() {
- fileList.registerTabView(new OCA.Sharing.ShareTabView('shareTabView'));
+ var shareTab = new OCA.Sharing.ShareTabView('shareTabView');
+ // detect changes and change the matching list entry
+ shareTab.on('sharesChanged', function(shareModel) {
+ var fileInfoModel = shareModel.fileInfoModel;
+ var $tr = fileList.findFileEl(fileInfoModel.get('name'));
+ OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel);
+ if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), shareModel.hasLinkShare())) {
+ // remove icon, if applicable
+ OC.Share.markFileAsShared($tr, false, false);
+ }
});
+ fileList.registerTabView(shareTab);
+ },
+
+ /**
+ * Update file list data attributes
+ */
+ _updateFileListDataAttributes: function(fileList, $tr, shareModel) {
+ // files app current cannot show recipients on load, so we don't update the
+ // icon when changed for consistency
+ if (fileList.id === 'files') {
+ return;
+ }
+ var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname');
+ // note: we only update the data attribute because updateIcon()
+ if (recipients.length) {
+ $tr.attr('data-share-recipients', OCA.Sharing.Util.formatRecipients(recipients));
+ }
+ else {
+ $tr.removeAttr('data-share-recipients');
+ }
},
/**
* Update the file action share icon for the given file
*
* @param $tr file element of the file to update
+ * @param {bool} hasUserShares true if a user share exists
+ * @param {bool} hasLinkShare true if a link share exists
+ *
+ * @return {bool} true if the icon was set, false otherwise
*/
- updateFileActionIcon: function($tr) {
+ _updateFileActionIcon: function($tr, hasUserShares, hasLinkShare) {
// if the statuses are loaded already, use them for the icon
// (needed when scrolling to the next page)
- var shareStatus = OC.Share.statuses[$tr.data('id')];
- if (shareStatus || $tr.attr('data-share-recipients') || $tr.attr('data-share-owner')) {
+ if (hasUserShares || hasLinkShare || $tr.attr('data-share-recipients') || $tr.attr('data-share-owner')) {
var permissions = $tr.data('permissions');
- var hasLink = !!(shareStatus && shareStatus.link);
- OC.Share.markFileAsShared($tr, true, hasLink);
+ OC.Share.markFileAsShared($tr, true, hasLinkShare);
if ((permissions & OC.PERMISSION_SHARE) === 0 && $tr.attr('data-share-owner')) {
// if no share action exists because the admin disabled sharing for this user
// we create a share notification action to inform the user about files
@@ -187,7 +177,9 @@
return $result;
});
}
+ return true;
}
+ return false;
},
/**
diff --git a/apps/files_sharing/js/sharetabview.js b/apps/files_sharing/js/sharetabview.js
index ee572b747ea..e24320604fb 100644
--- a/apps/files_sharing/js/sharetabview.js
+++ b/apps/files_sharing/js/sharetabview.js
@@ -10,7 +10,9 @@
(function() {
var TEMPLATE =
- '<div><ul>{{#if owner}}<li>Owner: {{owner}}</li>{{/if}}</ul></div>';
+ '<div>' +
+ '<div class="dialogContainer"></div>' +
+ '</div>';
/**
* @memberof OCA.Sharing
@@ -20,7 +22,12 @@
id: 'shareTabView',
className: 'tab shareTabView',
- _template: null,
+ template: function(params) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+ return this._template(params);
+ },
getLabel: function() {
return t('files_sharing', 'Sharing');
@@ -30,23 +37,40 @@
* Renders this details view
*/
render: function() {
- this.$el.empty();
-
- if (!this._template) {
- this._template = Handlebars.compile(TEMPLATE);
+ var self = this;
+ if (this._dialog) {
+ // remove/destroy older instance
+ this._dialog.model.off();
+ this._dialog.remove();
+ this._dialog = null;
}
if (this.model) {
- console.log(this.model);
- var owner = this.model.get('shareOwner');
- if (owner === OC.currentUser) {
- owner = null;
- }
- this.$el.append(this._template({
- owner: owner
- }));
+ this.$el.html(this.template());
+ // TODO: the model should read these directly off the passed fileInfoModel
+ var attributes = {
+ itemType: this.model.isDirectory() ? 'folder' : 'file',
+ itemSource: this.model.get('id'),
+ possiblePermissions: this.model.get('sharePermissions')
+ };
+ var configModel = new OC.Share.ShareConfigModel();
+ var shareModel = new OC.Share.ShareItemModel(attributes, {
+ configModel: configModel,
+ fileInfoModel: this.model
+ });
+ this._dialog = new OC.Share.ShareDialogView({
+ configModel: configModel,
+ model: shareModel
+ });
+ this.$el.find('.dialogContainer').append(this._dialog.$el);
+ this._dialog.render();
+ this._dialog.model.fetch();
+ this._dialog.model.on('change', function() {
+ self.trigger('sharesChanged', shareModel);
+ });
} else {
+ this.$el.empty();
// TODO: render placeholder text?
}
}
diff --git a/apps/files_sharing/lib/notifier.php b/apps/files_sharing/lib/notifier.php
index cc2deb3f439..02765fcfd1c 100644
--- a/apps/files_sharing/lib/notifier.php
+++ b/apps/files_sharing/lib/notifier.php
@@ -55,7 +55,7 @@ class Notifier implements INotifier {
case 'remote_share':
$params = $notification->getSubjectParameters();
$notification->setParsedSubject(
- (string) $l->t('You received %s as a remote share from %s', $params)
+ (string) $l->t('You received %2$s as a remote share from %1$s', $params)
);
// Deal with the actions for a known subject
diff --git a/apps/files_sharing/tests/js/shareSpec.js b/apps/files_sharing/tests/js/shareSpec.js
index b6368b901ee..96a96f1b814 100644
--- a/apps/files_sharing/tests/js/shareSpec.js
+++ b/apps/files_sharing/tests/js/shareSpec.js
@@ -97,7 +97,6 @@ describe('OCA.Sharing.Util tests', function() {
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
- expect($action.hasClass('permanent')).toEqual(true);
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder.svg');
expect($action.find('img').length).toEqual(1);
@@ -116,7 +115,6 @@ describe('OCA.Sharing.Util tests', function() {
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('Shared');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder-shared.svg');
@@ -137,7 +135,6 @@ describe('OCA.Sharing.Util tests', function() {
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('Shared');
expect(OC.basename($action.find('img').attr('src'))).toEqual('public.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder-public.svg');
@@ -158,7 +155,6 @@ describe('OCA.Sharing.Util tests', function() {
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('User One');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder-shared.svg');
@@ -178,7 +174,6 @@ describe('OCA.Sharing.Util tests', function() {
}]);
$tr = fileList.$el.find('tbody tr:first');
$action = $tr.find('.action-share');
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('Shared with User One, User Two');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder-shared.svg');
@@ -200,7 +195,6 @@ describe('OCA.Sharing.Util tests', function() {
$tr = fileList.$el.find('tbody tr:first');
expect($tr.find('.action-share').length).toEqual(0);
$action = $tr.find('.action-share-notification');
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('User One');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
expect(OC.basename(getImageUrl($tr.find('.filename .thumbnail')))).toEqual('folder-shared.svg');
@@ -225,7 +219,7 @@ describe('OCA.Sharing.Util tests', function() {
});
});
describe('Share action', function() {
- var showDropDownStub;
+ var shareTab;
function makeDummyShareItem(displayName) {
return {
@@ -234,12 +228,35 @@ describe('OCA.Sharing.Util tests', function() {
}
beforeEach(function() {
- showDropDownStub = sinon.stub(OC.Share, 'showDropDown', function() {
- $('#testArea').append($('<div id="dropdown"></div>'));
- });
+ // make it look like not the "All files" list
+ fileList.id = 'test';
+ shareTab = fileList._detailsView._tabViews[0];
});
afterEach(function() {
- showDropDownStub.restore();
+ shareTab = null;
+ });
+ it('clicking share action opens sidebar and share tab', function() {
+ var showDetailsViewStub = sinon.stub(fileList, 'showDetailsView');
+
+ fileList.setFiles([{
+ id: 1,
+ type: 'file',
+ name: 'One.txt',
+ path: '/subdir',
+ mimetype: 'text/plain',
+ size: 12,
+ permissions: OC.PERMISSION_ALL,
+ etag: 'abc'
+ }]);
+
+ var $tr = fileList.$el.find('tr:first');
+ $tr.find('.action-share').click();
+
+ expect(showDetailsViewStub.calledOnce).toEqual(true);
+ expect(showDetailsViewStub.getCall(0).args[0]).toEqual('One.txt');
+ expect(showDetailsViewStub.getCall(0).args[1]).toEqual('shareTabView');
+
+ showDetailsViewStub.restore();
});
it('adds share icon after sharing a non-shared file', function() {
var $action, $tr;
@@ -257,24 +274,20 @@ describe('OCA.Sharing.Util tests', function() {
$action = fileList.$el.find('tbody tr:first .action-share');
$tr = fileList.$el.find('tr:first');
- expect($action.hasClass('permanent')).toEqual(true);
-
$tr.find('.action-share').click();
- expect(showDropDownStub.calledOnce).toEqual(true);
-
- // simulate what the dropdown does
- var shares = {};
- OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user1', 'user2'];
- OC.Share.itemShares[OC.Share.SHARE_TYPE_GROUP] = ['group1', 'group2'];
- shares[OC.Share.SHARE_TYPE_USER] = _.map(['User One', 'User Two'], makeDummyShareItem);
- shares[OC.Share.SHARE_TYPE_GROUP] = _.map(['Group One', 'Group Two'], makeDummyShareItem);
- $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+ // simulate updating shares
+ shareTab._dialog.model.set({
+ shares: [
+ {share_with_displayname: 'User One'},
+ {share_with_displayname: 'User Two'},
+ {share_with_displayname: 'Group One'},
+ {share_with_displayname: 'Group Two'}
+ ]
+ });
expect($tr.attr('data-share-recipients')).toEqual('Group One, Group Two, User One, User Two');
- OC.Share.updateIcon('file', 1);
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('Shared with Group One, Group Two, User One, User Two');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
});
@@ -294,23 +307,19 @@ describe('OCA.Sharing.Util tests', function() {
$action = fileList.$el.find('tbody tr:first .action-share');
$tr = fileList.$el.find('tr:first');
- expect($action.hasClass('permanent')).toEqual(true);
-
$tr.find('.action-share').click();
- expect(showDropDownStub.calledOnce).toEqual(true);
-
- // simulate what the dropdown does
- var shares = {};
- OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user1', 'user2', 'user3'];
- shares[OC.Share.SHARE_TYPE_USER] = _.map(['User One', 'User Two', 'User Three'], makeDummyShareItem);
- $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+ // simulate updating shares
+ shareTab._dialog.model.set({
+ shares: [
+ {share_with_displayname: 'User One'},
+ {share_with_displayname: 'User Two'},
+ {share_with_displayname: 'User Three'}
+ ]
+ });
expect($tr.attr('data-share-recipients')).toEqual('User One, User Three, User Two');
- OC.Share.updateIcon('file', 1);
-
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('Shared with User One, User Three, User Two');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
});
@@ -331,20 +340,14 @@ describe('OCA.Sharing.Util tests', function() {
$action = fileList.$el.find('tbody tr:first .action-share');
$tr = fileList.$el.find('tr:first');
- expect($action.hasClass('permanent')).toEqual(true);
-
$tr.find('.action-share').click();
- expect(showDropDownStub.calledOnce).toEqual(true);
-
- // simulate what the dropdown does
- OC.Share.itemShares = {};
- $('#dropdown').trigger(new $.Event('sharesChanged', {shares: {}}));
+ // simulate updating shares
+ shareTab._dialog.model.set({
+ shares: []
+ });
expect($tr.attr('data-share-recipients')).not.toBeDefined();
-
- OC.Share.updateIcon('file', 1);
- expect($action.hasClass('permanent')).toEqual(true);
});
it('keep share text after updating reshare', function() {
var $action, $tr;
@@ -363,23 +366,15 @@ describe('OCA.Sharing.Util tests', function() {
$action = fileList.$el.find('tbody tr:first .action-share');
$tr = fileList.$el.find('tr:first');
- expect($action.hasClass('permanent')).toEqual(true);
-
$tr.find('.action-share').click();
- expect(showDropDownStub.calledOnce).toEqual(true);
-
- // simulate what the dropdown does
- var shares = {};
- OC.Share.itemShares[OC.Share.SHARE_TYPE_USER] = ['user2'];
- shares[OC.Share.SHARE_TYPE_USER] = _.map(['User Two'], makeDummyShareItem);
- $('#dropdown').trigger(new $.Event('sharesChanged', {shares: shares}));
+ // simulate updating shares
+ shareTab._dialog.model.set({
+ shares: [{share_with_displayname: 'User Two'}]
+ });
expect($tr.attr('data-share-recipients')).toEqual('User Two');
- OC.Share.updateIcon('file', 1);
-
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('User One');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
});
@@ -401,21 +396,15 @@ describe('OCA.Sharing.Util tests', function() {
$action = fileList.$el.find('tbody tr:first .action-share');
$tr = fileList.$el.find('tr:first');
- expect($action.hasClass('permanent')).toEqual(true);
-
$tr.find('.action-share').click();
- expect(showDropDownStub.calledOnce).toEqual(true);
-
- // simulate what the dropdown does
- OC.Share.itemShares = {};
- $('#dropdown').trigger(new $.Event('sharesChanged', {shares: {}}));
+ // simulate updating shares
+ shareTab._dialog.model.set({
+ shares: []
+ });
expect($tr.attr('data-share-recipients')).not.toBeDefined();
- OC.Share.updateIcon('file', 1);
-
- expect($action.hasClass('permanent')).toEqual(true);
expect($action.find('>span').text().trim()).toEqual('User One');
expect(OC.basename($action.find('img').attr('src'))).toEqual('share.svg');
});
diff --git a/apps/files_versions/appinfo/application.php b/apps/files_versions/appinfo/application.php
index bab36b48510..b61b03dab13 100644
--- a/apps/files_versions/appinfo/application.php
+++ b/apps/files_versions/appinfo/application.php
@@ -22,6 +22,7 @@
namespace OCA\Files_Versions\AppInfo;
use OCP\AppFramework\App;
+use OCA\Files_Versions\Expiration;
class Application extends App {
public function __construct(array $urlParams = array()) {
@@ -33,5 +34,15 @@ class Application extends App {
* Register capabilities
*/
$container->registerCapability('OCA\Files_Versions\Capabilities');
+
+ /*
+ * Register expiration
+ */
+ $container->registerService('Expiration', function($c) {
+ return new Expiration(
+ $c->query('ServerContainer')->getConfig(),
+ $c->query('OCP\AppFramework\Utility\ITimeFactory')
+ );
+ });
}
}
diff --git a/apps/files_versions/lib/expiration.php b/apps/files_versions/lib/expiration.php
new file mode 100644
index 00000000000..d42c62f0ee3
--- /dev/null
+++ b/apps/files_versions/lib/expiration.php
@@ -0,0 +1,185 @@
+<?php
+/**
+ * @author Victor Dubiniuk <dubiniuk@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Files_Versions;
+
+use \OCP\IConfig;
+use \OCP\AppFramework\Utility\ITimeFactory;
+
+class Expiration {
+
+ // how long do we keep files a version if no other value is defined in the config file (unit: days)
+ const NO_OBLIGATION = -1;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var string */
+ private $retentionObligation;
+
+ /** @var int */
+ private $minAge;
+
+ /** @var int */
+ private $maxAge;
+
+ /** @var bool */
+ private $canPurgeToSaveSpace;
+
+ public function __construct(IConfig $config,ITimeFactory $timeFactory){
+ $this->timeFactory = $timeFactory;
+ $this->retentionObligation = $config->getSystemValue('versions_retention_obligation', 'auto');
+
+ if ($this->retentionObligation !== 'disabled') {
+ $this->parseRetentionObligation();
+ }
+ }
+
+ /**
+ * Is versions expiration enabled
+ * @return bool
+ */
+ public function isEnabled(){
+ return $this->retentionObligation !== 'disabled';
+ }
+
+ /**
+ * Is default expiration active
+ */
+ public function shouldAutoExpire(){
+ return $this->minAge === self::NO_OBLIGATION
+ || $this->maxAge === self::NO_OBLIGATION;
+ }
+
+ /**
+ * Check if given timestamp in expiration range
+ * @param int $timestamp
+ * @param bool $quotaExceeded
+ * @return bool
+ */
+ public function isExpired($timestamp, $quotaExceeded = false){
+ // No expiration if disabled
+ if (!$this->isEnabled()) {
+ return false;
+ }
+
+ // Purge to save space (if allowed)
+ if ($quotaExceeded && $this->canPurgeToSaveSpace) {
+ return true;
+ }
+
+ $time = $this->timeFactory->getTime();
+ // Never expire dates in future e.g. misconfiguration or negative time
+ // adjustment
+ if ($time<$timestamp) {
+ return false;
+ }
+
+ // Purge as too old
+ if ($this->maxAge !== self::NO_OBLIGATION) {
+ $maxTimestamp = $time - ($this->maxAge * 86400);
+ $isOlderThanMax = $timestamp < $maxTimestamp;
+ } else {
+ $isOlderThanMax = false;
+ }
+
+ if ($this->minAge !== self::NO_OBLIGATION) {
+ // older than Min obligation and we are running out of quota?
+ $minTimestamp = $time - ($this->minAge * 86400);
+ $isMinReached = ($timestamp < $minTimestamp) && $quotaExceeded;
+ } else {
+ $isMinReached = false;
+ }
+
+ return $isOlderThanMax || $isMinReached;
+ }
+
+ /**
+ * Read versions_retention_obligation, validate it
+ * and set private members accordingly
+ */
+ private function parseRetentionObligation(){
+ $splitValues = explode(',', $this->retentionObligation);
+ if (!isset($splitValues[0])) {
+ $minValue = 'auto';
+ } else {
+ $minValue = trim($splitValues[0]);
+ }
+
+ if (!isset($splitValues[1])) {
+ $maxValue = self::NO_OBLIGATION;
+ } else {
+ $maxValue = trim($splitValues[1]);
+ }
+
+ $isValid = true;
+ // Validate
+ if (!ctype_digit($minValue) && $minValue !== 'auto') {
+ $isValid = false;
+ \OC::$server->getLogger()->warning(
+ $minValue . ' is not a valid value for minimal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
+ ['app'=>'files_versions']
+ );
+ }
+
+ if (!ctype_digit($maxValue) && $maxValue !== 'auto') {
+ $isValid = false;
+ \OC::$server->getLogger()->warning(
+ $maxValue . ' is not a valid value for maximal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.',
+ ['app'=>'files_versions']
+ );
+ }
+
+ if (!$isValid){
+ $minValue = 'auto';
+ $maxValue = 'auto';
+ }
+
+
+ if ($minValue === 'auto' && $maxValue === 'auto') {
+ // Default: Delete anytime if space needed
+ $this->minAge = self::NO_OBLIGATION;
+ $this->maxAge = self::NO_OBLIGATION;
+ $this->canPurgeToSaveSpace = true;
+ } elseif ($minValue !== 'auto' && $maxValue === 'auto') {
+ // Keep for X days but delete anytime if space needed
+ $this->minAge = intval($minValue);
+ $this->maxAge = self::NO_OBLIGATION;
+ $this->canPurgeToSaveSpace = true;
+ } elseif ($minValue === 'auto' && $maxValue !== 'auto') {
+ // Delete anytime if space needed, Delete all older than max automatically
+ $this->minAge = self::NO_OBLIGATION;
+ $this->maxAge = intval($maxValue);
+ $this->canPurgeToSaveSpace = true;
+ } elseif ($minValue !== 'auto' && $maxValue !== 'auto') {
+ // Delete all older than max OR older than min if space needed
+
+ // Max < Min as per https://github.com/owncloud/core/issues/16301
+ if ($maxValue < $minValue) {
+ $maxValue = $minValue;
+ }
+
+ $this->minAge = intval($minValue);
+ $this->maxAge = intval($maxValue);
+ $this->canPurgeToSaveSpace = false;
+ }
+ }
+}
diff --git a/apps/files_versions/lib/storage.php b/apps/files_versions/lib/storage.php
index e0034f6165f..ba2b78ff4d2 100644
--- a/apps/files_versions/lib/storage.php
+++ b/apps/files_versions/lib/storage.php
@@ -40,6 +40,7 @@
namespace OCA\Files_Versions;
+use OCA\Files_Versions\AppInfo\Application;
use OCA\Files_Versions\Command\Expire;
class Storage {
@@ -67,6 +68,9 @@ class Storage {
//until the end one version per week
6 => array('intervalEndsAfter' => -1, 'step' => 604800),
);
+
+ /** @var \OCA\Files_Versions\AppInfo\Application */
+ private static $application;
public static function getUidAndFilename($filename) {
$uid = \OC\Files\Filesystem::getOwner($filename);
@@ -479,10 +483,36 @@ class Storage {
* get list of files we want to expire
* @param array $versions list of versions
* @param integer $time
+ * @param bool $quotaExceeded is versions storage limit reached
* @return array containing the list of to deleted versions and the size of them
*/
- protected static function getExpireList($time, $versions) {
+ protected static function getExpireList($time, $versions, $quotaExceeded = false) {
+ $expiration = self::getExpiration();
+ if ($expiration->shouldAutoExpire()) {
+ list($toDelete, $size) = self::getAutoExpireList($time, $versions);
+ } else {
+ $size = 0;
+ $toDelete = []; // versions we want to delete
+ }
+
+ foreach ($versions as $key => $version) {
+ if ($expiration->isExpired($version['version'], $quotaExceeded) && !isset($toDelete[$key])) {
+ $size += $version['size'];
+ $toDelete[$key] = $version['path'] . '.v' . $version['version'];
+ }
+ }
+
+ return [$toDelete, $size];
+ }
+
+ /**
+ * 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
+ */
+ protected static function getAutoExpireList($time, $versions) {
$size = 0;
$toDelete = array(); // versions we want to delete
@@ -529,7 +559,6 @@ class Storage {
}
return array($toDelete, $size);
-
}
/**
@@ -541,8 +570,12 @@ class Storage {
* @param int $neededSpace requested versions size
*/
private static function scheduleExpire($uid, $fileName, $versionsSize = null, $neededSpace = 0) {
- $command = new Expire($uid, $fileName, $versionsSize, $neededSpace);
- \OC::$server->getCommandBus()->push($command);
+ // let the admin disable auto expire
+ $expiration = self::getExpiration();
+ if ($expiration->isEnabled()) {
+ $command = new Expire($uid, $fileName, $versionsSize, $neededSpace);
+ \OC::$server->getCommandBus()->push($command);
+ }
}
/**
@@ -555,7 +588,9 @@ class Storage {
*/
public static function expire($filename, $versionsSize = null, $offset = 0) {
$config = \OC::$server->getConfig();
- if($config->getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
+ $expiration = self::getExpiration();
+
+ if($config->getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' && $expiration->isEnabled()) {
list($uid, $filename) = self::getUidAndFilename($filename);
if (empty($filename)) {
// file maybe renamed or deleted
@@ -599,7 +634,7 @@ class Storage {
$allVersions = Storage::getVersions($uid, $filename);
$time = time();
- list($toDelete, $sizeOfDeletedVersions) = self::getExpireList($time, $allVersions);
+ list($toDelete, $sizeOfDeletedVersions) = self::getExpireList($time, $allVersions, $availableSpace <= 0);
$availableSpace = $availableSpace + $sizeOfDeletedVersions;
$versionsSize = $versionsSize - $sizeOfDeletedVersions;
@@ -610,7 +645,7 @@ class Storage {
$allVersions = $result['all'];
foreach ($result['by_file'] as $versions) {
- list($toDeleteNew, $size) = self::getExpireList($time, $versions);
+ list($toDeleteNew, $size) = self::getExpireList($time, $versions, $availableSpace <= 0);
$toDelete = array_merge($toDelete, $toDeleteNew);
$sizeOfDeletedVersions += $size;
}
@@ -672,4 +707,15 @@ class Storage {
}
}
+ /**
+ * Static workaround
+ * @return Expiration
+ */
+ protected static function getExpiration(){
+ if (is_null(self::$application)) {
+ self::$application = new Application();
+ }
+ return self::$application->getContainer()->query('Expiration');
+ }
+
}
diff --git a/apps/files_versions/tests/expirationtest.php b/apps/files_versions/tests/expirationtest.php
new file mode 100644
index 00000000000..54024b85b78
--- /dev/null
+++ b/apps/files_versions/tests/expirationtest.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ * @author Victor Dubiniuk <dubiniuk@owncloud.com>
+ *
+ * @copyright Copyright (c) 2015, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OCA\Files_Versions\Tests;
+
+use \OCA\Files_Versions\Expiration;
+
+class Expiration_Test extends \Test\TestCase {
+ const SECONDS_PER_DAY = 86400; //60*60*24
+
+ public function expirationData(){
+ $today = 100*self::SECONDS_PER_DAY;
+ $back10Days = (100-10)*self::SECONDS_PER_DAY;
+ $back20Days = (100-20)*self::SECONDS_PER_DAY;
+ $back30Days = (100-30)*self::SECONDS_PER_DAY;
+ $back35Days = (100-35)*self::SECONDS_PER_DAY;
+
+ // it should never happen, but who knows :/
+ $ahead100Days = (100+100)*self::SECONDS_PER_DAY;
+
+ return [
+ // Expiration is disabled - always should return false
+ [ 'disabled', $today, $back10Days, false, false],
+ [ 'disabled', $today, $back10Days, true, false],
+ [ 'disabled', $today, $ahead100Days, true, false],
+
+ // Default: expire in 30 days or earlier when quota requirements are met
+ [ 'auto', $today, $back10Days, false, false],
+ [ 'auto', $today, $back35Days, false, false],
+ [ 'auto', $today, $back10Days, true, true],
+ [ 'auto', $today, $back35Days, true, true],
+ [ 'auto', $today, $ahead100Days, true, true],
+
+ // The same with 'auto'
+ [ 'auto, auto', $today, $back10Days, false, false],
+ [ 'auto, auto', $today, $back35Days, false, false],
+ [ 'auto, auto', $today, $back10Days, true, true],
+ [ 'auto, auto', $today, $back35Days, true, true],
+
+ // Keep for 15 days but expire anytime if space needed
+ [ '15, auto', $today, $back10Days, false, false],
+ [ '15, auto', $today, $back20Days, false, false],
+ [ '15, auto', $today, $back10Days, true, true],
+ [ '15, auto', $today, $back20Days, true, true],
+ [ '15, auto', $today, $ahead100Days, true, true],
+
+ // Expire anytime if space needed, Expire all older than max
+ [ 'auto, 15', $today, $back10Days, false, false],
+ [ 'auto, 15', $today, $back20Days, false, true],
+ [ 'auto, 15', $today, $back10Days, true, true],
+ [ 'auto, 15', $today, $back20Days, true, true],
+ [ 'auto, 15', $today, $ahead100Days, true, true],
+
+ // Expire all older than max OR older than min if space needed
+ [ '15, 25', $today, $back10Days, false, false],
+ [ '15, 25', $today, $back20Days, false, false],
+ [ '15, 25', $today, $back30Days, false, true],
+ [ '15, 25', $today, $back10Days, false, false],
+ [ '15, 25', $today, $back20Days, true, true],
+ [ '15, 25', $today, $back30Days, true, true],
+ [ '15, 25', $today, $ahead100Days, true, false],
+
+ // Expire all older than max OR older than min if space needed
+ // Max<Min case
+ [ '25, 15', $today, $back10Days, false, false],
+ [ '25, 15', $today, $back20Days, false, false],
+ [ '25, 15', $today, $back30Days, false, true],
+ [ '25, 15', $today, $back10Days, false, false],
+ [ '25, 15', $today, $back20Days, true, false],
+ [ '25, 15', $today, $back30Days, true, true],
+ [ '25, 15', $today, $ahead100Days, true, false],
+ ];
+ }
+
+ /**
+ * @dataProvider expirationData
+ *
+ * @param string $retentionObligation
+ * @param int $timeNow
+ * @param int $timestamp
+ * @param bool $quotaExceeded
+ * @param string $expectedResult
+ */
+ public function testExpiration($retentionObligation, $timeNow, $timestamp, $quotaExceeded, $expectedResult){
+ $mockedConfig = $this->getMockedConfig($retentionObligation);
+ $mockedTimeFactory = $this->getMockedTimeFactory($timeNow);
+
+ $expiration = new Expiration($mockedConfig, $mockedTimeFactory);
+ $actualResult = $expiration->isExpired($timestamp, $quotaExceeded);
+
+ $this->assertEquals($expectedResult, $actualResult);
+ }
+
+
+ public function configData(){
+ return [
+ [ 'disabled', null, null, null],
+ [ 'auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
+ [ 'auto,auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
+ [ 'auto, auto', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
+ [ 'auto, 3', Expiration::NO_OBLIGATION, 3, true ],
+ [ '5, auto', 5, Expiration::NO_OBLIGATION, true ],
+ [ '3, 5', 3, 5, false ],
+ [ '10, 3', 10, 10, false ],
+ [ 'g,a,r,b,a,g,e', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ],
+ [ '-3,8', Expiration::NO_OBLIGATION, Expiration::NO_OBLIGATION, true ]
+ ];
+ }
+
+
+ /**
+ * @dataProvider configData
+ *
+ * @param string $configValue
+ * @param int $expectedMinAge
+ * @param int $expectedMaxAge
+ * @param bool $expectedCanPurgeToSaveSpace
+ */
+ public function testParseRetentionObligation($configValue, $expectedMinAge, $expectedMaxAge, $expectedCanPurgeToSaveSpace){
+ $mockedConfig = $this->getMockedConfig($configValue);
+ $mockedTimeFactory = $this->getMockedTimeFactory(
+ time()
+ );
+
+ $expiration = new Expiration($mockedConfig, $mockedTimeFactory);
+ $this->assertAttributeEquals($expectedMinAge, 'minAge', $expiration);
+ $this->assertAttributeEquals($expectedMaxAge, 'maxAge', $expiration);
+ $this->assertAttributeEquals($expectedCanPurgeToSaveSpace, 'canPurgeToSaveSpace', $expiration);
+ }
+
+ /**
+ *
+ * @param int $time
+ * @return \OCP\AppFramework\Utility\ITimeFactory
+ */
+ private function getMockedTimeFactory($time){
+ $mockedTimeFactory = $this->getMockBuilder('\OCP\AppFramework\Utility\ITimeFactory')
+ ->disableOriginalConstructor()
+ ->setMethods(['getTime'])
+ ->getMock()
+ ;
+ $mockedTimeFactory->expects($this->any())->method('getTime')->will(
+ $this->returnValue($time)
+ );
+
+ return $mockedTimeFactory;
+ }
+
+ /**
+ *
+ * @param string $returnValue
+ * @return \OCP\IConfig
+ */
+ private function getMockedConfig($returnValue){
+ $mockedConfig = $this->getMockBuilder('\OCP\IConfig')
+ ->disableOriginalConstructor()
+ ->setMethods(
+ [
+ 'setSystemValues',
+ 'setSystemValue',
+ 'getSystemValue',
+ 'deleteSystemValue',
+ 'getAppKeys',
+ 'setAppValue',
+ 'getAppValue',
+ 'deleteAppValue',
+ 'deleteAppValues',
+ 'setUserValue',
+ 'getUserValue',
+ 'getUserValueForUsers',
+ 'getUserKeys',
+ 'deleteUserValue',
+ 'deleteAllUserValues',
+ 'deleteAppFromAllUsers',
+ 'getUsersForUserValue'
+ ]
+ )
+ ->getMock()
+ ;
+ $mockedConfig->expects($this->any())->method('getSystemValue')->will(
+ $this->returnValue($returnValue)
+ );
+
+ return $mockedConfig;
+ }
+}