diff options
75 files changed, 1734 insertions, 695 deletions
diff --git a/.drone.yml b/.drone.yml index 2798176c9a2..6016ebd6981 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,18 +3,6 @@ build: image: nextcloudci/jsunit:1.0.6 commands: - ./autotest-js.sh - nodb-php5.4: - image: nextcloudci/php5.4:1.0.7 - commands: - - rm -rf data/* config/config.php # TODO: remove this - temporary fix for CI issues - - git submodule update --init - - NOCOVERAGE=true TEST_SELECTION=NODB ./autotest.sh sqlite - nodb-php5.5: - image: nextcloudci/php5.5:1.0.7 - commands: - - rm -rf data/* config/config.php # TODO: remove this - temporary fix for CI issues - - git submodule update --init - - NOCOVERAGE=true TEST_SELECTION=NODB ./autotest.sh sqlite nodb-php5.6: image: nextcloudci/php5.6:1.0.6 commands: @@ -27,18 +15,6 @@ build: - rm -rf data/* config/config.php # TODO: remove this - temporary fix for CI issues - git submodule update --init - NOCOVERAGE=true TEST_SELECTION=NODB ./autotest.sh sqlite - sqlite-php5.4: - image: nextcloudci/php5.4:1.0.7 - commands: - - rm -rf data/* config/config.php # TODO: remove this - temporary fix for CI issues - - git submodule update --init - - NOCOVERAGE=true TEST_SELECTION=DB ./autotest.sh sqlite - sqlite-php5.5: - image: nextcloudci/php5.5:1.0.7 - commands: - - rm -rf data/* config/config.php # TODO: remove this - temporary fix for CI issues - - git submodule update --init - - NOCOVERAGE=true TEST_SELECTION=DB ./autotest.sh sqlite sqlite-php5.6: image: nextcloudci/php5.6:1.0.6 commands: diff --git a/.travis.yml b/.travis.yml index 05b07beac3d..ddcba167af1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false language: php php: - - 5.4 + - 5.6 env: global: @@ -41,21 +41,17 @@ matrix: include: - php: 7.0 env: TC=autoloader;TEST_DAV=0 - - php: 5.4 + - php: 5.6 env: DB=pgsql;TC=litmus-v1 - - php: 5.4 + - php: 5.6 env: DB=sqlite;TC=carddav - - php: 5.4 + - php: 5.6 env: DB=sqlite;TC=caldav - - php: 5.4 - env: DB=sqlite;TC=syntax;TEST_DAV=0 - - php: 5.5 - env: DB=sqlite;TC=syntax;TEST_DAV=0 - php: 5.6 env: DB=sqlite;TC=syntax;TEST_DAV=0 - php: 7.0 env: DB=sqlite;TC=syntax;TEST_DAV=0 - - php: 5.4 + - php: 5.6 env: DB=sqlite;TC=app:check-code;TEST_DAV=0 fast_finish: true diff --git a/apps/comments/js/commentstabview.js b/apps/comments/js/commentstabview.js index 9451e828f91..eae18c1d485 100644 --- a/apps/comments/js/commentstabview.js +++ b/apps/comments/js/commentstabview.js @@ -32,7 +32,7 @@ '{{/if}}' + ' </div>' + ' <form class="newCommentForm">' + - ' <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{{message}}}" />' + + ' <input type="text" class="message" placeholder="{{newMessagePlaceholder}}" value="{{message}}" />' + ' <input class="submit icon-confirm" type="submit" value="" />' + '{{#if isEditMode}}' + ' <input class="cancel pull-right" type="button" value="{{cancelText}}" />' + diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index c83ee613d37..9de54eec33d 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -28,6 +28,7 @@ use OCP\Constants; use OCP\IAddressBook; use OCP\IURLGenerator; use Sabre\VObject\Component\VCard; +use Sabre\VObject\Property; use Sabre\VObject\Property\Text; use Sabre\VObject\Reader; use Sabre\VObject\UUIDUtil; @@ -225,7 +226,7 @@ class AddressBookImpl implements IAddressBook { ]; foreach ($vCard->children as $property) { - $result[$property->name] = $property->getValue(); + /** @var \Sabre\VObject\Property\Unknown $property */ if ($property->name === 'PHOTO' && $property->getValueType() === 'BINARY') { $url = $this->urlGenerator->getAbsoluteURL( $this->urlGenerator->linkTo('', 'remote.php') . '/dav/'); @@ -237,14 +238,53 @@ class AddressBookImpl implements IAddressBook { ]) . '?photo'; $result['PHOTO'] = 'VALUE=uri:' . $url; + + } else if ($property->name === 'X-SOCIALPROFILE') { + $type = $this->getTypeFromProperty($property); + + // Type is the social network, when it's empty we don't need this. + if ($type !== null) { + if (!isset($result[$property->name])) { + $result[$property->name] = []; + } + $result[$property->name][$type] = $property->getValue(); + } + + // The following properties can be set multiple times + } else if (in_array($property->name, ['CLOUD', 'EMAIL', 'IMPP', 'TEL', 'URL'])) { + if (!isset($result[$property->name])) { + $result[$property->name] = []; + } + + $result[$property->name][] = $property->getValue(); + } else { $result[$property->name] = $property->getValue(); } } + if ($this->addressBookInfo['principaluri'] === 'principals/system/system' && $this->addressBookInfo['uri'] === 'system') { $result['isLocalSystemBook'] = true; } return $result; } + + /** + * Get the type of the current property + * + * @param Property $property + * @return null|string + */ + protected function getTypeFromProperty(Property $property) { + $parameters = $property->parameters(); + // Type is the social network, when it's empty we don't need this. + if (isset($parameters['TYPE'])) { + /** @var \Sabre\VObject\Parameter $type */ + $type = $parameters['TYPE']; + return $type->getValue(); + } + + return null; + } } diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index c472f75b6bf..dd5f958ed4c 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -324,18 +324,13 @@ class FilesPlugin extends ServerPlugin { return $displayName; }); - $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { - if ($node->getPath() === '/') { - return $this->config->getSystemValue('data-fingerprint', ''); - } - }); - $propFind->handle(self::HAS_PREVIEW_PROPERTYNAME, function () use ($node) { return json_encode($this->previewManager->isAvailable($node->getFileInfo())); }); } - if ($node instanceof \OCA\DAV\Files\FilesHome) { + if ($node instanceof \OCA\DAV\Connector\Sabre\Node + || $node instanceof \OCA\DAV\Files\FilesHome) { $propFind->handle(self::DATA_FINGERPRINT_PROPERTYNAME, function() use ($node) { return $this->config->getSystemValue('data-fingerprint', ''); }); diff --git a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php index ba8527dc76e..fa3cae27dec 100644 --- a/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php +++ b/apps/dav/tests/unit/CardDAV/AddressBookImplTest.php @@ -60,7 +60,9 @@ class AddressBookImplTest extends TestCase { $this->addressBookInfo = [ 'id' => 42, - '{DAV:}displayname' => 'display name' + 'uri' => 'system', + 'principaluri' => 'principals/system/system', + '{DAV:}displayname' => 'display name', ]; $this->addressBook = $this->getMockBuilder('OCA\DAV\CardDAV\AddressBook') ->disableOriginalConstructor()->getMock(); @@ -306,4 +308,65 @@ class AddressBookImplTest extends TestCase { $this->assertSame($expectedVCardSerialized, $resultSerialized); } + public function testVCard2Array() { + $vCard = new VCard(); + + $vCard->add($vCard->createProperty('FN', 'Full Name')); + + // Multi-value properties + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user1@localhost')); + $vCard->add($vCard->createProperty('CLOUD', 'cloud-user2@example.tld')); + $vCard->add($vCard->createProperty('EMAIL', 'email-user1@localhost')); + $vCard->add($vCard->createProperty('EMAIL', 'email-user2@example.tld')); + $vCard->add($vCard->createProperty('IMPP', 'impp-user1@localhost')); + $vCard->add($vCard->createProperty('IMPP', 'impp-user2@example.tld')); + $vCard->add($vCard->createProperty('TEL', '+49 123456789')); + $vCard->add($vCard->createProperty('TEL', '+1 555 123456789')); + $vCard->add($vCard->createProperty('URL', 'https://localhost')); + $vCard->add($vCard->createProperty('URL', 'https://example.tld')); + + // Type depending properties + $property = $vCard->createProperty('X-SOCIALPROFILE', 'tw-example'); + $property->add('TYPE', 'twitter'); + $vCard->add($property); + $property = $vCard->createProperty('X-SOCIALPROFILE', 'fb-example'); + $property->add('TYPE', 'facebook'); + $vCard->add($property); + + $array = $this->invokePrivate($this->addressBookImpl, 'vCard2Array', ['uri', $vCard]); + unset($array['PRODID']); + + $this->assertEquals([ + 'URI' => 'uri', + 'VERSION' => '3.0', + 'FN' => 'Full Name', + 'CLOUD' => [ + 'cloud-user1@localhost', + 'cloud-user2@example.tld', + ], + 'EMAIL' => [ + 'email-user1@localhost', + 'email-user2@example.tld', + ], + 'IMPP' => [ + 'impp-user1@localhost', + 'impp-user2@example.tld', + ], + 'TEL' => [ + '+49 123456789', + '+1 555 123456789', + ], + 'URL' => [ + 'https://localhost', + 'https://example.tld', + ], + + 'X-SOCIALPROFILE' => [ + 'twitter'=> 'tw-example', + 'facebook'=> 'fb-example', + ], + + 'isLocalSystemBook' => true, + ], $array); + } } diff --git a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php index 6630c027541..e2d63868af0 100644 --- a/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/FilesPluginTest.php @@ -213,7 +213,8 @@ class FilesPluginTest extends TestCase { $this->assertEquals('http://example.com/', $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); $this->assertEquals('foo', $propFind->get(self::OWNER_ID_PROPERTYNAME)); $this->assertEquals('M. Foo', $propFind->get(self::OWNER_DISPLAY_NAME_PROPERTYNAME)); - $this->assertEquals([self::SIZE_PROPERTYNAME, self::DATA_FINGERPRINT_PROPERTYNAME], $propFind->get404Properties()); + $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME)); + $this->assertEquals([self::SIZE_PROPERTYNAME], $propFind->get404Properties()); } public function testGetPropertiesForFileHome() { @@ -357,7 +358,8 @@ class FilesPluginTest extends TestCase { $this->assertEquals(1025, $propFind->get(self::SIZE_PROPERTYNAME)); $this->assertEquals('DWCKMSR', $propFind->get(self::PERMISSIONS_PROPERTYNAME)); $this->assertEquals(null, $propFind->get(self::DOWNLOADURL_PROPERTYNAME)); - $this->assertEquals([self::DOWNLOADURL_PROPERTYNAME, self::DATA_FINGERPRINT_PROPERTYNAME], $propFind->get404Properties()); + $this->assertEquals('my_fingerprint', $propFind->get(self::DATA_FINGERPRINT_PROPERTYNAME)); + $this->assertEquals([self::DOWNLOADURL_PROPERTYNAME], $propFind->get404Properties()); } public function testGetPropertiesForRootDirectory() { diff --git a/apps/federatedfilesharing/appinfo/routes.php b/apps/federatedfilesharing/appinfo/routes.php index a4f56e372c9..9caaa939348 100644 --- a/apps/federatedfilesharing/appinfo/routes.php +++ b/apps/federatedfilesharing/appinfo/routes.php @@ -26,5 +26,14 @@ return [ 'routes' => [ ['name' => 'MountPublicLink#createFederatedShare', 'url' => '/createFederatedShare', 'verb' => 'POST'], ['name' => 'MountPublicLink#askForFederatedShare', 'url' => '/askForFederatedShare', 'verb' => 'POST'], - ] + ], + 'ocs' => [ + ['root' => '/cloud', 'name' => 'RequestHandler#createShare', 'url' => '/shares', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#reShare', 'url' => '/shares/{id}/reshare', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#updatePermissions', 'url' => '/shares/{id}/permissions', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#acceptShare', 'url' => '/shares/{id}/accept', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#declineShare', 'url' => '/shares/{id}/decline', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#unshare', 'url' => '/shares/{id}/unshare', 'verb' => 'POST'], + ['root' => '/cloud', 'name' => 'RequestHandler#revoke', 'url' => '/shares/{id}/revoke', 'verb' => 'POST'], + ], ]; diff --git a/apps/federatedfilesharing/lib/AppInfo/Application.php b/apps/federatedfilesharing/lib/AppInfo/Application.php index b767a322505..b470bb3e584 100644 --- a/apps/federatedfilesharing/lib/AppInfo/Application.php +++ b/apps/federatedfilesharing/lib/AppInfo/Application.php @@ -24,7 +24,12 @@ namespace OCA\FederatedFileSharing\AppInfo; +use OC\AppFramework\Utility\SimpleContainer; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\Controller\RequestHandlerController; use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\FederatedFileSharing\Notifications; +use OCA\FederatedFileSharing\RequestHandler; use OCP\AppFramework\App; class Application extends App { @@ -34,6 +39,35 @@ class Application extends App { public function __construct() { parent::__construct('federatedfilesharing'); + + $container = $this->getContainer(); + $server = $container->getServer(); + + $container->registerService('RequestHandlerController', function(SimpleContainer $c) use ($server) { + $addressHandler = new AddressHandler( + $server->getURLGenerator(), + $server->getL10N('federatedfilesharing') + ); + $notification = new Notifications( + $addressHandler, + $server->getHTTPClientService(), + new \OCA\FederatedFileSharing\DiscoveryManager( + $server->getMemCacheFactory(), + $server->getHTTPClientService() + ), + \OC::$server->getJobList() + ); + return new RequestHandlerController( + $c->query('AppName'), + $server->getRequest(), + $this->getFederatedShareProvider(), + $server->getDatabaseConnection(), + $server->getShareManager(), + $notification, + $addressHandler, + $server->getUserManager() + ); + }); } /** diff --git a/apps/federatedfilesharing/lib/RequestHandler.php b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php index f531c7bcb4a..9a41962ee3a 100644 --- a/apps/federatedfilesharing/lib/RequestHandler.php +++ b/apps/federatedfilesharing/lib/Controller/RequestHandlerController.php @@ -24,25 +24,28 @@ * */ -namespace OCA\FederatedFileSharing; +namespace OCA\FederatedFileSharing\Controller; +use OCA\FederatedFileSharing\DiscoveryManager; use OCA\Files_Sharing\Activity; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\FederatedFileSharing\FederatedShareProvider; +use OCA\FederatedFileSharing\Notifications; use OCP\AppFramework\Http; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSException; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; use OCP\Constants; use OCP\Files\NotFoundException; use OCP\IDBConnection; use OCP\IRequest; use OCP\IUserManager; use OCP\Share; +use OCP\Share\IShare; -/** - * Class RequestHandler - * - * handles OCS Request to the federated share API - * - * @package OCA\FederatedFileSharing\API - */ -class RequestHandler { +class RequestHandlerController extends OCSController { /** @var FederatedShareProvider */ private $federatedShareProvider; @@ -53,9 +56,6 @@ class RequestHandler { /** @var Share\IManager */ private $shareManager; - /** @var IRequest */ - private $request; - /** @var Notifications */ private $notifications; @@ -71,41 +71,47 @@ class RequestHandler { /** * Server2Server constructor. * + * @param string $appName + * @param IRequest $request * @param FederatedShareProvider $federatedShareProvider * @param IDBConnection $connection * @param Share\IManager $shareManager - * @param IRequest $request * @param Notifications $notifications * @param AddressHandler $addressHandler * @param IUserManager $userManager */ - public function __construct(FederatedShareProvider $federatedShareProvider, + public function __construct($appName, + IRequest $request, + FederatedShareProvider $federatedShareProvider, IDBConnection $connection, Share\IManager $shareManager, - IRequest $request, Notifications $notifications, AddressHandler $addressHandler, IUserManager $userManager ) { + parent::__construct($appName, $request); + $this->federatedShareProvider = $federatedShareProvider; $this->connection = $connection; $this->shareManager = $shareManager; - $this->request = $request; $this->notifications = $notifications; $this->addressHandler = $addressHandler; $this->userManager = $userManager; } /** + * @NoCSRFRequired + * @PublicPage + * * create a new share * - * @param array $params - * @return \OC_OCS_Result + * @return Http\DataResponse + * @throws OCSException */ - public function createShare($params) { + public function createShare() { if (!$this->isS2SEnabled(true)) { - return new \OC_OCS_Result(null, 503, 'Server does not support federated cloud sharing'); + throw new OCSException('Server does not support federated cloud sharing', 503); } $remote = isset($_POST['remote']) ? $_POST['remote'] : null; @@ -121,7 +127,7 @@ class RequestHandler { if ($remote && $token && $name && $owner && $remoteId && $shareWith) { if(!\OCP\Util::isValidFileName($name)) { - return new \OC_OCS_Result(null, 400, 'The mountpoint name contains invalid characters.'); + throw new OCSException('The mountpoint name contains invalid characters.', 400); } // FIXME this should be a method in the user management instead @@ -134,7 +140,7 @@ class RequestHandler { \OCP\Util::writeLog('files_sharing', 'shareWith after, ' . $shareWith, \OCP\Util::DEBUG); if (!\OCP\User::userExists($shareWith)) { - return new \OC_OCS_Result(null, 400, 'User does not exists'); + throw new OCSException('User does not exists', 400); } \OC_Util::setupFS($shareWith); @@ -192,25 +198,30 @@ class RequestHandler { $notificationManager->notify($notification); - return new \OC_OCS_Result(); + return new Http\DataResponse(); } catch (\Exception $e) { \OCP\Util::writeLog('files_sharing', 'server can not add remote share, ' . $e->getMessage(), \OCP\Util::ERROR); - return new \OC_OCS_Result(null, 500, 'internal server error, was not able to add share from ' . $remote); + throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); } } - return new \OC_OCS_Result(null, 400, 'server can not add remote share, missing parameter'); + throw new OCSException('server can not add remote share, missing parameter', 400); } /** + * @NoCSRFRequired + * @PublicPage + * * create re-share on behalf of another user * - * @param $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSBadRequestException + * @throws OCSForbiddenException + * @throws OCSNotFoundException */ - public function reShare($params) { + public function reShare($id) { - $id = isset($params['id']) ? (int)$params['id'] : null; $token = $this->request->getParam('token', null); $shareWith = $this->request->getParam('shareWith', null); $permission = (int)$this->request->getParam('permission', null); @@ -222,13 +233,13 @@ class RequestHandler { $permission === null || $remoteId === null ) { - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); + throw new OCSBadRequestException(); } try { $share = $this->federatedShareProvider->getShareById($id); } catch (Share\Exceptions\ShareNotFound $e) { - return new \OC_OCS_Result(null, Http::STATUS_NOT_FOUND); + throw new OCSNotFoundException(); } // don't allow to share a file back to the owner @@ -236,7 +247,7 @@ class RequestHandler { $owner = $share->getShareOwner(); $currentServer = $this->addressHandler->generateRemoteURL(); if ($this->addressHandler->compareAddresses($user, $remote,$owner , $currentServer)) { - return new \OC_OCS_Result(null, Http::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } if ($this->verifyShare($share, $token)) { @@ -250,37 +261,42 @@ class RequestHandler { try { $result = $this->federatedShareProvider->create($share); $this->federatedShareProvider->storeRemoteId((int)$result->getId(), $remoteId); - return new \OC_OCS_Result(['token' => $result->getToken(), 'remoteId' => $result->getId()]); + return new Http\DataResponse([ + 'token' => $result->getToken(), + 'remoteId' => $result->getId() + ]); } catch (\Exception $e) { - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); + throw new OCSBadRequestException(); } } else { - return new \OC_OCS_Result(null, Http::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } } - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); - + throw new OCSBadRequestException(); } /** + * @NoCSRFRequired + * @PublicPage + * * accept server-to-server share * - * @param array $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSException */ - public function acceptShare($params) { + public function acceptShare($id) { if (!$this->isS2SEnabled()) { - return new \OC_OCS_Result(null, 503, 'Server does not support federated cloud sharing'); + throw new OCSException('Server does not support federated cloud sharing', 503); } - $id = $params['id']; $token = isset($_POST['token']) ? $_POST['token'] : null; try { $share = $this->federatedShareProvider->getShareById($id); } catch (Share\Exceptions\ShareNotFound $e) { - return new \OC_OCS_Result(); + return new Http\DataResponse(); } if ($this->verifyShare($share, $token)) { @@ -292,7 +308,7 @@ class RequestHandler { } } - return new \OC_OCS_Result(); + return new Http\DataResponse(); } protected function executeAcceptShare(Share\IShare $share) { @@ -309,24 +325,27 @@ class RequestHandler { } /** + * @NoCSRFRequired + * @PublicPage + * * decline server-to-server share * - * @param array $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSException */ - public function declineShare($params) { + public function declineShare($id) { if (!$this->isS2SEnabled()) { - return new \OC_OCS_Result(null, 503, 'Server does not support federated cloud sharing'); + throw new OCSException('Server does not support federated cloud sharing', 503); } - $id = (int)$params['id']; $token = isset($_POST['token']) ? $_POST['token'] : null; try { $share = $this->federatedShareProvider->getShareById($id); } catch (Share\Exceptions\ShareNotFound $e) { - return new \OC_OCS_Result(); + return new Http\DataResponse(); } if($this->verifyShare($share, $token)) { @@ -338,7 +357,7 @@ class RequestHandler { $this->executeDeclineShare($share); } - return new \OC_OCS_Result(); + return new Http\DataResponse(); } /** @@ -376,18 +395,21 @@ class RequestHandler { } /** + * @NoCSRFRequired + * @PublicPage + * * remove server-to-server share if it was unshared by the owner * - * @param array $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSException */ - public function unshare($params) { + public function unshare($id) { if (!$this->isS2SEnabled()) { - return new \OC_OCS_Result(null, 503, 'Server does not support federated cloud sharing'); + throw new OCSException('Server does not support federated cloud sharing', 503); } - $id = $params['id']; $token = isset($_POST['token']) ? $_POST['token'] : null; $query = \OCP\DB::prepare('SELECT * FROM `*PREFIX*share_external` WHERE `remote_id` = ? AND `share_token` = ?'); @@ -423,7 +445,7 @@ class RequestHandler { '', '', $user, Activity::TYPE_REMOTE_SHARE, Activity::PRIORITY_MEDIUM); } - return new \OC_OCS_Result(); + return new Http\DataResponse(); } private function cleanupRemote($remote) { @@ -434,24 +456,26 @@ class RequestHandler { /** + * @NoCSRFRequired + * @PublicPage + * * federated share was revoked, either by the owner or the re-sharer * - * @param $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSBadRequestException */ - public function revoke($params) { - $id = (int)$params['id']; + public function revoke($id) { $token = $this->request->getParam('token'); $share = $this->federatedShareProvider->getShareById($id); if ($this->verifyShare($share, $token)) { $this->federatedShareProvider->removeShareFromTable($share); - return new \OC_OCS_Result(); + return new Http\DataResponse(); } - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); - + throw new OCSBadRequestException(); } /** @@ -537,20 +561,23 @@ class RequestHandler { } /** + * @NoCSRFRequired + * @PublicPage + * * update share information to keep federated re-shares in sync * - * @param array $params - * @return \OC_OCS_Result + * @param int $id + * @return Http\DataResponse + * @throws OCSBadRequestException */ - public function updatePermissions($params) { - $id = (int)$params['id']; + public function updatePermissions($id) { $token = $this->request->getParam('token', null); $permissions = $this->request->getParam('permissions', null); try { $share = $this->federatedShareProvider->getShareById($id); } catch (Share\Exceptions\ShareNotFound $e) { - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); + throw new OCSBadRequestException(); } $validPermission = ctype_digit($permissions); @@ -558,10 +585,10 @@ class RequestHandler { if ($validPermission && $validToken) { $this->updatePermissionsInDatabase($share, (int)$permissions); } else { - return new \OC_OCS_Result(null, Http::STATUS_BAD_REQUEST); + throw new OCSBadRequestException(); } - return new \OC_OCS_Result(); + return new Http\DataResponse(); } /** diff --git a/apps/federatedfilesharing/tests/RequestHandlerTest.php b/apps/federatedfilesharing/tests/Controller/RequestHandlerControllerTest.php index 8f9f1384184..8e1000fb500 100644 --- a/apps/federatedfilesharing/tests/RequestHandlerTest.php +++ b/apps/federatedfilesharing/tests/Controller/RequestHandlerControllerTest.php @@ -30,7 +30,7 @@ namespace OCA\FederatedFileSharing\Tests; use OC\Files\Filesystem; use OCA\FederatedFileSharing\DiscoveryManager; use OCA\FederatedFileSharing\FederatedShareProvider; -use OCA\FederatedFileSharing\RequestHandler; +use OCA\FederatedFileSharing\Controller\RequestHandlerController; use OCP\IUserManager; use OCP\Share\IShare; @@ -40,7 +40,7 @@ use OCP\Share\IShare; * @package OCA\FederatedFileSharing\Tests * @group DB */ -class RequestHandlerTest extends TestCase { +class RequestHandlerControllerTest extends TestCase { const TEST_FOLDER_NAME = '/folder_share_api_test'; @@ -50,23 +50,23 @@ class RequestHandlerTest extends TestCase { private $connection; /** - * @var RequestHandler + * @var RequestHandlerController */ private $s2s; - /** @var \OCA\FederatedFileSharing\FederatedShareProvider | PHPUnit_Framework_MockObject_MockObject */ + /** @var \OCA\FederatedFileSharing\FederatedShareProvider|\PHPUnit_Framework_MockObject_MockObject */ private $federatedShareProvider; - /** @var \OCA\FederatedFileSharing\Notifications | PHPUnit_Framework_MockObject_MockObject */ + /** @var \OCA\FederatedFileSharing\Notifications|\PHPUnit_Framework_MockObject_MockObject */ private $notifications; - /** @var \OCA\FederatedFileSharing\AddressHandler | PHPUnit_Framework_MockObject_MockObject */ + /** @var \OCA\FederatedFileSharing\AddressHandler|\PHPUnit_Framework_MockObject_MockObject */ private $addressHandler; - /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ private $userManager; - /** @var IShare | \PHPUnit_Framework_MockObject_MockObject */ + /** @var IShare|\PHPUnit_Framework_MockObject_MockObject */ private $share; protected function setUp() { @@ -77,12 +77,12 @@ class RequestHandlerTest extends TestCase { $config = $this->getMockBuilder('\OCP\IConfig') ->disableOriginalConstructor()->getMock(); - $clientService = $this->getMock('\OCP\Http\Client\IClientService'); + $clientService = $this->getMockBuilder('\OCP\Http\Client\IClientService')->getMock(); $httpHelperMock = $this->getMockBuilder('\OC\HTTPHelper') ->setConstructorArgs([$config, $clientService]) ->getMock(); $httpHelperMock->expects($this->any())->method('post')->with($this->anything())->will($this->returnValue(true)); - $this->share = $this->getMock('\OCP\Share\IShare'); + $this->share = $this->getMockBuilder('\OCP\Share\IShare')->getMock(); $this->federatedShareProvider = $this->getMockBuilder('OCA\FederatedFileSharing\FederatedShareProvider') ->disableOriginalConstructor()->getMock(); $this->federatedShareProvider->expects($this->any()) @@ -96,15 +96,16 @@ class RequestHandlerTest extends TestCase { ->disableOriginalConstructor()->getMock(); $this->addressHandler = $this->getMockBuilder('OCA\FederatedFileSharing\AddressHandler') ->disableOriginalConstructor()->getMock(); - $this->userManager = $this->getMock('OCP\IUserManager'); + $this->userManager = $this->getMockBuilder('OCP\IUserManager')->getMock(); $this->registerHttpHelper($httpHelperMock); - $this->s2s = new RequestHandler( + $this->s2s = new RequestHandlerController( + 'federatedfilesharing', + \OC::$server->getRequest(), $this->federatedShareProvider, \OC::$server->getDatabaseConnection(), \OC::$server->getShareManager(), - \OC::$server->getRequest(), $this->notifications, $this->addressHandler, $this->userManager @@ -127,7 +128,7 @@ class RequestHandlerTest extends TestCase { /** * Register an http helper mock for testing purposes. - * @param $httpHelper http helper mock + * @param \OC\HTTPHelper $httpHelper helper mock */ private function registerHttpHelper($httpHelper) { $this->oldHttpHelper = \OC::$server->query('HTTPHelper'); @@ -158,9 +159,7 @@ class RequestHandlerTest extends TestCase { $_POST['shareWith'] = self::TEST_FILES_SHARING_API_USER2; $_POST['remoteId'] = 1; - $result = $this->s2s->createShare(null); - - $this->assertTrue($result->succeeded()); + $this->s2s->createShare(null); $query = \OCP\DB::prepare('SELECT * FROM `*PREFIX*share_external` WHERE `remote_id` = ?'); $result = $query->execute(array('1')); @@ -178,13 +177,14 @@ class RequestHandlerTest extends TestCase { function testDeclineShare() { - $this->s2s = $this->getMockBuilder('\OCA\FederatedFileSharing\RequestHandler') + $this->s2s = $this->getMockBuilder('\OCA\FederatedFileSharing\Controller\RequestHandlerController') ->setConstructorArgs( [ + 'federatedfilessharing', + \OC::$server->getRequest(), $this->federatedShareProvider, \OC::$server->getDatabaseConnection(), \OC::$server->getShareManager(), - \OC::$server->getRequest(), $this->notifications, $this->addressHandler, $this->userManager @@ -197,7 +197,7 @@ class RequestHandlerTest extends TestCase { $_POST['token'] = 'token'; - $this->s2s->declineShare(array('id' => 42)); + $this->s2s->declineShare(42); } diff --git a/apps/federation/appinfo/routes.php b/apps/federation/appinfo/routes.php index 03acc60c682..b9515812a01 100644 --- a/apps/federation/appinfo/routes.php +++ b/apps/federation/appinfo/routes.php @@ -41,8 +41,18 @@ $application->registerRoutes( 'url' => '/auto-add-servers', 'verb' => 'POST' ], - ] + ], + 'ocs' => [ + [ + 'name' => 'OCSAuthAPI#getSharedSecret', + 'url' => '/api/v1/shared-secret', + 'verb' => 'GET', + ], + [ + 'name' => 'OCSAuthAPI#requestSharedSecret', + 'url' => '/api/v1/request-shared-secret', + 'verb' => 'POST', + ], + ], ] ); - -$application->registerOCSApi(); diff --git a/apps/federation/lib/API/OCSAuthAPI.php b/apps/federation/lib/Controller/OCSAuthAPIController.php index a22de155d4c..6cd3b1890ef 100644 --- a/apps/federation/lib/API/OCSAuthAPI.php +++ b/apps/federation/lib/Controller/OCSAuthAPIController.php @@ -25,11 +25,13 @@ */ -namespace OCA\Federation\API; +namespace OCA\Federation\Controller; use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\AppFramework\OCSController; use OCP\BackgroundJob\IJobList; use OCP\ILogger; use OCP\IRequest; @@ -40,12 +42,9 @@ use OCP\Security\ISecureRandom; * * OCS API end-points to exchange shared secret between two connected ownClouds * - * @package OCA\Federation\API + * @package OCA\Federation\Controller */ -class OCSAuthAPI { - - /** @var IRequest */ - private $request; +class OCSAuthAPIController extends OCSController{ /** @var ISecureRandom */ private $secureRandom; @@ -65,6 +64,7 @@ class OCSAuthAPI { /** * OCSAuthAPI constructor. * + * @param string $appName * @param IRequest $request * @param ISecureRandom $secureRandom * @param IJobList $jobList @@ -73,6 +73,7 @@ class OCSAuthAPI { * @param ILogger $logger */ public function __construct( + $appName, IRequest $request, ISecureRandom $secureRandom, IJobList $jobList, @@ -80,7 +81,8 @@ class OCSAuthAPI { DbHandler $dbHandler, ILogger $logger ) { - $this->request = $request; + parent::__construct($appName, $request); + $this->secureRandom = $secureRandom; $this->jobList = $jobList; $this->trustedServers = $trustedServers; @@ -89,18 +91,20 @@ class OCSAuthAPI { } /** + * @NoCSRFRequired + * @PublicPage + * * request received to ask remote server for a shared secret * - * @return \OC_OCS_Result + * @param string $url + * @param string $token + * @return Http\DataResponse + * @throws OCSForbiddenException */ - public function requestSharedSecret() { - - $url = $this->request->getParam('url'); - $token = $this->request->getParam('token'); - + public function requestSharedSecret($url, $token) { if ($this->trustedServers->isTrustedServer($url) === false) { $this->logger->error('remote server not trusted (' . $url . ') while requesting shared secret', ['app' => 'federation']); - return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } // if both server initiated the exchange of the shared secret the greater @@ -111,7 +115,7 @@ class OCSAuthAPI { 'remote server (' . $url . ') presented lower token. We will initiate the exchange of the shared secret.', ['app' => 'federation'] ); - return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } // we ask for the shared secret so we no longer have to ask the other server @@ -131,23 +135,24 @@ class OCSAuthAPI { ] ); - return new \OC_OCS_Result(null, Http::STATUS_OK); - + return new Http\DataResponse(); } /** + * @NoCSRFRequired + * @PublicPage + * * create shared secret and return it * - * @return \OC_OCS_Result + * @param string $url + * @param string $token + * @return Http\DataResponse + * @throws OCSForbiddenException */ - public function getSharedSecret() { - - $url = $this->request->getParam('url'); - $token = $this->request->getParam('token'); - + public function getSharedSecret($url, $token) { if ($this->trustedServers->isTrustedServer($url) === false) { $this->logger->error('remote server not trusted (' . $url . ') while getting shared secret', ['app' => 'federation']); - return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } if ($this->isValidToken($url, $token) === false) { @@ -156,7 +161,7 @@ class OCSAuthAPI { 'remote server (' . $url . ') didn\'t send a valid token (got "' . $token . '" but expected "'. $expectedToken . '") while getting shared secret', ['app' => 'federation'] ); - return new \OC_OCS_Result(null, HTTP::STATUS_FORBIDDEN); + throw new OCSForbiddenException(); } $sharedSecret = $this->secureRandom->generate(32); @@ -165,8 +170,9 @@ class OCSAuthAPI { // reset token after the exchange of the shared secret was successful $this->dbHandler->addToken($url, ''); - return new \OC_OCS_Result(['sharedSecret' => $sharedSecret], Http::STATUS_OK); - + return new Http\DataResponse([ + 'sharedSecret' => $sharedSecret + ]); } protected function isValidToken($url, $token) { diff --git a/apps/federation/tests/API/OCSAuthAPITest.php b/apps/federation/tests/Controller/OCSAuthAPIControllerTest.php index 7861e917ff8..2b231b4fca0 100644 --- a/apps/federation/tests/API/OCSAuthAPITest.php +++ b/apps/federation/tests/Controller/OCSAuthAPIControllerTest.php @@ -22,20 +22,21 @@ */ -namespace OCA\Federation\Tests\API; +namespace OCA\Federation\Tests\Controller; use OC\BackgroundJob\JobList; -use OCA\Federation\API\OCSAuthAPI; +use OCA\Federation\Controller\OCSAuthAPIController; use OCA\Federation\DbHandler; use OCA\Federation\TrustedServers; use OCP\AppFramework\Http; +use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\ILogger; use OCP\IRequest; use OCP\Security\ISecureRandom; use Test\TestCase; -class OCSAuthAPITest extends TestCase { +class OCSAuthAPIControllerTest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | IRequest */ private $request; @@ -55,14 +56,14 @@ class OCSAuthAPITest extends TestCase { /** @var \PHPUnit_Framework_MockObject_MockObject | ILogger */ private $logger; - /** @var OCSAuthApi */ + /** @var OCSAuthAPIController */ private $ocsAuthApi; public function setUp() { parent::setUp(); - $this->request = $this->getMock('OCP\IRequest'); - $this->secureRandom = $this->getMock('OCP\Security\ISecureRandom'); + $this->request = $this->getMockBuilder('OCP\IRequest')->getMock(); + $this->secureRandom = $this->getMockBuilder('OCP\Security\ISecureRandom')->getMock(); $this->trustedServers = $this->getMockBuilder('OCA\Federation\TrustedServers') ->disableOriginalConstructor()->getMock(); $this->dbHandler = $this->getMockBuilder('OCA\Federation\DbHandler') @@ -72,7 +73,8 @@ class OCSAuthAPITest extends TestCase { $this->logger = $this->getMockBuilder('OCP\ILogger') ->disableOriginalConstructor()->getMock(); - $this->ocsAuthApi = new OCSAuthAPI( + $this->ocsAuthApi = new OCSAuthAPIController( + 'federation', $this->request, $this->secureRandom, $this->jobList, @@ -89,21 +91,19 @@ class OCSAuthAPITest extends TestCase { * @param string $token * @param string $localToken * @param bool $isTrustedServer - * @param int $expected + * @param bool $ok */ - public function testRequestSharedSecret($token, $localToken, $isTrustedServer, $expected) { + public function testRequestSharedSecret($token, $localToken, $isTrustedServer, $ok) { $url = 'url'; - $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url); - $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token); $this->trustedServers ->expects($this->once()) ->method('isTrustedServer')->with($url)->willReturn($isTrustedServer); $this->dbHandler->expects($this->any()) ->method('getToken')->with($url)->willReturn($localToken); - if ($expected === Http::STATUS_OK) { + if ($ok) { $this->jobList->expects($this->once())->method('add') ->with('OCA\Federation\BackgroundJob\GetSharedSecret', ['url' => $url, 'token' => $token]); $this->jobList->expects($this->once())->method('remove') @@ -113,15 +113,19 @@ class OCSAuthAPITest extends TestCase { $this->jobList->expects($this->never())->method('remove'); } - $result = $this->ocsAuthApi->requestSharedSecret(); - $this->assertSame($expected, $result->getStatusCode()); + try { + $this->ocsAuthApi->requestSharedSecret($url, $token); + $this->assertTrue($ok); + } catch (OCSForbiddenException $e) { + $this->assertFalse($ok); + } } public function dataTestRequestSharedSecret() { return [ - ['token2', 'token1', true, Http::STATUS_OK], - ['token1', 'token2', false, Http::STATUS_FORBIDDEN], - ['token1', 'token2', true, Http::STATUS_FORBIDDEN], + ['token2', 'token1', true, true], + ['token1', 'token2', false, false], + ['token1', 'token2', true, false], ]; } @@ -130,20 +134,18 @@ class OCSAuthAPITest extends TestCase { * * @param bool $isTrustedServer * @param bool $isValidToken - * @param int $expected + * @param bool $ok */ - public function testGetSharedSecret($isTrustedServer, $isValidToken, $expected) { + public function testGetSharedSecret($isTrustedServer, $isValidToken, $ok) { $url = 'url'; $token = 'token'; - $this->request->expects($this->at(0))->method('getParam')->with('url')->willReturn($url); - $this->request->expects($this->at(1))->method('getParam')->with('token')->willReturn($token); - - /** @var OCSAuthAPI | \PHPUnit_Framework_MockObject_MockObject $ocsAuthApi */ - $ocsAuthApi = $this->getMockBuilder('OCA\Federation\API\OCSAuthAPI') + /** @var OCSAuthAPIController | \PHPUnit_Framework_MockObject_MockObject $ocsAuthApi */ + $ocsAuthApi = $this->getMockBuilder('OCA\Federation\Controller\OCSAuthAPIController') ->setConstructorArgs( [ + 'federation', $this->request, $this->secureRandom, $this->jobList, @@ -159,7 +161,7 @@ class OCSAuthAPITest extends TestCase { $ocsAuthApi->expects($this->any()) ->method('isValidToken')->with($url, $token)->willReturn($isValidToken); - if($expected === Http::STATUS_OK) { + if($ok) { $this->secureRandom->expects($this->once())->method('generate')->with(32) ->willReturn('secret'); $this->trustedServers->expects($this->once()) @@ -173,22 +175,22 @@ class OCSAuthAPITest extends TestCase { $this->dbHandler->expects($this->never())->method('addToken'); } - $result = $ocsAuthApi->getSharedSecret(); - - $this->assertSame($expected, $result->getStatusCode()); - - if ($expected === Http::STATUS_OK) { + try { + $result = $ocsAuthApi->getSharedSecret($url, $token); + $this->assertTrue($ok); $data = $result->getData(); $this->assertSame('secret', $data['sharedSecret']); + } catch (OCSForbiddenException $e) { + $this->assertFalse($ok); } } public function dataTestGetSharedSecret() { return [ - [true, true, Http::STATUS_OK], - [false, true, Http::STATUS_FORBIDDEN], - [true, false, Http::STATUS_FORBIDDEN], - [false, false, Http::STATUS_FORBIDDEN], + [true, true, true], + [false, true, false], + [true, false, false], + [false, false, false], ]; } diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index f191ade240b..ca41012764a 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -199,6 +199,7 @@ * @param options.folderDropOptions folder drop options, disabled by default * @param options.scrollTo name of file to scroll to after the first load * @param {OC.Files.Client} [options.filesClient] files API client + * @param {OC.Backbone.Model} [options.filesConfig] files app configuration * @private */ initialize: function($el, options) { @@ -212,6 +213,10 @@ this._filesConfig = options.config; } else if (!_.isUndefined(OCA.Files) && !_.isUndefined(OCA.Files.App)) { this._filesConfig = OCA.Files.App.getFilesConfig(); + } else { + this._filesConfig = new OC.Backbone.Model({ + 'showhidden': false + }); } if (options.dragOptions) { @@ -239,6 +244,7 @@ this._filesConfig.on('change:showhidden', function() { var showHidden = this.get('showhidden'); self.$el.toggleClass('hide-hidden-files', !showHidden); + self.updateSelectionSummary(); if (!showHidden) { // hiding files could make the page too small, need to try rendering next page @@ -264,7 +270,7 @@ this.files = []; this._selectedFiles = {}; - this._selectionSummary = new OCA.Files.FileSummary(); + this._selectionSummary = new OCA.Files.FileSummary(undefined, {config: this._filesConfig}); // dummy root dir info this.dirInfo = new OC.Files.FileInfo({}); @@ -2304,7 +2310,7 @@ var $tr = $('<tr class="summary"></tr>'); this.$el.find('tfoot').append($tr); - return new OCA.Files.FileSummary($tr); + return new OCA.Files.FileSummary($tr, {config: this._filesConfig}); }, updateEmptyContent: function() { var permissions = this.getDirectoryPermissions(); @@ -2451,6 +2457,7 @@ var summary = this._selectionSummary.summary; var selection; + var showHidden = !!this._filesConfig.get('showhidden'); if (summary.totalFiles === 0 && summary.totalDirs === 0) { this.$el.find('#headerName a.name>span:first').text(t('files','Name')); this.$el.find('#headerSize a>span:first').text(t('files','Size')); @@ -2477,6 +2484,11 @@ selection = fileInfo; } + if (!showHidden && summary.totalHidden > 0) { + var hiddenInfo = n('files', 'including %n hidden', 'including %n hidden', summary.totalHidden); + selection += ' (' + hiddenInfo + ')'; + } + this.$el.find('#headerName a.name>span:first').text(selection); this.$el.find('#modified a>span:first').text(''); this.$el.find('table').addClass('multiselect'); diff --git a/apps/files/js/filesummary.js b/apps/files/js/filesummary.js index a4cefe692a8..519718cfc82 100644 --- a/apps/files/js/filesummary.js +++ b/apps/files/js/filesummary.js @@ -20,6 +20,15 @@ */ (function() { + var INFO_TEMPLATE = + '<span class="info">' + + '<span class="dirinfo"></span>' + + '<span class="connector"> and </span>' + + '<span class="fileinfo"></span>' + + '<span class="hiddeninfo"></span>' + + '<span class="filter"></span>' + + '</span>'; + /** * The FileSummary class encapsulates the file summary values and * the logic to render it in the given container @@ -28,26 +37,51 @@ * @memberof OCA.Files * * @param $tr table row element + * @param {OC.Backbone.Model} [options.filesConfig] files app configuration */ - var FileSummary = function($tr) { + var FileSummary = function($tr, options) { + options = options || {}; + var self = this; this.$el = $tr; + var filesConfig = options.config; + if (filesConfig) { + this._showHidden = !!filesConfig.get('showhidden'); + filesConfig.on('change:showhidden', function() { + self._showHidden = !!this.get('showhidden'); + self.update(); + }); + } this.clear(); this.render(); }; FileSummary.prototype = { + _showHidden: null, + summary: { totalFiles: 0, totalDirs: 0, + totalHidden: 0, totalSize: 0, filter:'', sumIsPending:false }, /** + * Returns whether the given file info must be hidden + * + * @param {OC.Files.FileInfo} fileInfo file info + * + * @return {boolean} true if the file is a hidden file, false otherwise + */ + _isHiddenFile: function(file) { + return file.name && file.name.charAt(0) === '.'; + }, + + /** * Adds file - * @param file file to add - * @param update whether to update the display + * @param {OC.Files.FileInfo} file file to add + * @param {boolean} update whether to update the display */ add: function(file, update) { if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) { @@ -59,6 +93,10 @@ else { this.summary.totalFiles++; } + if (this._isHiddenFile(file)) { + this.summary.totalHidden++; + } + var size = parseInt(file.size, 10) || 0; if (size >=0) { this.summary.totalSize += size; @@ -71,8 +109,8 @@ }, /** * Removes file - * @param file file to remove - * @param update whether to update the display + * @param {OC.Files.FileInfo} file file to remove + * @param {boolean} update whether to update the display */ remove: function(file, update) { if (file.name && file.name.toLowerCase().indexOf(this.summary.filter) === -1) { @@ -84,6 +122,9 @@ else { this.summary.totalFiles--; } + if (this._isHiddenFile(file)) { + this.summary.totalHidden--; + } var size = parseInt(file.size, 10) || 0; if (size >=0) { this.summary.totalSize -= size; @@ -111,6 +152,7 @@ var summary = { totalDirs: 0, totalFiles: 0, + totalHidden: 0, totalSize: 0, filter: this.summary.filter, sumIsPending: false @@ -127,6 +169,9 @@ else { summary.totalFiles++; } + if (this._isHiddenFile(file)) { + summary.totalHidden++; + } var size = parseInt(file.size, 10) || 0; if (size >=0) { summary.totalSize += size; @@ -154,6 +199,13 @@ this.update(); }, + _infoTemplate: function(data) { + if (!this._infoTemplateCompiled) { + this._infoTemplateCompiled = Handlebars.compile(INFO_TEMPLATE); + } + return this._infoTemplateCompiled(data); + }, + /** * Renders the file summary element */ @@ -171,10 +223,12 @@ var $fileInfo = this.$el.find('.fileinfo'); var $connector = this.$el.find('.connector'); var $filterInfo = this.$el.find('.filter'); + var $hiddenInfo = this.$el.find('.hiddeninfo'); // Substitute old content with new translations $dirInfo.html(n('files', '%n folder', '%n folders', this.summary.totalDirs)); $fileInfo.html(n('files', '%n file', '%n files', this.summary.totalFiles)); + $hiddenInfo.html(' (' + n('files', 'including %n hidden', 'including %n hidden', this.summary.totalHidden) + ')'); var fileSize = this.summary.sumIsPending ? t('files', 'Pending') : OC.Util.humanFileSize(this.summary.totalSize); this.$el.find('.filesize').html(fileSize); @@ -194,6 +248,7 @@ if (this.summary.totalDirs > 0 && this.summary.totalFiles > 0) { $connector.removeClass('hidden'); } + $hiddenInfo.toggleClass('hidden', this.summary.totalHidden === 0 || this._showHidden) if (this.summary.filter === '') { $filterInfo.html(''); $filterInfo.addClass('hidden'); @@ -206,19 +261,7 @@ if (!this.$el) { return; } - // TODO: ideally this should be separate to a template or something var summary = this.summary; - var directoryInfo = n('files', '%n folder', '%n folders', summary.totalDirs); - var fileInfo = n('files', '%n file', '%n files', summary.totalFiles); - var filterInfo = ''; - if (this.summary.filter !== '') { - filterInfo = ' ' + n('files', 'matches \'{filter}\'', 'match \'{filter}\'', summary.totalFiles + summary.totalDirs, {filter: summary.filter}); - } - - var infoVars = { - dirs: '<span class="dirinfo">'+directoryInfo+'</span><span class="connector">', - files: '</span><span class="fileinfo">'+fileInfo+'</span>' - }; // don't show the filesize column, if filesize is NaN (e.g. in trashbin) var fileSize = ''; @@ -227,15 +270,14 @@ fileSize = '<td class="filesize">' + fileSize + '</td>'; } - var info = t('files', '{dirs} and {files}', infoVars, null, {'escape': false}); - - var $summary = $('<td><span class="info">'+info+'<span class="filter">'+filterInfo+'</span></span></td>'+fileSize+'<td class="date"></td>'); - - if (!this.summary.totalFiles && !this.summary.totalDirs) { - this.$el.addClass('hidden'); - } - + var $summary = $( + '<td>' + this._infoTemplate() + '</td>' + + fileSize + + '<td class="date"></td>' + ); + this.$el.addClass('hidden'); this.$el.append($summary); + this.update(); } }; OCA.Files.FileSummary = FileSummary; diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php index 25933ae25aa..0234fb435a7 100644 --- a/apps/files/lib/Command/Scan.php +++ b/apps/files/lib/Command/Scan.php @@ -28,9 +28,11 @@ namespace OCA\Files\Command; +use Doctrine\DBAL\Connection; use OC\Core\Command\Base; use OC\ForbiddenException; use OCP\Files\StorageNotAvailableException; +use OCP\IDBConnection; use OCP\IUserManager; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -106,7 +108,8 @@ class Scan extends Base { } protected function scanFiles($user, $path, $verbose, OutputInterface $output, $backgroundScan = false) { - $scanner = new \OC\Files\Utils\Scanner($user, \OC::$server->getDatabaseConnection(), \OC::$server->getLogger()); + $connection = $this->reconnectToDatabase($output); + $scanner = new \OC\Files\Utils\Scanner($user, $connection, \OC::$server->getLogger()); # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception # printout and count if ($verbose) { @@ -318,4 +321,26 @@ class Scan extends Base { return date('H:i:s', $secs); } + /** + * @return \OCP\IDBConnection + */ + protected function reconnectToDatabase(OutputInterface $output) { + /** @var Connection | IDBConnection $connection*/ + $connection = \OC::$server->getDatabaseConnection(); + try { + $connection->close(); + } catch (\Exception $ex) { + $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>"); + } + while (!$connection->isConnected()) { + try { + $connection->connect(); + } catch (\Exception $ex) { + $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>"); + sleep(60); + } + } + return $connection; + } + } diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js index cf9f43f2d59..0a4812f3a81 100644 --- a/apps/files/tests/js/filelistSpec.js +++ b/apps/files/tests/js/filelistSpec.js @@ -385,8 +385,9 @@ describe('OCA.Files.FileList tests', function() { $summary = $('#filestable .summary'); expect($summary.hasClass('hidden')).toEqual(false); // yes, ugly... - expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.connector').hasClass('hidden')).toEqual(true); expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.filesize').text()).toEqual('12 B'); expect($('#filestable thead th').hasClass('hidden')).toEqual(false); @@ -456,7 +457,8 @@ describe('OCA.Files.FileList tests', function() { $summary = $('#filestable .summary'); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual('1 folder and 2 files'); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('2 files'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.filesize').text()).toEqual('69 KB'); @@ -511,7 +513,8 @@ describe('OCA.Files.FileList tests', function() { $summary = $('#filestable .summary'); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual('1 folder and 1 file'); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.fileinfo').hasClass('hidden')).toEqual(false); expect($summary.find('.filesize').text()).toEqual('57 KB'); @@ -677,12 +680,14 @@ describe('OCA.Files.FileList tests', function() { deferredRename.resolve(201); - expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('3 files'); }); it('Leaves the summary alone when cancel renaming', function() { var $summary = $('#filestable .summary'); doCancelRename(); - expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('3 files'); }); it('Shows busy state while rename in progress', function() { var $tr; @@ -856,11 +861,14 @@ describe('OCA.Files.FileList tests', function() { }); var $tr = fileList.add(fileData); - expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); var model = fileList.getModelForFile('test file'); model.set({size: '100'}); - expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); }); }) describe('List rendering', function() { @@ -877,7 +885,8 @@ describe('OCA.Files.FileList tests', function() { fileList.setFiles(testFiles); $summary = $('#filestable .summary'); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual('1 folder and 3 files'); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('3 files'); expect($summary.find('.filesize').text()).toEqual('69 KB'); }); it('shows headers, summary and hide empty content message after setting files', function(){ @@ -962,10 +971,12 @@ describe('OCA.Files.FileList tests', function() { fileList.setFiles([testFiles[0]]); $summary = $('#filestable .summary'); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); fileList.remove('unexist.txt'); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual('0 folders and 1 file'); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); }); }); describe('Filtered list rendering', function() { @@ -987,14 +998,18 @@ describe('OCA.Files.FileList tests', function() { expect($('#fileList tr:not(.hidden)').length).toEqual(3); expect(fileList.files.length).toEqual(4); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("1 folder and 2 files match 'e'"); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('2 files'); + expect($summary.find('.filter').text()).toEqual(" match 'e'"); expect($nofilterresults.hasClass('hidden')).toEqual(true); fileList.setFilter('ee'); expect($('#fileList tr:not(.hidden)').length).toEqual(1); expect(fileList.files.length).toEqual(4); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches 'ee'"); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); + expect($summary.find('.filter').text()).toEqual(" matches 'ee'"); expect($nofilterresults.hasClass('hidden')).toEqual(true); fileList.setFilter('eee'); @@ -1007,21 +1022,26 @@ describe('OCA.Files.FileList tests', function() { expect($('#fileList tr:not(.hidden)').length).toEqual(1); expect(fileList.files.length).toEqual(4); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches 'ee'"); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); + expect($summary.find('.filter').text()).toEqual(" matches 'ee'"); expect($nofilterresults.hasClass('hidden')).toEqual(true); fileList.setFilter('e'); expect($('#fileList tr:not(.hidden)').length).toEqual(3); expect(fileList.files.length).toEqual(4); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("1 folder and 2 files match 'e'"); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('2 files'); + expect($summary.find('.filter').text()).toEqual(" match 'e'"); expect($nofilterresults.hasClass('hidden')).toEqual(true); fileList.setFilter(''); expect($('#fileList tr:not(.hidden)').length).toEqual(4); expect(fileList.files.length).toEqual(4); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("1 folder and 3 files"); + expect($summary.find('.dirinfo').text()).toEqual('1 folder'); + expect($summary.find('.fileinfo').text()).toEqual('3 files'); expect($nofilterresults.hasClass('hidden')).toEqual(true); }); it('filters the list of non-rendered rows using filter()', function() { @@ -1032,7 +1052,9 @@ describe('OCA.Files.FileList tests', function() { fileList.setFilter('63'); expect($('#fileList tr:not(.hidden)').length).toEqual(1); expect($summary.hasClass('hidden')).toEqual(false); - expect($summary.find('.info').text()).toEqual("0 folders and 1 file matches '63'"); + expect($summary.find('.dirinfo').hasClass('hidden')).toEqual(true); + expect($summary.find('.fileinfo').text()).toEqual('1 file'); + expect($summary.find('.filter').text()).toEqual(" matches '63'"); expect($nofilterresults.hasClass('hidden')).toEqual(true); }); it('hides the emptyfiles notice when using filter()', function() { @@ -1654,6 +1676,18 @@ describe('OCA.Files.FileList tests', function() { $('#fileList tr td.filename input:checkbox').click(); expect($('.select-all').prop('checked')).toEqual(false); }); + it('Selecting all files also selects hidden files when invisible', function() { + filesConfig.set('showhidden', false); + var $tr = fileList.add(new FileInfo({ + name: '.hidden', + type: 'dir', + mimetype: 'httpd/unix-directory', + size: 150 + })); + $('.select-all').click(); + expect($tr.find('td.filename input:checkbox').prop('checked')).toEqual(true); + expect(_.pluck(fileList.getSelectedFiles(), 'name')).toContain('.hidden'); + }); it('Clicking "select all" will select/deselect all files', function() { fileList.setFiles(generateFiles(0, 41)); $('.select-all').click(); @@ -1731,6 +1765,44 @@ describe('OCA.Files.FileList tests', function() { fileList.findFileEl('One.txt').find('input:checkbox').click().click(); expect($summary.text()).toEqual('Name'); }); + it('Displays the number of hidden files in selection summary if hidden files are invisible', function() { + filesConfig.set('showhidden', false); + var $tr = fileList.add(new FileInfo({ + name: '.hidden', + type: 'dir', + mimetype: 'httpd/unix-directory', + size: 150 + })); + $('.select-all').click(); + var $summary = $('#headerName a.name>span:first'); + expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)'); + }); + it('Does not displays the number of hidden files in selection summary if hidden files are visible', function() { + filesConfig.set('showhidden', true); + var $tr = fileList.add(new FileInfo({ + name: '.hidden', + type: 'dir', + mimetype: 'httpd/unix-directory', + size: 150 + })); + $('.select-all').click(); + var $summary = $('#headerName a.name>span:first'); + expect($summary.text()).toEqual('2 folders and 3 files'); + }); + it('Toggling hidden file visibility updates selection summary', function() { + filesConfig.set('showhidden', false); + var $tr = fileList.add(new FileInfo({ + name: '.hidden', + type: 'dir', + mimetype: 'httpd/unix-directory', + size: 150 + })); + $('.select-all').click(); + var $summary = $('#headerName a.name>span:first'); + expect($summary.text()).toEqual('2 folders and 3 files (including 1 hidden)'); + filesConfig.set('showhidden', true); + expect($summary.text()).toEqual('2 folders and 3 files'); + }); it('Select/deselect files shows/hides file actions', function() { var $actions = $('#headerName .selectedActions'); var $checkbox = fileList.findFileEl('One.txt').find('input:checkbox'); diff --git a/apps/files/tests/js/filesummarySpec.js b/apps/files/tests/js/filesummarySpec.js index ec94c28acb6..e3f24d9ad43 100644 --- a/apps/files/tests/js/filesummarySpec.js +++ b/apps/files/tests/js/filesummarySpec.js @@ -39,7 +39,8 @@ describe('OCA.Files.FileSummary tests', function() { totalSize: 256000 }); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('5 folders and 2 files'); + expect($container.find('.dirinfo').text()).toEqual('5 folders'); + expect($container.find('.fileinfo').text()).toEqual('2 files'); expect($container.find('.filesize').text()).toEqual('250 KB'); }); it('hides summary when no files or folders', function() { @@ -62,7 +63,8 @@ describe('OCA.Files.FileSummary tests', function() { s.add({type: 'dir', size: 100}); s.update(); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('6 folders and 3 files'); + expect($container.find('.dirinfo').text()).toEqual('6 folders'); + expect($container.find('.fileinfo').text()).toEqual('3 files'); expect($container.find('.filesize').text()).toEqual('500 KB'); expect(s.summary.totalDirs).toEqual(6); expect(s.summary.totalFiles).toEqual(3); @@ -79,7 +81,8 @@ describe('OCA.Files.FileSummary tests', function() { s.remove({type: 'dir', size: 100}); s.update(); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('4 folders and 1 file'); + expect($container.find('.dirinfo').text()).toEqual('4 folders'); + expect($container.find('.fileinfo').text()).toEqual('1 file'); expect($container.find('.filesize').text()).toEqual('125 KB'); expect(s.summary.totalDirs).toEqual(4); expect(s.summary.totalFiles).toEqual(1); @@ -95,7 +98,9 @@ describe('OCA.Files.FileSummary tests', function() { filter: 'foo' }); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('5 folders and 2 files match \'foo\''); + expect($container.find('.dirinfo').text()).toEqual('5 folders'); + expect($container.find('.fileinfo').text()).toEqual('2 files'); + expect($container.find('.filter').text()).toEqual(' match \'foo\''); expect($container.find('.filesize').text()).toEqual('250 KB'); }); it('hides filtered summary when no files or folders', function() { @@ -122,7 +127,9 @@ describe('OCA.Files.FileSummary tests', function() { s.add({name: 'foo', type: 'dir', size: 102}); s.update(); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('6 folders and 3 files match \'foo\''); + expect($container.find('.dirinfo').text()).toEqual('6 folders'); + expect($container.find('.fileinfo').text()).toEqual('3 files'); + expect($container.find('.filter').text()).toEqual(' match \'foo\''); expect($container.find('.filesize').text()).toEqual('500 KB'); expect(s.summary.totalDirs).toEqual(6); expect(s.summary.totalFiles).toEqual(3); @@ -142,7 +149,9 @@ describe('OCA.Files.FileSummary tests', function() { s.remove({name: 'foo', type: 'dir', size: 98}); s.update(); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('4 folders and 1 file match \'foo\''); + expect($container.find('.dirinfo').text()).toEqual('4 folders'); + expect($container.find('.fileinfo').text()).toEqual('1 file'); + expect($container.find('.filter').text()).toEqual(' match \'foo\''); expect($container.find('.filesize').text()).toEqual('125 KB'); expect(s.summary.totalDirs).toEqual(4); expect(s.summary.totalFiles).toEqual(1); @@ -158,7 +167,8 @@ describe('OCA.Files.FileSummary tests', function() { s.add({type: 'dir', size: -1}); s.update(); expect($container.hasClass('hidden')).toEqual(false); - expect($container.find('.info').text()).toEqual('1 folder and 0 files'); + expect($container.find('.dirinfo').text()).toEqual('1 folder'); + expect($container.find('.fileinfo').hasClass('hidden')).toEqual(true); expect($container.find('.filesize').text()).toEqual('Pending'); expect(s.summary.totalDirs).toEqual(1); expect(s.summary.totalFiles).toEqual(0); @@ -175,10 +185,56 @@ describe('OCA.Files.FileSummary tests', function() { s.remove({type: 'dir', size: -1}); s.update(); expect($container.hasClass('hidden')).toEqual(true); - expect($container.find('.info').text()).toEqual('0 folders and 0 files'); - expect($container.find('.filesize').text()).toEqual('0 B'); expect(s.summary.totalDirs).toEqual(0); expect(s.summary.totalFiles).toEqual(0); expect(s.summary.totalSize).toEqual(0); }); + describe('hidden files', function() { + var config; + var summary; + + beforeEach(function() { + config = new OC.Backbone.Model(); + summary = new FileSummary($container, { + config: config + }); + }); + + it('renders hidden count section when hidden files are hidden', function() { + config.set('showhidden', false); + summary.add({name: 'abc', type: 'file', size: 256000}); + summary.add({name: 'def', type: 'dir', size: 100}); + summary.add({name: '.hidden', type: 'dir', size: 512000}); + summary.update(); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.dirinfo').text()).toEqual('2 folders'); + expect($container.find('.fileinfo').text()).toEqual('1 file'); + expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(false); + expect($container.find('.hiddeninfo').text()).toEqual(' (including 1 hidden)'); + expect($container.find('.filesize').text()).toEqual('750 KB'); + }); + it('does not render hidden count section when hidden files exist but are visible', function() { + config.set('showhidden', true); + summary.add({name: 'abc', type: 'file', size: 256000}); + summary.add({name: 'def', type: 'dir', size: 100}); + summary.add({name: '.hidden', type: 'dir', size: 512000}); + summary.update(); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.dirinfo').text()).toEqual('2 folders'); + expect($container.find('.fileinfo').text()).toEqual('1 file'); + expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(true); + expect($container.find('.filesize').text()).toEqual('750 KB'); + }); + it('does not render hidden count section when no hidden files exist', function() { + config.set('showhidden', false); + summary.add({name: 'abc', type: 'file', size: 256000}); + summary.add({name: 'def', type: 'dir', size: 100}); + summary.update(); + expect($container.hasClass('hidden')).toEqual(false); + expect($container.find('.dirinfo').text()).toEqual('1 folder'); + expect($container.find('.fileinfo').text()).toEqual('1 file'); + expect($container.find('.hiddeninfo').hasClass('hidden')).toEqual(true); + expect($container.find('.filesize').text()).toEqual('250 KB'); + }); + }); }); diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index b884aa9f1d4..a2063803450 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -321,7 +321,7 @@ class ShareesAPIController extends OCSController { $this->result['remotes'] = []; } - if (!$foundRemoteById && substr_count($search, '@') >= 1 && substr_count($search, ' ') === 0 && $this->offset === 0) { + if (!$foundRemoteById && substr_count($search, '@') >= 1 && $this->offset === 0) { $this->result['exact']['remotes'][] = [ 'label' => $search, 'value' => [ diff --git a/apps/files_sharing/lib/sharedstorage.php b/apps/files_sharing/lib/sharedstorage.php index 3ceca430424..e1875fe2394 100644 --- a/apps/files_sharing/lib/sharedstorage.php +++ b/apps/files_sharing/lib/sharedstorage.php @@ -106,6 +106,16 @@ class Shared extends \OC\Files\Storage\Wrapper\Jail implements ISharedStorage { } /** + * @inheritdoc + */ + public function instanceOfStorage($class) { + if (in_array($class, ['\OC\Files\Storage\Home', '\OC\Files\ObjectStore\HomeObjectStoreStorage'])) { + return false; + } + return parent::instanceOfStorage($class); + } + + /** * @return string */ public function getShareId() { diff --git a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php index 5cb073ecf08..161cc8a184b 100644 --- a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php @@ -945,6 +945,58 @@ class ShareesAPIControllerTest extends TestCase { [], true, ], + // contact with space + [ + 'user name@localhost', + [ + [ + 'FN' => 'User3 @ Localhost', + ], + [ + 'FN' => 'User2 @ Localhost', + 'CLOUD' => [ + ], + ], + [ + 'FN' => 'User Name @ Localhost', + 'CLOUD' => [ + 'user name@localhost', + ], + ], + ], + false, + [ + ['label' => 'User Name @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user name@localhost', 'server' => 'localhost']], + ], + [], + true, + ], + // remote with space, no contact + [ + 'user space@remote', + [ + [ + 'FN' => 'User3 @ Localhost', + ], + [ + 'FN' => 'User2 @ Localhost', + 'CLOUD' => [ + ], + ], + [ + 'FN' => 'User @ Localhost', + 'CLOUD' => [ + 'username@localhost', + ], + ], + ], + false, + [ + ['label' => 'user space@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote']], + ], + [], + true, + ], ]; } diff --git a/apps/files_trashbin/appinfo/register_command.php b/apps/files_trashbin/appinfo/register_command.php index 676e3f5891c..e0dafc60cd9 100644 --- a/apps/files_trashbin/appinfo/register_command.php +++ b/apps/files_trashbin/appinfo/register_command.php @@ -21,10 +21,16 @@ */ +use OCA\Files_Trashbin\AppInfo\Application; use OCA\Files_Trashbin\Command\CleanUp; +use OCA\Files_Trashbin\Command\ExpireTrash; +$app = new Application(); +$expiration = $app->getContainer()->query('Expiration'); $userManager = OC::$server->getUserManager(); $rootFolder = \OC::$server->getRootFolder(); $dbConnection = \OC::$server->getDatabaseConnection(); + /** @var Symfony\Component\Console\Application $application */ $application->add(new CleanUp($rootFolder, $userManager, $dbConnection)); +$application->add(new ExpireTrash($userManager, $expiration)); diff --git a/apps/files_trashbin/lib/Command/ExpireTrash.php b/apps/files_trashbin/lib/Command/ExpireTrash.php new file mode 100644 index 00000000000..ff827718885 --- /dev/null +++ b/apps/files_trashbin/lib/Command/ExpireTrash.php @@ -0,0 +1,127 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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 <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_Trashbin\Command; + +use OCP\IUser; +use OCP\IUserManager; +use OCA\Files_Trashbin\Expiration; +use OCA\Files_Trashbin\Helper; +use OCA\Files_Trashbin\Trashbin; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExpireTrash extends Command { + + /** + * @var Expiration + */ + private $expiration; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @param IUserManager|null $userManager + * @param Expiration|null $expiration + */ + public function __construct(IUserManager $userManager = null, + Expiration $expiration = null) { + parent::__construct(); + + $this->userManager = $userManager; + $this->expiration = $expiration; + } + + protected function configure() { + $this + ->setName('trashbin:expire') + ->setDescription('Expires the users trashbin') + ->addArgument( + 'user_id', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'expires the trashbin of the given user(s), if no user is given the trash for all users will be expired' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + + $maxAge = $this->expiration->getMaxAgeAsTimestamp(); + if (!$maxAge) { + $output->writeln("No expiry configured."); + return; + } + + $users = $input->getArgument('user_id'); + if (!empty($users)) { + foreach ($users as $user) { + if ($this->userManager->userExists($user)) { + $output->writeln("Remove deleted files of <info>$user</info>"); + $userObject = $this->userManager->get($user); + $this->expireTrashForUser($userObject); + } else { + $output->writeln("<error>Unknown user $user</error>"); + } + } + } else { + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForAllUsers(function(IUser $user) use ($p) { + $p->advance(); + $this->expireTrashForUser($user); + }); + $p->finish(); + $output->writeln(''); + } + } + + function expireTrashForUser(IUser $user) { + $uid = $user->getUID(); + if ($user->getLastLogin() === 0 || !$this->setupFS($uid)) { + return; + } + $dirContent = Helper::getTrashFiles('/', $uid, 'mtime'); + Trashbin::deleteExpiredFiles($dirContent, $uid); + } + + /** + * Act on behalf on trash item owner + * @param string $user + * @return boolean + */ + protected function setupFS($user) { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($user); + + // Check if this user has a trashbin directory + $view = new \OC\Files\View('/' . $user); + if (!$view->is_dir('/files_trashbin/files')) { + return false; + } + + return true; + } +} diff --git a/apps/files_versions/appinfo/register_command.php b/apps/files_versions/appinfo/register_command.php index b991c72b944..bca869075aa 100644 --- a/apps/files_versions/appinfo/register_command.php +++ b/apps/files_versions/appinfo/register_command.php @@ -21,9 +21,14 @@ */ +use OCA\Files_Versions\AppInfo\Application; use OCA\Files_Versions\Command\CleanUp; +use OCA\Files_Versions\Command\ExpireVersions; +$app = new Application(); +$expiration = $app->getContainer()->query('Expiration'); $userManager = OC::$server->getUserManager(); $rootFolder = \OC::$server->getRootFolder(); /** @var Symfony\Component\Console\Application $application */ $application->add(new CleanUp($rootFolder, $userManager)); +$application->add(new ExpireVersions($userManager, $expiration)); diff --git a/apps/files_versions/lib/Command/ExpireVersions.php b/apps/files_versions/lib/Command/ExpireVersions.php new file mode 100644 index 00000000000..f384420f22f --- /dev/null +++ b/apps/files_versions/lib/Command/ExpireVersions.php @@ -0,0 +1,125 @@ +<?php +/** + * @author Thomas Müller <thomas.mueller@tmit.eu> + * + * @copyright Copyright (c) 2016, 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 <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Files_Versions\Command; + +use OCA\Files_Versions\Expiration; +use OCA\Files_Versions\Storage; +use OCP\IUser; +use OCP\IUserManager; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class ExpireVersions extends Command { + + /** + * @var Expiration + */ + private $expiration; + + /** + * @var IUserManager + */ + private $userManager; + + /** + * @param IUserManager|null $userManager + * @param Expiration|null $expiration + */ + public function __construct(IUserManager $userManager = null, + Expiration $expiration = null) { + parent::__construct(); + + $this->userManager = $userManager; + $this->expiration = $expiration; + } + + protected function configure() { + $this + ->setName('versions:expire') + ->setDescription('Expires the users file versions') + ->addArgument( + 'user_id', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'expire file versions of the given user(s), if no user is given file versions for all users will be expired.' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) { + + $maxAge = $this->expiration->getMaxAgeAsTimestamp(); + if (!$maxAge) { + $output->writeln("No expiry configured."); + return; + } + + $users = $input->getArgument('user_id'); + if (!empty($users)) { + foreach ($users as $user) { + if ($this->userManager->userExists($user)) { + $output->writeln("Remove deleted files of <info>$user</info>"); + $userObject = $this->userManager->get($user); + $this->expireVersionsForUser($userObject); + } else { + $output->writeln("<error>Unknown user $user</error>"); + } + } + } else { + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForAllUsers(function(IUser $user) use ($p) { + $p->advance(); + $this->expireVersionsForUser($user); + }); + $p->finish(); + $output->writeln(''); + } + } + + function expireVersionsForUser(IUser $user) { + $uid = $user->getUID(); + if ($user->getLastLogin() === 0 || !$this->setupFS($uid)) { + return; + } + Storage::expireOlderThanMaxForUser($uid); + } + + /** + * Act on behalf on versions item owner + * @param string $user + * @return boolean + */ + protected function setupFS($user) { + \OC_Util::tearDownFS(); + \OC_Util::setupFS($user); + + // Check if this user has a version directory + $view = new \OC\Files\View('/' . $user); + if (!$view->is_dir('/files_versions')) { + return false; + } + + return true; + } +} diff --git a/apps/theming/css/settings-admin.css b/apps/theming/css/settings-admin.css index 4139b2f46a3..5d2b08f5e43 100644 --- a/apps/theming/css/settings-admin.css +++ b/apps/theming/css/settings-admin.css @@ -9,12 +9,13 @@ #theming .theme-undo { cursor: pointer; opacity: .5; - padding: 9px; - vertical-align: bottom; + padding: 11px 5px; + vertical-align: top; + display: inline-block; } -#theming .icon { - display: inline-block; +#theming .icon-loading-small:after { + margin: -10px 0 0 -10px; } #theming label span { @@ -23,10 +24,11 @@ padding: 8px 0px; } -#theming .icon-upload { +#theming .icon-upload, +#theming .icon-loading-small { display: inline-flex; padding: 8px; - margin: 0; + margin: 2px 0px; } div#theming_settings_msg { @@ -46,4 +48,4 @@ div#theming_settings_msg { max-width: 20%; max-height: 20%; margin-top: 20px; -} +}
\ No newline at end of file diff --git a/apps/theming/js/settings-admin.js b/apps/theming/js/settings-admin.js index c896da321c8..216463b15e0 100644 --- a/apps/theming/js/settings-admin.js +++ b/apps/theming/js/settings-admin.js @@ -25,6 +25,7 @@ function setThemingValue(setting, value) { OC.generateUrl('/apps/theming/ajax/updateStylesheet'), {'setting' : setting, 'value' : value} ).done(function(response) { OC.msg.finishedSaving('#theming_settings_msg', response); + hideUndoButton(setting, value); }).fail(function(response) { OC.msg.finishedSaving('#theming_settings_msg', response); }); @@ -114,7 +115,6 @@ function preview(setting, value) { if (setting === 'name') { window.document.title = t('core', 'Admin') + " - " + value; } - hideUndoButton(setting, value); } function hideUndoButton(setting, value) { @@ -153,12 +153,16 @@ $(document).ready(function () { done: function (e, response) { preview('logoMime', response.result.data.name); OC.msg.finishedSaving('#theming_settings_msg', response.result); + $('label#uploadlogo').addClass('icon-upload').removeClass('icon-loading-small'); + $('.theme-undo[data-setting=logoMime]').show(); }, submit: function(e, response) { OC.msg.startSaving('#theming_settings_msg'); + $('label#uploadlogo').removeClass('icon-upload').addClass('icon-loading-small'); }, fail: function (e, response){ OC.msg.finishedError('#theming_settings_msg', response._response.jqXHR.responseJSON.data.message); + $('label#uploadlogo').addClass('icon-upload').removeClass('icon-loading-small'); } }; var uploadParamsLogin = { @@ -167,11 +171,15 @@ $(document).ready(function () { done: function (e, response) { preview('backgroundMime', response.result.data.name); OC.msg.finishedSaving('#theming_settings_msg', response.result); + $('label#upload-login-background').addClass('icon-upload').removeClass('icon-loading-small'); + $('.theme-undo[data-setting=backgroundMime]').show(); }, submit: function(e, response) { OC.msg.startSaving('#theming_settings_msg'); + $('label#upload-login-background').removeClass('icon-upload').addClass('icon-loading-small'); }, fail: function (e, response){ + $('label#upload-login-background').removeClass('icon-loading-small').addClass('icon-upload'); OC.msg.finishedError('#theming_settings_msg', response._response.jqXHR.responseJSON.data.message); } }; @@ -216,6 +224,7 @@ $(document).ready(function () { $('.theme-undo').click(function (e) { var setting = $(this).data('setting'); OC.msg.startSaving('#theming_settings_msg'); + $('.theme-undo[data-setting=' + setting + ']').hide(); $.post( OC.generateUrl('/apps/theming/ajax/undoChanges'), {'setting' : setting} ).done(function(response) { diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index fbb4c904773..b4e3a95710f 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -304,6 +304,13 @@ class ThemingController extends Controller { $responseCss = ''; $color = $this->config->getAppValue($this->appName, 'color'); $elementColor = $this->util->elementColor($color); + + if($this->util->invertTextColor($color)) { + $textColor = '#000000'; + } else { + $textColor = '#ffffff'; + } + if($color !== '') { $responseCss .= sprintf( '#body-user #header,#body-settings #header,#body-public #header,#body-login,.searchbox input[type="search"]:focus,.searchbox input[type="search"]:active,.searchbox input[type="search"]:valid {background-color: %s}' . "\n", @@ -321,19 +328,26 @@ class ThemingController extends Controller { 'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($elementColor).'\');' . "}\n"; $responseCss .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' . - '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' . - '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . - '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . - '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' . 'border: 1px solid '.$elementColor.';'. 'background-color: '.$elementColor.';'. - 'opacity: 0.8' . + 'opacity: 0.8;' . + 'color: ' . $textColor . ';'. "}\n" . '.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' . '.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' . 'border: 1px solid '.$elementColor.';'. 'background-color: '.$elementColor.';'. 'opacity: 1.0;' . + 'color: ' . $textColor . ';'. + "}\n" . + '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . + '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . + '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. + 'opacity: 0.4;' . + 'color: '.$textColor.';'. "}\n"; $responseCss .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n"; $responseCss .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' . diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php index afd74ced217..22ab5650e5b 100644 --- a/apps/theming/lib/Settings/Admin.php +++ b/apps/theming/lib/Settings/Admin.php @@ -61,7 +61,7 @@ class Admin implements ISettings { $theme = $this->config->getSystemValue('theme', ''); if ($theme !== '') { $themable = false; - $errorMessage = $this->l->t('You already use a custom theme'); + $errorMessage = $this->l->t('You are already using a custom theme'); } $parameters = [ diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index d053d8c1a1c..193e0bdcb4b 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -437,19 +437,26 @@ class ThemingControllerTest extends TestCase { 'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' . "}\n"; $expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' . - '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' . - '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . - '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . - '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . - 'border: 1px solid '.$color .';'. + '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' . + 'border: 1px solid '.$color.';'. 'background-color: '.$color.';'. - 'opacity: 0.8' . + 'opacity: 0.8;' . + 'color: #ffffff;'. "}\n" . '.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' . '.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' . 'border: 1px solid '.$color.';'. 'background-color: '.$color.';'. 'opacity: 1.0;' . + 'color: #ffffff;'. + "}\n" . + '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . + '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . + '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + 'border: 1px solid '.$color.';'. + 'background-color: '.$color.';'. + 'opacity: 0.4;' . + 'color: #ffffff;'. "}\n"; $expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n"; $expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' . @@ -520,19 +527,26 @@ class ThemingControllerTest extends TestCase { 'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' . "}\n"; $expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' . - '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' . - '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . - '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . - '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . - 'border: 1px solid #555555;'. - 'background-color: #555555;'. - 'opacity: 0.8' . + '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' . + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. + 'opacity: 0.8;' . + 'color: #000000;'. "}\n" . '.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' . '.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' . - 'border: 1px solid #555555;'. - 'background-color: #555555;'. + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. 'opacity: 1.0;' . + 'color: #000000;'. + "}\n" . + '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . + '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . + '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. + 'opacity: 0.4;' . + 'color: #000000;'. "}\n"; $expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n"; $expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' . @@ -689,19 +703,26 @@ class ThemingControllerTest extends TestCase { 'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton($color).'\');' . "}\n"; $expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' . - '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' . - '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . - '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . - '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . - 'border: 1px solid '.$color .';'. + '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' . + 'border: 1px solid '.$color.';'. 'background-color: '.$color.';'. - 'opacity: 0.8' . + 'opacity: 0.8;' . + 'color: #ffffff;'. "}\n" . '.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' . '.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' . 'border: 1px solid '.$color.';'. 'background-color: '.$color.';'. 'opacity: 1.0;' . + 'color: #ffffff;'. + "}\n" . + '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . + '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . + '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + 'border: 1px solid '.$color.';'. + 'background-color: '.$color.';'. + 'opacity: 0.4;' . + 'color: #ffffff;'. "}\n"; $expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n"; $expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' . @@ -789,19 +810,26 @@ class ThemingControllerTest extends TestCase { 'background-image: url(\'data:image/svg+xml;base64,'.$this->util->generateRadioButton('#555555').'\');' . "}\n"; $expectedData .= '.primary, input[type="submit"].primary, input[type="button"].primary, button.primary, .button.primary,' . - '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active,' . - '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . - '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . - '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . - 'border: 1px solid #555555;'. - 'background-color: #555555;'. - 'opacity: 0.8' . + '.primary:active, input[type="submit"].primary:active, input[type="button"].primary:active, button.primary:active, .button.primary:active {' . + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. + 'opacity: 0.8;' . + 'color: #000000;'. "}\n" . '.primary:hover, input[type="submit"].primary:hover, input[type="button"].primary:hover, button.primary:hover, .button.primary:hover,' . '.primary:focus, input[type="submit"].primary:focus, input[type="button"].primary:focus, button.primary:focus, .button.primary:focus {' . - 'border: 1px solid #555555;'. - 'background-color: #555555;'. + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. 'opacity: 1.0;' . + 'color: #000000;'. + "}\n" . + '.primary:disabled, input[type="submit"].primary:disabled, input[type="button"].primary:disabled, button.primary:disabled, .button.primary:disabled,' . + '.primary:disabled:hover, input[type="submit"].primary:disabled:hover, input[type="button"].primary:disabled:hover, button.primary:disabled:hover, .button.primary:disabled:hover,' . + '.primary:disabled:focus, input[type="submit"].primary:disabled:focus, input[type="button"].primary:disabled:focus, button.primary:disabled:focus, .button.primary:disabled:focus {' . + 'border: 1px solid '.$elementColor.';'. + 'background-color: '.$elementColor.';'. + 'opacity: 0.4;' . + 'color: #000000;'. "}\n"; $expectedData .= '.ui-widget-header { border: 1px solid ' . $color . '; background: '. $color . '; color: #ffffff;' . "}\n"; $expectedData .= '.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active {' . diff --git a/apps/theming/tests/Settings/AdminTest.php b/apps/theming/tests/Settings/AdminTest.php index 73339cf86b7..d4f5490d352 100644 --- a/apps/theming/tests/Settings/AdminTest.php +++ b/apps/theming/tests/Settings/AdminTest.php @@ -112,8 +112,8 @@ class AdminTest extends TestCase { $this->l10n ->expects($this->once()) ->method('t') - ->with('You already use a custom theme') - ->willReturn('You already use a custom theme'); + ->with('You are already using a custom theme') + ->willReturn('You are already using a custom theme'); $this->themingDefaults ->expects($this->once()) ->method('getEntity') @@ -137,7 +137,7 @@ class AdminTest extends TestCase { ->willReturn('/my/route'); $params = [ 'themable' => false, - 'errorMessage' => 'You already use a custom theme', + 'errorMessage' => 'You are already using a custom theme', 'name' => 'MyEntity', 'url' => 'https://example.com', 'slogan' => 'MySlogan', diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index e9e20c047aa..e6da74601ba 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -344,4 +344,28 @@ trait BasicStructure { rmdir("../../core/skeleton/PARENT"); } } + + /** + * @BeforeScenario @local_storage + */ + public static function removeFilesFromLocalStorageBefore(){ + $dir = "./work/local_storage/"; + $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); + $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); + foreach ( $ri as $file ) { + $file->isDir() ? rmdir($file) : unlink($file); + } + } + + /** + * @AfterScenario @local_storage + */ + public static function removeFilesFromLocalStorageAfter(){ + $dir = "./work/local_storage/"; + $di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS); + $ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST); + foreach ( $ri as $file ) { + $file->isDir() ? rmdir($file) : unlink($file); + } + } } diff --git a/build/integration/features/external-storage.feature b/build/integration/features/external-storage.feature new file mode 100644 index 00000000000..9e53b01346e --- /dev/null +++ b/build/integration/features/external-storage.feature @@ -0,0 +1,26 @@ +Feature: external-storage + Background: + Given using api version "1" + Given using dav path "remote.php/webdav" + + @local_storage + Scenario: Share by link a file inside a local external storage + Given user "user0" exists + And user "user1" exists + And As an "user0" + And user "user0" created a folder "/local_storage/foo" + And User "user0" moved file "/textfile0.txt" to "/local_storage/foo/textfile0.txt" + And folder "/local_storage/foo" of user "user0" is shared with user "user1" + And As an "user1" + When creating a share with + | path | foo | + | shareType | 3 | + Then the OCS status code should be "100" + And the HTTP status code should be "200" + And Share fields of last share match with + | id | A_NUMBER | + | url | AN_URL | + | token | A_TOKEN | + | mimetype | httpd/unix-directory | + + diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 38ed5213b19..785b795bf35 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -295,6 +295,7 @@ Feature: provisioning | theming | | updatenotification | | workflowengine | + | files_external | Scenario: get app info Given As an "admin" @@ -304,19 +305,19 @@ Feature: provisioning Scenario: enable an app Given As an "admin" - And app "files_external" is disabled - When sending "POST" to "/cloud/apps/files_external" + And app "testing" is disabled + When sending "POST" to "/cloud/apps/testing" Then the OCS status code should be "100" And the HTTP status code should be "200" - And app "files_external" is enabled + And app "testing" is enabled Scenario: disable an app Given As an "admin" - And app "files_external" is enabled - When sending "DELETE" to "/cloud/apps/files_external" + And app "testing" is enabled + When sending "DELETE" to "/cloud/apps/testing" Then the OCS status code should be "100" And the HTTP status code should be "200" - And app "files_external" is disabled + And app "testing" is disabled Scenario: disable an user Given As an "admin" diff --git a/build/integration/features/webdav-related.feature b/build/integration/features/webdav-related.feature index c49db4f8a5d..a59d65a2674 100644 --- a/build/integration/features/webdav-related.feature +++ b/build/integration/features/webdav-related.feature @@ -59,6 +59,24 @@ Feature: webdav-related |{DAV:}quota-available-bytes| And the single response should contain a property "{DAV:}quota-available-bytes" with value "10485421" + Scenario: Uploading a file as recipient using webdav having quota + Given using dav path "remote.php/webdav" + And As an "admin" + And user "user0" exists + And user "user1" exists + And user "user0" has a quota of "10 MB" + And user "user1" has a quota of "10 MB" + And As an "user1" + And user "user1" created a folder "/testquota" + And as "user1" creating a share with + | path | testquota | + | shareType | 0 | + | permissions | 31 | + | shareWith | user0 | + And As an "user0" + When User "user0" uploads file "data/textfile.txt" to "/testquota/asdf.txt" + Then the HTTP status code should be "201" + Scenario: download a public shared file with range Given user "user0" exists And As an "user0" diff --git a/build/integration/run.sh b/build/integration/run.sh index eccb378eec4..cf42ed75e4c 100755 --- a/build/integration/run.sh +++ b/build/integration/run.sh @@ -36,12 +36,27 @@ echo $PHPPID_FED export TEST_SERVER_URL="http://localhost:$PORT/ocs/" export TEST_SERVER_FED_URL="http://localhost:$PORT_FED/ocs/" +#Enable external storage app +../../occ app:enable files_external + +mkdir -p work/local_storage +OUTPUT_CREATE_STORAGE=`../../occ files_external:create local_storage local null::null -c datadir=./build/integration/work/local_storage` + +ID_STORAGE=`echo $OUTPUT_CREATE_STORAGE | awk {'print $5'}` + +../../occ files_external:option $ID_STORAGE enable_sharing true + vendor/bin/behat -f junit -f pretty $SCENARIO_TO_RUN RESULT=$? kill $PHPPID kill $PHPPID_FED +../../occ files_external:delete -y $ID_STORAGE + +#Disable external storage app +../../occ app:disable files_external + if [ -z $HIDE_OC_LOGS ]; then tail "../../data/nextcloud.log" fi diff --git a/core/Application.php b/core/Application.php index 0c69394c979..9a6d0878fee 100644 --- a/core/Application.php +++ b/core/Application.php @@ -73,14 +73,6 @@ class Application extends App { $c->query('TimeFactory') ); }); - $container->registerService('UserController', function(SimpleContainer $c) { - return new UserController( - $c->query('AppName'), - $c->query('Request'), - $c->query('UserManager'), - $c->query('Defaults') - ); - }); $container->registerService('LoginController', function(SimpleContainer $c) { return new LoginController( $c->query('AppName'), diff --git a/core/Controller/AvatarController.php b/core/Controller/AvatarController.php index 3aa002634d8..5b64320948a 100644 --- a/core/Controller/AvatarController.php +++ b/core/Controller/AvatarController.php @@ -29,8 +29,8 @@ namespace OC\Core\Controller; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; @@ -111,7 +111,7 @@ class AvatarController extends Controller { * * @param string $userId * @param int $size - * @return DataResponse|DataDisplayResponse + * @return JSONResponse|DataDisplayResponse */ public function getAvatar($userId, $size) { if ($size > 2048) { @@ -128,13 +128,13 @@ class AvatarController extends Controller { $resp->setETag($avatar->getEtag()); } catch (NotFoundException $e) { $user = $this->userManager->get($userId); - $resp = new DataResponse([ + $resp = new JSONResponse([ 'data' => [ 'displayname' => $user->getDisplayName(), ], ]); } catch (\Exception $e) { - $resp = new DataResponse([ + $resp = new JSONResponse([ 'data' => [ 'displayname' => '', ], @@ -152,25 +152,22 @@ class AvatarController extends Controller { * @NoAdminRequired * * @param string $path - * @return DataResponse + * @return JSONResponse */ public function postAvatar($path) { $files = $this->request->getUploadedFile('files'); - $headers = []; - if (isset($path)) { $path = stripslashes($path); $userFolder = $this->rootFolder->getUserFolder($this->userId); $node = $userFolder->get($path); if (!($node instanceof File)) { - return new DataResponse(['data' => ['message' => $this->l->t('Please select a file.')]], Http::STATUS_OK, $headers); + return new JSONResponse(['data' => ['message' => $this->l->t('Please select a file.')]]); } if ($node->getSize() > 20*1024*1024) { - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('File is too big')]], - Http::STATUS_BAD_REQUEST, - $headers + Http::STATUS_BAD_REQUEST ); } $content = $node->getContent(); @@ -181,28 +178,25 @@ class AvatarController extends Controller { !\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0]) ) { if ($files['size'][0] > 20*1024*1024) { - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('File is too big')]], - Http::STATUS_BAD_REQUEST, - $headers + Http::STATUS_BAD_REQUEST ); } $this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200); $content = $this->cache->get('avatar_upload'); unlink($files['tmp_name'][0]); } else { - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('Invalid file provided')]], - Http::STATUS_BAD_REQUEST, - $headers + Http::STATUS_BAD_REQUEST ); } } else { //Add imgfile - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('No image or file provided')]], - Http::STATUS_BAD_REQUEST, - $headers + Http::STATUS_BAD_REQUEST ); } @@ -214,57 +208,54 @@ class AvatarController extends Controller { if ($image->valid()) { $mimeType = $image->mimeType(); if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') { - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('Unknown filetype')]], - Http::STATUS_OK, - $headers + Http::STATUS_OK ); } $this->cache->set('tmpAvatar', $image->data(), 7200); - return new DataResponse( + return new JSONResponse( ['data' => 'notsquare'], - Http::STATUS_OK, - $headers + Http::STATUS_OK ); } else { - return new DataResponse( + return new JSONResponse( ['data' => ['message' => $this->l->t('Invalid image')]], - Http::STATUS_OK, - $headers + Http::STATUS_OK ); } } catch (\Exception $e) { $this->logger->logException($e, ['app' => 'core']); - return new DataResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK, $headers); + return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK); } } /** * @NoAdminRequired * - * @return DataResponse + * @return JSONResponse */ public function deleteAvatar() { try { $avatar = $this->avatarManager->getAvatar($this->userId); $avatar->remove(); - return new DataResponse(); + return new JSONResponse(); } catch (\Exception $e) { $this->logger->logException($e, ['app' => 'core']); - return new DataResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST); } } /** * @NoAdminRequired * - * @return DataResponse|DataDisplayResponse + * @return JSONResponse|DataDisplayResponse */ public function getTmpAvatar() { $tmpAvatar = $this->cache->get('tmpAvatar'); if (is_null($tmpAvatar)) { - return new DataResponse(['data' => [ + return new JSONResponse(['data' => [ 'message' => $this->l->t("No temporary profile picture available, try again") ]], Http::STATUS_NOT_FOUND); @@ -286,22 +277,22 @@ class AvatarController extends Controller { * @NoAdminRequired * * @param array $crop - * @return DataResponse + * @return JSONResponse */ public function postCroppedAvatar($crop) { if (is_null($crop)) { - return new DataResponse(['data' => ['message' => $this->l->t("No crop data provided")]], + return new JSONResponse(['data' => ['message' => $this->l->t("No crop data provided")]], Http::STATUS_BAD_REQUEST); } if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) { - return new DataResponse(['data' => ['message' => $this->l->t("No valid crop data provided")]], + return new JSONResponse(['data' => ['message' => $this->l->t("No valid crop data provided")]], Http::STATUS_BAD_REQUEST); } $tmpAvatar = $this->cache->get('tmpAvatar'); if (is_null($tmpAvatar)) { - return new DataResponse(['data' => [ + return new JSONResponse(['data' => [ 'message' => $this->l->t("No temporary profile picture available, try again") ]], Http::STATUS_BAD_REQUEST); @@ -314,13 +305,13 @@ class AvatarController extends Controller { $avatar->set($image); // Clean up $this->cache->remove('tmpAvatar'); - return new DataResponse(['status' => 'success']); + return new JSONResponse(['status' => 'success']); } catch (\OC\NotSquareException $e) { - return new DataResponse(['data' => ['message' => $this->l->t('Crop is not square')]], + return new JSONResponse(['data' => ['message' => $this->l->t('Crop is not square')]], Http::STATUS_BAD_REQUEST); } catch (\Exception $e) { $this->logger->logException($e, ['app' => 'core']); - return new DataResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST); + return new JSONResponse(['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST); } } } diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 67e1e215289..083f4bb0518 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -25,7 +25,6 @@ namespace OC\Core\Controller; -use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\TwoFactorAuth\Manager; use OC\Security\Bruteforce\Throttler; use OC\User\Session; @@ -242,12 +241,26 @@ class LoginController extends Controller { if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { $this->twoFactorManager->prepareTwoFactorLogin($loginResult); + + $providers = $this->twoFactorManager->getProviders($loginResult); + if (count($providers) === 1) { + // Single provider, hence we can redirect to that provider's challenge page directly + /* @var $provider IProvider */ + $provider = array_pop($providers); + $url = 'core.TwoFactorChallenge.showChallenge'; + $urlParams = [ + 'challengeProviderId' => $provider->getId(), + ]; + } else { + $url = 'core.TwoFactorChallenge.selectChallenge'; + $urlParams = []; + } + if (!is_null($redirect_url)) { - return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', [ - 'redirect_url' => $redirect_url - ])); + $urlParams['redirect_url'] = $redirect_url; } - return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + + return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams)); } return $this->generateRedirect($redirect_url); diff --git a/core/Controller/LostController.php b/core/Controller/LostController.php index fe6be1e6852..b1111559a6c 100644 --- a/core/Controller/LostController.php +++ b/core/Controller/LostController.php @@ -40,7 +40,6 @@ use \OCP\IConfig; use OCP\IUserManager; use OCP\Mail\IMailer; use OCP\Security\ISecureRandom; -use OCP\Security\StringUtils; /** * Class LostController @@ -144,7 +143,7 @@ class LostController extends Controller { } /** - * @param string $userId + * @param string $token * @param string $userId * @throws \Exception */ @@ -161,7 +160,7 @@ class LostController extends Controller { throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is expired')); } - if (!StringUtils::equals($splittedToken[1], $token)) { + if (!hash_equals($splittedToken[1], $token)) { throw new \Exception($this->l10n->t('Couldn\'t reset password because the token is invalid')); } } diff --git a/core/Controller/TokenController.php b/core/Controller/TokenController.php index 9d4fd7c9656..6e3ff50fa1d 100644 --- a/core/Controller/TokenController.php +++ b/core/Controller/TokenController.php @@ -24,13 +24,10 @@ namespace OC\Core\Controller; use OC\AppFramework\Http; -use OC\AppFramework\Utility\TimeFactory; -use OC\Authentication\Token\DefaultTokenProvider; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager; use OC\User\Manager as UserManager; -use OCA\User_LDAP\User\Manager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; @@ -100,9 +97,9 @@ class TokenController extends Controller { $token = $this->secureRandom->generate(128); $this->tokenProvider->generateToken($token, $user->getUID(), $loginName, $password, $name, IToken::PERMANENT_TOKEN); - return [ + return new JSONResponse([ 'token' => $token, - ]; + ]); } } diff --git a/core/Controller/TwoFactorChallengeController.php b/core/Controller/TwoFactorChallengeController.php index b9e10b147ce..c19cf523279 100644 --- a/core/Controller/TwoFactorChallengeController.php +++ b/core/Controller/TwoFactorChallengeController.php @@ -96,7 +96,7 @@ class TwoFactorChallengeController extends Controller { * * @param string $challengeProviderId * @param string $redirect_url - * @return TemplateResponse + * @return TemplateResponse|RedirectResponse */ public function showChallenge($challengeProviderId, $redirect_url) { $user = $this->userSession->getUser(); diff --git a/core/Controller/UserController.php b/core/Controller/UserController.php index 0cede94eb6e..fc282e36d9b 100644 --- a/core/Controller/UserController.php +++ b/core/Controller/UserController.php @@ -26,26 +26,20 @@ namespace OC\Core\Controller; use \OCP\AppFramework\Controller; use \OCP\AppFramework\Http\JSONResponse; use \OCP\IRequest; +use \OCP\IUserManager; class UserController extends Controller { /** - * @var \OCP\IUserManager + * @var IUserManager */ protected $userManager; - /** - * @var \OC_Defaults - */ - protected $defaults; - public function __construct($appName, IRequest $request, - $userManager, - $defaults + IUserManager $userManager ) { parent::__construct($appName, $request); $this->userManager = $userManager; - $this->defaults = $defaults; } /** diff --git a/core/ajax/preview.php b/core/ajax/preview.php index 2894efdc8e3..6cfba6aef30 100644 --- a/core/ajax/preview.php +++ b/core/ajax/preview.php @@ -53,6 +53,8 @@ $info = \OC\Files\Filesystem::getFileInfo($file); if (!$info instanceof OCP\Files\FileInfo || !$always && !\OC::$server->getPreviewManager()->isAvailable($info)) { \OC_Response::setStatus(404); +} else if (!$info->isReadable()) { + \OC_Response::setStatus(403); } else { $preview = new \OC\Preview(\OC_User::getUser(), 'files'); $preview->setFile($file, $info); diff --git a/core/css/multiselect.css b/core/css/multiselect.css index ef56044fd05..cc1d6a3b468 100644 --- a/core/css/multiselect.css +++ b/core/css/multiselect.css @@ -42,6 +42,8 @@ ul.multiselectoptions > li input[type='checkbox']+label { width: 100%; padding: 5px 27px; margin-left: -27px; /* to have area around checkbox clickable as well */ + text-overflow: ellipsis; + overflow: hidden; } ul.multiselectoptions > li input[type='checkbox']:checked+label { font-weight: bold; diff --git a/core/css/styles.css b/core/css/styles.css index c2b883e4a36..25bc2d086d5 100644 --- a/core/css/styles.css +++ b/core/css/styles.css @@ -301,7 +301,8 @@ body { -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; opacity: .7; } -#body-login input[type="password"] { +#body-login input[type="password"], +#body-login input[name="adminpass-clone"] { padding-right: 40px; box-sizing: border-box; min-width: 269px; diff --git a/core/js/mimetypelist.js b/core/js/mimetypelist.js index 08b892ce8bb..e1b9dba14af 100644 --- a/core/js/mimetypelist.js +++ b/core/js/mimetypelist.js @@ -65,6 +65,9 @@ OC.MimeTypeList={ "application/x-font": "image", "application/x-gimp": "image", "application/x-gzip": "package/x-generic", + "application/x-iwork-keynote-sffkey": "x-office/presentation", + "application/x-iwork-numbers-sffnumbers": "x-office/spreadsheet", + "application/x-iwork-pages-sffpages": "x-office/document", "application/x-mobipocket-ebook": "text", "application/x-perl": "text/code", "application/x-photoshop": "image", diff --git a/core/js/multiselect.js b/core/js/multiselect.js index 71cf3e10a69..bdf420a2f7f 100644 --- a/core/js/multiselect.js +++ b/core/js/multiselect.js @@ -32,7 +32,7 @@ 'onuncheck':false, 'minWidth': 'default;' }; - var slideDuration = 200; + var slideDuration = 0; $(this).attr('data-msid', multiSelectId); $.extend(settings,options); $.each(this.children(),function(i,option) { @@ -75,6 +75,26 @@ var self = this; self.menuDirection = 'down'; + + function closeDropDown() { + if(!button.parent().data('preventHide')) { + // How can I save the effect in a var? + if(self.menuDirection === 'down') { + button.parent().children('ul').slideUp(slideDuration,function() { + button.parent().children('ul').remove(); + button.removeClass('active down'); + $(self).trigger($.Event('dropdownclosed', settings)); + }); + } else { + button.parent().children('ul').fadeOut(slideDuration,function() { + button.parent().children('ul').remove(); + button.removeClass('active up'); + $(self).trigger($.Event('dropdownclosed', settings)); + }); + } + } + } + button.click(function(event){ var button=$(this); @@ -83,21 +103,20 @@ button.parent().children('ul').slideUp(slideDuration,function() { button.parent().children('ul').remove(); button.removeClass('active down'); + $(self).trigger($.Event('dropdownclosed', settings)); }); } else { button.parent().children('ul').fadeOut(slideDuration,function() { button.parent().children('ul').remove(); button.removeClass('active up'); + $(self).trigger($.Event('dropdownclosed', settings)); }); } return; } + // tell other lists to shut themselves var lists=$('ul.multiselectoptions'); - lists.slideUp(slideDuration,function(){ - lists.remove(); - $('div.multiselect').removeClass('active'); - button.addClass('active'); - }); + lists.trigger($.Event('shut')); button.addClass('active'); event.stopPropagation(); var options=$(this).parent().next().children(); @@ -309,29 +328,16 @@ list.detach().insertBefore($(this)); list.addClass('up'); button.addClass('up'); - list.fadeIn(); + list.show(); self.menuDirection = 'up'; } list.click(function(event) { event.stopPropagation(); }); + list.one('shut', closeDropDown); }); - $(window).click(function() { - if(!button.parent().data('preventHide')) { - // How can I save the effect in a var? - if(self.menuDirection === 'down') { - button.parent().children('ul').slideUp(slideDuration,function() { - button.parent().children('ul').remove(); - button.removeClass('active down'); - }); - } else { - button.parent().children('ul').fadeOut(slideDuration,function() { - button.parent().children('ul').remove(); - button.removeClass('active up'); - }); - } - } - }); + + $(window).click(closeDropDown); return span; }; diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js index b77063a9eae..75c8ef9020e 100644 --- a/core/js/oc-dialogs.js +++ b/core/js/oc-dialogs.js @@ -218,6 +218,13 @@ var OCdialogs = { self.$filePicker = null; } }); + + // We can access primary class only from oc-dialog. + // Hence this is one of the approach to get the choose button. + var getOcDialog = self.$filePicker.closest('.oc-dialog'); + var buttonEnableDisable = getOcDialog.find('.primary'); + buttonEnableDisable.prop("disabled", "true"); + if (!OC.Util.hasSVGSupport()) { OC.Util.replaceSVG(self.$filePicker.parent()); } @@ -812,18 +819,25 @@ var OCdialogs = { var self = event.data; var dir = $(event.target).data('dir'); self._fillFilePicker(dir); + var getOcDialog = this.closest('.oc-dialog'); + var buttonEnableDisable = $('.primary', getOcDialog); + buttonEnableDisable.prop("disabled", true); }, /** * handle clicks made in the filepicker */ _handlePickerClick:function(event, $element) { + var getOcDialog = this.$filePicker.closest('.oc-dialog'); + var buttonEnableDisable = getOcDialog.find('.primary'); if ($element.data('type') === 'file') { if (this.$filePicker.data('multiselect') !== true || !event.ctrlKey) { this.$filelist.find('.filepicker_element_selected').removeClass('filepicker_element_selected'); } $element.toggleClass('filepicker_element_selected'); + buttonEnableDisable.prop("disabled", false); } else if ( $element.data('type') === 'dir' ) { this._fillFilePicker(this.$filePicker.data('path') + '/' + $element.data('entryname')); + buttonEnableDisable.prop("disabled", true); } } }; diff --git a/core/js/sharedialoglinkshareview.js b/core/js/sharedialoglinkshareview.js index 83bf7979000..1d6a0f03d4d 100644 --- a/core/js/sharedialoglinkshareview.js +++ b/core/js/sharedialoglinkshareview.js @@ -330,7 +330,7 @@ publicUpload: publicUpload && isLinkShare, publicUploadChecked: publicUploadChecked, hideFileListChecked: hideFileListChecked, - publicUploadLabel: t('core', 'Allow editing'), + publicUploadLabel: t('core', 'Allow upload and editing'), hideFileListLabel: t('core', 'Hide file listing'), mailPublicNotificationEnabled: isLinkShare && this.configModel.isMailPublicNotificationEnabled(), mailPrivatePlaceholder: t('core', 'Email link to person'), diff --git a/index.php b/index.php index e42a0bbd7bc..8c3066d2409 100644 --- a/index.php +++ b/index.php @@ -25,10 +25,10 @@ * */ -// Show warning if a PHP version below 5.4.0 is used, this has to happen here -// because base.php will already use 5.4 syntax. -if (version_compare(PHP_VERSION, '5.4.0') === -1) { - echo 'This version of ownCloud requires at least PHP 5.4.0<br/>'; +// Show warning if a PHP version below 5.6.0 is used, this has to happen here +// because base.php will already use 5.6 syntax. +if (version_compare(PHP_VERSION, '5.6.0') === -1) { + echo 'This version of Nextcloud requires at least PHP 5.6.0<br/>'; echo 'You are currently running ' . PHP_VERSION . '. Please update your PHP version.'; return; } diff --git a/lib/base.php b/lib/base.php index a69a4dffef8..fe7419e6ff3 100644 --- a/lib/base.php +++ b/lib/base.php @@ -268,7 +268,7 @@ class OC { if (OC::$CLI) { throw new Exception('Not installed'); } else { - $url = 'http://' . $_SERVER['SERVER_NAME'] . OC::$WEBROOT . '/index.php'; + $url = OC::$WEBROOT . '/index.php'; header('Location: ' . $url); } exit(); @@ -922,6 +922,9 @@ class OC { $request = \OC::$server->getRequest(); $requestPath = $request->getRawPathInfo(); + if ($requestPath === '/heartbeat') { + return; + } if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade self::checkMaintenanceMode(); self::checkUpgrade(); diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index 99b127ab220..8a076084ac5 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -57,15 +57,15 @@ class Storage { $this->storageId = self::adjustStorageId($this->storageId); if ($row = self::getStorageById($this->storageId)) { - $this->numericId = $row['numeric_id']; + $this->numericId = (int)$row['numeric_id']; } else { $connection = \OC::$server->getDatabaseConnection(); $available = $isAvailable ? 1 : 0; if ($connection->insertIfNotExist('*PREFIX*storages', ['id' => $this->storageId, 'available' => $available])) { - $this->numericId = $connection->lastInsertId('*PREFIX*storages'); + $this->numericId = (int)$connection->lastInsertId('*PREFIX*storages'); } else { if ($row = self::getStorageById($this->storageId)) { - $this->numericId = $row['numeric_id']; + $this->numericId = (int)$row['numeric_id']; } else { throw new \RuntimeException('Storage could neither be inserted nor be selected from the database'); } @@ -132,7 +132,7 @@ class Storage { $storageId = self::adjustStorageId($storageId); if ($row = self::getStorageById($storageId)) { - return $row['numeric_id']; + return (int)$row['numeric_id']; } else { return null; } diff --git a/lib/private/Preview.php b/lib/private/Preview.php index 70b000a30ee..67838a8d4a3 100644 --- a/lib/private/Preview.php +++ b/lib/private/Preview.php @@ -763,7 +763,7 @@ class Preview { $this->preview = null; $fileInfo = $this->getFileInfo(); - if ($fileInfo === null || $fileInfo === false) { + if ($fileInfo === null || $fileInfo === false || !$fileInfo->isReadable()) { return new \OC_Image(); } diff --git a/lib/private/Repair/RepairUnmergedShares.php b/lib/private/Repair/RepairUnmergedShares.php index 353877bb873..d57bc3779f8 100644 --- a/lib/private/Repair/RepairUnmergedShares.php +++ b/lib/private/Repair/RepairUnmergedShares.php @@ -93,7 +93,7 @@ class RepairUnmergedShares implements IRepairStep { */ $query = $this->connection->getQueryBuilder(); $query - ->select('item_source', 'id', 'file_target', 'permissions', 'parent', 'share_type') + ->select('item_source', 'id', 'file_target', 'permissions', 'parent', 'share_type', 'stime') ->from('share') ->where($query->expr()->eq('share_type', $query->createParameter('shareType'))) ->andWhere($query->expr()->in('share_with', $query->createParameter('shareWiths'))) @@ -148,6 +148,52 @@ class RepairUnmergedShares implements IRepairStep { return $groupedShares; } + private function isPotentialDuplicateName($name) { + return (preg_match('/\(\d+\)(\.[^\.]+)?$/', $name) === 1); + } + + /** + * Decide on the best target name based on all group shares and subshares, + * goal is to increase the likeliness that the chosen name matches what + * the user is expecting. + * + * For this, we discard the entries with parenthesis "(2)". + * In case the user also renamed the duplicates to a legitimate name, this logic + * will still pick the most recent one as it's the one the user is most likely to + * remember renaming. + * + * If no suitable subshare is found, use the least recent group share instead. + * + * @param array $groupShares group share entries + * @param array $subShares sub share entries + * + * @return string chosen target name + */ + private function findBestTargetName($groupShares, $subShares) { + $pickedShare = null; + // sort by stime, this also properly sorts the direct user share if any + @usort($subShares, function($a, $b) { + return ((int)$a['stime'] - (int)$b['stime']); + }); + + foreach ($subShares as $subShare) { + // skip entries that have parenthesis with numbers + if ($this->isPotentialDuplicateName($subShare['file_target'])) { + continue; + } + // pick any share found that would match, the last being the most recent + $pickedShare = $subShare; + } + + // no suitable subshare found + if ($pickedShare === null) { + // use least recent group share target instead + $pickedShare = $groupShares[0]; + } + + return $pickedShare['file_target']; + } + /** * Fix the given received share represented by the set of group shares * and matching sub shares @@ -171,7 +217,7 @@ class RepairUnmergedShares implements IRepairStep { return false; } - $targetPath = $groupShares[0]['file_target']; + $targetPath = $this->findBestTargetName($groupShares, $subShares); // check whether the user opted out completely of all subshares $optedOut = true; diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php index 11a343918c6..031c5ffd411 100644 --- a/lib/private/Security/Bruteforce/Throttler.php +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -225,8 +225,11 @@ class Throttler { * Will sleep for the defined amount of time * * @param string $ip + * @return int the time spent sleeping */ public function sleepDelay($ip) { - usleep($this->getDelay($ip) * 1000); + $delay = $this->getDelay($ip); + usleep($delay * 1000); + return $delay; } } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index f41468d4926..7d8c6d48b2c 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -157,6 +157,16 @@ class Manager extends PublicEmitter implements IUserManager { return $this->cachedUsers[$uid]; } + if (method_exists($backend, 'loginName2UserName')) { + $loginName = $backend->loginName2UserName($uid); + if ($loginName !== false) { + $uid = $loginName; + } + if (isset($this->cachedUsers[$uid])) { + return $this->cachedUsers[$uid]; + } + } + $user = new User($uid, $backend, $this, $this->config); if ($cacheUser) { $this->cachedUsers[$uid] = $user; diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index 3b357b69bcf..dec959820f8 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -309,8 +309,7 @@ class Session implements IUserSession, Emitter { $password, IRequest $request, OC\Security\Bruteforce\Throttler $throttler) { - $currentDelay = $throttler->getDelay($request->getRemoteAddress()); - $throttler->sleepDelay($request->getRemoteAddress()); + $currentDelay = $throttler->sleepDelay($request->getRemoteAddress()); $isTokenPassword = $this->isTokenPassword($password); if (!$isTokenPassword && $this->isTokenAuthEnforced()) { diff --git a/lib/private/legacy/image.php b/lib/private/legacy/image.php index 2c20daf5d44..fee1a805c40 100644 --- a/lib/private/legacy/image.php +++ b/lib/private/legacy/image.php @@ -84,11 +84,6 @@ class OC_Image implements \OCP\IImage { $this->logger = \OC::$server->getLogger(); } - if (!extension_loaded('gd') || !function_exists('gd_info')) { - $this->logger->error(__METHOD__ . '(): GD module not installed', array('app' => 'core')); - return false; - } - if (\OC_Util::fileInfoLoaded()) { $this->fileInfo = new finfo(FILEINFO_MIME_TYPE); } @@ -802,8 +797,8 @@ class OC_Image implements \OCP\IImage { $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); return false; } - $widthOrig = imageSX($this->resource); - $heightOrig = imageSY($this->resource); + $widthOrig = imagesx($this->resource); + $heightOrig = imagesy($this->resource); $ratioOrig = $widthOrig / $heightOrig; if ($ratioOrig > 1) { @@ -828,8 +823,8 @@ class OC_Image implements \OCP\IImage { $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); return false; } - $widthOrig = imageSX($this->resource); - $heightOrig = imageSY($this->resource); + $widthOrig = imagesx($this->resource); + $heightOrig = imagesy($this->resource); $process = imagecreatetruecolor($width, $height); if ($process == false) { @@ -867,8 +862,8 @@ class OC_Image implements \OCP\IImage { $this->logger->error('OC_Image->centerCrop, No image loaded', array('app' => 'core')); return false; } - $widthOrig = imageSX($this->resource); - $heightOrig = imageSY($this->resource); + $widthOrig = imagesx($this->resource); + $heightOrig = imagesy($this->resource); if ($widthOrig === $heightOrig and $size == 0) { return true; } @@ -967,8 +962,8 @@ class OC_Image implements \OCP\IImage { $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); return false; } - $widthOrig = imageSX($this->resource); - $heightOrig = imageSY($this->resource); + $widthOrig = imagesx($this->resource); + $heightOrig = imagesy($this->resource); $ratio = $widthOrig / $heightOrig; $newWidth = min($maxWidth, $ratio * $maxHeight); @@ -990,8 +985,8 @@ class OC_Image implements \OCP\IImage { $this->logger->error(__METHOD__ . '(): No image loaded', array('app' => 'core')); return false; } - $widthOrig = imageSX($this->resource); - $heightOrig = imageSY($this->resource); + $widthOrig = imagesx($this->resource); + $heightOrig = imagesy($this->resource); if ($widthOrig > $maxWidth || $heightOrig > $maxHeight) { return $this->fitIn($maxWidth, $maxHeight); @@ -1024,6 +1019,7 @@ if (!function_exists('imagebmp')) { * @link http://www.programmierer-forum.de/imagebmp-gute-funktion-gefunden-t143716.htm * @author mgutt <marc@gutt.it> * @version 1.00 + * @param resource $im * @param string $fileName [optional] <p>The path to save the file to.</p> * @param int $bit [optional] <p>Bit depth, (default is 24).</p> * @param int $compression [optional] diff --git a/ocs/routes.php b/ocs/routes.php index d14f32e045c..3085cd9db65 100644 --- a/ocs/routes.php +++ b/ocs/routes.php @@ -75,76 +75,3 @@ API::register( 'core', API::USER_AUTH ); - -// Server-to-Server Sharing -if (\OC::$server->getAppManager()->isEnabledForUser('files_sharing')) { - $federatedSharingApp = new \OCA\FederatedFileSharing\AppInfo\Application(); - $addressHandler = new \OCA\FederatedFileSharing\AddressHandler( - \OC::$server->getURLGenerator(), - \OC::$server->getL10N('federatedfilesharing') - ); - $notification = new \OCA\FederatedFileSharing\Notifications( - $addressHandler, - \OC::$server->getHTTPClientService(), - new \OCA\FederatedFileSharing\DiscoveryManager(\OC::$server->getMemCacheFactory(), \OC::$server->getHTTPClientService()), - \OC::$server->getJobList() - ); - $s2s = new OCA\FederatedFileSharing\RequestHandler( - $federatedSharingApp->getFederatedShareProvider(), - \OC::$server->getDatabaseConnection(), - \OC::$server->getShareManager(), - \OC::$server->getRequest(), - $notification, - $addressHandler, - \OC::$server->getUserManager() - ); - API::register('post', - '/cloud/shares', - array($s2s, 'createShare'), - 'files_sharing', - API::GUEST_AUTH - ); - - API::register('post', - '/cloud/shares/{id}/reshare', - array($s2s, 'reShare'), - 'files_sharing', - API::GUEST_AUTH - ); - - API::register('post', - '/cloud/shares/{id}/permissions', - array($s2s, 'updatePermissions'), - 'files_sharing', - API::GUEST_AUTH - ); - - - API::register('post', - '/cloud/shares/{id}/accept', - array($s2s, 'acceptShare'), - 'files_sharing', - API::GUEST_AUTH - ); - - API::register('post', - '/cloud/shares/{id}/decline', - array($s2s, 'declineShare'), - 'files_sharing', - API::GUEST_AUTH - ); - - API::register('post', - '/cloud/shares/{id}/unshare', - array($s2s, 'unshare'), - 'files_sharing', - API::GUEST_AUTH - ); - - API::register('post', - '/cloud/shares/{id}/revoke', - array($s2s, 'revoke'), - 'files_sharing', - API::GUEST_AUTH - ); -} diff --git a/resources/config/mimetypealiases.dist.json b/resources/config/mimetypealiases.dist.json index 8b47867447f..602f70393ae 100644 --- a/resources/config/mimetypealiases.dist.json +++ b/resources/config/mimetypealiases.dist.json @@ -65,6 +65,9 @@ "application/x-font": "image", "application/x-gimp": "image", "application/x-gzip": "package/x-generic", + "application/x-iwork-keynote-sffkey": "x-office/presentation", + "application/x-iwork-numbers-sffnumbers": "x-office/spreadsheet", + "application/x-iwork-pages-sffpages": "x-office/document", "application/x-mobipocket-ebook": "text", "application/x-perl": "text/code", "application/x-photoshop": "image", diff --git a/settings/css/settings.css b/settings/css/settings.css index d3fd395747e..6ed707f7c45 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -267,6 +267,15 @@ span.usersLastLoginTooltip { white-space: nowrap; } top: 3px; } +#newuser .groups { + display: inline; +} + +#newuser .groupsListContainer.hidden, +#userlist .groupsListContainer.hidden { + display: none; +} + tr:hover>td.password>span, tr:hover>td.displayName>span { margin:0; cursor:pointer; } tr:hover>td.remove>a, tr:hover>td.password>img,tr:hover>td.displayName>img, tr:hover>td.quota>img { visibility:visible; cursor:pointer; } td.remove { diff --git a/settings/js/users/groups.js b/settings/js/users/groups.js index e83f00970c2..8f4d95432a8 100644 --- a/settings/js/users/groups.js +++ b/settings/js/users/groups.js @@ -138,10 +138,6 @@ GroupList = { var addedGroup = result.groupname; UserList.availableGroups = $.unique($.merge(UserList.availableGroups, [addedGroup])); GroupList.addGroup(result.groupname); - - $('.groupsselect, .subadminsselect') - .append($('<option>', { value: result.groupname }) - .text(result.groupname)); } GroupList.toggleAddGroup(); }).fail(function(result) { diff --git a/settings/js/users/users.js b/settings/js/users/users.js index 4ce77648826..f24bf82209b 100644 --- a/settings/js/users/users.js +++ b/settings/js/users/users.js @@ -59,9 +59,6 @@ var UserList = { var $tr = $userListBody.find('tr:first-child').clone(); // this removes just the `display:none` of the template row $tr.removeAttr('style'); - var subAdminsEl; - var subAdminSelect; - var groupsSelect; /** * Avatar or placeholder @@ -88,32 +85,17 @@ var UserList = { $tr.find('td.mailAddress > .action').tooltip({placement: 'top'}); $tr.find('td.password > .action').tooltip({placement: 'top'}); + /** * groups and subadmins */ - // make them look like the multiselect buttons - // until they get time to really get initialized - groupsSelect = $('<select multiple="multiple" class="groupsselect multiselect button" data-placehoder="Groups" title="' + t('settings', 'No group') + '"></select>') - .data('username', user.name) - .data('user-groups', user.groups); - if ($tr.find('td.subadmins').length > 0) { - subAdminSelect = $('<select multiple="multiple" class="subadminsselect multiselect button" data-placehoder="subadmins" title="' + t('settings', 'No group') + '">') - .data('username', user.name) - .data('user-groups', user.groups) - .data('subadmin', user.subadmin); - $tr.find('td.subadmins').empty(); - } - $.each(this.availableGroups, function (i, group) { - groupsSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>')); - if (typeof subAdminSelect !== 'undefined' && group !== 'admin') { - subAdminSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>')); - } - }); - $tr.find('td.groups').empty().append(groupsSelect); - subAdminsEl = $tr.find('td.subadmins'); - if (subAdminsEl.length > 0) { - subAdminsEl.append(subAdminSelect); - } + var $tdGroups = $tr.find('td.groups'); + this._updateGroupListLabel($tdGroups, user.groups); + $tdGroups.find('.action').tooltip({placement: 'top'}); + + var $tdSubadmins = $tr.find('td.subadmins'); + this._updateGroupListLabel($tdSubadmins, user.subadmin); + $tdSubadmins.find('.action').tooltip({placement: 'top'}); /** * remove action @@ -200,10 +182,6 @@ var UserList = { // defer init so the user first sees the list appear more quickly window.setTimeout(function(){ $quotaSelect.singleSelect(); - UserList.applyGroupSelect(groupsSelect); - if (subAdminSelect) { - UserList.applySubadminSelect(subAdminSelect); - } }, 0); return $tr; }, @@ -324,7 +302,7 @@ var UserList = { }, markRemove: function(uid) { var $tr = UserList.getRow(uid); - var groups = $tr.find('.groups .groupsselect').val(); + var groups = $tr.find('.groups').data('groups'); for(var i in groups) { var gid = groups[i]; var $li = GroupList.getGroupLI(gid); @@ -339,7 +317,7 @@ var UserList = { }, undoRemove: function(uid) { var $tr = UserList.getRow(uid); - var groups = $tr.find('.groups .groupsselect').val(); + var groups = $tr.find('.groups').data('groups'); for(var i in groups) { var gid = groups[i]; var $li = GroupList.getGroupLI(gid); @@ -440,19 +418,9 @@ var UserList = { }); }, - applyGroupSelect: function (element) { - var checked = []; + applyGroupSelect: function (element, user, checked) { var $element = $(element); - var user = UserList.getUID($element); - if ($element.data('user-groups')) { - if (typeof $element.data('user-groups') === 'string') { - checked = $element.data('user-groups').split(", "); - } - else { - checked = $element.data('user-groups'); - } - } var checkHandler = null; if(user) { // Only if in a user row, and not the #newusergroups select checkHandler = function (group) { @@ -492,13 +460,6 @@ var UserList = { }; } var addGroup = function (select, group) { - $('select[multiple]').each(function (index, element) { - $element = $(element); - if ($element.find('option').filterAttr('value', group).length === 0 && - select.data('msid') !== $element.data('msid')) { - $element.append('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>'); - } - }); GroupList.addGroup(escapeHTML(group)); }; var label; @@ -519,19 +480,8 @@ var UserList = { }); }, - applySubadminSelect: function (element) { - var checked = []; + applySubadminSelect: function (element, user, checked) { var $element = $(element); - var user = UserList.getUID($element); - - if ($element.data('subadmin')) { - if (typeof $element.data('subadmin') === 'string') { - checked = $element.data('subadmin').split(", "); - } - else { - checked = $element.data('subadmin'); - } - } var checkHandler = function (group) { if (group === 'admin') { return false; @@ -547,15 +497,7 @@ var UserList = { ); }; - var addSubAdmin = function (group) { - $('select[multiple]').each(function (index, element) { - if ($(element).find('option').filterAttr('value', group).length === 0) { - $(element).append('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>'); - } - }); - }; $element.multiSelect({ - createCallback: addSubAdmin, createText: null, checked: checked, oncheck: checkHandler, @@ -613,6 +555,76 @@ var UserList = { } } ); + }, + + /** + * Creates a temporary jquery.multiselect selector on the given group field + */ + _triggerGroupEdit: function($td, isSubadminSelect) { + var $groupsListContainer = $td.find('.groupsListContainer'); + var placeholder = $groupsListContainer.attr('data-placeholder') || t('settings', 'no group'); + var user = UserList.getUID($td); + var checked = $td.data('groups') || []; + var extraGroups = [].concat(checked); + + $td.find('.multiselectoptions').remove(); + + // jquery.multiselect can only work with select+options in DOM ? We'll give jquery.multiselect what it wants... + var $groupsSelect; + if (isSubadminSelect) { + $groupsSelect = $('<select multiple="multiple" class="groupsselect multiselect button" title="' + placeholder + '"></select>'); + } else { + $groupsSelect = $('<select multiple="multiple" class="subadminsselect multiselect button" title="' + placeholder + '"></select>') + } + + function createItem(group) { + if (isSubadminSelect && group === 'admin') { + // can't become subadmin of "admin" group + return; + } + $groupsSelect.append($('<option value="' + escapeHTML(group) + '">' + escapeHTML(group) + '</option>')); + } + + $.each(this.availableGroups, function (i, group) { + // some new groups might be selected but not in the available groups list yet + var extraIndex = extraGroups.indexOf(group); + if (extraIndex >= 0) { + // remove extra group as it was found + extraGroups.splice(extraIndex, 1); + } + createItem(group); + }); + $.each(extraGroups, function (i, group) { + createItem(group); + }); + + $td.append($groupsSelect); + + if (isSubadminSelect) { + UserList.applySubadminSelect($groupsSelect, user, checked); + } else { + UserList.applyGroupSelect($groupsSelect, user, checked); + } + + $groupsListContainer.addClass('hidden'); + $td.find('.multiselect:not(.groupsListContainer):first').click(); + $groupsSelect.on('dropdownclosed', function(e) { + $groupsSelect.remove(); + $td.find('.multiselect:not(.groupsListContainer)').parent().remove(); + $td.find('.multiselectoptions').remove(); + $groupsListContainer.removeClass('hidden'); + UserList._updateGroupListLabel($td, e.checked); + }); + }, + + /** + * Updates the groups list td with the given groups selection + */ + _updateGroupListLabel: function($td, groups) { + var placeholder = $td.find('.groupsListContainer').attr('data-placeholder'); + var $groupsEl = $td.find('.groupsList'); + $groupsEl.text(groups.join(', ') || placeholder || t('settings', 'no group')); + $td.data('groups', groups); } }; @@ -637,13 +649,6 @@ $(document).ready(function () { // TODO: move other init calls inside of initialize UserList.initialize($('#userlist')); - $('.groupsselect').each(function (index, element) { - UserList.applyGroupSelect(element); - }); - $('.subadminsselect').each(function (index, element) { - UserList.applySubadminSelect(element); - }); - $userListBody.on('click', '.password', function (event) { event.stopPropagation(); @@ -787,11 +792,24 @@ $(document).ready(function () { }); }); + $('#newuser .groupsListContainer').on('click', function (event) { + event.stopPropagation(); + var $div = $(this).closest('.groups'); + UserList._triggerGroupEdit($div); + }); + $userListBody.on('click', '.groups .groupsListContainer, .subadmins .groupsListContainer', function (event) { + event.stopPropagation(); + var $td = $(this).closest('td'); + var isSubadminSelect = $td.hasClass('subadmins'); + UserList._triggerGroupEdit($td, isSubadminSelect); + }); + // init the quota field select box after it is shown the first time $('#app-settings').one('show', function() { $(this).find('#default_quota').singleSelect().on('change', UserList.onQuotaSelect); }); + UserList._updateGroupListLabel($('#newuser .groups'), []); $('#newuser').submit(function (event) { event.preventDefault(); var username = $('#newusername').val(); @@ -827,7 +845,7 @@ $(document).ready(function () { } promise.then(function() { - var groups = $('#newusergroups').val() || []; + var groups = $('#newuser .groups').data('groups') || []; $.post( OC.generateUrl('/settings/users/users'), { diff --git a/settings/templates/users/main.php b/settings/templates/users/main.php index f50f83b38b3..b363a4c4da8 100644 --- a/settings/templates/users/main.php +++ b/settings/templates/users/main.php @@ -19,10 +19,10 @@ style('settings', 'settings'); $userlistParams = array(); $allGroups=array(); -foreach($_["groups"] as $group) { +foreach($_["adminGroup"] as $group) { $allGroups[] = $group['name']; } -foreach($_["adminGroup"] as $group) { +foreach($_["groups"] as $group) { $allGroups[] = $group['name']; } $userlistParams['subadmingroups'] = $allGroups; diff --git a/settings/templates/users/part.createuser.php b/settings/templates/users/part.createuser.php index 0fc5a2bdeaa..6f23d06cfa3 100644 --- a/settings/templates/users/part.createuser.php +++ b/settings/templates/users/part.createuser.php @@ -10,16 +10,7 @@ <input id="newemail" type="text" style="display:none" placeholder="<?php p($l->t('E-Mail'))?>" autocomplete="off" autocapitalize="off" autocorrect="off" /> - <select - class="groupsselect" id="newusergroups" data-placeholder="groups" - title="<?php p($l->t('Groups'))?>" multiple="multiple"> - <?php foreach($_["adminGroup"] as $adminGroup): ?> - <option value="<?php p($adminGroup['name']);?>"><?php p($adminGroup['name']); ?></option> - <?php endforeach; ?> - <?php foreach($_["groups"] as $group): ?> - <option value="<?php p($group['name']);?>"><?php p($group['name']);?></option> - <?php endforeach;?> - </select> + <div class="groups"><div class="groupsListContainer multiselect button" data-placeholder="<?php p($l->t('Groups'))?>"><span class="title groupsList"></span><span class="icon-triangle-s"></span></div></div> <input type="submit" class="button" value="<?php p($l->t('Create'))?>" /> </form> <?php if((bool)$_['recoveryAdminEnabled']): ?> diff --git a/settings/templates/users/part.userlist.php b/settings/templates/users/part.userlist.php index 2bdd0714a3c..bab68e5a765 100644 --- a/settings/templates/users/part.userlist.php +++ b/settings/templates/users/part.userlist.php @@ -38,9 +38,13 @@ src="<?php p(image_path('core', 'actions/rename.svg'))?>" alt="<?php p($l->t('change email address'))?>" title="<?php p($l->t('change email address'))?>"/> </td> - <td class="groups"></td> + <td class="groups"><div class="groupsListContainer multiselect button" + ><span class="title groupsList"></span><span class="icon-triangle-s"></span></div> + </td> <?php if(is_array($_['subadmins']) || $_['subadmins']): ?> - <td class="subadmins"></td> + <td class="subadmins"><div class="groupsListContainer multiselect button" + ><span class="title groupsList"></span><span class="icon-triangle-s"></span></div> + </td> <?php endif;?> <td class="quota"> <select class="quota-user" data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>"> diff --git a/status.php b/status.php index 0d7c2285679..5b4b950139b 100644 --- a/status.php +++ b/status.php @@ -35,12 +35,16 @@ try { $installed = (bool) $systemConfig->getValue('installed', false); $maintenance = (bool) $systemConfig->getValue('maintenance', false); + # see core/lib/private/legacy/defaults.php and core/themes/example/defaults.php + # for description and defaults + $defaults = new \OCP\Defaults(); $values=array( 'installed'=>$installed, 'maintenance' => $maintenance, 'version'=>implode('.', \OCP\Util::getVersion()), 'versionstring'=>OC_Util::getVersionString(), - 'edition'=>OC_Util::getEditionString()); + 'edition'=>OC_Util::getEditionString(), + 'productname'=>$defaults->getName()); if (OC::$CLI) { print_r($values); } else { diff --git a/tests/Core/Controller/AvatarControllerTest.php b/tests/Core/Controller/AvatarControllerTest.php index a275a8bd16a..fe1a44b28ab 100644 --- a/tests/Core/Controller/AvatarControllerTest.php +++ b/tests/Core/Controller/AvatarControllerTest.php @@ -228,7 +228,7 @@ class AvatarControllerTest extends \Test\TestCase { $this->logger->expects($this->once()) ->method('logException') ->with(new \Exception("foo")); - $expectedResponse = new Http\DataResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST); + $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST); $this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar()); } @@ -377,7 +377,7 @@ class AvatarControllerTest extends \Test\TestCase { $this->logger->expects($this->once()) ->method('logException') ->with(new \Exception("foo")); - $expectedResponse = new Http\DataResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_OK); + $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_OK); $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg')); } @@ -437,7 +437,7 @@ class AvatarControllerTest extends \Test\TestCase { $this->logger->expects($this->once()) ->method('logException') ->with(new \Exception('foo')); - $expectedResponse = new Http\DataResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST); + $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST); $this->assertEquals($expectedResponse, $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11])); } diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index 417a60a9e5f..ff50ac98fbd 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -505,7 +505,7 @@ class LoginControllerTest extends TestCase { $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); } - public function testLoginWithTwoFactorEnforced() { + public function testLoginWithOneTwoFactorProvider() { /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->getMockBuilder('\OCP\IUser')->getMock(); $user->expects($this->any()) @@ -513,6 +513,7 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue('john')); $password = 'secret'; $challengeUrl = 'challenge/url'; + $provider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); $this->request ->expects($this->exactly(2)) @@ -547,6 +548,79 @@ class LoginControllerTest extends TestCase { $this->twoFactorManager->expects($this->once()) ->method('prepareTwoFactorLogin') ->with($user); + $this->twoFactorManager->expects($this->once()) + ->method('getProviders') + ->with($user) + ->will($this->returnValue([$provider])); + $provider->expects($this->once()) + ->method('getId') + ->will($this->returnValue('u2f')); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.showChallenge', [ + 'challengeProviderId' => 'u2f', + ]) + ->will($this->returnValue($challengeUrl)); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('john', 'core', 'lostpassword'); + + $expected = new RedirectResponse($challengeUrl); + $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); + } + + public function testLoginWithMultpleTwoFactorProviders() { + /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('\OCP\IUser')->getMock(); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('john')); + $password = 'secret'; + $challengeUrl = 'challenge/url'; + $provider1 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); + $provider2 = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); + + $this->request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->request + ->expects($this->once()) + ->method('passesCSRFCheck') + ->willReturn(true); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->will($this->returnValue($user)); + $this->userSession->expects($this->once()) + ->method('login') + ->with('john@doe.com', $password); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, $user->getUID(), 'john@doe.com', $password); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(true)); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with($user); + $this->twoFactorManager->expects($this->once()) + ->method('getProviders') + ->with($user) + ->will($this->returnValue([$provider1, $provider2])); + $provider1->expects($this->never()) + ->method('getId'); + $provider2->expects($this->never()) + ->method('getId'); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with('core.TwoFactorChallenge.selectChallenge') diff --git a/tests/Core/Controller/TokenControllerTest.php b/tests/Core/Controller/TokenControllerTest.php index b6b54b14fad..0e965aac2e5 100644 --- a/tests/Core/Controller/TokenControllerTest.php +++ b/tests/Core/Controller/TokenControllerTest.php @@ -41,15 +41,17 @@ class TokenControllerTest extends TestCase { protected function setUp() { parent::setUp(); - $this->request = $this->getMock('\OCP\IRequest'); + $this->request = $this->getMockBuilder('\OCP\IRequest')->getMock(); $this->userManager = $this->getMockBuilder('\OC\User\Manager') ->disableOriginalConstructor() ->getMock(); - $this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider'); + $this->tokenProvider = $this->getMockBuilder('\OC\Authentication\Token\IProvider') + ->getMock(); $this->twoFactorAuthManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') ->disableOriginalConstructor() ->getMock(); - $this->secureRandom = $this->getMock('\OCP\Security\ISecureRandom'); + $this->secureRandom = $this->getMockBuilder('\OCP\Security\ISecureRandom') + ->getMock(); $this->tokenController = new TokenController('core', $this->request, $this->userManager, $this->tokenProvider, $this->twoFactorAuthManager, $this->secureRandom); } @@ -77,7 +79,7 @@ class TokenControllerTest extends TestCase { } public function testWithValidCredentials() { - $user = $this->getMock('\OCP\IUser'); + $user = $this->getMockBuilder('\OCP\IUser')->getMock(); $this->userManager->expects($this->once()) ->method('checkPassword') ->with('john', '123456') @@ -96,9 +98,9 @@ class TokenControllerTest extends TestCase { $this->tokenProvider->expects($this->once()) ->method('generateToken') ->with('verysecurerandomtoken', 'john', 'john', '123456', 'unknown client', IToken::PERMANENT_TOKEN); - $expected = [ + $expected = new JSONResponse([ 'token' => 'verysecurerandomtoken' - ]; + ]); $actual = $this->tokenController->generateToken('john', '123456'); @@ -106,7 +108,7 @@ class TokenControllerTest extends TestCase { } public function testWithValidCredentialsBut2faEnabled() { - $user = $this->getMock('\OCP\IUser'); + $user = $this->getMockBuilder('\OCP\IUser')->getMock(); $this->userManager->expects($this->once()) ->method('checkPassword') ->with('john', '123456') diff --git a/tests/lib/Repair/RepairLegacyStoragesTest.php b/tests/lib/Repair/RepairLegacyStoragesTest.php index aa51fe06a35..8d8366dde06 100644 --- a/tests/lib/Repair/RepairLegacyStoragesTest.php +++ b/tests/lib/Repair/RepairLegacyStoragesTest.php @@ -98,23 +98,9 @@ class RepairLegacyStoragesTest extends TestCase { $storageId = Storage::adjustStorageId($storageId); $numRows = $this->connection->executeUpdate($sql, array($storageId)); - $this->assertEquals(1, $numRows); + $this->assertSame(1, $numRows); - return \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages'); - } - - /** - * Returns the storage id based on the numeric id - * - * @param int $storageId numeric id of the storage - * @return string storage id or null if not found - */ - private function getStorageId($storageId) { - $numericId = Storage::getNumericStorageId($storageId); - if (!is_null($numericId)) { - return (int)$numericId; - } - return null; + return (int)\OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*storages'); } /** @@ -144,8 +130,8 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertNull($this->getStorageId($this->legacyStorageId)); - $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId)); + $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); + $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); } /** @@ -163,8 +149,8 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertNull($this->getStorageId($this->legacyStorageId)); - $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->newStorageId)); + $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); + $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId)); } /** @@ -185,8 +171,8 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertNull($this->getStorageId($this->legacyStorageId)); - $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->newStorageId)); + $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); + $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->newStorageId)); } /** @@ -208,8 +194,8 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertNull($this->getStorageId($this->legacyStorageId)); - $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId)); + $this->assertNull(Storage::getNumericStorageId($this->legacyStorageId)); + $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); } /** @@ -233,8 +219,8 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); // storages left alone - $this->assertEquals($legacyStorageNumId, $this->getStorageId($this->legacyStorageId)); - $this->assertEquals($newStorageNumId, $this->getStorageId($this->newStorageId)); + $this->assertSame($legacyStorageNumId, Storage::getNumericStorageId($this->legacyStorageId)); + $this->assertSame($newStorageNumId, Storage::getNumericStorageId($this->newStorageId)); // do not set the done flag $this->assertNotEquals('yes', $this->config->getAppValue('core', 'repairlegacystoragesdone')); @@ -255,7 +241,7 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertEquals($numId, $this->getStorageId($storageId)); + $this->assertSame($numId, Storage::getNumericStorageId($storageId)); } /** @@ -273,7 +259,7 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertEquals($numId, $this->getStorageId($storageId)); + $this->assertSame($numId, Storage::getNumericStorageId($storageId)); } /** @@ -291,7 +277,7 @@ class RepairLegacyStoragesTest extends TestCase { $this->repair->run($this->outputMock); - $this->assertEquals($numId, $this->getStorageId($storageId)); + $this->assertSame($numId, Storage::getNumericStorageId($storageId)); } /** diff --git a/tests/lib/Repair/RepairUnmergedSharesTest.php b/tests/lib/Repair/RepairUnmergedSharesTest.php index fe9b3e5b96f..7b9d2579389 100644 --- a/tests/lib/Repair/RepairUnmergedSharesTest.php +++ b/tests/lib/Repair/RepairUnmergedSharesTest.php @@ -28,6 +28,8 @@ use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; use Test\TestCase; use OC\Share20\DefaultShareProvider; +use OCP\IUserManager; +use OCP\IGroupManager; /** * Tests for repairing invalid shares @@ -44,6 +46,15 @@ class RepairUnmergedSharesTest extends TestCase { /** @var \OCP\IDBConnection */ private $connection; + /** @var int */ + private $lastShareTime; + + /** @var IUserManager */ + private $userManager; + + /** @var IGroupManager */ + private $groupManager; + protected function setUp() { parent::setUp(); @@ -58,42 +69,14 @@ class RepairUnmergedSharesTest extends TestCase { $this->connection = \OC::$server->getDatabaseConnection(); $this->deleteAllShares(); - $user1 = $this->getMock('\OCP\IUser'); - $user1->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('user1')); - - $user2 = $this->getMock('\OCP\IUser'); - $user2->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('user2')); - - $users = [$user1, $user2]; - - $groupManager = $this->getMock('\OCP\IGroupManager'); - $groupManager->expects($this->any()) - ->method('getUserGroupIds') - ->will($this->returnValueMap([ - // owner - [$user1, ['samegroup1', 'samegroup2']], - // recipient - [$user2, ['recipientgroup1', 'recipientgroup2']], - ])); + $this->userManager = $this->getMock('\OCP\IUserManager'); + $this->groupManager = $this->getMock('\OCP\IGroupManager'); - $userManager = $this->getMock('\OCP\IUserManager'); - $userManager->expects($this->once()) - ->method('countUsers') - ->will($this->returnValue([2])); - $userManager->expects($this->once()) - ->method('callForAllUsers') - ->will($this->returnCallback(function(\Closure $closure) use ($users) { - foreach ($users as $user) { - $closure($user); - } - })); + // used to generate incremental stimes + $this->lastShareTime = time(); /** @var \OCP\IConfig $config */ - $this->repair = new RepairUnmergedShares($config, $this->connection, $userManager, $groupManager); + $this->repair = new RepairUnmergedShares($config, $this->connection, $this->userManager, $this->groupManager); } protected function tearDown() { @@ -108,6 +91,7 @@ class RepairUnmergedSharesTest extends TestCase { } private function createShare($type, $sourceId, $recipient, $targetName, $permissions, $parentId = null) { + $this->lastShareTime += 100; $qb = $this->connection->getQueryBuilder(); $values = [ 'share_type' => $qb->expr()->literal($type), @@ -119,7 +103,7 @@ class RepairUnmergedSharesTest extends TestCase { 'file_source' => $qb->expr()->literal($sourceId), 'file_target' => $qb->expr()->literal($targetName), 'permissions' => $qb->expr()->literal($permissions), - 'stime' => $qb->expr()->literal(time()), + 'stime' => $qb->expr()->literal($this->lastShareTime), ]; if ($parentId !== null) { $values['parent'] = $qb->expr()->literal($parentId); @@ -204,7 +188,7 @@ class RepairUnmergedSharesTest extends TestCase { [ // #2 bogus share // - outsider shares with group1, group2 - // - one subshare for each group share + // - one subshare for each group share, both with parenthesis // - but the targets do not match when grouped [ [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], @@ -218,7 +202,7 @@ class RepairUnmergedSharesTest extends TestCase { [ ['/test', 31], ['/test', 31], - // reset to original name + // reset to original name as the sub-names have parenthesis ['/test', 31], ['/test', 31], // leave unrelated alone @@ -228,6 +212,54 @@ class RepairUnmergedSharesTest extends TestCase { [ // #3 bogus share // - outsider shares with group1, group2 + // - one subshare for each group share, both renamed manually + // - but the targets do not match when grouped + [ + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], + // child of the previous ones + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (1 legit paren)', 31, 0], + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed (2 legit paren)', 31, 1], + // different unrelated share + [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], + ], + [ + ['/test', 31], + ['/test', 31], + // reset to less recent subshare name + ['/test_renamed (2 legit paren)', 31], + ['/test_renamed (2 legit paren)', 31], + // leave unrelated alone + ['/test (4)', 31], + ] + ], + [ + // #4 bogus share + // - outsider shares with group1, group2 + // - one subshare for each group share, one with parenthesis + // - but the targets do not match when grouped + [ + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], + // child of the previous ones + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test (2)', 31, 0], + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/test_renamed', 31, 1], + // different unrelated share + [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (4)', 31], + ], + [ + ['/test', 31], + ['/test', 31], + // reset to less recent subshare name but without parenthesis + ['/test_renamed', 31], + ['/test_renamed', 31], + // leave unrelated alone + ['/test (4)', 31], + ] + ], + [ + // #5 bogus share + // - outsider shares with group1, group2 // - one subshare for each group share // - first subshare not renamed (as in real world scenario) // - but the targets do not match when grouped @@ -251,7 +283,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #4 bogus share: + // #6 bogus share: // - outsider shares with group1, group2 // - one subshare for each group share // - non-matching targets @@ -276,7 +308,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #5 bogus share: + // #7 bogus share: // - outsider shares with group1, group2 // - one subshare for each group share // - non-matching targets @@ -301,7 +333,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #6 bogus share: + // #8 bogus share: // - outsider shares with group1, group2 and also user2 // - one subshare for each group share // - one extra share entry for direct share to user2 @@ -329,7 +361,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #7 bogus share: + // #9 bogus share: // - outsider shares with group1 and also user2 // - no subshare at all // - one extra share entry for direct share to user2 @@ -350,7 +382,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #8 legitimate share with own group: + // #10 legitimate share with own group: // - insider shares with both groups the user is already in // - no subshares in this case [ @@ -368,7 +400,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #9 legitimate shares: + // #11 legitimate shares: // - group share with same group // - group share with other group // - user share where recipient renamed @@ -392,7 +424,7 @@ class RepairUnmergedSharesTest extends TestCase { ] ], [ - // #10 legitimate share: + // #12 legitimate share: // - outsider shares with group and user directly with different permissions // - no subshares // - same targets @@ -410,6 +442,42 @@ class RepairUnmergedSharesTest extends TestCase { ['/test (4)', 31], ] ], + [ + // #13 bogus share: + // - outsider shares with group1, user2 and then group2 + // - user renamed share as soon as it arrived before the next share (order) + // - one subshare for each group share + // - one extra share entry for direct share to user2 + // - non-matching targets + [ + // first share with group + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup1', '/test', 31], + // recipient renames + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/first', 31, 0], + // then direct share, user renames too + [Constants::SHARE_TYPE_USER, 123, 'user2', '/second', 31], + // another share with the second group + [Constants::SHARE_TYPE_GROUP, 123, 'recipientgroup2', '/test', 31], + // use renames it + [DefaultShareProvider::SHARE_TYPE_USERGROUP, 123, 'user2', '/third', 31, 1], + // different unrelated share + [Constants::SHARE_TYPE_GROUP, 456, 'recipientgroup1', '/test (5)', 31], + ], + [ + // group share with group1 left alone + ['/test', 31], + // first subshare repaired + ['/third', 31], + // direct user share repaired + ['/third', 31], + // group share with group2 left alone + ['/test', 31], + // second subshare repaired + ['/third', 31], + // leave unrelated alone + ['/test (5)', 31], + ] + ], ]; } @@ -419,6 +487,38 @@ class RepairUnmergedSharesTest extends TestCase { * @dataProvider sharesDataProvider */ public function testMergeGroupShares($shares, $expectedShares) { + $user1 = $this->getMock('\OCP\IUser'); + $user1->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('user1')); + + $user2 = $this->getMock('\OCP\IUser'); + $user2->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('user2')); + + $users = [$user1, $user2]; + + $this->groupManager->expects($this->any()) + ->method('getUserGroupIds') + ->will($this->returnValueMap([ + // owner + [$user1, ['samegroup1', 'samegroup2']], + // recipient + [$user2, ['recipientgroup1', 'recipientgroup2']], + ])); + + $this->userManager->expects($this->once()) + ->method('countUsers') + ->will($this->returnValue([2])); + $this->userManager->expects($this->once()) + ->method('callForAllUsers') + ->will($this->returnCallback(function(\Closure $closure) use ($users) { + foreach ($users as $user) { + $closure($user); + } + })); + $shareIds = []; foreach ($shares as $share) { @@ -445,5 +545,30 @@ class RepairUnmergedSharesTest extends TestCase { $this->assertEquals($expectedShare[1], $share['permissions']); } } + + public function duplicateNamesProvider() { + return [ + // matching + ['filename (1).txt', true], + ['folder (2)', true], + ['filename (1)(2).txt', true], + // non-matching + ['filename ().txt', false], + ['folder ()', false], + ['folder (1x)', false], + ['folder (x1)', false], + ['filename (a)', false], + ['filename (1).', false], + ['filename (1).txt.txt', false], + ['filename (1)..txt', false], + ]; + } + + /** + * @dataProvider duplicateNamesProvider + */ + public function testIsPotentialDuplicateName($name, $expectedResult) { + $this->assertEquals($expectedResult, $this->invokePrivate($this->repair, 'isPotentialDuplicateName', [$name])); + } } diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 379c7e39442..4b8067117b1 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -371,7 +371,7 @@ class SessionTest extends \Test\TestCase { ->with('token_auth_enforced', false) ->will($this->returnValue(true)); $request - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getRemoteAddress') ->willReturn('192.168.0.1'); $this->throttler @@ -379,7 +379,7 @@ class SessionTest extends \Test\TestCase { ->method('sleepDelay') ->with('192.168.0.1'); $this->throttler - ->expects($this->once()) + ->expects($this->any()) ->method('getDelay') ->with('192.168.0.1') ->willReturn(0); @@ -412,7 +412,7 @@ class SessionTest extends \Test\TestCase { ->method('set') ->with('app_password', 'I-AM-AN-APP-PASSWORD'); $request - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getRemoteAddress') ->willReturn('192.168.0.1'); $this->throttler @@ -420,7 +420,7 @@ class SessionTest extends \Test\TestCase { ->method('sleepDelay') ->with('192.168.0.1'); $this->throttler - ->expects($this->once()) + ->expects($this->any()) ->method('getDelay') ->with('192.168.0.1') ->willReturn(0); @@ -459,7 +459,7 @@ class SessionTest extends \Test\TestCase { ->will($this->returnValue(true)); $request - ->expects($this->exactly(2)) + ->expects($this->any()) ->method('getRemoteAddress') ->willReturn('192.168.0.1'); $this->throttler @@ -467,7 +467,7 @@ class SessionTest extends \Test\TestCase { ->method('sleepDelay') ->with('192.168.0.1'); $this->throttler - ->expects($this->once()) + ->expects($this->any()) ->method('getDelay') ->with('192.168.0.1') ->willReturn(0); |