diff options
113 files changed, 5249 insertions, 3149 deletions
diff --git a/.codecov.yml b/.codecov.yml index 39a2868bb74..47e0b94bec4 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -6,4 +6,6 @@ coverage: round: down range: "70...100" -comment: false +comment: + layout: "header, diff, changes, uncovered, tree" + behavior: default diff --git a/.drone.yml b/.drone.yml index 2b5407e0df6..35ed0c74593 100644 --- a/.drone.yml +++ b/.drone.yml @@ -33,6 +33,13 @@ pipeline: when: matrix: TESTS: signed-off-check + htaccess-checker: + image: nextcloudci/php7.0:php7.0-2 + commands: + - php ./build/htaccess-checker.php + when: + matrix: + TESTS: htaccess-checker syntax-php5.6: image: nextcloudci/php5.6:php5.6-2 commands: @@ -332,6 +339,7 @@ pipeline: matrix: include: - TESTS: signed-off-check + - TESTS: htaccess-checker - TESTS: nodb-codecov - TESTS: db-codecov - TESTS: integration-capabilities_features diff --git a/.gitignore b/.gitignore index f25fb52ce2d..964701eea63 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ !/apps/files !/apps/federation !/apps/federatedfilesharing +!/apps/sharebymail !/apps/encryption !/apps/files_external !/apps/files_sharing diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md index 75d64667761..75d64667761 100644 --- a/.github/CONTRIBUTING.md +++ b/CONTRIBUTING.md diff --git a/README.md b/README.md index 66a224aa227..17035a4d91f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Nextcloud Server -[](https://drone.nextcloud.com/nextcloud/server) [](https://scrutinizer-ci.com/g/nextcloud/server/?branch=master) +[](https://codecov.io/gh/nextcloud/server) +[](https://bestpractices.coreinfrastructure.org/projects/209) [](https://webchat.freenode.net/?channels=nextcloud) [](https://webchat.freenode.net/?channels=nextcloud-dev) @@ -50,7 +51,7 @@ changed it substantially: Please read the [Code of Conduct](https://nextcloud.com/community/code-of-conduct/). This document offers some guidance to ensure Nextcloud participants can cooperate effectively in a positive and inspiring atmosphere, and to explain how together we can strengthen and support each other. -Please review the [guidelines for contributing](https://github.com/nextcloud/server/blob/master/.github/CONTRIBUTING.md) to this repository. +Please review the [guidelines for contributing](https://github.com/nextcloud/server/blob/master/CONTRIBUTING.md) to this repository. More information how to contribute: [https://nextcloud.com/contribute/](https://nextcloud.com/contribute/) diff --git a/apps/federatedfilesharing/lib/AddressHandler.php b/apps/federatedfilesharing/lib/AddressHandler.php index 3c178b813e4..5fc41c2c804 100644 --- a/apps/federatedfilesharing/lib/AddressHandler.php +++ b/apps/federatedfilesharing/lib/AddressHandler.php @@ -161,6 +161,22 @@ class AddressHandler { } /** + * check if the url contain the protocol (http or https) + * + * @param string $url + * @return bool + */ + public function urlContainProtocol($url) { + if (strpos($url, 'https://') === 0 || + strpos($url, 'http://') === 0) { + + return true; + } + + return false; + } + + /** * Strips away a potential file names and trailing slashes: * - http://localhost * - http://localhost/ diff --git a/apps/federatedfilesharing/lib/FederatedShareProvider.php b/apps/federatedfilesharing/lib/FederatedShareProvider.php index 270bf86daf7..6481abe32e2 100644 --- a/apps/federatedfilesharing/lib/FederatedShareProvider.php +++ b/apps/federatedfilesharing/lib/FederatedShareProvider.php @@ -222,6 +222,8 @@ class FederatedShareProvider implements IShareProvider { $token ); + $failure = false; + try { $sharedByFederatedId = $share->getSharedBy(); if ($this->userManager->userExists($sharedByFederatedId)) { @@ -239,17 +241,22 @@ class FederatedShareProvider implements IShareProvider { ); if ($send === false) { - $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable.', - [$share->getNode()->getName(), $share->getSharedWith()]); - throw new \Exception($message_t); + $failure = true; } } catch (\Exception $e) { $this->logger->error('Failed to notify remote server of federated share, removing share (' . $e->getMessage() . ')'); + $failure = true; + } + + if($failure) { $this->removeShareFromTableById($shareId); - throw $e; + $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.', + [$share->getNode()->getName(), $share->getSharedWith()]); + throw new \Exception($message_t); } return $shareId; + } /** diff --git a/apps/federatedfilesharing/lib/Notifications.php b/apps/federatedfilesharing/lib/Notifications.php index 074991c43f8..8110b4915da 100644 --- a/apps/federatedfilesharing/lib/Notifications.php +++ b/apps/federatedfilesharing/lib/Notifications.php @@ -84,7 +84,6 @@ class Notifications { list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); if ($user && $remote) { - $url = $remote; $local = $this->addressHandler->generateRemoteURL(); $fields = array( @@ -99,8 +98,7 @@ class Notifications { 'remote' => $local, ); - $url = $this->addressHandler->removeProtocolFromUrl($url); - $result = $this->tryHttpPostToShareEndpoint($url, '', $fields); + $result = $this->tryHttpPostToShareEndpoint($remote, '', $fields); $status = json_decode($result['result'], true); if ($result['success'] && ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200)) { @@ -135,8 +133,7 @@ class Notifications { 'remoteId' => $shareId ); - $url = $this->addressHandler->removeProtocolFromUrl($remote); - $result = $this->tryHttpPostToShareEndpoint(rtrim($url, '/'), '/' . $id . '/reshare', $fields); + $result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $id . '/reshare', $fields); $status = json_decode($result['result'], true); $httpRequestSuccessful = $result['success']; @@ -193,7 +190,7 @@ class Notifications { /** * forward accept reShare to remote server - * + * * @param string $remote * @param int $remoteId * @param string $token @@ -231,8 +228,7 @@ class Notifications { $fields[$key] = $value; } - $url = $this->addressHandler->removeProtocolFromUrl($remote); - $result = $this->tryHttpPostToShareEndpoint(rtrim($url, '/'), '/' . $remoteId . '/' . $action, $fields); + $result = $this->tryHttpPostToShareEndpoint(rtrim($remote, '/'), '/' . $remoteId . '/' . $action, $fields); $status = json_decode($result['result'], true); if ($result['success'] && @@ -270,7 +266,8 @@ class Notifications { } /** - * try http post first with https and then with http as a fallback + * try http post with the given protocol, if no protocol is given we pick + * the secure one (https) * * @param string $remoteDomain * @param string $urlSuffix @@ -280,33 +277,31 @@ class Notifications { */ protected function tryHttpPostToShareEndpoint($remoteDomain, $urlSuffix, array $fields) { $client = $this->httpClientService->newClient(); - $protocol = 'https://'; + + if ($this->addressHandler->urlContainProtocol($remoteDomain) === false) { + $remoteDomain = 'https://' . $remoteDomain; + } + $result = [ 'success' => false, 'result' => '', ]; - $try = 0; - - while ($result['success'] === false && $try < 2) { - $endpoint = $this->discoveryManager->getShareEndpoint($protocol . $remoteDomain); - try { - $response = $client->post($protocol . $remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ - 'body' => $fields, - 'timeout' => 10, - 'connect_timeout' => 10, - ]); - $result['result'] = $response->getBody(); - $result['success'] = true; - break; - } catch (\Exception $e) { - // if flat re-sharing is not supported by the remote server - // we re-throw the exception and fall back to the old behaviour. - // (flat re-shares has been introduced in Nextcloud 9.1) - if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { - throw $e; - } - $try++; - $protocol = 'http://'; + + $endpoint = $this->discoveryManager->getShareEndpoint($remoteDomain); + try { + $response = $client->post($remoteDomain . $endpoint . $urlSuffix . '?format=' . self::RESPONSE_FORMAT, [ + 'body' => $fields, + 'timeout' => 10, + 'connect_timeout' => 10, + ]); + $result['result'] = $response->getBody(); + $result['success'] = true; + } catch (\Exception $e) { + // if flat re-sharing is not supported by the remote server + // we re-throw the exception and fall back to the old behaviour. + // (flat re-shares has been introduced in Nextcloud 9.1) + if ($e->getCode() === Http::STATUS_INTERNAL_SERVER_ERROR) { + throw $e; } } diff --git a/apps/federatedfilesharing/tests/AddressHandlerTest.php b/apps/federatedfilesharing/tests/AddressHandlerTest.php index c2e69fb2bd7..f62f3b62e03 100644 --- a/apps/federatedfilesharing/tests/AddressHandlerTest.php +++ b/apps/federatedfilesharing/tests/AddressHandlerTest.php @@ -178,6 +178,26 @@ class AddressHandlerTest extends \Test\TestCase { } /** + * @dataProvider dataTestUrlContainProtocol + * + * @param string $url + * @param bool $expectedResult + */ + public function testUrlContainProtocol($url, $expectedResult) { + $result = $this->addressHandler->urlContainProtocol($url); + $this->assertSame($expectedResult, $result); + } + + public function dataTestUrlContainProtocol() { + return [ + ['http://nextcloud.com', true], + ['https://nextcloud.com', true], + ['nextcloud.com', false], + ['httpserver.com', false], + ]; + } + + /** * @dataProvider dataTestFixRemoteUrl * * @param string $url diff --git a/apps/federatedfilesharing/tests/FederatedShareProviderTest.php b/apps/federatedfilesharing/tests/FederatedShareProviderTest.php index 92f6ac5e996..874d4b48a5c 100644 --- a/apps/federatedfilesharing/tests/FederatedShareProviderTest.php +++ b/apps/federatedfilesharing/tests/FederatedShareProviderTest.php @@ -227,7 +227,7 @@ class FederatedShareProviderTest extends \Test\TestCase { $share = $this->provider->create($share); $this->fail(); } catch (\Exception $e) { - $this->assertEquals('Sharing myFile failed, could not find user@server.com, maybe the server is currently unreachable.', $e->getMessage()); + $this->assertEquals('Sharing myFile failed, could not find user@server.com, maybe the server is currently unreachable or uses a self-signed certificate.', $e->getMessage()); } $qb = $this->connection->getQueryBuilder(); @@ -283,7 +283,7 @@ class FederatedShareProviderTest extends \Test\TestCase { $share = $this->provider->create($share); $this->fail(); } catch (\Exception $e) { - $this->assertEquals('dummy', $e->getMessage()); + $this->assertEquals('Sharing myFile failed, could not find user@server.com, maybe the server is currently unreachable or uses a self-signed certificate.', $e->getMessage()); } $qb = $this->connection->getQueryBuilder(); @@ -707,8 +707,8 @@ class FederatedShareProviderTest extends \Test\TestCase { $userManager = \OC::$server->getUserManager(); $rootFolder = \OC::$server->getRootFolder(); - $u1 = $userManager->createUser('testFed', 'test'); - $u2 = $userManager->createUser('testFed2', 'test'); + $u1 = $userManager->createUser('testFed', md5(time())); + $u2 = $userManager->createUser('testFed2', md5(time())); $folder1 = $rootFolder->getUserFolder($u1->getUID())->newFolder('foo'); $file1 = $folder1->newFile('bar1'); diff --git a/apps/federatedfilesharing/tests/NotificationsTest.php b/apps/federatedfilesharing/tests/NotificationsTest.php index dbcb1ef4e87..a5f5c6bc078 100644 --- a/apps/federatedfilesharing/tests/NotificationsTest.php +++ b/apps/federatedfilesharing/tests/NotificationsTest.php @@ -58,7 +58,7 @@ class NotificationsTest extends \Test\TestCase { /** * get instance of Notifications class - * + * * @param array $mockedMethods methods which should be mocked * @return Notifications | \PHPUnit_Framework_MockObject_MockObject */ @@ -81,7 +81,7 @@ class NotificationsTest extends \Test\TestCase { ] )->setMethods($mockedMethods)->getMock(); } - + return $instance; } @@ -94,7 +94,7 @@ class NotificationsTest extends \Test\TestCase { * @param bool $expected */ public function testSendUpdateToRemote($try, $httpRequestResult, $expected) { - $remote = 'remote'; + $remote = 'http://remote'; $id = 42; $timestamp = 63576; $token = 'token'; @@ -106,9 +106,6 @@ class NotificationsTest extends \Test\TestCase { ->with($remote, '/'.$id.'/unshare', ['token' => $token, 'data1Key' => 'data1Value']) ->willReturn($httpRequestResult); - $this->addressHandler->expects($this->once())->method('removeProtocolFromUrl') - ->with($remote)->willReturn($remote); - // only add background job on first try if ($try === 0 && $expected === false) { $this->jobList->expects($this->once())->method('add') diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index d6f88581b96..138b68601cb 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -200,7 +200,8 @@ class ApiController extends Controller { \OCP\Share::SHARE_TYPE_USER, \OCP\Share::SHARE_TYPE_GROUP, \OCP\Share::SHARE_TYPE_LINK, - \OCP\Share::SHARE_TYPE_REMOTE + \OCP\Share::SHARE_TYPE_REMOTE, + \OCP\Share::SHARE_TYPE_EMAIL ]; foreach ($requestedShareTypes as $requestedShareType) { // one of each type is enough to find out about the types diff --git a/apps/files_sharing/lib/Controller/ShareAPIController.php b/apps/files_sharing/lib/Controller/ShareAPIController.php index ad9ac6c0851..1358663ea2b 100644 --- a/apps/files_sharing/lib/Controller/ShareAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareAPIController.php @@ -36,7 +36,6 @@ use OCP\IL10N; use OCP\IUserManager; use OCP\IRequest; use OCP\IURLGenerator; -use OCP\IUser; use OCP\Files\IRootFolder; use OCP\Lock\LockedException; use OCP\Share\IManager; @@ -186,7 +185,11 @@ class ShareAPIController extends OCSController { } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_REMOTE) { $result['share_with'] = $share->getSharedWith(); - $result['share_with_displayname'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'CLOUD'); + $result['token'] = $share->getToken(); + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $this->getDisplayNameFromAddressBook($share->getSharedWith(), 'EMAIL'); $result['token'] = $share->getToken(); } @@ -196,6 +199,28 @@ class ShareAPIController extends OCSController { } /** + * Check if one of the users address books knows the exact property, if + * yes we return the full name. + * + * @param string $query + * @param string $property + * @return string + */ + private function getDisplayNameFromAddressBook($query, $property) { + // FIXME: If we inject the contacts manager it gets initialized bofore any address books are registered + $result = \OC::$server->getContactsManager()->search($query, [$property]); + foreach ($result as $r) { + foreach($r[$property] as $value) { + if ($value === $query) { + return $r['FN']; + } + } + } + + return $query; + } + + /** * Get a specific share by id * * @NoAdminRequired @@ -245,11 +270,17 @@ class ShareAPIController extends OCSController { throw new OCSNotFoundException($this->l->t('could not delete share')); } - if (!$this->canAccessShare($share, false)) { + if (!$this->canAccessShare($share)) { throw new OCSNotFoundException($this->l->t('Could not delete share')); } - $this->shareManager->deleteShare($share); + if ($share->getShareType() === \OCP\Share::SHARE_TYPE_GROUP && + $share->getShareOwner() !== $this->currentUser && + $share->getSharedBy() !== $this->currentUser) { + $this->shareManager->deleteFromSelf($share, $this->currentUser); + } else { + $this->shareManager->deleteShare($share); + } return new DataResponse(); } @@ -400,6 +431,17 @@ class ShareAPIController extends OCSController { $share->setSharedWith($shareWith); $share->setPermissions($permissions); + } else if ($shareType === \OCP\Share::SHARE_TYPE_EMAIL) { + if ($share->getNodeType() === 'file') { + $share->setPermissions(\OCP\Constants::PERMISSION_READ); + } else { + $share->setPermissions( + \OCP\Constants::PERMISSION_READ | + \OCP\Constants::PERMISSION_CREATE | + \OCP\Constants::PERMISSION_UPDATE | + \OCP\Constants::PERMISSION_DELETE); + } + $share->setSharedWith($shareWith); } else { throw new OCSBadRequestException($this->l->t('Unknown share type')); } @@ -466,6 +508,9 @@ class ShareAPIController extends OCSController { $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $node, false, -1, 0)); $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $node, false, -1, 0)); $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $node, false, -1, 0)); + if($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) { + $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $node, false, -1, 0)); + } if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { $shares = array_merge($shares, $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $node, false, -1, 0)); } @@ -541,7 +586,12 @@ class ShareAPIController extends OCSController { $userShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_USER, $path, $reshares, -1, 0); $groupShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_GROUP, $path, $reshares, -1, 0); $linkShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_LINK, $path, $reshares, -1, 0); - $shares = array_merge($userShares, $groupShares, $linkShares); + if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) { + $mailShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_EMAIL, $path, $reshares, -1, 0); + } else { + $mailShares = []; + } + $shares = array_merge($userShares, $groupShares, $linkShares, $mailShares); if ($this->shareManager->outgoingServer2ServerSharesAllowed()) { $federatedShares = $this->shareManager->getSharesBy($this->currentUser, \OCP\Share::SHARE_TYPE_REMOTE, $path, $reshares, -1, 0); @@ -774,13 +824,24 @@ class ShareAPIController extends OCSController { // First check if it is an internal share. try { $share = $this->shareManager->getShareById('ocinternal:' . $id); + return $share; } catch (ShareNotFound $e) { - if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { - throw new ShareNotFound(); + // Do nothing, just try the other share type + } + + try { + if ($this->shareManager->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) { + $share = $this->shareManager->getShareById('ocMailShare:' . $id); + return $share; } + } catch (ShareNotFound $e) { + // Do nothing, just try the other share type + } - $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id); + if (!$this->shareManager->outgoingServer2ServerSharesAllowed()) { + throw new ShareNotFound(); } + $share = $this->shareManager->getShareById('ocFederatedSharing:' . $id); return $share; } diff --git a/apps/files_sharing/lib/Controller/ShareesAPIController.php b/apps/files_sharing/lib/Controller/ShareesAPIController.php index 99c6b55240d..5e01c9bfb09 100644 --- a/apps/files_sharing/lib/Controller/ShareesAPIController.php +++ b/apps/files_sharing/lib/Controller/ShareesAPIController.php @@ -273,15 +273,15 @@ class ShareesAPIController extends OCSController { /** * @param string $search - * @return array possible sharees + * @return array */ protected function getRemote($search) { - $this->result['remotes'] = []; + $result = ['results' => [], 'exact' => []]; // Search in contacts //@todo Pagination missing $addressBookContacts = $this->contactsManager->search($search, ['CLOUD', 'FN']); - $foundRemoteById = false; + $result['exactIdMatch'] = false; foreach ($addressBookContacts as $contact) { if (isset($contact['isLocalSystemBook'])) { continue; @@ -295,10 +295,10 @@ class ShareesAPIController extends OCSController { list(, $serverUrl) = $this->splitUserRemote($cloudId); if (strtolower($contact['FN']) === strtolower($search) || strtolower($cloudId) === strtolower($search)) { if (strtolower($cloudId) === strtolower($search)) { - $foundRemoteById = true; + $result['exactIdMatch'] = true; } - $this->result['exact']['remotes'][] = [ - 'label' => $contact['FN'], + $result['exact'][] = [ + 'label' => $contact['FN'] . " ($cloudId)", 'value' => [ 'shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => $cloudId, @@ -306,8 +306,8 @@ class ShareesAPIController extends OCSController { ], ]; } else { - $this->result['remotes'][] = [ - 'label' => $contact['FN'], + $result['results'][] = [ + 'label' => $contact['FN'] . " ($cloudId)", 'value' => [ 'shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => $cloudId, @@ -320,11 +320,11 @@ class ShareesAPIController extends OCSController { } if (!$this->shareeEnumeration) { - $this->result['remotes'] = []; + $result['results'] = []; } - if (!$foundRemoteById && substr_count($search, '@') >= 1 && $this->offset === 0) { - $this->result['exact']['remotes'][] = [ + if (!$result['exactIdMatch'] && substr_count($search, '@') >= 1 && $this->offset === 0) { + $result['exact'][] = [ 'label' => $search, 'value' => [ 'shareType' => Share::SHARE_TYPE_REMOTE, @@ -334,6 +334,8 @@ class ShareesAPIController extends OCSController { } $this->reachedEndFor[] = 'remotes'; + + return $result; } /** @@ -405,68 +407,6 @@ class ShareesAPIController extends OCSController { } /** - * @param string $search - */ - protected function getEmails($search) { - $this->result['emails'] = []; - $this->result['exact']['emails'] = []; - - $foundEmail = false; - - // Search in contacts - //@todo Pagination missing - $addressBookContacts = $this->contactsManager->search($search, ['FN', 'EMAIL']); - foreach ($addressBookContacts as $contact) { - if (!isset($contact['EMAIL'])) { - continue; - } - - $emails = $contact['EMAIL']; - if (!is_array($emails)) { - $emails = [$emails]; - } - - foreach ($emails as $email) { - if (strtolower($search) === strtolower($contact['FN']) || - strtolower($search) === strtolower($email) - ) { - if (strtolower($search) === strtolower($email)) { - $foundEmail = true; - } - - $this->result['exact']['emails'][] = [ - 'label' => $contact['FN'], - 'value' => [ - 'shareType' => Share::SHARE_TYPE_EMAIL, - 'shareWith' => $email, - ], - ]; - } else if ($this->shareeEnumeration) { - $this->result['emails'][] = [ - 'label' => $contact['FN'], - 'value' => [ - 'shareType' => Share::SHARE_TYPE_EMAIL, - 'shareWith' => $email, - ], - ]; - } - } - } - - if (!$foundEmail && substr_count($search, '@') >= 1 && $this->offset === 0) { - $this->result['exact']['emails'][] = [ - 'label' => $search, - 'value' => [ - 'shareType' => Share::SHARE_TYPE_EMAIL, - 'shareWith' => $search, - ], - ]; - } - - $this->reachedEndFor[] = 'emails'; - } - - /** * @NoAdminRequired * * @param string $search @@ -487,17 +427,16 @@ class ShareesAPIController extends OCSController { $shareTypes = [ Share::SHARE_TYPE_USER, + Share::SHARE_TYPE_REMOTE, + Share::SHARE_TYPE_EMAIL ]; if ($this->shareManager->allowGroupSharing()) { $shareTypes[] = Share::SHARE_TYPE_GROUP; } - $shareTypes[] = Share::SHARE_TYPE_EMAIL; - $shareTypes[] = Share::SHARE_TYPE_REMOTE; - - if (is_array($shareType)) { - $shareTypes = array_intersect($shareTypes, $shareType); + if (isset($_GET['shareType']) && is_array($_GET['shareType'])) { + $shareTypes = array_intersect($shareTypes, $_GET['shareType']); sort($shareTypes); } else if (is_numeric($shareType)) { $shareTypes = array_intersect($shareTypes, [(int) $shareType]); @@ -509,6 +448,11 @@ class ShareesAPIController extends OCSController { $shareTypes = array_diff($shareTypes, [Share::SHARE_TYPE_REMOTE]); } + if (!$this->shareManager->shareProviderExists(Share::SHARE_TYPE_EMAIL)) { + // Remove mail shares from type array, because the share provider is not loaded + $shareTypes = array_diff($shareTypes, [Share::SHARE_TYPE_EMAIL]); + } + $this->shareWithGroupOnly = $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes'; $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; $this->limit = (int) $perPage; @@ -560,13 +504,30 @@ class ShareesAPIController extends OCSController { } // Get remote + $remoteResults = ['results' => [], 'exact' => [], 'exactIdMatch' => false]; if (in_array(Share::SHARE_TYPE_REMOTE, $shareTypes)) { - $this->getRemote($search); + $remoteResults = $this->getRemote($search); } - // Get email + $mailResults = ['results' => [], 'exact' => [], 'exactIdMatch' => false]; if (in_array(Share::SHARE_TYPE_EMAIL, $shareTypes)) { - $this->getEmails($search); + $mailResults = $this->getEmail($search); + } + + // if we have a exact match, either for the federated cloud id or for the + // email address we only return the exact match. It is highly unlikely + // that the exact same email address and federated cloud id exists + if ($mailResults['exactIdMatch'] && !$remoteResults['exactIdMatch']) { + $this->result['emails'] = $mailResults['results']; + $this->result['exact']['emails'] = $mailResults['exact']; + } else if (!$mailResults['exactIdMatch'] && $remoteResults['exactIdMatch']) { + $this->result['remotes'] = $remoteResults['results']; + $this->result['exact']['remotes'] = $remoteResults['exact']; + } else { + $this->result['remotes'] = $remoteResults['results']; + $this->result['exact']['remotes'] = $remoteResults['exact']; + $this->result['emails'] = $mailResults['results']; + $this->result['exact']['emails'] = $mailResults['exact']; } $response = new Http\DataResponse($this->result); @@ -584,6 +545,70 @@ class ShareesAPIController extends OCSController { } /** + * @param string $search + * @return array + */ + protected function getEmail($search) { + $result = ['results' => [], 'exact' => []]; + + // Search in contacts + //@todo Pagination missing + $addressBookContacts = $this->contactsManager->search($search, ['EMAIL', 'FN']); + $result['exactIdMatch'] = false; + foreach ($addressBookContacts as $contact) { + if (isset($contact['isLocalSystemBook'])) { + continue; + } + if (isset($contact['EMAIL'])) { + $emailAddresses = $contact['EMAIL']; + if (!is_array($emailAddresses)) { + $emailAddresses = [$emailAddresses]; + } + foreach ($emailAddresses as $emailAddress) { + if (strtolower($contact['FN']) === strtolower($search) || strtolower($emailAddress) === strtolower($search)) { + if (strtolower($emailAddress) === strtolower($search)) { + $result['exactIdMatch'] = true; + } + $result['exact'][] = [ + 'label' => $contact['FN'] . " ($emailAddress)", + 'value' => [ + 'shareType' => Share::SHARE_TYPE_EMAIL, + 'shareWith' => $emailAddress, + ], + ]; + } else { + $result['results'][] = [ + 'label' => $contact['FN'] . " ($emailAddress)", + 'value' => [ + 'shareType' => Share::SHARE_TYPE_EMAIL, + 'shareWith' => $emailAddress, + ], + ]; + } + } + } + } + + if (!$this->shareeEnumeration) { + $result['results'] = []; + } + + if (!$result['exactIdMatch'] && filter_var($search, FILTER_VALIDATE_EMAIL)) { + $result['exact'][] = [ + 'label' => $search, + 'value' => [ + 'shareType' => Share::SHARE_TYPE_EMAIL, + 'shareWith' => $search, + ], + ]; + } + + $this->reachedEndFor[] = 'emails'; + + return $result; + } + + /** * Generates a bunch of pagination links for the current page * * @param int $page Current page diff --git a/apps/files_sharing/tests/ApiTest.php b/apps/files_sharing/tests/ApiTest.php index 6ab0cb4e1a6..540905a7dc9 100644 --- a/apps/files_sharing/tests/ApiTest.php +++ b/apps/files_sharing/tests/ApiTest.php @@ -234,6 +234,7 @@ class ApiTest extends TestCase { function testEnfoceLinkPassword() { + $password = md5(time()); $appConfig = \OC::$server->getAppConfig(); $appConfig->setValue('core', 'shareapi_enforce_links_password', 'yes'); @@ -257,14 +258,14 @@ class ApiTest extends TestCase { // share with password should succeed $ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1); - $result = $ocs->createShare($this->folder, \OCP\Constants::PERMISSION_ALL, \OCP\Share::SHARE_TYPE_LINK, null, 'false', 'bar'); + $result = $ocs->createShare($this->folder, \OCP\Constants::PERMISSION_ALL, \OCP\Share::SHARE_TYPE_LINK, null, 'false', $password); $ocs->cleanup(); $data = $result->getData(); // setting new password should succeed $ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1); - $ocs->updateShare($data['id'], null, 'bar'); + $ocs->updateShare($data['id'], null, $password); $ocs->cleanup(); // removing password should fail @@ -887,6 +888,9 @@ class ApiTest extends TestCase { * @depends testCreateShareLink */ function testUpdateShare() { + + $password = md5(time()); + $node1 = $this->userFolder->get($this->filename); $share1 = $this->shareManager->newShare(); $share1->setNode($node1) @@ -915,7 +919,7 @@ class ApiTest extends TestCase { $this->assertNull($share2->getPassword()); $ocs = $this->createOCS(self::TEST_FILES_SHARING_API_USER1); - $ocs->updateShare($share2->getId(), null, 'foo'); + $ocs->updateShare($share2->getId(), null, $password); $ocs->cleanup(); $share2 = $this->shareManager->getShareById('ocinternal:' . $share2->getId()); diff --git a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php index 6ad0576b6fa..890fdb6eda0 100644 --- a/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareAPIControllerTest.php @@ -23,8 +23,10 @@ */ namespace OCA\Files_Sharing\Tests\Controller; +use OC\ContactsManager; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\Contacts; use OCP\Files\Folder; use OCP\IL10N; use OCA\Files_Sharing\Controller\ShareAPIController; diff --git a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php index 6ee1ff596e4..e8ee55d1845 100644 --- a/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php +++ b/apps/files_sharing/tests/Controller/ShareesAPIControllerTest.php @@ -771,28 +771,44 @@ class ShareesAPIControllerTest extends TestCase { $this->assertCount((int) $reachedEnd, $this->invokePrivate($this->sharees, 'reachedEndFor')); } + /** + * @dataProvider dataGetRemote + * + * @param string $searchTerm + * @param array $contacts + * @param bool $shareeEnumeration + * @param array $expected + * @param bool $reachedEnd + */ + public function testGetRemote($searchTerm, $contacts, $shareeEnumeration, $expected, $reachedEnd) { + $this->invokePrivate($this->sharees, 'shareeEnumeration', [$shareeEnumeration]); + $this->contactsManager->expects($this->any()) + ->method('search') + ->with($searchTerm, ['CLOUD', 'FN']) + ->willReturn($contacts); + + $result = $this->invokePrivate($this->sharees, 'getRemote', [$searchTerm]); + + $this->assertEquals($expected, $result); + $this->assertCount((int) $reachedEnd, $this->invokePrivate($this->sharees, 'reachedEndFor')); + } + public function dataGetRemote() { return [ - ['test', [], true, [], [], true], - ['test', [], false, [], [], true], + ['test', [], true, ['results' => [], 'exact' => [], 'exactIdMatch' => false], true], + ['test', [], false, ['results' => [], 'exact' => [], 'exactIdMatch' => false], true], [ 'test@remote', [], true, - [ - ['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']], - ], - [], + ['results' => [], 'exact' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]], 'exactIdMatch' => false], true, ], [ 'test@remote', [], false, - [ - ['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']], - ], - [], + ['results' => [], 'exact' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]], 'exactIdMatch' => false], true, ], [ @@ -814,10 +830,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], true, - [], - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']], - ], + ['results' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => [], 'exactIdMatch' => false], true, ], [ @@ -839,8 +852,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], false, - [], - [], + ['results' => [], 'exact' => [], 'exactIdMatch' => false], true, ], [ @@ -862,12 +874,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], true, - [ - ['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']], - ], - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']], - ], + ['results' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exact' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]], 'exactIdMatch' => false], true, ], [ @@ -889,10 +896,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], false, - [ - ['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']], - ], - [], + ['results' => [], 'exact' => [['label' => 'test@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'test@remote']]], 'exactIdMatch' => false], true, ], [ @@ -914,10 +918,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], true, - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']], - ], - [], + ['results' => [], 'exact' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exactIdMatch' => true], true, ], [ @@ -939,10 +940,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], false, - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']], - ], - [], + ['results' => [], 'exact' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'username@localhost', 'server' => 'localhost']]], 'exactIdMatch' => true], true, ], // contact with space @@ -965,10 +963,7 @@ class ShareesAPIControllerTest extends TestCase { ], ], false, - [ - ['label' => 'User Name @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user name@localhost', 'server' => 'localhost']], - ], - [], + ['results' => [], 'exact' => [['label' => 'User Name @ Localhost (user name@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user name@localhost', 'server' => 'localhost']]], 'exactIdMatch' => true], true, ], // remote with space, no contact @@ -991,62 +986,57 @@ class ShareesAPIControllerTest extends TestCase { ], ], false, - [ - ['label' => 'user space@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote']], - ], - [], + ['results' => [], 'exact' => [['label' => 'user space@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'user space@remote']]], 'exactIdMatch' => false], true, ], ]; } /** - * @dataProvider dataGetRemote + * @dataProvider dataGetEmail * * @param string $searchTerm * @param array $contacts * @param bool $shareeEnumeration - * @param array $exactExpected * @param array $expected * @param bool $reachedEnd */ - public function testGetRemote($searchTerm, $contacts, $shareeEnumeration, $exactExpected, $expected, $reachedEnd) { + public function testGetEmail($searchTerm, $contacts, $shareeEnumeration, $expected, $reachedEnd) { $this->invokePrivate($this->sharees, 'shareeEnumeration', [$shareeEnumeration]); $this->contactsManager->expects($this->any()) ->method('search') - ->with($searchTerm, ['CLOUD', 'FN']) + ->with($searchTerm, ['EMAIL', 'FN']) ->willReturn($contacts); - $this->invokePrivate($this->sharees, 'getRemote', [$searchTerm]); - $result = $this->invokePrivate($this->sharees, 'result'); + $result = $this->invokePrivate($this->sharees, 'getEmail', [$searchTerm]); - $this->assertEquals($exactExpected, $result['exact']['remotes']); - $this->assertEquals($expected, $result['remotes']); + $this->assertEquals($expected, $result); $this->assertCount((int) $reachedEnd, $this->invokePrivate($this->sharees, 'reachedEndFor')); } - public function dataGetEmails() { + public function dataGetEmail() { return [ - ['test', [], true, [], [], true], - ['test', [], false, [], [], true], + ['test', [], true, ['results' => [], 'exact' => [], 'exactIdMatch' => false], true], + ['test', [], false, ['results' => [], 'exact' => [], 'exactIdMatch' => false], true], [ 'test@remote.com', [], true, - [ - ['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']], - ], + ['results' => [], 'exact' => [['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']]], 'exactIdMatch' => false], + true, + ], + [ // no valid email address + 'test@remote', [], true, + ['results' => [], 'exact' => [], 'exactIdMatch' => false], + true, ], [ 'test@remote.com', [], false, - [ - ['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']], - ], - [], + ['results' => [], 'exact' => [['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']]], 'exactIdMatch' => false], true, ], [ @@ -1063,15 +1053,12 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], true, - [], - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost.com']], - ], + ['results' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost']]], 'exact' => [], 'exactIdMatch' => false], true, ], [ @@ -1088,13 +1075,12 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], false, - [], - [], + ['results' => [], 'exact' => [], 'exactIdMatch' => false], true, ], [ @@ -1111,17 +1097,12 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], true, - [ - ['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']], - ], - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost.com']], - ], + ['results' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost']]], 'exact' => [['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']]], 'exactIdMatch' => false], true, ], [ @@ -1138,19 +1119,16 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], false, - [ - ['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']], - ], - [], + ['results' => [], 'exact' => [['label' => 'test@remote.com', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'test@remote.com']]], 'exactIdMatch' => false], true, ], [ - 'username@localhost.com', + 'username@localhost', [ [ 'FN' => 'User3 @ Localhost', @@ -1163,19 +1141,16 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], true, - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost.com']], - ], - [], + ['results' => [], 'exact' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost']]], 'exactIdMatch' => true], true, ], [ - 'username@localhost.com', + 'username@localhost', [ [ 'FN' => 'User3 @ Localhost', @@ -1188,20 +1163,40 @@ class ShareesAPIControllerTest extends TestCase { [ 'FN' => 'User @ Localhost', 'EMAIL' => [ - 'username@localhost.com', + 'username@localhost', ], ], ], false, + ['results' => [], 'exact' => [['label' => 'User @ Localhost (username@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost']]], 'exactIdMatch' => true], + true, + ], + // contact with space + [ + 'user name@localhost', [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost.com']], + [ + 'FN' => 'User3 @ Localhost', + ], + [ + 'FN' => 'User2 @ Localhost', + 'EMAIL' => [ + ], + ], + [ + 'FN' => 'User Name @ Localhost', + 'EMAIL' => [ + 'user name@localhost', + ], + ], ], - [], + false, + ['results' => [], 'exact' => [['label' => 'User Name @ Localhost (user name@localhost)', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'user name@localhost']]], 'exactIdMatch' => true], true, ], - // Test single email + // remote with space, no contact [ - 'username@localhost.com', + 'user space@remote.com', [ [ 'FN' => 'User3 @ Localhost', @@ -1213,137 +1208,106 @@ class ShareesAPIControllerTest extends TestCase { ], [ 'FN' => 'User @ Localhost', - 'EMAIL' => 'username@localhost.com', + 'EMAIL' => [ + 'username@localhost', + ], ], ], false, - [ - ['label' => 'User @ Localhost', 'value' => ['shareType' => Share::SHARE_TYPE_EMAIL, 'shareWith' => 'username@localhost.com']], - ], - [], + ['results' => [], 'exact' => [], 'exactIdMatch' => false], true, ], ]; } - /** - * @dataProvider dataGetEmails - * - * @param string $searchTerm - * @param array $contacts - * @param bool $shareeEnumeration - * @param array $exactExpected - * @param array $expected - * @param bool $reachedEnd - */ - public function testGetEmails($searchTerm, $contacts, $shareeEnumeration, $exactExpected, $expected, $reachedEnd) { - $this->invokePrivate($this->sharees, 'shareeEnumeration', [$shareeEnumeration]); - $this->contactsManager->expects($this->any()) - ->method('search') - ->with($searchTerm, ['FN', 'EMAIL']) - ->willReturn($contacts); - - $this->invokePrivate($this->sharees, 'getEmails', [$searchTerm]); - $result = $this->invokePrivate($this->sharees, 'result'); - - $this->assertEquals($exactExpected, $result['exact']['emails']); - $this->assertEquals($expected, $result['emails']); - $this->assertCount((int) $reachedEnd, $this->invokePrivate($this->sharees, 'reachedEndFor')); - } - public function dataSearch() { - $allTypes = [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_EMAIL, Share::SHARE_TYPE_REMOTE]; + $allTypes = [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE, Share::SHARE_TYPE_EMAIL]; return [ - [[], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + [[], '', 'yes', true, true, $allTypes, false, true, true], // Test itemType [[ 'search' => '', - ], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'search' => 'foobar', - ], '', 'yes', true, 'foobar', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'search' => 0, - ], '', 'yes', true, '0', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], // Test itemType [[ 'itemType' => '', - ], '', 'yes', true, '', '', $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'itemType' => 'folder', - ], '', 'yes', true, '', 'folder', $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'itemType' => 0, - ], '', 'yes', true, '', '0', $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], // Test shareType [[ - ], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'shareType' => 0, - ], '', 'yes', true, '', null, [0], 1, 200, false, true, true], + ], '', 'yes', true, false, [0], false, true, true], [[ 'shareType' => '0', - ], '', 'yes', true, '', null, [0], 1, 200, false, true, true], + ], '', 'yes', true, false, [0], false, true, true], [[ 'shareType' => 1, - ], '', 'yes', true, '', null, [1], 1, 200, false, true, true], + ], '', 'yes', true, false, [1], false, true, true], [[ 'shareType' => 12, - ], '', 'yes', true, '', null, [], 1, 200, false, true, true], + ], '', 'yes', true, false, [], false, true, true], [[ 'shareType' => 'foobar', - ], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'shareType' => [0, 1, 2], - ], '', 'yes', true, '', null, [0, 1], 1, 200, false, true, true], + ], '', 'yes', false, false, [0, 1], false, true, true], [[ 'shareType' => [0, 1], - ], '', 'yes', true, '', null, [0, 1], 1, 200, false, true, true], + ], '', 'yes', false, false, [0, 1], false, true, true], [[ 'shareType' => $allTypes, - ], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'shareType' => $allTypes, - ], '', 'yes', false, '', null, [0, 1, 4], 1, 200, false, true, true], + ], '', 'yes', false, false, [0, 1], false, true, true], [[ 'shareType' => $allTypes, - ], '', 'yes', true, '', null, [0, 4, 6], 1, 200, false, true, false], + ], '', 'yes', true, false, [0, 6], false, true, false], [[ 'shareType' => $allTypes, - ], '', 'yes', false, '', null, [0, 4], 1, 200, false, true, false], + ], '', 'yes', false, true, [0, 4], false, true, false], // Test pagination [[ 'page' => 1, - ], '', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'page' => 10, - ], '', 'yes', true, '', null, $allTypes, 10, 200, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], // Test perPage [[ 'perPage' => 1, - ], '', 'yes', true, '', null, $allTypes, 1, 1, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], [[ 'perPage' => 10, - ], '', 'yes', true, '', null, $allTypes, 1, 10, false, true, true], + ], '', 'yes', true, true, $allTypes, false, true, true], // Test $shareWithGroupOnly setting - [[], 'no', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], - [[], 'yes', 'yes', true, '', null, $allTypes, 1, 200, true, true, true], + [[], 'no', 'yes', true, true, $allTypes, false, true, true], + [[], 'yes', 'yes', true, true, $allTypes, true, true, true], // Test $shareeEnumeration setting - [[], 'no', 'yes', true, '', null, $allTypes, 1, 200, false, true, true], - [[], 'no', 'no', true, '', null, $allTypes, 1, 200, false, false, true], - - // Test keep case for search - [[ - 'search' => 'foo@example.com/ownCloud', - ], '', 'yes', true, 'foo@example.com/ownCloud', null, $allTypes, 1, 200, false, true, true], + [[], 'no', 'yes', true, true, $allTypes, false, true, true], + [[], 'no', 'no', true, true, $allTypes, false, false, true], ]; } @@ -1354,16 +1318,12 @@ class ShareesAPIControllerTest extends TestCase { * @param string $apiSetting * @param string $enumSetting * @param bool $remoteSharingEnabled - * @param string $search - * @param string $itemType * @param array $shareTypes - * @param int $page - * @param int $perPage * @param bool $shareWithGroupOnly * @param bool $shareeEnumeration * @param bool $allowGroupSharing */ - public function testSearch($getData, $apiSetting, $enumSetting, $remoteSharingEnabled, $search, $itemType, $shareTypes, $page, $perPage, $shareWithGroupOnly, $shareeEnumeration, $allowGroupSharing) { + public function testSearch($getData, $apiSetting, $enumSetting, $remoteSharingEnabled, $emailSharingEnabled, $shareTypes, $shareWithGroupOnly, $shareeEnumeration, $allowGroupSharing) { $search = isset($getData['search']) ? $getData['search'] : ''; $itemType = isset($getData['itemType']) ? $getData['itemType'] : null; $page = isset($getData['page']) ? $getData['page'] : 1; @@ -1399,11 +1359,10 @@ class ShareesAPIControllerTest extends TestCase { $this->getMockBuilder('OCP\ILogger')->disableOriginalConstructor()->getMock(), $this->shareManager ]) - ->setMethods(array('searchSharees', 'isRemoteSharingAllowed')) + ->setMethods(array('searchSharees', 'isRemoteSharingAllowed', 'shareProviderExists')) ->getMock(); $sharees->expects($this->once()) ->method('searchSharees') - ->with($search, $itemType, $shareTypes, $page, $perPage) ->willReturnCallback(function ($isearch, $iitemType, $ishareTypes, $ipage, $iperPage) use ($search, $itemType, $shareTypes, $page, $perPage) { @@ -1411,7 +1370,10 @@ class ShareesAPIControllerTest extends TestCase { // We are doing strict comparisons here, so we can differ 0/'' and null on shareType/itemType $this->assertSame($search, $isearch); $this->assertSame($itemType, $iitemType); - $this->assertSame($shareTypes, $ishareTypes); + $this->assertSame(count($shareTypes), count($ishareTypes)); + foreach($shareTypes as $expected) { + $this->assertTrue(in_array($expected, $ishareTypes)); + } $this->assertSame($page, $ipage); $this->assertSame($perPage, $iperPage); return new Http\DataResponse(); @@ -1421,6 +1383,11 @@ class ShareesAPIControllerTest extends TestCase { ->with($itemType) ->willReturn($remoteSharingEnabled); + $this->shareManager->expects($this->any()) + ->method('shareProviderExists') + ->with(\OCP\Share::SHARE_TYPE_EMAIL) + ->willReturn($emailSharingEnabled); + $this->assertInstanceOf(Http\DataResponse::class, $sharees->search($search, $itemType, $page, $perPage, $shareType)); $this->assertSame($shareWithGroupOnly, $this->invokePrivate($sharees, 'shareWithGroupOnly')); @@ -1519,7 +1486,7 @@ class ShareesAPIControllerTest extends TestCase { public function dataSearchSharees() { return [ - ['test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [], [], [], + ['test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [], [], ['results' => [], 'exact' => [], 'exactIdMatch' => false], [ 'exact' => ['users' => [], 'groups' => [], 'remotes' => [], 'emails' => []], 'users' => [], @@ -1527,7 +1494,7 @@ class ShareesAPIControllerTest extends TestCase { 'remotes' => [], 'emails' => [], ], false], - ['test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [], [], [], + ['test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_GROUP, Share::SHARE_TYPE_REMOTE], 1, 2, false, [], [], ['results' => [], 'exact' => [], 'exactIdMatch' => false], [ 'exact' => ['users' => [], 'groups' => [], 'remotes' => [], 'emails' => []], 'users' => [], @@ -1541,7 +1508,7 @@ class ShareesAPIControllerTest extends TestCase { ], [ ['label' => 'testgroup1', 'value' => ['shareType' => Share::SHARE_TYPE_GROUP, 'shareWith' => 'testgroup1']], ], [ - ['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']], + 'results' => [['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']]], 'exact' => [], 'exactIdMatch' => false, ], [ 'exact' => ['users' => [], 'groups' => [], 'remotes' => [], 'emails' => []], @@ -1562,7 +1529,7 @@ class ShareesAPIControllerTest extends TestCase { 'test', 'folder', [Share::SHARE_TYPE_USER, Share::SHARE_TYPE_REMOTE], 1, 2, false, [ ['label' => 'test One', 'value' => ['shareType' => Share::SHARE_TYPE_USER, 'shareWith' => 'test1']], ], null, [ - ['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']], + 'results' => [['label' => 'testz@remote', 'value' => ['shareType' => Share::SHARE_TYPE_REMOTE, 'shareWith' => 'testz@remote']]], 'exact' => [], 'exactIdMatch' => false ], [ 'exact' => ['users' => [], 'groups' => [], 'remotes' => [], 'emails' => []], @@ -1660,14 +1627,11 @@ class ShareesAPIControllerTest extends TestCase { $result['groups'] = $mockedGroupsResult; $this->invokePrivate($sharees, 'result', [$result]); }); + $sharees->expects(($mockedRemotesResult === null) ? $this->never() : $this->once()) ->method('getRemote') ->with($searchTerm) - ->willReturnCallback(function() use ($sharees, $mockedRemotesResult) { - $result = $this->invokePrivate($sharees, 'result'); - $result['remotes'] = $mockedRemotesResult; - $this->invokePrivate($sharees, 'result', [$result]); - }); + ->willReturn($mockedRemotesResult); $ocs = $this->invokePrivate($sharees, 'searchSharees', [$searchTerm, $itemType, $shareTypes, $page, $perPage, $shareWithGroupOnly]); $this->assertInstanceOf('\OCP\AppFramework\Http\DataResponse', $ocs); diff --git a/apps/files_sharing/tests/UpdaterTest.php b/apps/files_sharing/tests/UpdaterTest.php index da292826f0a..bb320336d48 100644 --- a/apps/files_sharing/tests/UpdaterTest.php +++ b/apps/files_sharing/tests/UpdaterTest.php @@ -71,7 +71,8 @@ class UpdaterTest extends TestCase { */ function testDeleteParentFolder() { $status = \OC_App::isEnabled('files_trashbin'); - \OC_App::enable('files_trashbin'); + (new \OC_App())->enable('files_trashbin'); + \OCA\Files_Trashbin\Trashbin::registerHooks(); diff --git a/apps/provisioning_api/lib/Controller/AppsController.php b/apps/provisioning_api/lib/Controller/AppsController.php index 3821fc343ad..7d11d92b55a 100644 --- a/apps/provisioning_api/lib/Controller/AppsController.php +++ b/apps/provisioning_api/lib/Controller/AppsController.php @@ -37,25 +37,20 @@ use OCP\IRequest; class AppsController extends OCSController { /** @var \OCP\App\IAppManager */ private $appManager; - /** @var OCSClient */ - private $ocsClient; /** * @param string $appName * @param IRequest $request * @param IAppManager $appManager - * @param OCSClient $ocsClient */ public function __construct( $appName, IRequest $request, - IAppManager $appManager, - OCSClient $ocsClient + IAppManager $appManager ) { parent::__construct($appName, $request); $this->appManager = $appManager; - $this->ocsClient = $ocsClient; } /** @@ -64,7 +59,7 @@ class AppsController extends OCSController { * @throws OCSException */ public function getApps($filter = null) { - $apps = OC_App::listAllApps(false, true, $this->ocsClient); + $apps = (new OC_App())->listAllApps(); $list = []; foreach($apps as $app) { $list[] = $app['id']; diff --git a/apps/provisioning_api/tests/Controller/AppsControllerTest.php b/apps/provisioning_api/tests/Controller/AppsControllerTest.php index 9ac4a8290e4..c891433258f 100644 --- a/apps/provisioning_api/tests/Controller/AppsControllerTest.php +++ b/apps/provisioning_api/tests/Controller/AppsControllerTest.php @@ -48,8 +48,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { private $api; /** @var IUserSession */ private $userSession; - /** @var OCSClient|\PHPUnit_Framework_MockObject_MockObject */ - private $ocsClient; protected function setUp() { parent::setUp(); @@ -57,9 +55,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $this->appManager = \OC::$server->getAppManager(); $this->groupManager = \OC::$server->getGroupManager(); $this->userSession = \OC::$server->getUserSession(); - $this->ocsClient = $this->getMockBuilder('OC\OCSClient') - ->disableOriginalConstructor() - ->getMock(); $request = $this->getMockBuilder('OCP\IRequest') ->disableOriginalConstructor() @@ -68,8 +63,7 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $this->api = new AppsController( 'provisioning_api', $request, - $this->appManager, - $this->ocsClient + $this->appManager ); } @@ -88,10 +82,6 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { } public function testGetApps() { - $this->ocsClient - ->expects($this->any()) - ->method($this->anything()) - ->will($this->returnValue(null)); $user = $this->generateUsers(); $this->groupManager->get('admin')->addUser($user); $this->userSession->setUser($user); @@ -99,7 +89,7 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { $result = $this->api->getApps(); $data = $result->getData(); - $this->assertEquals(count(\OC_App::listAllApps(false, true, $this->ocsClient)), count($data['apps'])); + $this->assertEquals(count((new \OC_App())->listAllApps()), count($data['apps'])); } public function testGetAppsEnabled() { @@ -109,13 +99,9 @@ class AppsControllerTest extends \OCA\Provisioning_API\Tests\TestCase { } public function testGetAppsDisabled() { - $this->ocsClient - ->expects($this->any()) - ->method($this->anything()) - ->will($this->returnValue(null)); $result = $this->api->getApps('disabled'); $data = $result->getData(); - $apps = \OC_App::listAllApps(false, true, $this->ocsClient); + $apps = (new \OC_App)->listAllApps(); $list = array(); foreach($apps as $app) { $list[] = $app['id']; diff --git a/apps/sharebymail/appinfo/app.php b/apps/sharebymail/appinfo/app.php new file mode 100644 index 00000000000..5ef7b6f18cb --- /dev/null +++ b/apps/sharebymail/appinfo/app.php @@ -0,0 +1,24 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +$settings = new \OCA\ShareByMail\Settings(); + +\OCP\Util::connectHook('\OCP\Config', 'js', $settings, 'announceShareProvider'); diff --git a/apps/sharebymail/appinfo/info.xml b/apps/sharebymail/appinfo/info.xml new file mode 100644 index 00000000000..f1771fc9551 --- /dev/null +++ b/apps/sharebymail/appinfo/info.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<info> + <id>sharebymail</id> + <name>Share by mail</name> + <description>Share provider which allows you to share files by mail</description> + <licence>AGPL</licence> + <author>Bjoern Schiessle</author> + <version>1.0.0</version> + <namespace>ShareByMail</namespace> + <category>other</category> + <dependencies> + <owncloud min-version="9.2" max-version="9.2" /> + </dependencies> + <default_enable/> +</info> diff --git a/apps/sharebymail/lib/Settings.php b/apps/sharebymail/lib/Settings.php new file mode 100644 index 00000000000..4ab1622425b --- /dev/null +++ b/apps/sharebymail/lib/Settings.php @@ -0,0 +1,38 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\ShareByMail; + + +class Settings { + + /** + * announce that the share-by-mail share provider is enabled + * + * @param array $settings + */ + public function announceShareProvider(array $settings) { + $array = json_decode($settings['array']['oc_appconfig'], true); + $array['shareByMailEnabled'] = true; + $settings['array']['oc_appconfig'] = json_encode($array); + } +} diff --git a/apps/sharebymail/lib/ShareByMailProvider.php b/apps/sharebymail/lib/ShareByMailProvider.php new file mode 100644 index 00000000000..a332bd13d88 --- /dev/null +++ b/apps/sharebymail/lib/ShareByMailProvider.php @@ -0,0 +1,709 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\ShareByMail; + +use OC\HintException; +use OC\Share20\Exception\InvalidShare; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use OC\Share20\Share; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IShare; +use OCP\Share\IShareProvider; + +/** + * Class ShareByMail + * + * @package OCA\ShareByMail + */ +class ShareByMailProvider implements IShareProvider { + + /** @var IDBConnection */ + private $dbConnection; + + /** @var ILogger */ + private $logger; + + /** @var ISecureRandom */ + private $secureRandom; + + /** @var IUserManager */ + private $userManager; + + /** @var IRootFolder */ + private $rootFolder; + + /** @var IL10N */ + private $l; + + /** @var IMailer */ + private $mailer; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** + * Return the identifier of this provider. + * + * @return string Containing only [a-zA-Z0-9] + */ + public function identifier() { + return 'ocShareByMail'; + } + + /** + * DefaultShareProvider constructor. + * + * @param IDBConnection $connection + * @param ISecureRandom $secureRandom + * @param IUserManager $userManager + * @param IRootFolder $rootFolder + * @param IL10N $l + * @param ILogger $logger + * @param IMailer $mailer + * @param IURLGenerator $urlGenerator + */ + public function __construct( + IDBConnection $connection, + ISecureRandom $secureRandom, + IUserManager $userManager, + IRootFolder $rootFolder, + IL10N $l, + ILogger $logger, + IMailer $mailer, + IURLGenerator $urlGenerator + ) { + $this->dbConnection = $connection; + $this->secureRandom = $secureRandom; + $this->userManager = $userManager; + $this->rootFolder = $rootFolder; + $this->l = $l; + $this->logger = $logger; + $this->mailer = $mailer; + $this->urlGenerator = $urlGenerator; + } + + /** + * Share a path + * + * @param IShare $share + * @return IShare The share object + * @throws ShareNotFound + * @throws \Exception + */ + public function create(IShare $share) { + + $shareWith = $share->getSharedWith(); + /* + * Check if file is not already shared with the remote user + */ + $alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0); + if (!empty($alreadyShared)) { + $message = 'Sharing %s failed, this item is already shared with %s'; + $message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); + throw new \Exception($message_t); + } + + $shareId = $this->createMailShare($share); + + $data = $this->getRawShare($shareId); + return $this->createShareObject($data); + + } + + /** + * @param IShare $share + * @return int + * @throws \Exception + */ + protected function createMailShare(IShare $share) { + $share->setToken($this->generateToken()); + $shareId = $this->addShareToDB( + $share->getNodeId(), + $share->getNodeType(), + $share->getSharedWith(), + $share->getSharedBy(), + $share->getShareOwner(), + $share->getPermissions(), + $share->getToken() + ); + + try { + $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', + ['token' => $share->getToken()]); + $this->sendMailNotification($share->getNode()->getName(), + $link, + $share->getShareOwner(), + $share->getSharedBy(), $share->getSharedWith()); + } catch (HintException $hintException) { + $this->logger->error('Failed to send share by mail: ' . $hintException->getMessage()); + $this->removeShareFromTable($shareId); + throw $hintException; + } catch (\Exception $e) { + $this->logger->error('Failed to send share by mail: ' . $e->getMessage()); + $this->removeShareFromTable($shareId); + throw new HintException('Failed to send share by mail', + $this->l->t('Failed to send share by E-mail')); + } + + return $shareId; + + } + + protected function sendMailNotification($filename, $link, $owner, $initiator, $shareWith) { + if ($owner === $initiator) { + $subject = (string)$this->l->t('%s shared »%s« with you', array($owner, $filename)); + } else { + $subject = (string)$this->l->t('%s shared »%s« with you on behalf of %s', array($owner, $filename, $initiator)); + } + + $message = $this->mailer->createMessage(); + $htmlBody = $this->createMailBody('mail', $filename, $link, $owner, $initiator); + $textBody = $this->createMailBody('altmail', $filename, $link, $owner, $initiator); + $message->setTo([$shareWith]); + $message->setSubject($subject); + $message->setBody($textBody, 'text/plain'); + $message->setHtmlBody($htmlBody); + $this->mailer->send($message); + + } + + /** + * create mail body + * + * @param $filename + * @param $link + * @param $owner + * @param $initiator + * @return string plain text mail + * @throws HintException + */ + protected function createMailBody($template, $filename, $link, $owner, $initiator) { + + $mailBodyTemplate = new \OC_Template('sharebymail', $template, ''); + $mailBodyTemplate->assign ('filename', $filename); + $mailBodyTemplate->assign ('link', $link); + $mailBodyTemplate->assign ('owner', $owner); + $mailBodyTemplate->assign ('initiator', $initiator); + $mailBodyTemplate->assign ('onBehalfOf', $initiator !== $owner); + $mailBody = $mailBodyTemplate->fetchPage(); + + if (is_string($mailBody)) { + return $mailBody; + } + + throw new HintException('Failed to create the E-mail', + $this->l->t('Failed to create the E-mail')); + } + + /** + * generate share token + * + * @return string + */ + protected function generateToken() { + $token = $this->secureRandom->generate( + 15, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); + return $token; + } + + /** + * Get all children of this share + * + * @param IShare $parent + * @return IShare[] + */ + public function getChildren(IShare $parent) { + $children = []; + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->orderBy('id'); + + $cursor = $qb->execute(); + while($data = $cursor->fetch()) { + $children[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * add share to the database and return the ID + * + * @param int $itemSource + * @param string $itemType + * @param string $shareWith + * @param string $sharedBy + * @param string $uidOwner + * @param int $permissions + * @param string $token + * @return int + */ + protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue('file_target', $qb->createNamedParameter('')); + + $qb->execute(); + $id = $qb->getLastInsertId(); + + return (int)$id; + } + + /** + * Update a share + * + * @param IShare $share + * @return IShare The share object + */ + public function update(IShare $share) { + /* + * We allow updating the permissions of mail shares + */ + $qb = $this->dbConnection->getQueryBuilder(); + $qb->update('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) + ->execute(); + + return $share; + } + + /** + * @inheritdoc + */ + public function move(IShare $share, $recipient) { + /** + * nothing to do here, mail shares are only outgoing shares + */ + return $share; + } + + /** + * Delete a share (owner unShares the file) + * + * @param IShare $share + */ + public function delete(IShare $share) { + $this->removeShareFromTable($share->getId()); + } + + /** + * @inheritdoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // nothing to do here, mail shares are only outgoing shares + return; + } + + /** + * @inheritdoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + //Special case for old shares created via the web UI + $or1 = $qb->expr()->andX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->isNull('uid_initiator') + ); + + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), + $or1 + ) + ); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->setFirstResult($offset); + $qb->orderBy('id'); + + $cursor = $qb->execute(); + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getShareById($id, $recipientId = null) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound(); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound(); + } + + return $share; + } + + /** + * Get shares for a given path + * + * @param \OCP\Files\Node $path + * @return IShare[] + */ + public function getSharesByPath(Node $path) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->execute(); + + $shares = []; + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + + /** + * @inheritdoc + */ + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { + /** @var IShare[] $shares */ + $shares = []; + + //Get shares directly with this user + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + // Order by id + $qb->orderBy('id'); + + // Set limit and offset + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + $qb->setFirstResult($offset); + + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); + + // Filter by node if provided + if ($node !== null) { + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); + } + + $cursor = $qb->execute(); + + while($data = $cursor->fetch()) { + $shares[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + + return $shares; + } + + /** + * Get a share by token + * + * @param string $token + * @return IShare + * @throws ShareNotFound + */ + public function getShareByToken($token) { + $qb = $this->dbConnection->getQueryBuilder(); + + $cursor = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) + ->execute(); + + $data = $cursor->fetch(); + + if ($data === false) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + try { + $share = $this->createShareObject($data); + } catch (InvalidShare $e) { + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); + } + + return $share; + } + + /** + * remove share from table + * + * @param string $shareId + */ + protected function removeShareFromTable($shareId) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); + $qb->execute(); + } + + /** + * Create a share object from an database row + * + * @param array $data + * @return IShare + * @throws InvalidShare + * @throws ShareNotFound + */ + protected function createShareObject($data) { + + $share = new Share($this->rootFolder, $this->userManager); + $share->setId((int)$data['id']) + ->setShareType((int)$data['share_type']) + ->setPermissions((int)$data['permissions']) + ->setTarget($data['file_target']) + ->setMailSend((bool)$data['mail_send']) + ->setToken($data['token']); + + $shareTime = new \DateTime(); + $shareTime->setTimestamp((int)$data['stime']); + $share->setShareTime($shareTime); + $share->setSharedWith($data['share_with']); + + if ($data['uid_initiator'] !== null) { + $share->setShareOwner($data['uid_owner']); + $share->setSharedBy($data['uid_initiator']); + } else { + //OLD SHARE + $share->setSharedBy($data['uid_owner']); + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); + + $owner = $path->getOwner(); + $share->setShareOwner($owner->getUID()); + } + + $share->setNodeId((int)$data['file_source']); + $share->setNodeType($data['item_type']); + + $share->setProviderId($this->identifier()); + + return $share; + } + + /** + * Get the node with file $id for $user + * + * @param string $userId + * @param int $id + * @return \OCP\Files\File|\OCP\Files\Folder + * @throws InvalidShare + */ + private function getNode($userId, $id) { + try { + $userFolder = $this->rootFolder->getUserFolder($userId); + } catch (NotFoundException $e) { + throw new InvalidShare(); + } + + $nodes = $userFolder->getById($id); + + if (empty($nodes)) { + throw new InvalidShare(); + } + + return $nodes[0]; + } + + /** + * A user is deleted from the system + * So clean up the relevant shares. + * + * @param string $uid + * @param int $shareType + */ + public function userDeleted($uid, $shareType) { + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->delete('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) + ->execute(); + } + + /** + * This provider does not support group shares + * + * @param string $gid + */ + public function groupDeleted($gid) { + return; + } + + /** + * This provider does not support group shares + * + * @param string $uid + * @param string $gid + */ + public function userDeletedFromGroup($uid, $gid) { + return; + } + + /** + * get database row of a give share + * + * @param $id + * @return array + * @throws ShareNotFound + */ + protected function getRawShare($id) { + + // Now fetch the inserted share and create a complete share object + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); + + $cursor = $qb->execute(); + $data = $cursor->fetch(); + $cursor->closeCursor(); + + if ($data === false) { + throw new ShareNotFound; + } + + return $data; + } + + public function getSharesInFolder($userId, Folder $node, $reshares) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share', 's') + ->andWhere($qb->expr()->orX( + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) + )) + ->andWhere( + $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) + ); + + /** + * Reshares for this user are shares where they are the owner. + */ + if ($reshares === false) { + $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); + } else { + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) + ) + ); + } + + $qb->innerJoin('s', 'filecache' ,'f', 's.file_source = f.fileid'); + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); + + $qb->orderBy('id'); + + $cursor = $qb->execute(); + $shares = []; + while ($data = $cursor->fetch()) { + $shares[$data['fileid']][] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $shares; + } + +} diff --git a/apps/sharebymail/templates/altmail.php b/apps/sharebymail/templates/altmail.php new file mode 100644 index 00000000000..7b9de6295ff --- /dev/null +++ b/apps/sharebymail/templates/altmail.php @@ -0,0 +1,36 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** @var OC_Theme $theme */ +/** @var array $_ */ +if ($_['onBehalfOf']) { + print_unescaped($l->t("Hey there,\n\n%s shared »%s« with you on behalf of %s.\n\n%s\n\n", [$_['owner'], $_['filename'], $_['initiator'], $_['link']])); +} else { + print_unescaped($l->t("Hey there,\n\n%s shared »%s« with you.\n\n%s\n\n", [$_['owner'], $_['filename'], $_['link']])); +} +// TRANSLATORS term at the end of a mail +p($l->t("Cheers!")); +print_unescaped("\n"); +?> + +-- +<?php p($theme->getName() . ' - ' . $theme->getSlogan()); ?> +<?php print_unescaped("\n".$theme->getBaseUrl()); diff --git a/apps/sharebymail/templates/mail.php b/apps/sharebymail/templates/mail.php new file mode 100644 index 00000000000..48f7dcb063a --- /dev/null +++ b/apps/sharebymail/templates/mail.php @@ -0,0 +1,64 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** @var OC_Theme $theme */ +/** @var array $_ */ +?> + +<table cellspacing="0" cellpadding="0" border="0" width="100%"> + <tr><td> + <table cellspacing="0" cellpadding="0" border="0" width="600px"> + <tr> + <td bgcolor="<?php p($theme->getMailHeaderColor());?>" width="20px"> </td> + <td bgcolor="<?php p($theme->getMailHeaderColor());?>"> + <img src="<?php p(\OC::$server->getURLGenerator()->getAbsoluteURL(image_path('', 'logo-mail.gif'))); ?>" alt="<?php p($theme->getName()); ?>"/> + </td> + </tr> + <tr><td colspan="2"> </td></tr> + <tr> + <td width="20px"> </td> + <td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;"> + <?php + if ($_['onBehalfOf']) { + print_unescaped($l->t('Hey there,<br><br>%s shared <a href="%s">%s</a> with you on behalf of %s.<br><br>', [$_['owner'], $_['link'], $_['filename'], $_['initiator']])); + } else { + print_unescaped($l->t('Hey there,<br><br>%s shared <a href="%s">%s</a> with you.<br><br>', [$_['owner'], $_['link'], $_['filename']])); + } + // TRANSLATORS term at the end of a mail + p($l->t('Cheers!')); + ?> + </td> + </tr> + <tr><td colspan="2"> </td></tr> + <tr> + <td width="20px"> </td> + <td style="font-weight:normal; font-size:0.8em; line-height:1.2em; font-family:verdana,'arial',sans;">--<br> + <?php p($theme->getName()); ?> - + <?php p($theme->getSlogan()); ?> + <br><a href="<?php p($theme->getBaseUrl()); ?>"><?php p($theme->getBaseUrl());?></a> + </td> + </tr> + <tr> + <td colspan="2"> </td> + </tr> + </table> + </td></tr> +</table> diff --git a/apps/sharebymail/tests/SettingsTest.php b/apps/sharebymail/tests/SettingsTest.php new file mode 100644 index 00000000000..f415421b0cf --- /dev/null +++ b/apps/sharebymail/tests/SettingsTest.php @@ -0,0 +1,64 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\ShareByMail\Tests; + + +use OCA\ShareByMail\Settings; +use Test\TestCase; + +class SettingsTest extends TestCase { + + /** @var Settings */ + private $instance; + + public function setUp() { + parent::setUp(); + + $this->instance = new Settings(); + } + + public function testAnnounceShareProvider() { + $before = [ + 'oc_appconfig' => + json_encode([ + 'key1' => 'value1', + 'key2' => 'value2' + ]), + 'oc_foo' => 'oc_bar' + ]; + + $after = [ + 'oc_appconfig' => + json_encode([ + 'key1' => 'value1', + 'key2' => 'value2', + 'shareByMailEnabled' => true + ]), + 'oc_foo' => 'oc_bar' + ]; + + $this->instance->announceShareProvider(['array' => &$before]); + $this->assertSame($after, $before); + } + +} diff --git a/apps/sharebymail/tests/ShareByMailProviderTest.php b/apps/sharebymail/tests/ShareByMailProviderTest.php new file mode 100644 index 00000000000..eedce286a03 --- /dev/null +++ b/apps/sharebymail/tests/ShareByMailProviderTest.php @@ -0,0 +1,655 @@ +<?php +/** + * @copyright Copyright (c) 2016 Bjoern Schiessle <bjoern@schiessle.org> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + + +namespace OCA\ShareByMail\Tests; + + +use OC\HintException; +use OCA\ShareByMail\ShareByMailProvider; +use OCP\Files\IRootFolder; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Mail\IMailer; +use OCP\Security\ISecureRandom; +use OCP\Share\Exceptions\ShareNotFound; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Test\TestCase; + +/** + * Class ShareByMailProviderTest + * + * @package OCA\ShareByMail\Tests + * @group DB + */ +class ShareByMailProviderTest extends TestCase { + + /** @var IDBConnection */ + private $connection; + + /** @var IManager */ + private $shareManager; + + /** @var IL10N | \PHPUnit_Framework_MockObject_MockObject */ + private $l; + + /** @var ILogger | \PHPUnit_Framework_MockObject_MockObject */ + private $logger; + + /** @var IRootFolder | \PHPUnit_Framework_MockObject_MockObject */ + private $rootFolder; + + /** @var IUserManager | \PHPUnit_Framework_MockObject_MockObject */ + private $userManager; + + /** @var ISecureRandom | \PHPUnit_Framework_MockObject_MockObject */ + private $secureRandom; + + /** @var IMailer | \PHPUnit_Framework_MockObject_MockObject */ + private $mailer; + + /** @var IURLGenerator | \PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + + /** @var IShare | \PHPUnit_Framework_MockObject_MockObject */ + private $share; + + public function setUp() { + parent::setUp(); + + $this->shareManager = \OC::$server->getShareManager(); + $this->connection = \OC::$server->getDatabaseConnection(); + + $this->l = $this->getMockBuilder('OCP\IL10N')->getMock(); + $this->l->method('t') + ->will($this->returnCallback(function($text, $parameters = []) { + return vsprintf($text, $parameters); + })); + $this->logger = $this->getMockBuilder('OCP\ILogger')->getMock(); + $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock(); + $this->userManager = $this->getMockBuilder('OCP\IUserManager')->getMock(); + $this->secureRandom = $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(); + $this->mailer = $this->getMockBuilder('\OCP\Mail\IMailer')->getMock(); + $this->urlGenerator = $this->getMockBuilder('\OCP\IUrlGenerator')->getMock(); + $this->share = $this->getMockBuilder('\OCP\Share\IShare')->getMock(); + + $this->userManager->expects($this->any())->method('userExists')->willReturn(true); + } + + /** + * get instance of Mocked ShareByMailProvider + * + * @param array $mockedMethods internal methods which should be mocked + * @return \PHPUnit_Framework_MockObject_MockObject | ShareByMailProvider + */ + private function getInstance(array $mockedMethods = []) { + + $instance = $this->getMockBuilder('OCA\ShareByMail\ShareByMailProvider') + ->setConstructorArgs( + [ + $this->connection, + $this->secureRandom, + $this->userManager, + $this->rootFolder, + $this->l, + $this->logger, + $this->mailer, + $this->urlGenerator + ] + ); + + if (!empty($mockedMethods)) { + $instance->setMethods($mockedMethods); + return $instance->getMock(); + } + + return new ShareByMailProvider( + $this->connection, + $this->secureRandom, + $this->userManager, + $this->rootFolder, + $this->l, + $this->logger, + $this->mailer, + $this->urlGenerator + ); + + } + + public function tearDown() { + $this->connection->getQueryBuilder()->delete('share')->execute(); + + return parent::tearDown(); + } + + public function testCreate() { + $share = $this->getMockBuilder('\OCP\Share\IShare')->getMock(); + $share->expects($this->once())->method('getSharedWith')->willReturn('user1'); + + $instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject']); + + $instance->expects($this->once())->method('getSharedWith')->willReturn([]); + $instance->expects($this->once())->method('createMailShare')->with($share)->willReturn(42); + $instance->expects($this->once())->method('getRawShare')->with(42)->willReturn('rawShare'); + $instance->expects($this->once())->method('createShareObject')->with('rawShare')->willReturn('shareObject'); + + $this->assertSame('shareObject', + $instance->create($share) + ); + } + + /** + * @expectedException \Exception + */ + public function testCreateFailed() { + $this->share->expects($this->once())->method('getSharedWith')->willReturn('user1'); + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node->expects($this->any())->method('getName')->willReturn('fileName'); + $this->share->expects($this->any())->method('getNode')->willReturn($node); + + $instance = $this->getInstance(['getSharedWith', 'createMailShare', 'getRawShare', 'createShareObject']); + + $instance->expects($this->once())->method('getSharedWith')->willReturn(['found']); + $instance->expects($this->never())->method('createMailShare'); + $instance->expects($this->never())->method('getRawShare'); + $instance->expects($this->never())->method('createShareObject'); + + $this->assertSame('shareObject', + $instance->create($this->share) + ); + } + + public function testCreateMailShare() { + $this->share->expects($this->any())->method('getToken')->willReturn('token'); + $this->share->expects($this->once())->method('setToken')->with('token'); + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node->expects($this->any())->method('getName')->willReturn('fileName'); + $this->share->expects($this->any())->method('getNode')->willReturn($node); + + $instance = $this->getInstance(['generateToken', 'addShareToDB', 'sendMailNotification']); + + $instance->expects($this->once())->method('generateToken')->willReturn('token'); + $instance->expects($this->once())->method('addShareToDB')->willReturn(42); + $instance->expects($this->once())->method('sendMailNotification'); + $this->urlGenerator->expects($this->once())->method('linkToRouteAbsolute') + ->with('files_sharing.sharecontroller.showShare', ['token' => 'token']); + $instance->expects($this->once())->method('sendMailNotification'); + + $this->assertSame(42, + $this->invokePrivate($instance, 'createMailShare', [$this->share]) + ); + + } + + /** + * @expectedException \OC\HintException + */ + public function testCreateMailShareFailed() { + $this->share->expects($this->any())->method('getToken')->willReturn('token'); + $this->share->expects($this->once())->method('setToken')->with('token'); + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node->expects($this->any())->method('getName')->willReturn('fileName'); + $this->share->expects($this->any())->method('getNode')->willReturn($node); + + $instance = $this->getInstance(['generateToken', 'addShareToDB', 'sendMailNotification']); + + $instance->expects($this->once())->method('generateToken')->willReturn('token'); + $instance->expects($this->once())->method('addShareToDB')->willReturn(42); + $instance->expects($this->once())->method('sendMailNotification'); + $this->urlGenerator->expects($this->once())->method('linkToRouteAbsolute') + ->with('files_sharing.sharecontroller.showShare', ['token' => 'token']); + $instance->expects($this->once())->method('sendMailNotification') + ->willReturnCallback( + function() { + throw new \Exception('should be converted to a hint exception'); + } + ); + + $this->assertSame(42, + $this->invokePrivate($instance, 'createMailShare', [$this->share]) + ); + + } + + public function testGenerateToken() { + $instance = $this->getInstance(); + + $this->secureRandom->expects($this->once())->method('generate')->willReturn('token'); + + $this->assertSame('token', + $this->invokePrivate($instance, 'generateToken') + ); + } + + public function testAddShareToDB() { + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + + $instance = $this->getInstance(); + $id = $this->invokePrivate( + $instance, + 'addShareToDB', + [ + $itemSource, + $itemType, + $shareWith, + $sharedBy, + $uidOwner, + $permissions, + $token + ] + ); + + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->execute()->fetchAll(); + + $this->assertSame(1, count($result)); + + $this->assertSame($itemSource, (int)$result[0]['item_source']); + $this->assertSame($itemType, $result[0]['item_type']); + $this->assertSame($shareWith, $result[0]['share_with']); + $this->assertSame($sharedBy, $result[0]['uid_initiator']); + $this->assertSame($uidOwner, $result[0]['uid_owner']); + $this->assertSame($permissions, (int)$result[0]['permissions']); + $this->assertSame($token, $result[0]['token']); + + } + + public function testUpdate() { + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + + $instance = $this->getInstance(); + + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $this->share->expects($this->once())->method('getPermissions')->willReturn($permissions + 1); + $this->share->expects($this->once())->method('getShareOwner')->willReturn($uidOwner); + $this->share->expects($this->once())->method('getSharedBy')->willReturn($sharedBy); + $this->share->expects($this->once())->method('getId')->willReturn($id); + + $this->assertSame($this->share, + $instance->update($this->share) + ); + + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select('*') + ->from('share') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) + ->execute()->fetchAll(); + + $this->assertSame(1, count($result)); + + $this->assertSame($itemSource, (int)$result[0]['item_source']); + $this->assertSame($itemType, $result[0]['item_type']); + $this->assertSame($shareWith, $result[0]['share_with']); + $this->assertSame($sharedBy, $result[0]['uid_initiator']); + $this->assertSame($uidOwner, $result[0]['uid_owner']); + $this->assertSame($permissions + 1, (int)$result[0]['permissions']); + $this->assertSame($token, $result[0]['token']); + } + + public function testDelete() { + $instance = $this->getInstance(['removeShareFromTable']); + $this->share->expects($this->once())->method('getId')->willReturn(42); + $instance->expects($this->once())->method('removeShareFromTable')->with(42); + $instance->delete($this->share); + } + + public function testGetShareById() { + $instance = $this->getInstance(['createShareObject']); + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $this->createDummyShare($itemType, $itemSource, $shareWith, "user1wrong", "user2wrong", $permissions, $token); + $id2 = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $instance->expects($this->once())->method('createShareObject') + ->willReturnCallback( + function ($data) use ($uidOwner, $sharedBy, $id2) { + $this->assertSame($uidOwner, $data['uid_owner']); + $this->assertSame($sharedBy, $data['uid_initiator']); + $this->assertSame($id2, (int)$data['id']); + return $this->share; + } + ); + + $result = $instance->getShareById($id2); + + $this->assertInstanceOf('OCP\Share\IShare', $result); + } + + /** + * @expectedException \OCP\Share\Exceptions\ShareNotFound + */ + public function testGetShareByIdFailed() { + $instance = $this->getInstance(['createShareObject']); + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $instance->getShareById($id+1); + } + + public function testGetShareByPath() { + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $node = $this->getMockBuilder('OCP\Files\Node')->getMock(); + $node->expects($this->once())->method('getId')->willReturn($itemSource); + + + $instance = $this->getInstance(['createShareObject']); + + $this->createDummyShare($itemType, 111, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $instance->expects($this->once())->method('createShareObject') + ->willReturnCallback( + function ($data) use ($uidOwner, $sharedBy, $id) { + $this->assertSame($uidOwner, $data['uid_owner']); + $this->assertSame($sharedBy, $data['uid_initiator']); + $this->assertSame($id, (int)$data['id']); + return $this->share; + } + ); + + $result = $instance->getSharesByPath($node); + + $this->assertTrue(is_array($result)); + $this->assertSame(1, count($result)); + $this->assertInstanceOf('OCP\Share\IShare', $result[0]); + } + + public function testGetShareByToken() { + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $instance = $this->getInstance(['createShareObject']); + + $idMail = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, \OCP\Share::SHARE_TYPE_LINK); + + $this->assertTrue($idMail !== $idPublic); + + $instance->expects($this->once())->method('createShareObject') + ->willReturnCallback( + function ($data) use ($idMail) { + $this->assertSame($idMail, (int)$data['id']); + return $this->share; + } + ); + + $this->assertInstanceOf('OCP\Share\IShare', + $instance->getShareByToken('token') + ); + } + + /** + * @expectedException \OCP\Share\Exceptions\ShareNotFound + */ + public function testGetShareByTokenFailed() { + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $instance = $this->getInstance(['createShareObject']); + + $idMail = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + $idPublic = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, "token2", \OCP\Share::SHARE_TYPE_LINK); + + $this->assertTrue($idMail !== $idPublic); + + $this->assertInstanceOf('OCP\Share\IShare', + $instance->getShareByToken('token2') + ); + } + + public function testRemoveShareFromTable() { + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $instance = $this->getInstance(); + + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $query = $this->connection->getQueryBuilder(); + $query->select('*')->from('share') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $before = $query->execute()->fetchAll(); + + $this->assertTrue(is_array($before)); + $this->assertSame(1, count($before)); + + $this->invokePrivate($instance, 'removeShareFromTable', [$id]); + + $query = $this->connection->getQueryBuilder(); + $query->select('*')->from('share') + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + $after = $query->execute()->fetchAll(); + + $this->assertTrue(is_array($after)); + $this->assertEmpty($after); + } + + public function testUserDeleted() { + + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, 'user2Wrong', $permissions, $token); + + $query = $this->connection->getQueryBuilder(); + $query->select('*')->from('share'); + $before = $query->execute()->fetchAll(); + + $this->assertTrue(is_array($before)); + $this->assertSame(2, count($before)); + + + $instance = $this->getInstance(); + + $instance->userDeleted($uidOwner, \OCP\Share::SHARE_TYPE_EMAIL); + + $query = $this->connection->getQueryBuilder(); + $query->select('*')->from('share'); + $after = $query->execute()->fetchAll(); + + $this->assertTrue(is_array($after)); + $this->assertSame(1, count($after)); + $this->assertSame($id, (int)$after[0]['id']); + + } + + public function testGetRawShare() { + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $instance = $this->getInstance(); + + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $result = $this->invokePrivate($instance, 'getRawShare', [$id]); + + $this->assertTrue(is_array($result)); + $this->assertSame($itemSource, (int)$result['item_source']); + $this->assertSame($itemType, $result['item_type']); + $this->assertSame($shareWith, $result['share_with']); + $this->assertSame($sharedBy, $result['uid_initiator']); + $this->assertSame($uidOwner, $result['uid_owner']); + $this->assertSame($permissions, (int)$result['permissions']); + $this->assertSame($token, $result['token']); + } + + /** + * @expectedException \OCP\Share\Exceptions\ShareNotFound + */ + public function testGetRawShareFailed() { + $itemSource = 11; + $itemType = 'file'; + $shareWith = 'user@server.com'; + $sharedBy = 'user1'; + $uidOwner = 'user2'; + $permissions = 1; + $token = 'token'; + + $instance = $this->getInstance(); + + $id = $this->createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token); + + $this->invokePrivate($instance, 'getRawShare', [$id+1]); + } + + private function createDummyShare($itemType, $itemSource, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $shareType = \OCP\Share::SHARE_TYPE_EMAIL) { + $qb = $this->connection->getQueryBuilder(); + $qb->insert('share') + ->setValue('share_type', $qb->createNamedParameter($shareType)) + ->setValue('item_type', $qb->createNamedParameter($itemType)) + ->setValue('item_source', $qb->createNamedParameter($itemSource)) + ->setValue('file_source', $qb->createNamedParameter($itemSource)) + ->setValue('share_with', $qb->createNamedParameter($shareWith)) + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) + ->setValue('permissions', $qb->createNamedParameter($permissions)) + ->setValue('token', $qb->createNamedParameter($token)) + ->setValue('stime', $qb->createNamedParameter(time())); + + /* + * Added to fix https://github.com/owncloud/core/issues/22215 + * Can be removed once we get rid of ajax/share.php + */ + $qb->setValue('file_target', $qb->createNamedParameter('')); + + $qb->execute(); + $id = $qb->getLastInsertId(); + + return (int)$id; + } + + public function testGetSharesInFolder() { + $userManager = \OC::$server->getUserManager(); + $rootFolder = \OC::$server->getRootFolder(); + + $provider = $this->getInstance(['sendMailNotification']); + + $u1 = $userManager->createUser('testFed', md5(time())); + $u2 = $userManager->createUser('testFed2', md5(time())); + + $folder1 = $rootFolder->getUserFolder($u1->getUID())->newFolder('foo'); + $file1 = $folder1->newFile('bar1'); + $file2 = $folder1->newFile('bar2'); + + $share1 = $this->shareManager->newShare(); + $share1->setSharedWith('user@server.com') + ->setSharedBy($u1->getUID()) + ->setShareOwner($u1->getUID()) + ->setPermissions(\OCP\Constants::PERMISSION_READ) + ->setNode($file1); + $provider->create($share1); + + $share2 = $this->shareManager->newShare(); + $share2->setSharedWith('user@server.com') + ->setSharedBy($u2->getUID()) + ->setShareOwner($u1->getUID()) + ->setPermissions(\OCP\Constants::PERMISSION_READ) + ->setNode($file2); + $provider->create($share2); + + $result = $provider->getSharesInFolder($u1->getUID(), $folder1, false); + $this->assertCount(1, $result); + $this->assertCount(1, $result[$file1->getId()]); + + $result = $provider->getSharesInFolder($u1->getUID(), $folder1, true); + $this->assertCount(2, $result); + $this->assertCount(1, $result[$file1->getId()]); + $this->assertCount(1, $result[$file2->getId()]); + + $u1->delete(); + $u2->delete(); + } + +} diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index 09b4a14f2b0..f274d245887 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -319,7 +319,7 @@ class ThemingController extends Controller { $responseCss .= sprintf('input[type="checkbox"].checkbox:checked:enabled:not(.checkbox--white) + label:before {' . 'background-image:url(\'%s/core/img/actions/checkmark-white.svg\');' . 'background-color: %s; background-position: center center; background-size:contain;' . - 'width:12px; height:12px; padding:0; margin:2px 6px 6px 2px; border-radius:1px;' . + 'width:12px; height:12px; padding:0; margin:2px 6px 6px 9px; border-radius:1px;' . "}\n", \OC::$WEBROOT, $elementColor diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index d9d5005e25f..4325e1988b2 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -428,7 +428,7 @@ class ThemingControllerTest extends TestCase { $expectedData .= sprintf('input[type="checkbox"].checkbox:checked:enabled:not(.checkbox--white) + label:before {' . 'background-image:url(\'%s/core/img/actions/checkmark-white.svg\');' . 'background-color: %s; background-position: center center; background-size:contain;' . - 'width:12px; height:12px; padding:0; margin:2px 6px 6px 2px; border-radius:1px;' . + 'width:12px; height:12px; padding:0; margin:2px 6px 6px 9px; border-radius:1px;' . "}\n", \OC::$WEBROOT, $color @@ -517,7 +517,7 @@ class ThemingControllerTest extends TestCase { $expectedData .= sprintf('input[type="checkbox"].checkbox:checked:enabled:not(.checkbox--white) + label:before {' . 'background-image:url(\'%s/core/img/actions/checkmark-white.svg\');' . 'background-color: #555555; background-position: center center; background-size:contain;' . - 'width:12px; height:12px; padding:0; margin:2px 6px 6px 2px; border-radius:1px;' . + 'width:12px; height:12px; padding:0; margin:2px 6px 6px 9px; border-radius:1px;' . "}\n", \OC::$WEBROOT ); @@ -691,7 +691,7 @@ class ThemingControllerTest extends TestCase { $expectedData .= sprintf('input[type="checkbox"].checkbox:checked:enabled:not(.checkbox--white) + label:before {' . 'background-image:url(\'%s/core/img/actions/checkmark-white.svg\');' . 'background-color: %s; background-position: center center; background-size:contain;' . - 'width:12px; height:12px; padding:0; margin:2px 6px 6px 2px; border-radius:1px;' . + 'width:12px; height:12px; padding:0; margin:2px 6px 6px 9px; border-radius:1px;' . "}\n", \OC::$WEBROOT, $color @@ -797,7 +797,7 @@ class ThemingControllerTest extends TestCase { $expectedData .= sprintf('input[type="checkbox"].checkbox:checked:enabled:not(.checkbox--white) + label:before {' . 'background-image:url(\'%s/core/img/actions/checkmark-white.svg\');' . 'background-color: #555555; background-position: center center; background-size:contain;' . - 'width:12px; height:12px; padding:0; margin:2px 6px 6px 2px; border-radius:1px;' . + 'width:12px; height:12px; padding:0; margin:2px 6px 6px 9px; border-radius:1px;' . "}\n", \OC::$WEBROOT ); diff --git a/apps/updatenotification/lib/Notification/BackgroundJob.php b/apps/updatenotification/lib/Notification/BackgroundJob.php index 3a1aa5e0f16..7bcc0e86905 100644 --- a/apps/updatenotification/lib/Notification/BackgroundJob.php +++ b/apps/updatenotification/lib/Notification/BackgroundJob.php @@ -22,7 +22,6 @@ namespace OCA\UpdateNotification\Notification; - use OC\BackgroundJob\TimedJob; use OC\Installer; use OC\Updater\VersionCheck; @@ -215,6 +214,6 @@ class BackgroundJob extends TimedJob { * @return string|false */ protected function isUpdateAvailable($app) { - return Installer::isUpdateAvailable($app); + return Installer::isUpdateAvailable($app, \OC::$server->getAppFetcher()); } } diff --git a/build/htaccess-checker.php b/build/htaccess-checker.php new file mode 100644 index 00000000000..b6932d6543a --- /dev/null +++ b/build/htaccess-checker.php @@ -0,0 +1,35 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @author Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +/** + * Script to check that the main .htaccess file doesn't include some automated + * changes done by Nextcloud. + */ + +$htaccess = file_get_contents(__DIR__ . '/../.htaccess'); +if(strpos($htaccess, 'DO NOT CHANGE ANYTHING') !== false) { + echo(".htaccess file has invalid changes!\n"); + exit(1); +} else { + exit(0); +} diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php index cdb1fc3fdfd..69962a7282d 100644 --- a/build/integration/features/bootstrap/WebDav.php +++ b/build/integration/features/bootstrap/WebDav.php @@ -42,6 +42,8 @@ trait WebDav { private $davPath = "remote.php/webdav"; /** @var ResponseInterface */ private $response; + /** @var map with user as key and another map as value, which has path as key and etag as value */ + private $storedETAG = NULL; /** * @Given /^using dav path "([^"]*)"$/ @@ -599,4 +601,33 @@ trait WebDav { public function asGetsPropertiesOfFileWith($user, $path, $propertiesTable) { $this->asGetsPropertiesOfFolderWith($user, $path, $propertiesTable); } + + /** + * @Given user :user stores etag of element :path + */ + public function userStoresEtagOfElement($user, $path){ + $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); + $this->asGetsPropertiesOfFolderWith($user, $path, $propertiesTable); + $pathETAG[$path] = $this->response['{DAV:}getetag']; + $this->storedETAG[$user]= $pathETAG; + print_r($this->storedETAG[$user][$path]); + } + + /** + * @Then etag of element :path of user :user has not changed + */ + public function checkIfETAGHasNotChanged($path, $user){ + $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); + $this->asGetsPropertiesOfFolderWith($user, $path, $propertiesTable); + PHPUnit_Framework_Assert::assertEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]); + } + + /** + * @Then etag of element :path of user :user has changed + */ + public function checkIfETAGHasChanged($path, $user){ + $propertiesTable = new \Behat\Gherkin\Node\TableNode([['{DAV:}getetag']]); + $this->asGetsPropertiesOfFolderWith($user, $path, $propertiesTable); + PHPUnit_Framework_Assert::assertNotEquals($this->response['{DAV:}getetag'], $this->storedETAG[$user][$path]); + } } diff --git a/build/integration/features/provisioning-v1.feature b/build/integration/features/provisioning-v1.feature index 98bf321dc64..9103a71ab92 100644 --- a/build/integration/features/provisioning-v1.feature +++ b/build/integration/features/provisioning-v1.feature @@ -255,7 +255,7 @@ Feature: provisioning Scenario: Delete a user Given As an "admin" And user "brand-new-user" exists - When sending "DELETE" to "/cloud/users/brand-new-user" + When sending "DELETE" to "/cloud/users/brand-new-user" Then the OCS status code should be "100" And the HTTP status code should be "200" And user "brand-new-user" does not exist @@ -291,6 +291,7 @@ Feature: provisioning | files_trashbin | | files_versions | | provisioning_api | + | sharebymail | | systemtags | | theming | | twofactor_backupcodes | diff --git a/build/integration/features/sharing-v1.feature b/build/integration/features/sharing-v1.feature index 9b19690d6d2..4d913876cc5 100644 --- a/build/integration/features/sharing-v1.feature +++ b/build/integration/features/sharing-v1.feature @@ -775,7 +775,7 @@ Feature: sharing | shareWith | group1 | When As an "user1" And Deleting last share - Then the OCS status code should be "404" + Then the OCS status code should be "100" And the HTTP status code should be "200" Scenario: Merging shares for recipient when shared from outside with group and member @@ -952,3 +952,18 @@ Feature: sharing | shareType | 1 | Then the OCS status code should be "100" And the HTTP status code should be "200" + + Scenario: unshare from self + Given As an "admin" + And user "user0" exists + And user "user1" exists + And group "sharing-group" exists + And user "user0" belongs to group "sharing-group" + And user "user1" belongs to group "sharing-group" + And file "/PARENT/parent.txt" of user "user0" is shared with group "sharing-group" + And user "user0" stores etag of element "/PARENT" + And user "user1" stores etag of element "/" + And As an "user1" + When Deleting last share + Then etag of element "/" of user "user1" has changed + And etag of element "/PARENT" of user "user0" has not changed diff --git a/config/config.sample.php b/config/config.sample.php index 7f4b3345642..fc52edbc778 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -674,20 +674,6 @@ $CONFIG = array( 'appstoreenabled' => true, /** - * The URL of the appstore to use. - */ -'appstoreurl' => 'https://api.owncloud.com/v1', - -/** - * Whether to show experimental apps in the appstore interface - * - * Experimental apps are not checked for security issues and are new or known - * to be unstable and under heavy development. Installing these can cause data - * loss or security breaches. - */ -'appstore.experimental.enabled' => false, - -/** * Use the ``apps_paths`` parameter to set the location of the Apps directory, * which should be scanned for available apps, and where user-specific apps * should be installed from the Apps store. The ``path`` defines the absolute diff --git a/core/Command/App/Enable.php b/core/Command/App/Enable.php index 19f24d82e43..4aa38cd6f8f 100644 --- a/core/Command/App/Enable.php +++ b/core/Command/App/Enable.php @@ -75,11 +75,12 @@ class Enable extends Command implements CompletionAwareInterface { } $groups = $input->getOption('groups'); + $appClass = new \OC_App(); if (empty($groups)) { - \OC_App::enable($appId); + $appClass->enable($appId); $output->writeln($appId . ' enabled'); } else { - \OC_App::enable($appId, $groups); + $appClass->enable($appId, $groups); $output->writeln($appId . ' enabled for groups: ' . implode(', ', $groups)); } return 0; diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 884eea8869e..71478470ffe 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -196,9 +196,10 @@ class LoginController extends Controller { * @param string $user * @param string $password * @param string $redirect_url + * @param boolean $remember_login * @return RedirectResponse */ - public function tryLogin($user, $password, $redirect_url) { + public function tryLogin($user, $password, $redirect_url, $remember_login = false) { $currentDelay = $this->throttler->getDelay($this->request->getRemoteAddress()); $this->throttler->sleepDelay($this->request->getRemoteAddress()); @@ -236,13 +237,13 @@ class LoginController extends Controller { // TODO: remove password checks from above and let the user session handle failures // requires https://github.com/owncloud/core/pull/24616 $this->userSession->login($user, $password); - $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $user, $password); + $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $user, $password, $remember_login); // User has successfully logged in, now remove the password reset link, when it is available $this->config->deleteUserValue($loginResult->getUID(), 'core', 'lostpassword'); if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { - $this->twoFactorManager->prepareTwoFactorLogin($loginResult); + $this->twoFactorManager->prepareTwoFactorLogin($loginResult, $remember_login); $providers = $this->twoFactorManager->getProviders($loginResult); if (count($providers) === 1) { @@ -265,6 +266,10 @@ class LoginController extends Controller { return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams)); } + if ($remember_login) { + $this->userSession->createRememberMeToken($loginResult); + } + return $this->generateRedirect($redirect_url); } diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js index 5a5bdc33d5d..fc0734e9df4 100644 --- a/core/js/oc-dialogs.js +++ b/core/js/oc-dialogs.js @@ -77,6 +77,25 @@ var OCdialogs = { ); }, /** + * displays confirmation dialog + * @param text content of dialog + * @param title dialog title + * @param callback which will be triggered when user presses YES or NO + * (true or false would be passed to callback respectively) + * @param modal make the dialog modal + */ + confirmHtml:function(text, title, callback, modal) { + return this.message( + text, + title, + 'notice', + OCdialogs.YES_NO_BUTTONS, + callback, + modal, + true + ); + }, + /** * displays prompt dialog * @param text content of dialog * @param title dialog title @@ -251,7 +270,7 @@ var OCdialogs = { * Displays raw dialog * You better use a wrapper instead ... */ - message:function(content, title, dialogType, buttons, callback, modal) { + message:function(content, title, dialogType, buttons, callback, modal, allowHtml) { return $.when(this._getMessageTemplate()).then(function($tmpl) { var dialogName = 'oc-dialog-' + OCdialogs.dialogsCounter + '-content'; var dialogId = '#' + dialogName; @@ -260,7 +279,7 @@ var OCdialogs = { title: title, message: content, type: dialogType - }); + }, allowHtml ? {escapeFunction: ''} : {}); if (modal === undefined) { modal = false; } diff --git a/core/js/shareconfigmodel.js b/core/js/shareconfigmodel.js index b04c2acae16..24922d62636 100644 --- a/core/js/shareconfigmodel.js +++ b/core/js/shareconfigmodel.js @@ -25,6 +25,7 @@ isDefaultExpireDateEnforced: oc_appconfig.core.defaultExpireDateEnforced === true, isDefaultExpireDateEnabled: oc_appconfig.core.defaultExpireDateEnabled === true, isRemoteShareAllowed: oc_appconfig.core.remoteShareAllowed, + isMailShareAllowed: oc_appconfig.shareByMailEnabled !== undefined, defaultExpireDate: oc_appconfig.core.defaultExpireDate, isResharingAllowed: oc_appconfig.core.resharingAllowed, allowGroupSharing: oc_appconfig.core.allowGroupSharing diff --git a/core/js/sharedialogshareelistview.js b/core/js/sharedialogshareelistview.js index 66ed85eda33..775eaa554b0 100644 --- a/core/js/sharedialogshareelistview.js +++ b/core/js/sharedialogshareelistview.js @@ -24,25 +24,28 @@ '{{#if avatarEnabled}}' + '<div class="avatar {{#if modSeed}}imageplaceholderseed{{/if}}" data-username="{{shareWith}}" {{#if modSeed}}data-seed="{{shareWith}} {{shareType}}"{{/if}}></div>' + '{{/if}}' + - '<span class="has-tooltip username" title="{{shareWith}}">{{shareWithDisplayName}}</span>' + + '<span class="has-tooltip username" title="{{shareWithTitle}}">{{shareWithDisplayName}}</span>' + '<span class="sharingOptionsGroup">' + '{{#if editPermissionPossible}}' + + '{{#unless isFileSharedByMail}}' + '<span class="shareOption">' + '<input id="canEdit-{{cid}}-{{shareWith}}" type="checkbox" name="edit" class="permissions checkbox" {{#if hasEditPermission}}checked="checked"{{/if}} />' + '<label for="canEdit-{{cid}}-{{shareWith}}">{{canEditLabel}}</label>' + '</span>' + + '{{/unless}}' + '{{/if}}' + + '{{#unless isMailShare}}' + '<a href="#"><span class="icon icon-more"></span></a>' + '<div class="popovermenu bubble hidden menu">' + '<ul>' + - '{{#if isResharingAllowed}} {{#if sharePermissionPossible}}' + + '{{#if isResharingAllowed}} {{#if sharePermissionPossible}} {{#unless isMailShare}}' + '<li>' + '<span class="shareOption">' + '<input id="canShare-{{cid}}-{{shareWith}}" type="checkbox" name="share" class="permissions checkbox" {{#if hasSharePermission}}checked="checked"{{/if}} data-permissions="{{sharePermission}}" />' + '<label for="canShare-{{cid}}-{{shareWith}}">{{canShareLabel}}</label>' + '</span>' + '</li>' + - '{{/if}} {{/if}}' + + '{{/unless}} {{/if}} {{/if}}' + '{{#if isFolder}}' + '{{#if createPermissionPossible}}' + '<li>' + @@ -74,7 +77,9 @@ '</li>' + '</ul>' + '</div>' + - '</span>' + + '{{/unless}}' + + '<a href="#" class="unshare"><span class="icon-loading-small hidden"></span><span class="icon icon-delete"></span><span class="hidden-visually">{{unshareLabel}}</span></a>' + + '</span>' + '</li>' + '{{/each}}' + '{{#each linkReshares}}' + @@ -141,6 +146,7 @@ getShareeObject: function(shareIndex) { var shareWith = this.model.getShareWith(shareIndex); var shareWithDisplayName = this.model.getShareWithDisplayName(shareIndex); + var shareWithTitle = ''; var shareType = this.model.getShareType(shareIndex); var hasPermissionOverride = {}; @@ -148,6 +154,16 @@ shareWithDisplayName = shareWithDisplayName + " (" + t('core', 'group') + ')'; } else if (shareType === OC.Share.SHARE_TYPE_REMOTE) { shareWithDisplayName = shareWithDisplayName + " (" + t('core', 'remote') + ')'; + } else if (shareType === OC.Share.SHARE_TYPE_EMAIL) { + shareWithDisplayName = shareWithDisplayName + " (" + t('core', 'email') + ')'; + } + + if (shareType === OC.Share.SHARE_TYPE_GROUP) { + shareWithTitle = shareWith + " (" + t('core', 'group') + ')'; + } else if (shareType === OC.Share.SHARE_TYPE_REMOTE) { + shareWithTitle = shareWith + " (" + t('core', 'remote') + ')'; + } else if (shareType === OC.Share.SHARE_TYPE_EMAIL) { + shareWithTitle = shareWith + " (" + t('core', 'email') + ')'; } return _.extend(hasPermissionOverride, { @@ -160,10 +176,13 @@ wasMailSent: this.model.notificationMailWasSent(shareIndex), shareWith: shareWith, shareWithDisplayName: shareWithDisplayName, + shareWithTitle: shareWithTitle, shareType: shareType, shareId: this.model.get('shares')[shareIndex].id, modSeed: shareType !== OC.Share.SHARE_TYPE_USER, - isRemoteShare: shareType === OC.Share.SHARE_TYPE_REMOTE + isRemoteShare: shareType === OC.Share.SHARE_TYPE_REMOTE, + isMailShare: shareType === OC.Share.SHARE_TYPE_EMAIL, + isFileSharedByMail: shareType === OC.Share.SHARE_TYPE_EMAIL && !this.model.isFolder() }); }, diff --git a/core/js/sharedialogview.js b/core/js/sharedialogview.js index 7efb361f512..3b2a7480c47 100644 --- a/core/js/sharedialogview.js +++ b/core/js/sharedialogview.js @@ -154,10 +154,16 @@ var users = result.ocs.data.exact.users.concat(result.ocs.data.users); var groups = result.ocs.data.exact.groups.concat(result.ocs.data.groups); var remotes = result.ocs.data.exact.remotes.concat(result.ocs.data.remotes); + if (typeof(result.ocs.data.emails) !== 'undefined') { + var emails = result.ocs.data.exact.emails.concat(result.ocs.data.emails); + } else { + var emails = []; + } var usersLength; var groupsLength; var remotesLength; + var emailsLength; var i, j; @@ -212,10 +218,18 @@ break; } } + } else if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { + emailsLength = emails.length; + for (j = 0; j < emailsLength; j++) { + if (emails[j].value.shareWith === share.share_with) { + emails.splice(j, 1); + break; + } + } } } - var suggestions = users.concat(groups).concat(remotes); + var suggestions = users.concat(groups).concat(remotes).concat(emails); if (suggestions.length > 0) { $('.shareWithField').removeClass('error') @@ -258,16 +272,13 @@ sharee: text }); } else if (item.value.shareType === OC.Share.SHARE_TYPE_REMOTE) { - if (item.value.server) { - text = t('core', '{sharee} (at {server})', { - sharee: text, - server: item.value.server - }); - } else { text = t('core', '{sharee} (remote)', { sharee: text }); - } + } else if (item.value.shareType === OC.Share.SHARE_TYPE_EMAIL) { + text = t('core', '{sharee} (email)', { + sharee: text + }); } var insert = $("<div class='share-autocomplete-item'/>"); var avatar = $("<div class='avatardiv'></div>").appendTo(insert); @@ -397,7 +408,7 @@ var infoTemplate = this._getRemoteShareInfoTemplate(); remoteShareInfo = infoTemplate({ docLink: this.configModel.getFederatedShareDocLink(), - tooltip: t('core', 'Share with people on other Nextclouds using the syntax username@example.com/nextcloud') + tooltip: t('core', 'Share with people on other servers using the syntax username@example.com/nextcloud') }); } @@ -405,19 +416,33 @@ }, _renderSharePlaceholderPart: function () { - var sharePlaceholder = t('core', 'Share with users…'); + var allowGroupSharing = this.configModel.get('allowGroupSharing'); + var allowRemoteSharing = this.configModel.get('isRemoteShareAllowed'); + var allowMailSharing = this.configModel.get('isMailShareAllowed'); - if (this.configModel.get('allowGroupSharing')) { - if (this.configModel.get('isRemoteShareAllowed')) { - sharePlaceholder = t('core', 'Share with users, groups or remote users…'); - } else { - sharePlaceholder = t('core', 'Share with users or groups…'); - } - } else if (this.configModel.get('isRemoteShareAllowed')) { - sharePlaceholder = t('core', 'Share with users or remote users…'); + if (!allowGroupSharing && !allowRemoteSharing && allowMailSharing) { + return t('core', 'Share with users or by mail...'); + } + if (!allowGroupSharing && allowRemoteSharing && !allowMailSharing) { + return t('core', 'Share with users or remote users...'); + } + if (!allowGroupSharing && allowRemoteSharing && allowMailSharing) { + return t('core', 'Share with users, remote users or by mail...'); + } + if (allowGroupSharing && !allowRemoteSharing && !allowMailSharing) { + return t('core', 'Share with users or groups...'); + } + if (allowGroupSharing && !allowRemoteSharing && allowMailSharing) { + return t('core', 'Share with users, groups or by mail...'); + } + if (allowGroupSharing && allowRemoteSharing && !allowMailSharing) { + return t('core', 'Share with users, groups or remote users...'); + } + if (allowGroupSharing && allowRemoteSharing && allowMailSharing) { + return t('core', 'Share with users, groups, remote users or by mail...'); } - return sharePlaceholder; + return t('core', 'Share with users...'); }, /** diff --git a/core/shipped.json b/core/shipped.json index ee8a2b05ef6..9855a663847 100644 --- a/core/shipped.json +++ b/core/shipped.json @@ -25,6 +25,7 @@ "password_policy", "provisioning_api", "serverinfo", + "sharebymail", "survey_client", "systemtags", "templateeditor", diff --git a/db_structure.xml b/db_structure.xml index 77f6d768986..09dbde710d3 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1126,6 +1126,15 @@ </field> <field> + <name>remember</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>1</length> + </field> + + <field> <name>last_activity</name> <type>integer</type> <default>0</default> diff --git a/.github/issue_template.md b/issue_template.md index 70eca36c9b6..70eca36c9b6 100644 --- a/.github/issue_template.md +++ b/issue_template.md diff --git a/lib/base.php b/lib/base.php index 40e48289102..909a62040ee 100644 --- a/lib/base.php +++ b/lib/base.php @@ -1045,6 +1045,12 @@ class OC { if ($userSession->tryTokenLogin($request)) { return true; } + if (isset($_COOKIE['nc_username']) + && isset($_COOKIE['nc_token']) + && isset($_COOKIE['nc_session_id']) + && $userSession->loginWithCookie($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id'])) { + return true; + } if ($userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler())) { return true; } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 532a6f39848..ddd531868d4 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -279,6 +279,11 @@ return array( 'OC\\AppFramework\\Utility\\TimeFactory' => $baseDir . '/lib/private/AppFramework/Utility/TimeFactory.php', 'OC\\AppHelper' => $baseDir . '/lib/private/AppHelper.php', 'OC\\App\\AppManager' => $baseDir . '/lib/private/App/AppManager.php', + 'OC\\App\\AppStore\\Fetcher\\AppFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/AppFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\Fetcher' => $baseDir . '/lib/private/App/AppStore/Fetcher/Fetcher.php', + 'OC\\App\\AppStore\\Version\\Version' => $baseDir . '/lib/private/App/AppStore/Version/Version.php', + 'OC\\App\\AppStore\\Version\\VersionParser' => $baseDir . '/lib/private/App/AppStore/Version/VersionParser.php', 'OC\\App\\CodeChecker\\AbstractCheck' => $baseDir . '/lib/private/App/CodeChecker/AbstractCheck.php', 'OC\\App\\CodeChecker\\CodeChecker' => $baseDir . '/lib/private/App/CodeChecker/CodeChecker.php', 'OC\\App\\CodeChecker\\DeprecationCheck' => $baseDir . '/lib/private/App/CodeChecker/DeprecationCheck.php', @@ -602,7 +607,6 @@ return array( 'OC\\Notification\\Action' => $baseDir . '/lib/private/Notification/Action.php', 'OC\\Notification\\Manager' => $baseDir . '/lib/private/Notification/Manager.php', 'OC\\Notification\\Notification' => $baseDir . '/lib/private/Notification/Notification.php', - 'OC\\OCSClient' => $baseDir . '/lib/private/OCSClient.php', 'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\Exception' => $baseDir . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Person' => $baseDir . '/lib/private/OCS/Person.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c0a3e9b50c6..99a3c3d540e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -309,6 +309,11 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\AppFramework\\Utility\\TimeFactory' => __DIR__ . '/../../..' . '/lib/private/AppFramework/Utility/TimeFactory.php', 'OC\\AppHelper' => __DIR__ . '/../../..' . '/lib/private/AppHelper.php', 'OC\\App\\AppManager' => __DIR__ . '/../../..' . '/lib/private/App/AppManager.php', + 'OC\\App\\AppStore\\Fetcher\\AppFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/AppFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\CategoryFetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/CategoryFetcher.php', + 'OC\\App\\AppStore\\Fetcher\\Fetcher' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Fetcher/Fetcher.php', + 'OC\\App\\AppStore\\Version\\Version' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/Version.php', + 'OC\\App\\AppStore\\Version\\VersionParser' => __DIR__ . '/../../..' . '/lib/private/App/AppStore/Version/VersionParser.php', 'OC\\App\\CodeChecker\\AbstractCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/AbstractCheck.php', 'OC\\App\\CodeChecker\\CodeChecker' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/CodeChecker.php', 'OC\\App\\CodeChecker\\DeprecationCheck' => __DIR__ . '/../../..' . '/lib/private/App/CodeChecker/DeprecationCheck.php', @@ -632,7 +637,6 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Notification\\Action' => __DIR__ . '/../../..' . '/lib/private/Notification/Action.php', 'OC\\Notification\\Manager' => __DIR__ . '/../../..' . '/lib/private/Notification/Manager.php', 'OC\\Notification\\Notification' => __DIR__ . '/../../..' . '/lib/private/Notification/Notification.php', - 'OC\\OCSClient' => __DIR__ . '/../../..' . '/lib/private/OCSClient.php', 'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\Exception' => __DIR__ . '/../../..' . '/lib/private/OCS/Exception.php', 'OC\\OCS\\Person' => __DIR__ . '/../../..' . '/lib/private/OCS/Person.php', diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php new file mode 100644 index 00000000000..19e61d416a0 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -0,0 +1,56 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Http\Client\IClientService; +use OCP\IConfig; + +class AppFetcher extends Fetcher { + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + * @param IConfig $config; + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config) { + parent::__construct( + $appData, + $clientService, + $timeFactory + ); + + $this->fileName = 'apps.json'; + + $versionArray = \OC_Util::getVersion(); + $this->endpointUrl = sprintf( + 'https://apps.nextcloud.com/api/v1/platform/%d.%d.%d/apps.json', + $versionArray[0], + $versionArray[1], + $versionArray[2] + ); + } +} diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php new file mode 100644 index 00000000000..74201ec3737 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -0,0 +1,45 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Http\Client\IClientService; + +class CategoryFetcher extends Fetcher { + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory) { + parent::__construct( + $appData, + $clientService, + $timeFactory + ); + $this->fileName = 'categories.json'; + $this->endpointUrl = 'https://apps.nextcloud.com/api/v1/categories.json'; + } +} diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php new file mode 100644 index 00000000000..cffff9176e2 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -0,0 +1,92 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\AppStore\Fetcher; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Http\Client\IClientService; + +abstract class Fetcher { + const INVALIDATE_AFTER_SECONDS = 300; + + /** @var IAppData */ + private $appData; + /** @var IClientService */ + private $clientService; + /** @var ITimeFactory */ + private $timeFactory; + /** @var string */ + protected $fileName; + /** @var string */ + protected $endpointUrl; + + /** + * @param IAppData $appData + * @param IClientService $clientService + * @param ITimeFactory $timeFactory + */ + public function __construct(IAppData $appData, + IClientService $clientService, + ITimeFactory $timeFactory) { + $this->appData = $appData; + $this->clientService = $clientService; + $this->timeFactory = $timeFactory; + } + + /** + * Returns the array with the categories on the appstore server + * + * @return array + */ + public function get() { + $rootFolder = $this->appData->getFolder('/'); + + try { + // File does already exists + $file = $rootFolder->getFile($this->fileName); + $jsonBlob = json_decode($file->getContent(), true); + if(is_array($jsonBlob)) { + // If the timestamp is older than 300 seconds request the files new + if((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) { + return $jsonBlob['data']; + } + } + } catch (NotFoundException $e) { + // File does not already exists + $file = $rootFolder->newFile($this->fileName); + } + + // Refresh the file content + $client = $this->clientService->newClient(); + try { + $response = $client->get($this->endpointUrl); + $responseJson = []; + $responseJson['data'] = json_decode($response->getBody(), true); + $responseJson['timestamp'] = $this->timeFactory->getTime(); + $file->putContent(json_encode($responseJson)); + return json_decode($file->getContent(), true)['data']; + } catch (\Exception $e) { + return []; + } + } +} diff --git a/lib/private/App/AppStore/Version/Version.php b/lib/private/App/AppStore/Version/Version.php new file mode 100644 index 00000000000..ca182ae078b --- /dev/null +++ b/lib/private/App/AppStore/Version/Version.php @@ -0,0 +1,52 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\AppStore\Version; + +class Version { + /** @var string */ + private $minVersion; + /** @var string */ + private $maxVersion; + + /** + * @param string $minVersion + * @param string $maxVersion + */ + public function __construct($minVersion, $maxVersion) { + $this->minVersion = $minVersion; + $this->maxVersion = $maxVersion; + } + + /** + * @return string + */ + public function getMinimumVersion() { + return $this->minVersion; + } + + /** + * @return string + */ + public function getMaximumVersion() { + return $this->maxVersion; + } +} diff --git a/lib/private/App/AppStore/Version/VersionParser.php b/lib/private/App/AppStore/Version/VersionParser.php new file mode 100644 index 00000000000..b548ef386d9 --- /dev/null +++ b/lib/private/App/AppStore/Version/VersionParser.php @@ -0,0 +1,83 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\App\AppStore\Version; + +/** + * Class VersionParser parses the versions as sent by the Nextcloud app store + * + * @package OC\App\AppStore + */ +class VersionParser { + /** + * @param string $versionString + * @return bool + */ + private function isValidVersionString($versionString) { + return (bool)preg_match('/^[0-9.]+$/', $versionString); + } + + /** + * Returns the version for a version string + * + * @param string $versionSpec + * @return Version + * @throws \Exception If the version cannot be parsed + */ + public function getVersion($versionSpec) { + // * indicates that the version is compatible with all versions + if($versionSpec === '*') { + return new Version('', ''); + } + + // Count the amount of =, if it is one then it's either maximum or minimum + // version. If it is two then it is maximum and minimum. + $versionElements = explode(' ', $versionSpec); + $firstVersion = isset($versionElements[0]) ? $versionElements[0] : ''; + $firstVersionNumber = substr($firstVersion, 2); + $secondVersion = isset($versionElements[1]) ? $versionElements[1] : ''; + $secondVersionNumber = substr($secondVersion, 2); + + switch(count($versionElements)) { + case 1: + if(!$this->isValidVersionString($firstVersionNumber)) { + break; + } + if(substr($firstVersion, 0, 1) === '>') { + return new Version($firstVersionNumber, ''); + } else { + return new Version('', $firstVersionNumber); + } + case 2: + if(!$this->isValidVersionString($firstVersionNumber) || !$this->isValidVersionString($secondVersionNumber)) { + break; + } + return new Version($firstVersionNumber, $secondVersionNumber); + } + + throw new \Exception( + sprintf( + 'Version cannot be parsed: %s', + $versionSpec + ) + ); + } +} diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php index 67268981e99..c24b25ff14d 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Bernhard Posselt <dev@bernhard-posselt.com> * @author Joas Schilling <coding@schilljs.com> @@ -294,7 +295,9 @@ class DependencyAnalyzer { private function analyzeOC(array $dependencies, array $appInfo) { $missing = []; $minVersion = null; - if (isset($dependencies['owncloud']['@attributes']['min-version'])) { + if (isset($dependencies['nextcloud']['@attributes']['min-version'])) { + $minVersion = $dependencies['nextcloud']['@attributes']['min-version']; + } elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) { $minVersion = $dependencies['owncloud']['@attributes']['min-version']; } elseif (isset($appInfo['requiremin'])) { $minVersion = $appInfo['requiremin']; @@ -302,7 +305,9 @@ class DependencyAnalyzer { $minVersion = $appInfo['require']; } $maxVersion = null; - if (isset($dependencies['owncloud']['@attributes']['max-version'])) { + if (isset($dependencies['nextcloud']['@attributes']['max-version'])) { + $maxVersion = $dependencies['nextcloud']['@attributes']['max-version']; + } elseif (isset($dependencies['owncloud']['@attributes']['max-version'])) { $maxVersion = $dependencies['owncloud']['@attributes']['max-version']; } elseif (isset($appInfo['requiremax'])) { $maxVersion = $appInfo['requiremax']; diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index a1e845f132e..e1516c47ed6 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -290,6 +290,7 @@ class DIContainer extends SimpleContainer implements IAppContainer { $this->registerService('OCP\\IUserSession', function($c) { return $this->getServer()->getUserSession(); }); + $this->registerAlias(\OC\User\Session::class, \OCP\IUserSession::class); $this->registerService('OCP\\ISession', function($c) { return $this->getServer()->getSession(); diff --git a/lib/private/Archive/Archive.php b/lib/private/Archive/Archive.php index da2c53f2aa1..fadc12d2a24 100644 --- a/lib/private/Archive/Archive.php +++ b/lib/private/Archive/Archive.php @@ -32,26 +32,7 @@ namespace OC\Archive; -abstract class Archive{ - /** - * Open any of the supported archive types - * - * @param string $path - * @return Archive|void - */ - public static function open($path) { - $mime = \OC::$server->getMimeTypeDetector()->detect($path); - - switch($mime) { - case 'application/zip': - return new ZIP($path); - case 'application/x-gzip': - return new TAR($path); - case 'application/x-bzip2': - return new TAR($path); - } - } - +abstract class Archive { /** * @param $source */ diff --git a/lib/private/Authentication/Token/DefaultToken.php b/lib/private/Authentication/Token/DefaultToken.php index 904df9baa28..faef2f73b33 100644 --- a/lib/private/Authentication/Token/DefaultToken.php +++ b/lib/private/Authentication/Token/DefaultToken.php @@ -35,6 +35,8 @@ use OCP\AppFramework\Db\Entity; * @method string getToken() * @method void setType(string $type) * @method int getType() + * @method void setRemember(int $remember) + * @method int getRemember() * @method void setLastActivity(int $lastActivity) * @method int getLastActivity() */ @@ -73,6 +75,11 @@ class DefaultToken extends Entity implements IToken { /** * @var int */ + protected $remember; + + /** + * @var int + */ protected $lastActivity; /** diff --git a/lib/private/Authentication/Token/DefaultTokenMapper.php b/lib/private/Authentication/Token/DefaultTokenMapper.php index 0ce26197ccf..752974ff240 100644 --- a/lib/private/Authentication/Token/DefaultTokenMapper.php +++ b/lib/private/Authentication/Token/DefaultTokenMapper.php @@ -40,24 +40,25 @@ class DefaultTokenMapper extends Mapper { * @param string $token */ public function invalidate($token) { + /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->andWhere($qb->expr()->eq('token', $qb->createParameter('token'))) + ->where($qb->expr()->eq('token', $qb->createParameter('token'))) ->setParameter('token', $token) ->execute(); } /** * @param int $olderThan + * @param int $remember */ - public function invalidateOld($olderThan) { + public function invalidateOld($olderThan, $remember = IToken::DO_NOT_REMEMBER) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); $qb->delete('authtoken') - ->where($qb->expr()->lt('last_activity', $qb->createParameter('last_activity'))) - ->andWhere($qb->expr()->eq('type', $qb->createParameter('type'))) - ->setParameter('last_activity', $olderThan, IQueryBuilder::PARAM_INT) - ->setParameter('type', IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT) + ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) ->execute(); } @@ -71,7 +72,7 @@ class DefaultTokenMapper extends Mapper { public function getToken($token) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'token', 'last_activity', 'last_check') + $result = $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'remember', 'token', 'last_activity', 'last_check') ->from('authtoken') ->where($qb->expr()->eq('token', $qb->createParameter('token'))) ->setParameter('token', $token) @@ -97,7 +98,7 @@ class DefaultTokenMapper extends Mapper { public function getTokenByUser(IUser $user) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'token', 'last_activity', 'last_check') + $qb->select('id', 'uid', 'login_name', 'password', 'name', 'type', 'remember', 'token', 'last_activity', 'last_check') ->from('authtoken') ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user->getUID()))) ->setMaxResults(1000); diff --git a/lib/private/Authentication/Token/DefaultTokenProvider.php b/lib/private/Authentication/Token/DefaultTokenProvider.php index b0fbeb9b47e..87f434c684c 100644 --- a/lib/private/Authentication/Token/DefaultTokenProvider.php +++ b/lib/private/Authentication/Token/DefaultTokenProvider.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Christoph Wurst <christoph@winzerhof-wurst.at> * * @author Christoph Wurst <christoph@owncloud.com> * @@ -56,7 +57,11 @@ class DefaultTokenProvider implements IProvider { * @param ILogger $logger * @param ITimeFactory $time */ - public function __construct(DefaultTokenMapper $mapper, ICrypto $crypto, IConfig $config, ILogger $logger, ITimeFactory $time) { + public function __construct(DefaultTokenMapper $mapper, + ICrypto $crypto, + IConfig $config, + ILogger $logger, + ITimeFactory $time) { $this->mapper = $mapper; $this->crypto = $crypto; $this->config = $config; @@ -73,9 +78,10 @@ class DefaultTokenProvider implements IProvider { * @param string|null $password * @param string $name * @param int $type token type + * @param int $remember whether the session token should be used for remember-me * @return IToken */ - public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN) { + public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN, $remember = IToken::DO_NOT_REMEMBER) { $dbToken = new DefaultToken(); $dbToken->setUid($uid); $dbToken->setLoginName($loginName); @@ -85,6 +91,7 @@ class DefaultTokenProvider implements IProvider { $dbToken->setName($name); $dbToken->setToken($this->hashToken($token)); $dbToken->setType($type); + $dbToken->setRemember($remember); $dbToken->setLastActivity($this->time->getTime()); $this->mapper->insert($dbToken); @@ -96,6 +103,7 @@ class DefaultTokenProvider implements IProvider { * Save the updated token * * @param IToken $token + * @throws InvalidTokenException */ public function updateToken(IToken $token) { if (!($token instanceof DefaultToken)) { @@ -152,6 +160,28 @@ class DefaultTokenProvider implements IProvider { } /** + * @param string $oldSessionId + * @param string $sessionId + * @throws InvalidTokenException + */ + public function renewSessionToken($oldSessionId, $sessionId) { + $token = $this->getToken($oldSessionId); + + $newToken = new DefaultToken(); + $newToken->setUid($token->getUID()); + $newToken->setLoginName($token->getLoginName()); + if (!is_null($token->getPassword())) { + $password = $this->decryptPassword($token->getPassword(), $oldSessionId); + $newToken->setPassword($this->encryptPassword($password, $sessionId)); + } + $newToken->setName($token->getName()); + $newToken->setToken($this->hashToken($sessionId)); + $newToken->setType(IToken::TEMPORARY_TOKEN); + $newToken->setLastActivity($this->time->getTime()); + $this->mapper->insert($newToken); + } + + /** * @param IToken $savedToken * @param string $tokenId session token * @throws InvalidTokenException @@ -207,8 +237,11 @@ class DefaultTokenProvider implements IProvider { */ public function invalidateOldTokens() { $olderThan = $this->time->getTime() - (int) $this->config->getSystemValue('session_lifetime', 60 * 60 * 24); - $this->logger->info('Invalidating tokens older than ' . date('c', $olderThan)); - $this->mapper->invalidateOld($olderThan); + $this->logger->info('Invalidating session tokens older than ' . date('c', $olderThan)); + $this->mapper->invalidateOld($olderThan, IToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - (int) $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + $this->logger->info('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold)); + $this->mapper->invalidateOld($rememberThreshold, IToken::REMEMBER); } /** diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index 65b515960ea..ce14a5880c5 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -28,6 +28,7 @@ use OCP\IUser; interface IProvider { + /** * Create and persist a new token * @@ -37,9 +38,10 @@ interface IProvider { * @param string|null $password * @param string $name * @param int $type token type + * @param int $remember whether the session token should be used for remember-me * @return IToken */ - public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN); + public function generateToken($token, $uid, $loginName, $password, $name, $type = IToken::TEMPORARY_TOKEN, $remember = IToken::DO_NOT_REMEMBER); /** * Get a token by token id @@ -51,6 +53,15 @@ interface IProvider { public function getToken($tokenId) ; /** + * Duplicate an existing session token + * + * @param string $oldSessionId + * @param string $sessionId + * @throws InvalidTokenException + */ + public function renewSessionToken($oldSessionId, $sessionId); + + /** * Invalidate (delete) the given session token * * @param string $token diff --git a/lib/private/Authentication/Token/IToken.php b/lib/private/Authentication/Token/IToken.php index e1e78ca369a..14811dd3201 100644 --- a/lib/private/Authentication/Token/IToken.php +++ b/lib/private/Authentication/Token/IToken.php @@ -28,6 +28,8 @@ interface IToken extends JsonSerializable { const TEMPORARY_TOKEN = 0; const PERMANENT_TOKEN = 1; + const DO_NOT_REMEMBER = 0; + const REMEMBER = 1; /** * Get the token ID diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 1bea7aa3478..d84ba4aee7e 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -37,6 +37,7 @@ class Manager { const SESSION_UID_KEY = 'two_factor_auth_uid'; const BACKUP_CODES_APP_ID = 'twofactor_backupcodes'; const BACKUP_CODES_PROVIDER_ID = 'backup_codes'; + const REMEMBER_LOGIN = 'two_factor_remember_login'; /** @var AppManager */ private $appManager; @@ -114,6 +115,7 @@ class Manager { * @param IUser $user * @param bool $includeBackupApp * @return IProvider[] + * @throws Exception */ public function getProviders(IUser $user, $includeBackupApp = false) { $allApps = $this->appManager->getEnabledAppsForUser($user); @@ -171,11 +173,16 @@ class Manager { return false; } - $result = $provider->verifyChallenge($user, $challenge); - if ($result) { + $passed = $provider->verifyChallenge($user, $challenge); + if ($passed) { + if ($this->session->get(self::REMEMBER_LOGIN) === true) { + // TODO: resolve cyclic dependency and use DI + \OC::$server->getUserSession()->createRememberMeToken($user); + } $this->session->remove(self::SESSION_UID_KEY); + $this->session->remove(self::REMEMBER_LOGIN); } - return $result; + return $passed; } /** @@ -202,12 +209,14 @@ class Manager { } /** - * Prepare the 2FA login (set session value) + * Prepare the 2FA login * * @param IUser $user + * @param boolean $rememberMe */ - public function prepareTwoFactorLogin(IUser $user) { + public function prepareTwoFactorLogin(IUser $user, $rememberMe) { $this->session->set(self::SESSION_UID_KEY, $user->getUID()); + $this->session->set(self::REMEMBER_LOGIN, $rememberMe); } } diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 009df790585..2366b762654 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Bart Visscher <bartv@thisnet.nl> @@ -40,90 +41,64 @@ namespace OC; +use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\CodeChecker\CodeChecker; use OC\App\CodeChecker\EmptyCheck; use OC\App\CodeChecker\PrivateCheck; +use OC\Archive\Archive; +use OC\Archive\TAR; use OC_App; use OC_DB; use OC_Helper; +use OCP\Http\Client\IClientService; +use OCP\ILogger; +use OCP\ITempManager; +use phpseclib\File\X509; /** - * This class provides the functionality needed to install, update and remove plugins/apps + * This class provides the functionality needed to install, update and remove apps */ class Installer { + /** @var AppFetcher */ + private $appFetcher; + /** @var IClientService */ + private $clientService; + /** @var ITempManager */ + private $tempManager; + /** @var ILogger */ + private $logger; /** + * @param AppFetcher $appFetcher + * @param IClientService $clientService + * @param ITempManager $tempManager + * @param ILogger $logger + */ + public function __construct(AppFetcher $appFetcher, + IClientService $clientService, + ITempManager $tempManager, + ILogger $logger) { + $this->appFetcher = $appFetcher; + $this->clientService = $clientService; + $this->tempManager = $tempManager; + $this->logger = $logger; + } + + /** + * Installs an app that is located in one of the app folders already * - * This function installs an app. All information needed are passed in the - * associative array $data. - * The following keys are required: - * - source: string, can be "path" or "http" - * - * One of the following keys is required: - * - path: path to the file containing the app - * - href: link to the downloadable file containing the app - * - * The following keys are optional: - * - pretend: boolean, if set true the system won't do anything - * - noinstall: boolean, if true appinfo/install.php won't be loaded - * - inactive: boolean, if set true the appconfig/app.sample.php won't be - * renamed - * - * This function works as follows - * -# fetching the file - * -# unzipping it - * -# check the code - * -# installing the database at appinfo/database.xml - * -# including appinfo/install.php - * -# setting the installed version - * - * It is the task of oc_app_install to create the tables and do whatever is - * needed to get the app working. - * - * Installs an app - * @param array $data with all information + * @param string $appId App to install * @throws \Exception * @return integer */ - public static function installApp( $data = array()) { - $l = \OC::$server->getL10N('lib'); - - list($extractDir, $path) = self::downloadApp($data); - - $info = self::checkAppsIntegrity($data, $extractDir, $path); - $appId = OC_App::cleanAppId($info['id']); - $basedir = OC_App::getInstallPath().'/'.$appId; - //check if the destination directory already exists - if(is_dir($basedir)) { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); - } - throw new \Exception($l->t("App directory already exists")); - } - - if(!empty($data['pretent'])) { - return false; + public function installApp($appId) { + $app = \OC_App::findAppInDirectories($appId); + if($app === false) { + throw new \Exception('App not found in any app directory'); } - //copy the app to the correct place - if(@!mkdir($basedir)) { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); - } - throw new \Exception($l->t("Can't create app folder. Please fix permissions. %s", array($basedir))); - } - - $extractDir .= '/' . $info['id']; - if(!file_exists($extractDir)) { - OC_Helper::rmdirr($basedir); - throw new \Exception($l->t("Archive does not contain a directory named %s", $info['id'])); - } - OC_Helper::copyr($extractDir, $basedir); - - //remove temporary files - OC_Helper::rmdirr($extractDir); + $basedir = $app['path'].'/'.$appId; + $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true); //install the database if(is_file($basedir.'/appinfo/database.xml')) { @@ -168,259 +143,189 @@ class Installer { * * Checks whether or not an app is installed, i.e. registered in apps table. */ - public static function isInstalled( $app ) { + public static function isInstalled( $app ) { return (\OC::$server->getConfig()->getAppValue($app, "installed_version", null) !== null); } /** - * @brief Update an application - * @param array $info - * @param bool $isShipped - * @throws \Exception - * @return bool - * - * This function could work like described below, but currently it disables and then - * enables the app again. This does result in an updated app. - * - * - * This function installs an app. All information needed are passed in the - * associative array $info. - * The following keys are required: - * - source: string, can be "path" or "http" - * - * One of the following keys is required: - * - path: path to the file containing the app - * - href: link to the downloadable file containing the app - * - * The following keys are optional: - * - pretend: boolean, if set true the system won't do anything - * - noupgrade: boolean, if true appinfo/upgrade.php won't be loaded - * - * This function works as follows - * -# fetching the file - * -# removing the old files - * -# unzipping new file - * -# including appinfo/upgrade.php - * -# setting the installed version - * - * upgrade.php can determine the current installed version of the app using - * "\OC::$server->getAppConfig()->getValue($appid, 'installed_version')" - */ - public static function updateApp($info=array(), $isShipped=false) { - list($extractDir, $path) = self::downloadApp($info); - $info = self::checkAppsIntegrity($info, $extractDir, $path, $isShipped); - - $currentDir = OC_App::getAppPath($info['id']); - $basedir = OC_App::getInstallPath(); - $basedir .= '/'; - $basedir .= $info['id']; - - if($currentDir !== false && is_writable($currentDir)) { - $basedir = $currentDir; - } - if(is_dir($basedir)) { - OC_Helper::rmdirr($basedir); - } - - $appInExtractDir = $extractDir; - if (substr($extractDir, -1) !== '/') { - $appInExtractDir .= '/'; - } - - $appInExtractDir .= $info['id']; - OC_Helper::copyr($appInExtractDir, $basedir); - OC_Helper::rmdirr($extractDir); - - return OC_App::updateApp($info['id']); - } - - /** - * update an app by it's id + * Updates the specified app from the appstore * - * @param integer $ocsId + * @param string $appId * @return bool - * @throws \Exception */ - public static function updateAppByOCSId($ocsId) { - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - \OC::$server->getConfig(), - \OC::$server->getLogger() - ); - $appData = $ocsClient->getApplication($ocsId, \OCP\Util::getVersion()); - $download = $ocsClient->getApplicationDownload($ocsId, \OCP\Util::getVersion()); - - if (isset($download['downloadlink']) && trim($download['downloadlink']) !== '') { - $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); - $info = array( - 'source' => 'http', - 'href' => $download['downloadlink'], - 'appdata' => $appData - ); - } else { - throw new \Exception('Could not fetch app info!'); + public function updateAppstoreApp($appId) { + if(self::isUpdateAvailable($appId, $this->appFetcher)) { + try { + $this->downloadApp($appId); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['app' => 'core']); + return false; + } + return OC_App::updateApp($appId); } - return self::updateApp($info); + return false; } /** - * @param array $data - * @return array - * @throws \Exception + * Downloads an app and puts it into the app directory + * + * @param string $appId + * + * @throws \Exception If the installation was not successful */ - public static function downloadApp($data = array()) { - $l = \OC::$server->getL10N('lib'); - - if(!isset($data['source'])) { - throw new \Exception($l->t("No source specified when installing app")); - } + public function downloadApp($appId) { + $appId = strtolower($appId); + + $apps = $this->appFetcher->get(); + foreach($apps as $app) { + if($app['id'] === $appId) { + // Load the certificate + $certificate = new X509(); + $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt')); + $loadedCertificate = $certificate->loadX509($app['certificate']); + + // Verify if the certificate has been revoked + $crl = new X509(); + $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt')); + $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl')); + if($crl->validateSignature() !== true) { + throw new \Exception('Could not validate CRL signature'); + } + $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString(); + $revoked = $crl->getRevoked($csn); + if ($revoked !== false) { + throw new \Exception( + sprintf( + 'Certificate "%s" has been revoked', + $csn + ) + ); + } - //download the file if necessary - if($data['source']=='http') { - $pathInfo = pathinfo($data['href']); - $extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : ''; - $path = \OC::$server->getTempManager()->getTemporaryFile($extension); - if(!isset($data['href'])) { - throw new \Exception($l->t("No href specified when installing app from http")); - } - $client = \OC::$server->getHTTPClientService()->newClient(); - $client->get($data['href'], ['save_to' => $path]); - } else { - if(!isset($data['path'])) { - throw new \Exception($l->t("No path specified when installing app from local file")); - } - $path=$data['path']; - } + // Verify if the certificate has been issued by the Nextcloud Code Authority CA + if($certificate->validateSignature() !== true) { + throw new \Exception( + sprintf( + 'App with id %s has a certificate not issued by a trusted Code Signing Authority', + $appId + ) + ); + } - //detect the archive type - $mime = \OC::$server->getMimeTypeDetector()->detect($path); - if ($mime !=='application/zip' && $mime !== 'application/x-gzip' && $mime !== 'application/x-bzip2') { - throw new \Exception($l->t("Archives of type %s are not supported", array($mime))); - } + // Verify if the certificate is issued for the requested app id + $certInfo = openssl_x509_parse($app['certificate']); + if(!isset($certInfo['subject']['CN'])) { + throw new \Exception( + sprintf( + 'App with id %s has a cert with no CN', + $appId + ) + ); + } + if($certInfo['subject']['CN'] !== $appId) { + throw new \Exception( + sprintf( + 'App with id %s has a cert issued to %s', + $appId, + $certInfo['subject']['CN'] + ) + ); + } - //extract the archive in a temporary folder - $extractDir = \OC::$server->getTempManager()->getTemporaryFolder(); - OC_Helper::rmdirr($extractDir); - mkdir($extractDir); - if($archive=\OC\Archive\Archive::open($path)) { - $archive->extract($extractDir); - } else { - OC_Helper::rmdirr($extractDir); - if($data['source']=='http') { - unlink($path); - } - throw new \Exception($l->t("Failed to open archive when installing app")); - } + // Download the release + $tempFile = $this->tempManager->getTemporaryFile('.tar.gz'); + $client = $this->clientService->newClient(); + $client->get($app['releases'][0]['download'], ['save_to' => $tempFile]); + + // Check if the signature actually matches the downloaded content + $certificate = openssl_get_publickey($app['certificate']); + $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512); + openssl_free_key($certificate); + + if($verified === true) { + // Seems to match, let's proceed + $extractDir = $this->tempManager->getTemporaryFolder(); + $archive = new TAR($tempFile); + + if($archive) { + $archive->extract($extractDir); + $allFiles = scandir($extractDir); + $folders = array_diff($allFiles, ['.', '..']); + $folders = array_values($folders); + + if(count($folders) > 1) { + throw new \Exception( + sprintf( + 'Extracted app %s has more than 1 folder', + $appId + ) + ); + } - return array( - $extractDir, - $path - ); - } + // Check if appinfo/info.xml has the same app ID as well + $loadEntities = libxml_disable_entity_loader(false); + $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml'); + libxml_disable_entity_loader($loadEntities); + if((string)$xml->id !== $appId) { + throw new \Exception( + sprintf( + 'App for id %s has a wrong app ID in info.xml: %s', + $appId, + (string)$xml->id + ) + ); + } - /** - * check an app's integrity - * @param array $data - * @param string $extractDir - * @param string $path - * @param bool $isShipped - * @return array - * @throws \Exception - */ - public static function checkAppsIntegrity($data, $extractDir, $path, $isShipped = false) { - $l = \OC::$server->getL10N('lib'); - //load the info.xml file of the app - if(!is_file($extractDir.'/appinfo/info.xml')) { - //try to find it in a subdir - $dh=opendir($extractDir); - if(is_resource($dh)) { - while (($folder = readdir($dh)) !== false) { - if($folder[0]!='.' and is_dir($extractDir.'/'.$folder)) { - if(is_file($extractDir.'/'.$folder.'/appinfo/info.xml')) { - $extractDir.='/'.$folder; + $baseDir = OC_App::getInstallPath() . '/' . $appId; + // Remove old app with the ID if existent + OC_Helper::rmdirr($baseDir); + // Move to app folder + if(@mkdir($baseDir)) { + $extractDir .= '/' . $folders[0]; + OC_Helper::copyr($extractDir, $baseDir); } + OC_Helper::copyr($extractDir, $baseDir); + OC_Helper::rmdirr($extractDir); + return; + } else { + throw new \Exception( + sprintf( + 'Could not extract app with ID %s to %s', + $appId, + $extractDir + ) + ); } - } - } - } - if(!is_file($extractDir.'/appinfo/info.xml')) { - OC_Helper::rmdirr($extractDir); - if($data['source'] === 'http') { - unlink($path); - } - throw new \Exception($l->t("App does not provide an info.xml file")); - } - - $info = OC_App::getAppInfo($extractDir.'/appinfo/info.xml', true); - if(!is_array($info)) { - throw new \Exception($l->t('App cannot be installed because appinfo file cannot be read.')); - } - - // We can't trust the parsed info.xml file as it may have been tampered - // with by an attacker and thus we need to use the local data to check - // whether the application needs to be signed. - $appId = OC_App::cleanAppId($data['appdata']['id']); - $appBelongingToId = OC_App::getInternalAppIdByOcs($appId); - if(is_string($appBelongingToId)) { - $previouslySigned = \OC::$server->getConfig()->getAppValue($appBelongingToId, 'signed', 'false'); - } else { - $appBelongingToId = $info['id']; - $previouslySigned = 'false'; - } - if($data['appdata']['level'] === OC_App::officialApp || $previouslySigned === 'true') { - \OC::$server->getConfig()->setAppValue($appBelongingToId, 'signed', 'true'); - $integrityResult = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature( - $appBelongingToId, - $extractDir - ); - if($integrityResult !== []) { - $e = new \Exception( - $l->t( - 'Signature could not get checked. Please contact the app developer and check your admin screen.' + } else { + // Signature does not match + throw new \Exception( + sprintf( + 'App with id %s has invalid signature', + $appId ) - ); - throw $e; + ); + } } } - // check the code for not allowed calls - if(!$isShipped && !Installer::checkCode($extractDir)) { - OC_Helper::rmdirr($extractDir); - throw new \Exception($l->t("App can't be installed because of not allowed code in the App")); - } - - // check if the app is compatible with this version of ownCloud - if(!OC_App::isAppCompatible(\OCP\Util::getVersion(), $info)) { - OC_Helper::rmdirr($extractDir); - throw new \Exception($l->t("App can't be installed because it is not compatible with this version of the server")); - } - - // check if shipped tag is set which is only allowed for apps that are shipped with ownCloud - if(!$isShipped && isset($info['shipped']) && ($info['shipped']=='true')) { - OC_Helper::rmdirr($extractDir); - throw new \Exception($l->t("App can't be installed because it contains the <shipped>true</shipped> tag which is not allowed for non shipped apps")); - } - - // check if the ocs version is the same as the version in info.xml/version - $version = trim($info['version']); - - if(isset($data['appdata']['version']) && $version<>trim($data['appdata']['version'])) { - OC_Helper::rmdirr($extractDir); - throw new \Exception($l->t("App can't be installed because the version in info.xml is not the same as the version reported from the app store")); - } - - return $info; + throw new \Exception( + sprintf( + 'Could not download app %s', + $appId + ) + ); } /** * Check if an update for the app is available - * @param string $app - * @return string|false false or the version number of the update * - * The function will check if an update for a version is available + * @param string $appId + * @param AppFetcher $appFetcher + * @return string|false false or the version number of the update */ - public static function isUpdateAvailable( $app ) { + public static function isUpdateAvailable($appId, + AppFetcher $appFetcher) { static $isInstanceReadyForUpdates = null; if ($isInstanceReadyForUpdates === null) { @@ -436,27 +341,20 @@ class Installer { return false; } - $ocsid=\OC::$server->getAppConfig()->getValue( $app, 'ocsid', ''); - - if($ocsid<>'') { - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - \OC::$server->getConfig(), - \OC::$server->getLogger() - ); - $ocsdata = $ocsClient->getApplication($ocsid, \OCP\Util::getVersion()); - $ocsversion= (string) $ocsdata['version']; - $currentversion=OC_App::getAppVersion($app); - if (version_compare($ocsversion, $currentversion, '>')) { - return($ocsversion); - }else{ - return false; + $apps = $appFetcher->get(); + foreach($apps as $app) { + if($app['id'] === $appId) { + $currentVersion = OC_App::getAppVersion($appId); + $newestVersion = $app['releases'][0]['version']; + if (version_compare($newestVersion, $currentVersion, '>')) { + return $newestVersion; + } else { + return false; + } } - - }else{ - return false; } + return false; } /** @@ -466,7 +364,7 @@ class Installer { * * The function will check if the app is already downloaded in the apps repository */ - public static function isDownloaded( $name ) { + public function isDownloaded($name) { foreach(\OC::$APPSROOTS as $dir) { $dirToTest = $dir['path']; $dirToTest .= '/'; @@ -483,7 +381,7 @@ class Installer { /** * Removes an app - * @param string $name name of the application to remove + * @param string $appId ID of the application to remove * @return boolean * * @@ -494,12 +392,10 @@ class Installer { * The function will not delete preferences, tables and the configuration, * this has to be done by the function oc_app_uninstall(). */ - public static function removeApp($appId) { - - if(Installer::isDownloaded( $appId )) { - $appDir=OC_App::getInstallPath() . '/' . $appId; + public function removeApp($appId) { + if($this->isDownloaded( $appId )) { + $appDir = OC_App::getInstallPath() . '/' . $appId; OC_Helper::rmdirr($appDir); - return true; }else{ \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR); @@ -620,7 +516,7 @@ class Installer { } /** - * @param $basedir + * @param string $script */ private static function includeAppScript($script) { if ( file_exists($script) ){ diff --git a/lib/private/OCSClient.php b/lib/private/OCSClient.php deleted file mode 100644 index 76c0b136c06..00000000000 --- a/lib/private/OCSClient.php +++ /dev/null @@ -1,351 +0,0 @@ -<?php -/** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Brice Maron <brice@bmaron.net> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Frank Karlitschek <frank@karlitschek.de> - * @author Jarrett <JetUni@users.noreply.github.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Kamil Domanski <kdomanski@kdemail.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Sam Tuke <mail@samtuke.com> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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 OC; - -use OCP\Http\Client\IClientService; -use OCP\IConfig; -use OCP\ILogger; - -/** - * Class OCSClient is a class for communication with the ownCloud appstore - * - * @package OC - */ -class OCSClient { - /** @var IClientService */ - private $httpClientService; - /** @var IConfig */ - private $config; - /** @var ILogger */ - private $logger; - - /** - * @param IClientService $httpClientService - * @param IConfig $config - * @param ILogger $logger - */ - public function __construct(IClientService $httpClientService, - IConfig $config, - ILogger $logger) { - $this->httpClientService = $httpClientService; - $this->config = $config; - $this->logger = $logger; - } - - /** - * Returns whether the AppStore is enabled (i.e. because the AppStore is disabled for EE) - * - * @return bool - */ - public function isAppStoreEnabled() { - return $this->config->getSystemValue('appstoreenabled', true) === true; - } - - /** - * Get the url of the OCS AppStore server. - * - * @return string of the AppStore server - */ - private function getAppStoreUrl() { - return $this->config->getSystemValue('appstoreurl', 'https://api.owncloud.com/v1'); - } - - /** - * @param string $body - * @param string $action - * @return null|\SimpleXMLElement - */ - private function loadData($body, $action) { - $loadEntities = libxml_disable_entity_loader(true); - $data = @simplexml_load_string($body); - libxml_disable_entity_loader($loadEntities); - - if($data === false) { - libxml_clear_errors(); - $this->logger->error( - sprintf('Could not get %s, content was no valid XML', $action), - [ - 'app' => 'core', - ] - ); - return null; - } - - return $data; - } - - /** - * Get all the categories from the OCS server - * - * @param array $targetVersion The target ownCloud version - * @return array|null an array of category ids or null - * @note returns NULL if config value appstoreenabled is set to false - * This function returns a list of all the application categories on the OCS server - */ - public function getCategories(array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/categories', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get categories: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'categories'); - if($data === null) { - return null; - } - - $tmp = $data->data; - $cats = []; - - foreach ($tmp->category as $value) { - $id = (int)$value->id; - $name = (string)$value->name; - $cats[$id] = $name; - } - - return $cats; - } - - /** - * Get all the applications from the OCS server - * @param array $categories - * @param int $page - * @param string $filter - * @param array $targetVersion The target ownCloud version - * @return array An array of application data - */ - public function getApplications(array $categories, $page, $filter, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return []; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/data', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - 'filter' => $filter, - 'categories' => implode('x', $categories), - 'sortmode' => 'new', - 'page' => $page, - 'pagesize' => 100, - 'approved' => $filter - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get applications: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return []; - } - - $data = $this->loadData($response->getBody(), 'applications'); - if($data === null) { - return []; - } - - $tmp = $data->data->content; - $tmpCount = count($tmp); - - $apps = []; - for ($i = 0; $i < $tmpCount; $i++) { - $app = []; - $app['id'] = (string)$tmp[$i]->id; - $app['name'] = (string)$tmp[$i]->name; - $app['label'] = (string)$tmp[$i]->label; - $app['version'] = (string)$tmp[$i]->version; - $app['type'] = (string)$tmp[$i]->typeid; - $app['typename'] = (string)$tmp[$i]->typename; - $app['personid'] = (string)$tmp[$i]->personid; - $app['profilepage'] = (string)$tmp[$i]->profilepage; - $app['license'] = (string)$tmp[$i]->license; - $app['detailpage'] = (string)$tmp[$i]->detailpage; - $app['preview'] = (string)$tmp[$i]->smallpreviewpic1; - $app['preview-full'] = (string)$tmp[$i]->previewpic1; - $app['changed'] = strtotime($tmp[$i]->changed); - $app['description'] = (string)$tmp[$i]->description; - $app['score'] = (string)$tmp[$i]->score; - $app['downloads'] = (int)$tmp[$i]->downloads; - $app['level'] = (int)$tmp[$i]->approved; - - $apps[] = $app; - } - - return $apps; - } - - - /** - * Get an the applications from the OCS server - * - * @param string $id - * @param array $targetVersion The target ownCloud version - * @return array|null an array of application data or null - * - * This function returns an applications from the OCS server - */ - public function getApplication($id, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $this->getAppStoreUrl() . '/content/data/' . urlencode($id), - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get application: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'application'); - if($data === null) { - return null; - } - - $tmp = $data->data->content; - if (is_null($tmp)) { - \OCP\Util::writeLog('core', 'No update found at the ownCloud appstore for app ' . $id, \OCP\Util::DEBUG); - return null; - } - - $app = []; - $app['id'] = (int)$id; - $app['name'] = (string)$tmp->name; - $app['version'] = (string)$tmp->version; - $app['type'] = (string)$tmp->typeid; - $app['label'] = (string)$tmp->label; - $app['typename'] = (string)$tmp->typename; - $app['personid'] = (string)$tmp->personid; - $app['profilepage'] = (string)$tmp->profilepage; - $app['detailpage'] = (string)$tmp->detailpage; - $app['preview1'] = (string)$tmp->smallpreviewpic1; - $app['preview2'] = (string)$tmp->smallpreviewpic2; - $app['preview3'] = (string)$tmp->smallpreviewpic3; - $app['changed'] = strtotime($tmp->changed); - $app['description'] = (string)$tmp->description; - $app['detailpage'] = (string)$tmp->detailpage; - $app['score'] = (int)$tmp->score; - $app['level'] = (int)$tmp->approved; - - return $app; - } - - /** - * Get the download url for an application from the OCS server - * @param string $id - * @param array $targetVersion The target ownCloud version - * @return array|null an array of application data or null - */ - public function getApplicationDownload($id, array $targetVersion) { - if (!$this->isAppStoreEnabled()) { - return null; - } - $url = $this->getAppStoreUrl() . '/content/download/' . urlencode($id) . '/1'; - $client = $this->httpClientService->newClient(); - try { - $response = $client->get( - $url, - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', $targetVersion), - ], - ] - ); - } catch(\Exception $e) { - $this->logger->error( - sprintf('Could not get application download URL: %s', $e->getMessage()), - [ - 'app' => 'core', - ] - ); - return null; - } - - $data = $this->loadData($response->getBody(), 'application download URL'); - if($data === null) { - return null; - } - - $tmp = $data->data->content; - $app = []; - if (isset($tmp->downloadlink)) { - $app['downloadlink'] = (string)$tmp->downloadlink; - } else { - $app['downloadlink'] = ''; - } - return $app; - } - -} diff --git a/lib/private/Security/CSRF/CsrfToken.php b/lib/private/Security/CSRF/CsrfToken.php index dce9a83b727..e9bdf5b5204 100644 --- a/lib/private/Security/CSRF/CsrfToken.php +++ b/lib/private/Security/CSRF/CsrfToken.php @@ -51,8 +51,8 @@ class CsrfToken { */ public function getEncryptedValue() { if($this->encryptedValue === '') { - $sharedSecret = base64_encode(random_bytes(strlen($this->value))); - $this->encryptedValue = base64_encode($this->value ^ $sharedSecret) . ':' . $sharedSecret; + $sharedSecret = random_bytes(strlen($this->value)); + $this->encryptedValue = base64_encode($this->value ^ $sharedSecret) . ':' . base64_encode($sharedSecret); } return $this->encryptedValue; @@ -71,6 +71,6 @@ class CsrfToken { } $obfuscatedToken = $token[0]; $secret = $token[1]; - return base64_decode($obfuscatedToken) ^ $secret; + return base64_decode($obfuscatedToken) ^ base64_decode($secret); } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 21ec311401d..6f25098eb35 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Bart Visscher <bartv@thisnet.nl> @@ -41,6 +42,8 @@ namespace OC; use bantu\IniGetWrapper\IniGetWrapper; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\AppFramework\Http\Request; use OC\AppFramework\Db\Db; use OC\AppFramework\Utility\TimeFactory; @@ -242,7 +245,7 @@ class Server extends ServerContainer implements IServerContainer { $defaultTokenProvider = null; } - $userSession = new \OC\User\Session($manager, $session, $timeFactory, $defaultTokenProvider, $c->getConfig()); + $userSession = new \OC\User\Session($manager, $session, $timeFactory, $defaultTokenProvider, $c->getConfig(), $c->getSecureRandom()); $userSession->listen('\OC\User', 'preCreateUser', function ($uid, $password) { \OC_Hook::emit('OC_User', 'pre_createUser', array('run' => true, 'uid' => $uid, 'password' => $password)); }); @@ -283,7 +286,7 @@ class Server extends ServerContainer implements IServerContainer { return $userSession; }); - $this->registerService('\OC\Authentication\TwoFactorAuth\Manager', function (Server $c) { + $this->registerService(\OC\Authentication\TwoFactorAuth\Manager::class, function (Server $c) { return new \OC\Authentication\TwoFactorAuth\Manager($c->getAppManager(), $c->getSession(), $c->getConfig()); }); @@ -320,6 +323,21 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService('AppHelper', function ($c) { return new \OC\AppHelper(); }); + $this->registerService('AppFetcher', function ($c) { + return new AppFetcher( + $this->getAppDataDir('appstore'), + $this->getHTTPClientService(), + $this->query(TimeFactory::class), + $this->getConfig() + ); + }); + $this->registerService('CategoryFetcher', function ($c) { + return new CategoryFetcher( + $this->getAppDataDir('appstore'), + $this->getHTTPClientService(), + $this->query(TimeFactory::class) + ); + }); $this->registerService('UserCache', function ($c) { return new Cache\File(); }); @@ -580,13 +598,6 @@ class Server extends ServerContainer implements IServerContainer { $c->getThemingDefaults() ); }); - $this->registerService('OcsClient', function (Server $c) { - return new OCSClient( - $this->getHTTPClientService(), - $this->getConfig(), - $this->getLogger() - ); - }); $this->registerService('LDAPProvider', function(Server $c) { $config = $c->getConfig(); $factoryClass = $config->getSystemValue('ldapProviderFactory', null); @@ -1008,6 +1019,13 @@ class Server extends ServerContainer implements IServerContainer { } /** + * @return AppFetcher + */ + public function getAppFetcher() { + return $this->query('AppFetcher'); + } + + /** * Returns an ICache instance. Since 8.1.0 it returns a fake cache. Use * getMemCacheFactory() instead. * diff --git a/lib/private/Setup/MySQL.php b/lib/private/Setup/MySQL.php index c022616d8b3..d1399c8821c 100644 --- a/lib/private/Setup/MySQL.php +++ b/lib/private/Setup/MySQL.php @@ -87,14 +87,22 @@ class MySQL extends AbstractDatabase { * @throws \OC\DatabaseSetupException */ private function createDBUser($connection) { - $name = $this->dbUser; - $password = $this->dbPassword; - // we need to create 2 accounts, one for global use and one for local user. if we don't specify the local one, - // the anonymous user would take precedence when there is one. - $query = "CREATE USER '$name'@'localhost' IDENTIFIED BY '$password'"; - $connection->executeUpdate($query); - $query = "CREATE USER '$name'@'%' IDENTIFIED BY '$password'"; - $connection->executeUpdate($query); + try{ + $name = $this->dbUser; + $password = $this->dbPassword; + // we need to create 2 accounts, one for global use and one for local user. if we don't specify the local one, + // the anonymous user would take precedence when there is one. + $query = "CREATE USER '$name'@'localhost' IDENTIFIED BY '$password'"; + $connection->executeUpdate($query); + $query = "CREATE USER '$name'@'%' IDENTIFIED BY '$password'"; + $connection->executeUpdate($query); + } + catch (\Exception $ex){ + $this->logger->error('Database User creation failed: {error}', [ + 'app' => 'mysql.setup', + 'error' => $ex->getMessage() + ]); + } } /** diff --git a/lib/private/Share/Constants.php b/lib/private/Share/Constants.php index 13a5a044e8a..f13f83f8ba9 100644 --- a/lib/private/Share/Constants.php +++ b/lib/private/Share/Constants.php @@ -29,9 +29,9 @@ class Constants { const SHARE_TYPE_USER = 0; const SHARE_TYPE_GROUP = 1; const SHARE_TYPE_LINK = 3; - const SHARE_TYPE_EMAIL = 4; // ToDo Check if it is still in use otherwise remove it + const SHARE_TYPE_EMAIL = 4; const SHARE_TYPE_CONTACT = 5; // ToDo Check if it is still in use otherwise remove it - const SHARE_TYPE_REMOTE = 6; // ToDo Check if it is still in use otherwise remove it + const SHARE_TYPE_REMOTE = 6; const FORMAT_NONE = -1; const FORMAT_STATUSES = -2; diff --git a/lib/private/Share20/Manager.php b/lib/private/Share20/Manager.php index 838650ada15..0c49d0b6490 100644 --- a/lib/private/Share20/Manager.php +++ b/lib/private/Share20/Manager.php @@ -30,6 +30,7 @@ namespace OC\Share20; use OC\Cache\CappedMemoryCache; use OC\Files\Mount\MoveableMount; use OC\HintException; +use OC\Share20\Exception\ProviderException; use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; @@ -185,6 +186,10 @@ class Manager implements IManager { if ($share->getSharedWith() === null) { throw new \InvalidArgumentException('SharedWith should not be empty'); } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + if ($share->getSharedWith() === null) { + throw new \InvalidArgumentException('SharedWith should not be empty'); + } } else { // We can't handle other types yet throw new \InvalidArgumentException('unkown share type'); @@ -580,6 +585,16 @@ class Manager implements IManager { if ($share->getPassword() !== null) { $share->setPassword($this->hasher->hash($share->getPassword())); } + } else if ($share->getShareType() === \OCP\Share::SHARE_TYPE_EMAIL) { + $this->linkCreateChecks($share); + $share->setToken( + $this->secureRandom->generate( + \OC\Share\Constants::TOKEN_LENGTH, + \OCP\Security\ISecureRandom::CHAR_LOWER. + \OCP\Security\ISecureRandom::CHAR_UPPER. + \OCP\Security\ISecureRandom::CHAR_DIGITS + ) + ); } // Cannot share with the owner @@ -1034,6 +1049,16 @@ class Manager implements IManager { // If it is not a link share try to fetch a federated share by token if ($share === null) { $provider = $this->factory->getProviderForType(\OCP\Share::SHARE_TYPE_REMOTE); + try { + $share = $provider->getShareByToken($token); + } catch (ShareNotFound $e) { + $share = null; + } + } + + // If it is not a link share try to fetch a federated share by token + if ($share === null && $this->shareProviderExists(\OCP\Share::SHARE_TYPE_EMAIL)) { + $provider = $this->factory->getProviderForType(\OCP\Share::SHARE_TYPE_EMAIL); $share = $provider->getShareByToken($token); } @@ -1277,4 +1302,17 @@ class Manager implements IManager { return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes'; } + /** + * @inheritdoc + */ + public function shareProviderExists($shareType) { + try { + $this->factory->getProviderForType($shareType); + } catch (ProviderException $e) { + return false; + } + + return true; + } + } diff --git a/lib/private/Share20/ProviderFactory.php b/lib/private/Share20/ProviderFactory.php index 5cdc9a51a22..457cf117c69 100644 --- a/lib/private/Share20/ProviderFactory.php +++ b/lib/private/Share20/ProviderFactory.php @@ -28,6 +28,7 @@ use OCA\FederatedFileSharing\DiscoveryManager; use OCA\FederatedFileSharing\FederatedShareProvider; use OCA\FederatedFileSharing\Notifications; use OCA\FederatedFileSharing\TokenHandler; +use OCA\ShareByMail\ShareByMailProvider; use OCP\Share\IProviderFactory; use OC\Share20\Exception\ProviderException; use OCP\IServerContainer; @@ -45,6 +46,8 @@ class ProviderFactory implements IProviderFactory { private $defaultProvider = null; /** @var FederatedShareProvider */ private $federatedProvider = null; + /** @var ShareByMailProvider */ + private $shareByMailProvider; /** * IProviderFactory constructor. @@ -126,6 +129,39 @@ class ProviderFactory implements IProviderFactory { } /** + * Create the federated share provider + * + * @return FederatedShareProvider + */ + protected function getShareByMailProvider() { + if ($this->shareByMailProvider === null) { + /* + * Check if the app is enabled + */ + $appManager = $this->serverContainer->getAppManager(); + if (!$appManager->isEnabledForUser('sharebymail')) { + return null; + } + + $l = $this->serverContainer->getL10N('sharebymail'); + + $this->shareByMailProvider = new ShareByMailProvider( + $this->serverContainer->getDatabaseConnection(), + $this->serverContainer->getSecureRandom(), + $this->serverContainer->getUserManager(), + $this->serverContainer->getLazyRootFolder(), + $l, + $this->serverContainer->getLogger(), + $this->serverContainer->getMailer(), + $this->serverContainer->getURLGenerator() + ); + } + + return $this->shareByMailProvider; + } + + + /** * @inheritdoc */ public function getProvider($id) { @@ -134,6 +170,8 @@ class ProviderFactory implements IProviderFactory { $provider = $this->defaultShareProvider(); } else if ($id === 'ocFederatedSharing') { $provider = $this->federatedShareProvider(); + } else if ($id = 'ocMailShare') { + $provider = $this->getShareByMailProvider(); } if ($provider === null) { @@ -155,6 +193,8 @@ class ProviderFactory implements IProviderFactory { $provider = $this->defaultShareProvider(); } else if ($shareType === \OCP\Share::SHARE_TYPE_REMOTE) { $provider = $this->federatedShareProvider(); + } else if ($shareType === \OCP\Share::SHARE_TYPE_EMAIL) { + $provider = $this->getShareByMailProvider(); } if ($provider === null) { diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 91f24b903b8..e7f7a944902 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Frank Karlitschek <frank@karlitschek.de> @@ -429,11 +430,15 @@ class Updater extends BasicEmitter { private function upgradeAppStoreApps(array $disabledApps) { foreach($disabledApps as $app) { try { - if (Installer::isUpdateAvailable($app)) { - $ocsId = \OC::$server->getConfig()->getAppValue($app, 'ocsid', ''); - - $this->emit('\OC\Updater', 'upgradeAppStoreApp', array($app)); - Installer::updateAppByOCSId($ocsId); + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + $this->log + ); + if (Installer::isUpdateAvailable($app, \OC::$server->getAppFetcher())) { + $this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]); + $installer->updateAppstoreApp($app); } } catch (\Exception $ex) { $this->log->logException($ex, ['app' => 'core']); diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index a213ee48c2a..7215cbe4188 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -48,6 +48,7 @@ use OCP\ISession; use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Security\ISecureRandom; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\Util; @@ -89,23 +90,29 @@ class Session implements IUserSession, Emitter { /** @var User $activeUser */ protected $activeUser; + /** @var ISecureRandom */ + private $random; + /** * @param IUserManager $manager * @param ISession $session * @param ITimeFactory $timeFacory * @param IProvider $tokenProvider * @param IConfig $config + * @param ISecureRandom $random */ public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, - IConfig $config) { + IConfig $config, + ISecureRandom $random) { $this->manager = $manager; $this->session = $session; $this->timeFacory = $timeFacory; $this->tokenProvider = $tokenProvider; $this->config = $config; + $this->random = $random; } /** @@ -526,9 +533,10 @@ class Session implements IUserSession, Emitter { * @param string $uid user UID * @param string $loginName login name * @param string $password + * @param int $remember * @return boolean */ - public function createSessionToken(IRequest $request, $uid, $loginName, $password = null) { + public function createSessionToken(IRequest $request, $uid, $loginName, $password = null, $remember = IToken::DO_NOT_REMEMBER) { if (is_null($this->manager->get($uid))) { // User does not exist return false; @@ -537,7 +545,7 @@ class Session implements IUserSession, Emitter { try { $sessionId = $this->session->getId(); $pwd = $this->getPassword($password); - $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name); + $this->tokenProvider->generateToken($sessionId, $uid, $loginName, $pwd, $name, IToken::TEMPORARY_TOKEN, IToken::REMEMBER); return true; } catch (SessionNotAvailableException $ex) { // This can happen with OCC, where a memory session is used @@ -680,9 +688,10 @@ class Session implements IUserSession, Emitter { * * @param string $uid the username * @param string $currentToken + * @param string $oldSessionId * @return bool */ - public function loginWithCookie($uid, $currentToken) { + public function loginWithCookie($uid, $currentToken, $oldSessionId) { $this->session->regenerateId(); $this->manager->emit('\OC\User', 'preRememberedLogin', array($uid)); $user = $this->manager->get($uid); @@ -692,15 +701,26 @@ class Session implements IUserSession, Emitter { } // get stored tokens - $tokens = OC::$server->getConfig()->getUserKeys($uid, 'login_token'); + $tokens = $this->config->getUserKeys($uid, 'login_token'); // test cookies token against stored tokens if (!in_array($currentToken, $tokens, true)) { return false; } // replace successfully used token with a new one - OC::$server->getConfig()->deleteUserValue($uid, 'login_token', $currentToken); - $newToken = OC::$server->getSecureRandom()->generate(32); - OC::$server->getConfig()->setUserValue($uid, 'login_token', $newToken, time()); + $this->config->deleteUserValue($uid, 'login_token', $currentToken); + $newToken = $this->random->generate(32); + $this->config->setUserValue($uid, 'login_token', $newToken, $this->timeFacory->getTime()); + + try { + $sessionId = $this->session->getId(); + $this->tokenProvider->renewSessionToken($oldSessionId, $sessionId); + } catch (SessionNotAvailableException $ex) { + return false; + } catch (InvalidTokenException $ex) { + \OC::$server->getLogger()->warning('Renewing session token failed', ['app' => 'core']); + return false; + } + $this->setMagicInCookie($user->getUID(), $newToken); //login @@ -710,6 +730,15 @@ class Session implements IUserSession, Emitter { } /** + * @param IUser $user + */ + public function createRememberMeToken(IUser $user) { + $token = $this->random->generate(32); + $this->config->setUserValue($user->getUID(), 'login_token', $token, $this->timeFacory->getTime()); + $this->setMagicInCookie($user->getUID(), $token); + } + + /** * logout the user from the session */ public function logout() { @@ -736,10 +765,19 @@ class Session implements IUserSession, Emitter { */ public function setMagicInCookie($username, $token) { $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; - $expires = time() + OC::$server->getConfig()->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); - setcookie('oc_username', $username, $expires, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_token', $token, $expires, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_remember_login', '1', $expires, OC::$WEBROOT, '', $secureCookie, true); + $webRoot = \OC::$WEBROOT; + if ($webRoot === '') { + $webRoot = '/'; + } + + $expires = $this->timeFacory->getTime() + $this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); + setcookie('nc_username', $username, $expires, $webRoot, '', $secureCookie, true); + setcookie('nc_token', $token, $expires, $webRoot, '', $secureCookie, true); + try { + setcookie('nc_session_id', $this->session->getId(), $expires, $webRoot, '', $secureCookie, true); + } catch (SessionNotAvailableException $ex) { + // ignore + } } /** @@ -749,17 +787,17 @@ class Session implements IUserSession, Emitter { //TODO: DI for cookies and IRequest $secureCookie = OC::$server->getRequest()->getServerProtocol() === 'https'; - unset($_COOKIE['oc_username']); //TODO: DI - unset($_COOKIE['oc_token']); - unset($_COOKIE['oc_remember_login']); - setcookie('oc_username', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_token', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); - setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT, '', $secureCookie, true); + unset($_COOKIE['nc_username']); //TODO: DI + unset($_COOKIE['nc_token']); + unset($_COOKIE['nc_session_id']); + setcookie('nc_username', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); + setcookie('nc_token', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); + setcookie('nc_session_id', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT, '', $secureCookie, true); // old cookies might be stored under /webroot/ instead of /webroot // and Firefox doesn't like it! - setcookie('oc_username', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); - setcookie('oc_token', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); - setcookie('oc_remember_login', '', time() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_username', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_token', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); + setcookie('nc_session_id', '', $this->timeFacory->getTime() - 3600, OC::$WEBROOT . '/', '', $secureCookie, true); } /** @@ -779,4 +817,5 @@ class Session implements IUserSession, Emitter { } } + } diff --git a/lib/private/legacy/app.php b/lib/private/legacy/app.php index d25534aa822..a89a4650c5d 100644 --- a/lib/private/legacy/app.php +++ b/lib/private/legacy/app.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Arthur Schiwon <blizzz@arthur-schiwon.de> * @author Bart Visscher <bartv@thisnet.nl> @@ -326,24 +327,44 @@ class OC_App { /** * enables an app * - * @param mixed $app app + * @param string $appId * @param array $groups (optional) when set, only these groups will have access to the app * @throws \Exception * @return void * * This function set an app as enabled in appconfig. */ - public static function enable($app, $groups = null) { + public function enable($appId, + $groups = null) { self::$enabledAppsCache = []; // flush - if (!Installer::isInstalled($app)) { - $app = self::installApp($app); + $l = \OC::$server->getL10N('core'); + $config = \OC::$server->getConfig(); + + // Check if app is already downloaded + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + $isDownloaded = $installer->isDownloaded($appId); + + if(!$isDownloaded) { + $installer->downloadApp($appId); + } + + if (!Installer::isInstalled($appId)) { + $appId = self::installApp( + $appId, + $config, + $l + ); + $installer->installApp($appId); } else { // check for required dependencies - $config = \OC::$server->getConfig(); - $l = \OC::$server->getL10N('core'); - $info = self::getAppInfo($app); - + $info = self::getAppInfo($appId); self::checkAppDependencies($config, $l, $info); + $installer->installApp($appId); } $appManager = \OC::$server->getAppManager(); @@ -356,42 +377,21 @@ class OC_App { $groupsList[] = $groupManager->get($group); } } - $appManager->enableAppForGroups($app, $groupsList); + $appManager->enableAppForGroups($appId, $groupsList); } else { - $appManager->enableApp($app); + $appManager->enableApp($appId); } - $info = self::getAppInfo($app); + $info = self::getAppInfo($appId); if(isset($info['settings']) && is_array($info['settings'])) { - $appPath = self::getAppPath($app); - self::registerAutoloading($app, $appPath); + $appPath = self::getAppPath($appId); + self::registerAutoloading($appId, $appPath); \OC::$server->getSettingsManager()->setupSettings($info['settings']); } } /** * @param string $app - * @return int - */ - private static function downloadApp($app) { - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - \OC::$server->getConfig(), - \OC::$server->getLogger() - ); - $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); - $download = $ocsClient->getApplicationDownload($app, \OCP\Util::getVersion()); - if(isset($download['downloadlink']) and $download['downloadlink']!='') { - // Replace spaces in download link without encoding entire URL - $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']); - $info = array('source' => 'http', 'href' => $download['downloadlink'], 'appdata' => $appData); - $app = Installer::installApp($info); - } - return $app; - } - - /** - * @param string $app * @return bool */ public static function removeApp($app) { @@ -399,7 +399,13 @@ class OC_App { return false; } - return Installer::removeApp($app); + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + return $installer->removeApp($app); } /** @@ -409,11 +415,6 @@ class OC_App { * @throws Exception */ public static function disable($app) { - // Convert OCS ID to regular application identifier - if(self::getInternalAppIdByOcs($app) !== false) { - $app = self::getInternalAppIdByOcs($app); - } - // flush self::$enabledAppsCache = array(); @@ -554,7 +555,7 @@ class OC_App { * @param string $appId * @return false|string */ - protected static function findAppInDirectories($appId) { + public static function findAppInDirectories($appId) { $sanitizedAppId = self::cleanAppId($appId); if($sanitizedAppId !== $appId) { return false; @@ -613,18 +614,6 @@ class OC_App { return false; } - - /** - * check if an app's directory is writable - * - * @param string $appId - * @return bool - */ - public static function isAppDirWritable($appId) { - $path = self::getAppPath($appId); - return ($path !== false) ? is_writable($path) : false; - } - /** * Get the path for the given app on the access * If the app is defined in multiple directories, the first one is taken. (false if not found) @@ -837,20 +826,11 @@ class OC_App { /** * List all apps, this is used in apps.php * - * @param bool $onlyLocal - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @param OCSClient $ocsClient * @return array */ - public static function listAllApps($onlyLocal = false, - $includeUpdateInfo = true, - OCSClient $ocsClient) { + public function listAllApps() { $installedApps = OC_App::getAllApps(); - //TODO which apps do we want to blacklist and how do we integrate - // blacklisting with the multi apps folder feature? - //we don't want to show configuration for these $blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps(); $appList = array(); @@ -893,8 +873,6 @@ class OC_App { $info['removable'] = true; } - $info['update'] = ($includeUpdateInfo) ? Installer::isUpdateAvailable($app) : null; - $appPath = self::getAppPath($app); if($appPath !== false) { $appIcon = $appPath . '/img/' . $app . '.svg'; @@ -926,29 +904,8 @@ class OC_App { $appList[] = $info; } } - if ($onlyLocal) { - $remoteApps = []; - } else { - $remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient); - } - if ($remoteApps) { - // Remove duplicates - foreach ($appList as $app) { - foreach ($remoteApps AS $key => $remote) { - if ($app['name'] === $remote['name'] || - (isset($app['ocsid']) && - $app['ocsid'] === $remote['id']) - ) { - unset($remoteApps[$key]); - } - } - } - $combinedApps = array_merge($appList, $remoteApps); - } else { - $combinedApps = $appList; - } - return $combinedApps; + return $appList; } /** @@ -966,70 +923,6 @@ class OC_App { return false; } - /** - * Get a list of all apps on the appstore - * @param string $filter - * @param string|null $category - * @param OCSClient $ocsClient - * @return array|bool multi-dimensional array of apps. - * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description - */ - public static function getAppstoreApps($filter = 'approved', - $category = null, - OCSClient $ocsClient) { - $categories = [$category]; - - if (is_null($category)) { - $categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion()); - if (is_array($categoryNames)) { - // Check that categories of apps were retrieved correctly - if (!$categories = array_keys($categoryNames)) { - return false; - } - } else { - return false; - } - } - - $page = 0; - $remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion()); - $apps = []; - $i = 0; - $l = \OC::$server->getL10N('core'); - foreach ($remoteApps as $app) { - $potentialCleanId = self::getInternalAppIdByOcs($app['id']); - // enhance app info (for example the description) - $apps[$i] = OC_App::parseAppInfo($app); - $apps[$i]['author'] = $app['personid']; - $apps[$i]['ocs_id'] = $app['id']; - $apps[$i]['internal'] = 0; - $apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false; - $apps[$i]['update'] = false; - $apps[$i]['groups'] = false; - $apps[$i]['score'] = $app['score']; - $apps[$i]['removable'] = false; - if ($app['label'] == 'recommended') { - $apps[$i]['internallabel'] = (string)$l->t('Recommended'); - $apps[$i]['internalclass'] = 'recommendedapp'; - } - - // Apps from the appstore are always assumed to be compatible with the - // the current release as the initial filtering is done on the appstore - $apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion()); - $apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion()); - - $i++; - } - - - - if (empty($apps)) { - return false; - } else { - return $apps; - } - } - public static function shouldUpgrade($app) { $versions = self::getAppVersions(); $currentVersion = OC_App::getAppVersion($app); @@ -1083,7 +976,9 @@ class OC_App { public static function isAppCompatible($ocVersion, $appInfo) { $requireMin = ''; $requireMax = ''; - if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) { + $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version']; + } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; } else if (isset($appInfo['requiremin'])) { $requireMin = $appInfo['requiremin']; @@ -1091,7 +986,9 @@ class OC_App { $requireMin = $appInfo['require']; } - if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) { + $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version']; + } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version']; } else if (isset($appInfo['requiremax'])) { $requireMax = $appInfo['requiremax']; @@ -1132,46 +1029,16 @@ class OC_App { /** * @param string $app + * @param \OCP\IConfig $config + * @param \OCP\IL10N $l * @return bool + * * @throws Exception if app is not compatible with this version of ownCloud * @throws Exception if no app-name was specified */ - public static function installApp($app) { - $appName = $app; // $app will be overwritten, preserve name for error logging - $l = \OC::$server->getL10N('core'); - $config = \OC::$server->getConfig(); - $ocsClient = new OCSClient( - \OC::$server->getHTTPClientService(), - $config, - \OC::$server->getLogger() - ); - $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion()); - - // check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string - if (!is_numeric($app)) { - $shippedVersion = self::getAppVersion($app); - if ($appData && version_compare($shippedVersion, $appData['version'], '<')) { - $app = self::downloadApp($app); - } else { - $app = Installer::installShippedApp($app); - } - } else { - // Maybe the app is already installed - compare the version in this - // case and use the local already installed one. - // FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me. - $internalAppId = self::getInternalAppIdByOcs($app); - if($internalAppId !== false) { - if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) { - $app = self::downloadApp($app); - } else { - self::enable($internalAppId); - $app = $internalAppId; - } - } else { - $app = self::downloadApp($app); - } - } - + public function installApp($app, + \OCP\IConfig $config, + \OCP\IL10N $l) { if ($app !== false) { // check if the app is compatible with this version of ownCloud $info = self::getAppInfo($app); diff --git a/lib/private/legacy/user.php b/lib/private/legacy/user.php index af2382dbb86..ed0d14a1ab9 100644 --- a/lib/private/legacy/user.php +++ b/lib/private/legacy/user.php @@ -155,10 +155,11 @@ class OC_User { * @deprecated use \OCP\IUserSession::loginWithCookie() * @param string $uid The username of the user to log in * @param string $token + * @param string $oldSessionId * @return bool */ - public static function loginWithCookie($uid, $token) { - return self::getUserSession()->loginWithCookie($uid, $token); + public static function loginWithCookie($uid, $token, $oldSessionId) { + return self::getUserSession()->loginWithCookie($uid, $token, $oldSessionId); } /** diff --git a/lib/private/legacy/util.php b/lib/private/legacy/util.php index e4c2caeafd7..5cd92eaa415 100644 --- a/lib/private/legacy/util.php +++ b/lib/private/legacy/util.php @@ -757,6 +757,7 @@ class OC_Util { 'simplexml_load_string' => 'SimpleXML', 'hash' => 'HASH Message Digest Framework', 'curl_init' => 'cURL', + 'openssl_verify' => 'OpenSSL', ], 'defined' => array( 'PDO::ATTR_DRIVER_NAME' => 'PDO' diff --git a/lib/public/IRequest.php b/lib/public/IRequest.php index 11242c481f0..b36a934b0c2 100644 --- a/lib/public/IRequest.php +++ b/lib/public/IRequest.php @@ -145,7 +145,7 @@ interface IRequest { * Shortcut for getting cookie variables * * @param string $key the key that will be taken from the $_COOKIE array - * @return string the value in the $_COOKIE element + * @return string|null the value in the $_COOKIE element * @since 6.0.0 */ public function getCookie($key); diff --git a/lib/public/Share/IManager.php b/lib/public/Share/IManager.php index a74ab5fe796..137dc309280 100644 --- a/lib/public/Share/IManager.php +++ b/lib/public/Share/IManager.php @@ -286,4 +286,12 @@ interface IManager { */ public function outgoingServer2ServerSharesAllowed(); + /** + * Check if a given share provider exists + * @param int $shareType + * @return bool + * @since 9.2.0 + */ + public function shareProviderExists($shareType); + } diff --git a/resources/codesigning/root.crl b/resources/codesigning/root.crl new file mode 100644 index 00000000000..cb1b97fc87d --- /dev/null +++ b/resources/codesigning/root.crl @@ -0,0 +1,14 @@ +-----BEGIN X509 CRL----- +MIICDTCB9gIBATANBgkqhkiG9w0BAQsFADB7MQswCQYDVQQGEwJERTEbMBkGA1UE +CAwSQmFkZW4tV3VlcnR0ZW1iZXJnMRcwFQYDVQQKDA5OZXh0Y2xvdWQgR21iSDE2 +MDQGA1UEAwwtTmV4dGNsb3VkIENvZGUgU2lnbmluZyBJbnRlcm1lZGlhdGUgQXV0 +aG9yaXR5Fw0xNjEwMTcxMjA5MjhaFw0yNjA4MjYxMjA5MjhaMBUwEwICEBAXDTE2 +MTAxNzEyMDkxOVqgMDAuMB8GA1UdIwQYMBaAFG3qbqqpNyw8iS0XPv1G7sOeeO10 +MAsGA1UdFAQEAgIQAzANBgkqhkiG9w0BAQsFAAOCAQEAZGJNwERFseCv6cS6bfmq +hIIqHieG+/mp4kjqtk4mg8CEYZq/M0q2DMjh7xZUuflV3wadqTCDunDXoyUIV36K +TwLsrREKGFqpSDsVgnX6IYeG0Sf7rnV5PYD2ODWfXrjp3yU7/Jgc2qjco11X5psV +uUnqGDU7DoMwFB6GTTRXfjpCKn8SUtuETAEN013Ii6xXsfCJQTjzQaZByz/Xbypr +sPfotQRfpAhhfjowK5B2ESjXePdNuFlPEAJ114HDJrI89dndIzus95N+3q2sm80T +TFwdooAghAvVmABADC3GQ9bvQb9CUC14DQZJWesy/ps64fgKdXcnBhsX9uPJ7Fdb +hQ== +-----END X509 CRL-----
\ No newline at end of file diff --git a/settings/Application.php b/settings/Application.php index dd237e40c9d..d907cd666fb 100644 --- a/settings/Application.php +++ b/settings/Application.php @@ -30,7 +30,11 @@ namespace OC\Settings; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; +use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Token\IProvider; +use OC\Server; use OC\Settings\Middleware\SubadminMiddleware; use OCP\AppFramework\App; use OCP\IContainer; @@ -86,5 +90,24 @@ class Application extends App { $container->registerService(IManager::class, function (IContainer $c) { return $c->query('ServerContainer')->getSettingsManager(); }); + $container->registerService(AppFetcher::class, function (IContainer $c) { + /** @var Server $server */ + $server = $c->query('ServerContainer'); + return new AppFetcher( + $server->getAppDataDir('appstore'), + $server->getHTTPClientService(), + $server->query(TimeFactory::class), + $server->getConfig() + ); + }); + $container->registerService(CategoryFetcher::class, function (IContainer $c) { + /** @var Server $server */ + $server = $c->query('ServerContainer'); + return new CategoryFetcher( + $server->getAppDataDir('appstore'), + $server->getHTTPClientService(), + $server->query(TimeFactory::class) + ); + }); } } diff --git a/settings/Controller/AppSettingsController.php b/settings/Controller/AppSettingsController.php index 2efd3b8a847..8164dd1fcfa 100644 --- a/settings/Controller/AppSettingsController.php +++ b/settings/Controller/AppSettingsController.php @@ -1,6 +1,7 @@ <?php /** * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * * @author Christoph Wurst <christoph@owncloud.com> * @author Joas Schilling <coding@schilljs.com> @@ -26,19 +27,21 @@ namespace OC\Settings\Controller; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; +use OC\App\AppStore\Version\VersionParser; use OC\App\DependencyAnalyzer; use OC\App\Platform; -use OC\OCSClient; use OCP\App\IAppManager; use \OCP\AppFramework\Controller; use OCP\AppFramework\Http\ContentSecurityPolicy; -use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; -use OCP\ICacheFactory; use OCP\INavigationManager; use OCP\IRequest; use OCP\IL10N; use OCP\IConfig; +use OCP\L10N\IFactory; /** * @package OC\Settings\Controller @@ -51,95 +54,66 @@ class AppSettingsController extends Controller { private $l10n; /** @var IConfig */ private $config; - /** @var \OCP\ICache */ - private $cache; /** @var INavigationManager */ private $navigationManager; /** @var IAppManager */ private $appManager; - /** @var OCSClient */ - private $ocsClient; + /** @var CategoryFetcher */ + private $categoryFetcher; + /** @var AppFetcher */ + private $appFetcher; + /** @var IFactory */ + private $l10nFactory; /** * @param string $appName * @param IRequest $request * @param IL10N $l10n * @param IConfig $config - * @param ICacheFactory $cache * @param INavigationManager $navigationManager * @param IAppManager $appManager - * @param OCSClient $ocsClient + * @param CategoryFetcher $categoryFetcher + * @param AppFetcher $appFetcher + * @param IFactory $l10nFactory */ public function __construct($appName, IRequest $request, IL10N $l10n, IConfig $config, - ICacheFactory $cache, INavigationManager $navigationManager, IAppManager $appManager, - OCSClient $ocsClient) { + CategoryFetcher $categoryFetcher, + AppFetcher $appFetcher, + IFactory $l10nFactory) { parent::__construct($appName, $request); $this->l10n = $l10n; $this->config = $config; - $this->cache = $cache->create($appName); $this->navigationManager = $navigationManager; $this->appManager = $appManager; - $this->ocsClient = $ocsClient; - } - - /** - * Enables or disables the display of experimental apps - * @param bool $state - * @return DataResponse - */ - public function changeExperimentalConfigState($state) { - $this->config->setSystemValue('appstore.experimental.enabled', $state); - $this->appManager->clearAppsCache(); - return new DataResponse(); - } - - /** - * @param string|int $category - * @return int - */ - protected function getCategory($category) { - if (is_string($category)) { - foreach ($this->listCategories() as $cat) { - if (isset($cat['ident']) && $cat['ident'] === $category) { - $category = (int) $cat['id']; - break; - } - } - - // Didn't find the category, falling back to enabled - if (is_string($category)) { - $category = self::CAT_ENABLED; - } - } - return (int) $category; + $this->categoryFetcher = $categoryFetcher; + $this->appFetcher = $appFetcher; + $this->l10nFactory = $l10nFactory; } /** * @NoCSRFRequired + * * @param string $category * @return TemplateResponse */ public function viewApps($category = '') { - $categoryId = $this->getCategory($category); - if ($categoryId === self::CAT_ENABLED) { - // Do not use an arbitrary input string, because we put the category in html + if ($category === '') { $category = 'enabled'; } $params = []; - $params['experimentalEnabled'] = $this->config->getSystemValue('appstore.experimental.enabled', false); $params['category'] = $category; $params['appstoreEnabled'] = $this->config->getSystemValue('appstoreenabled', true) === true; $this->navigationManager->setActiveEntry('core_apps'); $templateResponse = new TemplateResponse($this->appName, 'apps', $params, 'user'); $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); $templateResponse->setContentSecurityPolicy($policy); return $templateResponse; @@ -147,139 +121,192 @@ class AppSettingsController extends Controller { /** * Get all available categories - * @return array + * + * @return JSONResponse */ public function listCategories() { + $currentLanguage = substr($this->l10nFactory->findLanguage(), 0, 2); - if(!is_null($this->cache->get('listCategories'))) { - return $this->cache->get('listCategories'); - } - $categories = [ + $formattedCategories = [ ['id' => self::CAT_ENABLED, 'ident' => 'enabled', 'displayName' => (string)$this->l10n->t('Enabled')], ['id' => self::CAT_DISABLED, 'ident' => 'disabled', 'displayName' => (string)$this->l10n->t('Not enabled')], ]; + $categories = $this->categoryFetcher->get(); + foreach($categories as $category) { + $formattedCategories[] = [ + 'id' => $category['id'], + 'ident' => $category['id'], + 'displayName' => isset($category['translations'][$currentLanguage]['name']) ? $category['translations'][$currentLanguage]['name'] : $category['translations']['en']['name'], + ]; + } + + return new JSONResponse($formattedCategories); + } - if($this->ocsClient->isAppStoreEnabled()) { - // apps from external repo via OCS - $ocs = $this->ocsClient->getCategories(\OCP\Util::getVersion()); - if ($ocs) { - foreach($ocs as $k => $v) { - $name = str_replace('ownCloud ', '', $v); - $ident = str_replace(' ', '-', urlencode(strtolower($name))); - $categories[] = [ - 'id' => $k, - 'ident' => $ident, - 'displayName' => $name, - ]; + /** + * Get all apps for a category + * + * @param string $requestedCategory + * @return array + */ + private function getAppsForCategory($requestedCategory) { + $versionParser = new VersionParser(); + $formattedApps = []; + $apps = $this->appFetcher->get(); + foreach($apps as $app) { + + // Skip all apps not in the requested category + $isInCategory = false; + foreach($app['categories'] as $category) { + if($category === $requestedCategory) { + $isInCategory = true; } } - } + if(!$isInCategory) { + continue; + } - $this->cache->set('listCategories', $categories, 3600); + $nextCloudVersion = $versionParser->getVersion($app['releases'][0]['rawPlatformVersionSpec']); + $nextCloudVersionDependencies = []; + if($nextCloudVersion->getMinimumVersion() !== '') { + $nextCloudVersionDependencies['owncloud']['@attributes']['min-version'] = $nextCloudVersion->getMinimumVersion(); + } + if($nextCloudVersion->getMaximumVersion() !== '') { + $nextCloudVersionDependencies['owncloud']['@attributes']['max-version'] = $nextCloudVersion->getMaximumVersion(); + } + $phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']); + $existsLocally = (\OC_App::getAppPath($app['id']) !== false) ? true : false; + $phpDependencies = []; + if($phpVersion->getMinimumVersion() !== '') { + $phpDependencies['php']['@attributes']['min-version'] = $phpVersion->getMinimumVersion(); + } + if($phpVersion->getMaximumVersion() !== '') { + $phpDependencies['php']['@attributes']['max-version'] = $phpVersion->getMaximumVersion(); + } + if(isset($app['releases'][0]['minIntSize'])) { + $phpDependencies['php']['@attributes']['min-int-size'] = $app['releases'][0]['minIntSize']; + } + $authors = ''; + foreach($app['authors'] as $key => $author) { + $authors .= $author['name']; + if($key !== count($app['authors']) - 1) { + $authors .= ', '; + } + } + + $currentLanguage = substr(\OC::$server->getL10NFactory()->findLanguage(), 0, 2); + $enabledValue = $this->config->getAppValue($app['id'], 'enabled', 'no'); + $groups = null; + if($enabledValue !== 'no' && $enabledValue !== 'yes') { + $groups = $enabledValue; + } - return $categories; + $currentVersion = ''; + if($this->appManager->isInstalled($app['id'])) { + $currentVersion = \OC_App::getAppVersion($app['id']); + } else { + $currentLanguage = $app['releases'][0]['version']; + } + + $formattedApps[] = [ + 'id' => $app['id'], + 'name' => isset($app['translations'][$currentLanguage]['name']) ? $app['translations'][$currentLanguage]['name'] : $app['translations']['en']['name'], + 'description' => isset($app['translations'][$currentLanguage]['description']) ? $app['translations'][$currentLanguage]['description'] : $app['translations']['en']['description'], + 'license' => $app['releases'][0]['licenses'], + 'author' => $authors, + 'shipped' => false, + 'version' => $currentVersion, + 'default_enable' => '', + 'types' => [], + 'documentation' => [ + 'admin' => $app['adminDocs'], + 'user' => $app['userDocs'], + 'developer' => $app['developerDocs'] + ], + 'website' => $app['website'], + 'bugs' => $app['issueTracker'], + 'detailpage' => $app['website'], + 'dependencies' => array_merge( + $nextCloudVersionDependencies, + $phpDependencies + ), + 'level' => ($app['featured'] === true) ? 200 : 100, + 'missingMaxOwnCloudVersion' => false, + 'missingMinOwnCloudVersion' => false, + 'canInstall' => true, + 'preview' => isset($app['screenshots'][0]['url']) ? 'https://usercontent.apps.nextcloud.com/'.base64_encode($app['screenshots'][0]['url']) : '', + 'score' => $app['ratingOverall'], + 'removable' => $existsLocally, + 'active' => $this->appManager->isEnabledForUser($app['id']), + 'needsDownload' => !$existsLocally, + 'groups' => $groups, + 'fromAppStore' => true, + ]; + + + $appFetcher = \OC::$server->getAppFetcher(); + $newVersion = \OC\Installer::isUpdateAvailable($app['id'], $appFetcher); + if($newVersion && $this->appManager->isInstalled($app['id'])) { + $formattedApps[count($formattedApps)-1]['update'] = $newVersion; + } + } + + return $formattedApps; } /** * Get all available apps in a category * * @param string $category - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @return array + * @return JSONResponse */ - public function listApps($category = '', $includeUpdateInfo = true) { - $category = $this->getCategory($category); - $cacheName = 'listApps-' . $category . '-' . (int) $includeUpdateInfo; - - if(!is_null($this->cache->get($cacheName))) { - $apps = $this->cache->get($cacheName); - } else { - switch ($category) { - // installed apps - case 0: - $apps = $this->getInstalledApps($includeUpdateInfo); - usort($apps, function ($a, $b) { - $a = (string)$a['name']; - $b = (string)$b['name']; - if ($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - }); - $version = \OCP\Util::getVersion(); - foreach($apps as $key => $app) { - if(!array_key_exists('level', $app) && array_key_exists('ocsid', $app)) { - $remoteAppEntry = $this->ocsClient->getApplication($app['ocsid'], $version); - - if(is_array($remoteAppEntry) && array_key_exists('level', $remoteAppEntry)) { - $apps[$key]['level'] = $remoteAppEntry['level']; - } - } + public function listApps($category = '') { + $appClass = new \OC_App(); + + switch ($category) { + // installed apps + case 'enabled': + $apps = $appClass->listAllApps(); + $apps = array_filter($apps, function ($app) { + return $app['active']; + }); + usort($apps, function ($a, $b) { + $a = (string)$a['name']; + $b = (string)$b['name']; + if ($a === $b) { + return 0; } - break; - // not-installed apps - case 1: - $apps = \OC_App::listAllApps(true, $includeUpdateInfo, $this->ocsClient); - $apps = array_filter($apps, function ($app) { - return !$app['active']; - }); - $version = \OCP\Util::getVersion(); - foreach($apps as $key => $app) { - if(!array_key_exists('level', $app) && array_key_exists('ocsid', $app)) { - $remoteAppEntry = $this->ocsClient->getApplication($app['ocsid'], $version); - - if(is_array($remoteAppEntry) && array_key_exists('level', $remoteAppEntry)) { - $apps[$key]['level'] = $remoteAppEntry['level']; - } - } + return ($a < $b) ? -1 : 1; + }); + break; + // disabled apps + case 'disabled': + $apps = $appClass->listAllApps(); + $apps = array_filter($apps, function ($app) { + return !$app['active']; + }); + usort($apps, function ($a, $b) { + $a = (string)$a['name']; + $b = (string)$b['name']; + if ($a === $b) { + return 0; } - usort($apps, function ($a, $b) { - $a = (string)$a['name']; - $b = (string)$b['name']; - if ($a === $b) { - return 0; - } - return ($a < $b) ? -1 : 1; - }); - break; - default: - $filter = $this->config->getSystemValue('appstore.experimental.enabled', false) ? 'all' : 'approved'; - - $apps = \OC_App::getAppstoreApps($filter, $category, $this->ocsClient); - if (!$apps) { - $apps = array(); - } else { - // don't list installed apps - $installedApps = $this->getInstalledApps(false); - $installedApps = array_map(function ($app) { - if (isset($app['ocsid'])) { - return $app['ocsid']; - } - return $app['id']; - }, $installedApps); - $apps = array_filter($apps, function ($app) use ($installedApps) { - return !in_array($app['id'], $installedApps); - }); - - // show tooltip if app is downloaded from remote server - $inactiveApps = $this->getInactiveApps(); - foreach ($apps as &$app) { - $app['needsDownload'] = !in_array($app['id'], $inactiveApps); - } + return ($a < $b) ? -1 : 1; + }); + break; + default: + $apps = $this->getAppsForCategory($category); + + // sort by score + usort($apps, function ($a, $b) { + $a = (int)$a['score']; + $b = (int)$b['score']; + if ($a === $b) { + return 0; } - - // sort by score - usort($apps, function ($a, $b) { - $a = (int)$a['score']; - $b = (int)$b['score']; - if ($a === $b) { - return 0; - } - return ($a > $b) ? -1 : 1; - }); - break; - } + return ($a > $b) ? -1 : 1; + }); + break; } // fix groups to be an array @@ -310,40 +337,6 @@ class AppSettingsController extends Controller { return $app; }, $apps); - $this->cache->set($cacheName, $apps, 300); - - return ['apps' => $apps, 'status' => 'success']; - } - - /** - * @param bool $includeUpdateInfo Should we check whether there is an update - * in the app store? - * @return array - */ - private function getInstalledApps($includeUpdateInfo = true) { - $apps = \OC_App::listAllApps(true, $includeUpdateInfo, $this->ocsClient); - $apps = array_filter($apps, function ($app) { - return $app['active']; - }); - return $apps; + return new JSONResponse(['apps' => $apps, 'status' => 'success']); } - - /** - * @return array - */ - private function getInactiveApps() { - $inactiveApps = \OC_App::listAllApps(true, false, $this->ocsClient); - $inactiveApps = array_filter($inactiveApps, - function ($app) { - return !$app['active']; - }); - $inactiveApps = array_map(function($app) { - if (isset($app['ocsid'])) { - return $app['ocsid']; - } - return $app['id']; - }, $inactiveApps); - return $inactiveApps; - } - } diff --git a/settings/ajax/enableapp.php b/settings/ajax/enableapp.php index db4503f20e7..b378b3c918d 100644 --- a/settings/ajax/enableapp.php +++ b/settings/ajax/enableapp.php @@ -31,8 +31,10 @@ OCP\JSON::callCheck(); $groups = isset($_POST['groups']) ? (array)$_POST['groups'] : null; try { - $app = OC_App::cleanAppId((string)$_POST['appid']); - OC_App::enable($app, $groups); + $app = new OC_App(); + $appId = (string)$_POST['appid']; + $appId = OC_App::cleanAppId($appId); + $app->enable($appId, $groups); OC_JSON::success(['data' => ['update_required' => \OC_App::shouldUpgrade($app)]]); } catch (Exception $e) { \OCP\Util::writeLog('core', $e->getMessage(), \OCP\Util::ERROR); diff --git a/settings/ajax/installapp.php b/settings/ajax/installapp.php index 8831305e223..75f3fea83b7 100644 --- a/settings/ajax/installapp.php +++ b/settings/ajax/installapp.php @@ -29,14 +29,15 @@ if (!array_key_exists('appid', $_POST)) { exit; } +$app = new OC_App(); $appId = (string)$_POST['appid']; $appId = OC_App::cleanAppId($appId); - -$result = OC_App::installApp($appId); +$result = $app->installApp( + $appId, + \OC::$server->getConfig(), + \OC::$server->getL10N('core') +); if($result !== false) { - // FIXME: Clear the cache - move that into some sane helper method - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-0'); - \OC::$server->getMemCacheFactory()->create('settings')->remove('listApps-1'); OC_JSON::success(array('data' => array('appid' => $appId))); } else { $l = \OC::$server->getL10N('settings'); diff --git a/settings/ajax/updateapp.php b/settings/ajax/updateapp.php index 47ecac26cf1..3020f828577 100644 --- a/settings/ajax/updateapp.php +++ b/settings/ajax/updateapp.php @@ -35,23 +35,18 @@ if (!array_key_exists('appid', $_POST)) { } $appId = (string)$_POST['appid']; - -if (!is_numeric($appId)) { - $appId = \OC::$server->getAppConfig()->getValue($appId, 'ocsid', null); - if ($appId === null) { - OCP\JSON::error(array( - 'message' => 'No OCS-ID found for app!' - )); - exit; - } -} - $appId = OC_App::cleanAppId($appId); $config = \OC::$server->getConfig(); $config->setSystemValue('maintenance', true); try { - $result = \OC\Installer::updateAppByOCSId($appId); + $installer = new \OC\Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + $result = $installer->updateAppstoreApp($appId); $config->setSystemValue('maintenance', false); } catch(Exception $ex) { $config->setSystemValue('maintenance', false); diff --git a/settings/css/settings.css b/settings/css/settings.css index 0dadf401c04..7d139a632d0 100644 --- a/settings/css/settings.css +++ b/settings/css/settings.css @@ -415,17 +415,6 @@ span.version { background-position: 5px center; padding-left: 25px; } -.app-level .approved { - border-color: #0082c9; -} -.app-level .experimental { - background-color: #ce3702; - border-color: #ce3702; - color: #fff; -} -.apps-experimental { - color: #ce3702; -} .app-score { position: relative; @@ -651,8 +640,13 @@ doesnotexist:-o-prefocus, .strengthify-wrapper { width: 129px; } - - +.trusted-domain-warning { + color: #fff; + padding: 5px; + background: #ce3702; + border-radius: 5px; + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; +} /* HELP */ diff --git a/settings/js/admin.js b/settings/js/admin.js index f5750b23790..7c2b507280f 100644 --- a/settings/js/admin.js +++ b/settings/js/admin.js @@ -3,8 +3,13 @@ $(document).ready(function(){ // Hack to add a trusted domain if (params.trustDomain) { - OC.dialogs.confirm(t('settings', 'Are you really sure you want add "{domain}" as trusted domain?', - {domain: params.trustDomain}), + var potentialDomain = params.trustDomain; + potentialDomain = encodeURI(escapeHTML(potentialDomain)); + potentialDomain = '<span class="trusted-domain-warning">' + potentialDomain + '</span>'; + + OC.dialogs.confirmHtml(t('settings', 'Are you really sure you want add {domain} as trusted domain?', { + domain: potentialDomain + }, undefined, {escape: false}), t('settings', 'Add trusted domain'), function(answer) { if(answer) { $.ajax({ diff --git a/settings/js/apps.js b/settings/js/apps.js index 5fc366c4921..654756af531 100644 --- a/settings/js/apps.js +++ b/settings/js/apps.js @@ -2,7 +2,7 @@ Handlebars.registerHelper('score', function() { if(this.score) { - var score = Math.round( this.score / 10 ); + var score = Math.round( this.score * 10 ); var imageName = 'rating/s' + score + '.svg'; return new Handlebars.SafeString('<img src="' + OC.imagePath('core', imageName) + '">'); @@ -13,10 +13,6 @@ Handlebars.registerHelper('level', function() { if(typeof this.level !== 'undefined') { if(this.level === 200) { return new Handlebars.SafeString('<span class="official icon-checkmark">' + t('settings', 'Official') + '</span>'); - } else if(this.level === 100) { - return new Handlebars.SafeString('<span class="approved">' + t('settings', 'Approved') + '</span>'); - } else { - return new Handlebars.SafeString('<span class="experimental">' + t('settings', 'Experimental') + '</span>'); } } }); @@ -211,7 +207,7 @@ OC.Settings.Apps = OC.Settings.Apps || { currentImage.onload = function() { page.find('.app-image') - .append(OC.Settings.Apps.imageUrl(app.preview, app.detailpage)) + .append(OC.Settings.Apps.imageUrl(app.preview, app.fromAppStore)) .fadeIn(); }; } @@ -248,7 +244,7 @@ OC.Settings.Apps = OC.Settings.Apps || { var img = '<svg width="72" height="72" viewBox="0 0 72 72">'; if (appfromstore) { - img += '<image x="0" y="0" width="72" height="72" preserveAspectRatio="xMinYMin meet" xlink:href="' + url + '?v=' + oc_config.version + '" class="app-icon" /></svg>'; + img += '<image x="0" y="0" width="72" height="72" preserveAspectRatio="xMinYMin meet" xlink:href="' + url + '" class="app-icon" /></svg>'; } else { img += '<image x="0" y="0" width="72" height="72" preserveAspectRatio="xMinYMin meet" filter="url(#invertIcon)" xlink:href="' + url + '?v=' + oc_config.version + '" class="app-icon"></image></svg>'; } diff --git a/settings/routes.php b/settings/routes.php index 64c4e549681..829474ce2bb 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -49,7 +49,6 @@ $application->registerRoutes($this, [ ['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'], ['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'], ['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], - ['name' => 'AppSettings#changeExperimentalConfigState', 'url' => '/settings/apps/experimental', 'verb' => 'POST'], ['name' => 'SecuritySettings#trustedDomains', 'url' => '/settings/admin/security/trustedDomains', 'verb' => 'POST'], ['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], ['name' => 'Users#setMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], diff --git a/settings/templates/apps.php b/settings/templates/apps.php index 46fd5bd0e40..36064f0981c 100644 --- a/settings/templates/apps.php +++ b/settings/templates/apps.php @@ -30,15 +30,6 @@ script( </script> <script id="app-template" type="text/x-handlebars"> - {{#if firstExperimental}} - <div class="section apps-experimental"> - <h2><?php p($l->t('Experimental applications ahead')) ?></h2> - <p> - <?php p($l->t('Experimental apps are not checked for security issues, new or known to be unstable and under heavy development. Installing them can cause data loss or security breaches.')) ?> - </p> - </div> - {{/if}} - <div class="section" id="app-{{id}}"> {{#if preview}} <div class="app-image{{#if previewAsIcon}} app-image-icon{{/if}} hidden"> @@ -160,16 +151,6 @@ script( <div id="app-settings-header"> <button class="settings-button" data-apps-slide-toggle="#app-settings-content"></button> </div> - - <div id="app-settings-content" class="apps-experimental"> - <input type="checkbox" id="enable-experimental-apps" <?php if($_['experimentalEnabled']) { print_unescaped('checked="checked"'); }?> class="checkbox"> - <label for="enable-experimental-apps"><?php p($l->t('Enable experimental apps')) ?></label> - <p> - <small> - <?php p($l->t('Experimental apps are not checked for security issues, new or known to be unstable and under heavy development. Installing them can cause data loss or security breaches.')) ?> - </small> - </p> - </div> </div> </div> <div id="app-content"> diff --git a/settings/templates/help.php b/settings/templates/help.php index 11726d60387..f849ea0f427 100644 --- a/settings/templates/help.php +++ b/settings/templates/help.php @@ -28,9 +28,8 @@ <?php if($_['admin']) { ?> <li> - <a href="https://github.com/nextcloud/server/blob/master/CONTRIBUTING.md" - target="_blank" rel="noreferrer"> - <?php p($l->t('Issue tracker')); ?> ↗ + <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer"> + <?php p($l->t('Getting help')); ?> ↗ </a> </li> <?php } ?> diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index ff50ac98fbd..d16b9b114f3 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -322,6 +322,8 @@ class LoginControllerTest extends TestCase { $this->userSession->expects($this->never()) ->method('createSessionToken'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $this->config->expects($this->never()) ->method('deleteUserValue'); @@ -363,7 +365,7 @@ class LoginControllerTest extends TestCase { ->with($user, $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), $user, $password); + ->with($this->request, $user->getUID(), $user, $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -371,11 +373,63 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('uid', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null)); } + public function testLoginWithValidCredentialsAndRememberMe() { + /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ + $user = $this->getMockBuilder('\OCP\IUser')->getMock(); + $user->expects($this->any()) + ->method('getUID') + ->will($this->returnValue('uid')); + $password = 'secret'; + $indexPageUrl = \OC_Util::getDefaultPageUrl(); + + $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($user, $password); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with($this->request, $user->getUID(), $user, $password, true); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->with($user) + ->will($this->returnValue(false)); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('uid', 'core', 'lostpassword'); + $this->userSession->expects($this->once()) + ->method('createRememberMeToken') + ->with($user); + + $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); + $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, null, true)); + } + public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn() { /** @var IUser | \PHPUnit_Framework_MockObject_MockObject $user */ $user = $this->getMockBuilder('\OCP\IUser')->getMock(); @@ -408,6 +462,8 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue(false)); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse(\OC_Util::getDefaultPageUrl()); $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); @@ -450,6 +506,8 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue($redirectUrl)); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new \OCP\AppFramework\Http\RedirectResponse($redirectUrl); $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); @@ -488,7 +546,7 @@ class LoginControllerTest extends TestCase { ->will($this->returnValue($user)); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'Jane', $password); + ->with($this->request, $user->getUID(), 'Jane', $password, false); $this->userSession->expects($this->once()) ->method('isLoggedIn') ->with() @@ -540,7 +598,7 @@ class LoginControllerTest extends TestCase { ->with('john@doe.com', $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password); + ->with($this->request, $user->getUID(), 'john@doe.com', $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -564,6 +622,8 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('john', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse($challengeUrl); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); @@ -605,7 +665,7 @@ class LoginControllerTest extends TestCase { ->with('john@doe.com', $password); $this->userSession->expects($this->once()) ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password); + ->with($this->request, $user->getUID(), 'john@doe.com', $password, false); $this->twoFactorManager->expects($this->once()) ->method('isTwoFactorAuthenticated') ->with($user) @@ -628,6 +688,8 @@ class LoginControllerTest extends TestCase { $this->config->expects($this->once()) ->method('deleteUserValue') ->with('john', 'core', 'lostpassword'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse($challengeUrl); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); @@ -680,6 +742,8 @@ class LoginControllerTest extends TestCase { ->with('login', '192.168.0.1', ['user' => 'john@doe.com']); $this->config->expects($this->never()) ->method('deleteUserValue'); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); $expected = new RedirectResponse(''); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null)); diff --git a/tests/Settings/Controller/AppSettingsControllerTest.php b/tests/Settings/Controller/AppSettingsControllerTest.php index 9dcc55e135b..a3e4a6fd828 100644 --- a/tests/Settings/Controller/AppSettingsControllerTest.php +++ b/tests/Settings/Controller/AppSettingsControllerTest.php @@ -2,6 +2,7 @@ /** * @author Lukas Reschke <lukas@owncloud.com> * + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * @copyright Copyright (c) 2015, ownCloud, Inc. * @license AGPL-3.0 * @@ -21,18 +22,19 @@ namespace Tests\Settings\Controller; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\Settings\Controller\AppSettingsController; use OCP\AppFramework\Http\ContentSecurityPolicy; -use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\TemplateResponse; +use OCP\L10N\IFactory; use Test\TestCase; use OCP\IRequest; use OCP\IL10N; use OCP\IConfig; -use OCP\ICache; use OCP\INavigationManager; use OCP\App\IAppManager; -use OC\OCSClient; /** * Class AppSettingsControllerTest @@ -42,95 +44,53 @@ use OC\OCSClient; class AppSettingsControllerTest extends TestCase { /** @var AppSettingsController */ private $appSettingsController; - /** @var IRequest */ + /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ private $request; - /** @var IL10N */ + /** @var IL10N|\PHPUnit_Framework_MockObject_MockObject */ private $l10n; - /** @var IConfig */ + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ private $config; - /** @var ICache */ - private $cache; - /** @var INavigationManager */ + /** @var INavigationManager|\PHPUnit_Framework_MockObject_MockObject */ private $navigationManager; - /** @var IAppManager */ + /** @var IAppManager|\PHPUnit_Framework_MockObject_MockObject */ private $appManager; - /** @var OCSClient */ - private $ocsClient; + /** @var CategoryFetcher|\PHPUnit_Framework_MockObject_MockObject */ + private $categoryFetcher; + /** @var AppFetcher|\PHPUnit_Framework_MockObject_MockObject */ + private $appFetcher; + /** @var IFactory|\PHPUnit_Framework_MockObject_MockObject */ + private $l10nFactory; public function setUp() { parent::setUp(); - $this->request = $this->getMockBuilder('\OCP\IRequest') - ->disableOriginalConstructor()->getMock(); - $this->l10n = $this->getMockBuilder('\OCP\IL10N') - ->disableOriginalConstructor()->getMock(); + $this->request = $this->createMock(IRequest::class); + $this->l10n = $this->createMock(IL10N::class); $this->l10n->expects($this->any()) ->method('t') ->will($this->returnArgument(0)); - $this->config = $this->getMockBuilder('\OCP\IConfig') - ->disableOriginalConstructor()->getMock(); - $cacheFactory = $this->getMockBuilder('\OCP\ICacheFactory') - ->disableOriginalConstructor()->getMock(); - $this->cache = $this->getMockBuilder('\OCP\ICache') - ->disableOriginalConstructor()->getMock(); - $cacheFactory - ->expects($this->once()) - ->method('create') - ->with('settings') - ->will($this->returnValue($this->cache)); - - $this->navigationManager = $this->getMockBuilder('\OCP\INavigationManager') - ->disableOriginalConstructor()->getMock(); - $this->appManager = $this->getMockBuilder('\OCP\App\IAppManager') - ->disableOriginalConstructor()->getMock(); - $this->ocsClient = $this->getMockBuilder('\OC\OCSClient') - ->disableOriginalConstructor()->getMock(); + $this->config = $this->createMock(IConfig::class); + $this->navigationManager = $this->createMock(INavigationManager::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->categoryFetcher = $this->createMock(CategoryFetcher::class); + $this->appFetcher = $this->createMock(AppFetcher::class); + $this->l10nFactory = $this->createMock(IFactory::class); $this->appSettingsController = new AppSettingsController( 'settings', $this->request, $this->l10n, $this->config, - $cacheFactory, $this->navigationManager, $this->appManager, - $this->ocsClient + $this->categoryFetcher, + $this->appFetcher, + $this->l10nFactory ); } - public function testChangeExperimentalConfigStateTrue() { - $this->config - ->expects($this->once()) - ->method('setSystemValue') - ->with('appstore.experimental.enabled', true); - $this->appManager - ->expects($this->once()) - ->method('clearAppsCache'); - $this->assertEquals(new DataResponse(), $this->appSettingsController->changeExperimentalConfigState(true)); - } - - public function testChangeExperimentalConfigStateFalse() { - $this->config - ->expects($this->once()) - ->method('setSystemValue') - ->with('appstore.experimental.enabled', false); - $this->appManager - ->expects($this->once()) - ->method('clearAppsCache'); - $this->assertEquals(new DataResponse(), $this->appSettingsController->changeExperimentalConfigState(false)); - } - - public function testListCategoriesCached() { - $this->cache - ->expects($this->exactly(2)) - ->method('get') - ->with('listCategories') - ->will($this->returnValue(['CachedArray'])); - $this->assertSame(['CachedArray'], $this->appSettingsController->listCategories()); - } - - public function testListCategoriesNotCachedWithoutAppStore() { - $expected = [ + public function testListCategories() { + $expected = new JSONResponse([ [ 'id' => 0, 'ident' => 'enabled', @@ -141,115 +101,69 @@ class AppSettingsControllerTest extends TestCase { 'ident' => 'disabled', 'displayName' => 'Not enabled', ], - ]; - $this->cache - ->expects($this->once()) - ->method('get') - ->with('listCategories') - ->will($this->returnValue(null)); - $this->cache - ->expects($this->once()) - ->method('set') - ->with('listCategories', $expected, 3600); - - - $this->assertSame($expected, $this->appSettingsController->listCategories()); - } - - public function testListCategoriesNotCachedWithAppStore() { - $expected = [ [ - 'id' => 0, - 'ident' => 'enabled', - 'displayName' => 'Enabled', + 'id' => 'auth', + 'ident' => 'auth', + 'displayName' => 'Authentication & authorization', ], [ - 'id' => 1, - 'ident' => 'disabled', - 'displayName' => 'Not enabled', + 'id' => 'customization', + 'ident' => 'customization', + 'displayName' => 'Customization', ], [ - 'id' => 0, - 'ident' => 'tools', - 'displayName' => 'Tools', + 'id' => 'files', + 'ident' => 'files', + 'displayName' => 'Files', ], [ - 'id' => 1, - 'ident' => 'games', - 'displayName' => 'Games', + 'id' => 'integration', + 'ident' => 'integration', + 'displayName' => 'Integration', ], [ - 'id' => 2, - 'ident' => 'productivity', - 'displayName' => 'Productivity', + 'id' => 'monitoring', + 'ident' => 'monitoring', + 'displayName' => 'Monitoring', ], [ - 'id' => 3, + 'id' => 'multimedia', 'ident' => 'multimedia', 'displayName' => 'Multimedia', ], - ]; + [ + 'id' => 'office', + 'ident' => 'office', + 'displayName' => 'Office & text', + ], + [ + 'id' => 'organization', + 'ident' => 'organization', + 'displayName' => 'Organization', + ], + [ + 'id' => 'social', + 'ident' => 'social', + 'displayName' => 'Social & communication', + ], + [ + 'id' => 'tools', + 'ident' => 'tools', + 'displayName' => 'Tools', + ], + ]); - $this->cache + $this->categoryFetcher ->expects($this->once()) ->method('get') - ->with('listCategories') - ->will($this->returnValue(null)); - $this->cache - ->expects($this->once()) - ->method('set') - ->with('listCategories', $expected, 3600); - - $this->ocsClient - ->expects($this->once()) - ->method('isAppStoreEnabled') - ->will($this->returnValue(true)); - $this->ocsClient - ->expects($this->once()) - ->method('getCategories') - ->will($this->returnValue( - [ - 'ownCloud Tools', - 'Games', - 'ownCloud Productivity', - 'Multimedia', - ] - )); + ->willReturn(json_decode('[{"id":"auth","translations":{"cs":{"name":"Autentizace & autorizace","description":"Aplikace poskytující služby dodatečného ověření nebo přihlášení"},"hu":{"name":"Azonosítás és hitelesítés","description":"Apps that provide additional authentication or authorization services"},"de":{"name":"Authentifizierung & Authorisierung","description":"Apps die zusätzliche Autentifizierungs- oder Autorisierungsdienste bereitstellen"},"nl":{"name":"Authenticatie & authorisatie","description":"Apps die aanvullende authenticatie- en autorisatiediensten bieden"},"nb":{"name":"Pålogging og tilgangsstyring","description":"Apper for å tilby ekstra pålogging eller tilgangsstyring"},"it":{"name":"Autenticazione e autorizzazione","description":"Apps that provide additional authentication or authorization services"},"fr":{"name":"Authentification et autorisations","description":"Applications qui fournissent des services d\'authentification ou d\'autorisations additionnels."},"ru":{"name":"Аутентификация и авторизация","description":"Apps that provide additional authentication or authorization services"},"en":{"name":"Authentication & authorization","description":"Apps that provide additional authentication or authorization services"}}},{"id":"customization","translations":{"cs":{"name":"Přizpůsobení","description":"Motivy a aplikace měnící rozvržení a uživatelské rozhraní"},"it":{"name":"Personalizzazione","description":"Applicazioni di temi, modifiche della disposizione e UX"},"de":{"name":"Anpassung","description":"Apps zur Änderung von Themen, Layout und Benutzererfahrung"},"hu":{"name":"Személyre szabás","description":"Témák, elrendezések felhasználói felület módosító alkalmazások"},"nl":{"name":"Maatwerk","description":"Thema\'s, layout en UX aanpassingsapps"},"nb":{"name":"Tilpasning","description":"Apper for å endre Tema, utseende og brukeropplevelse"},"fr":{"name":"Personalisation","description":"Thèmes, apparence et applications modifiant l\'expérience utilisateur"},"ru":{"name":"Настройка","description":"Themes, layout and UX change apps"},"en":{"name":"Customization","description":"Themes, layout and UX change apps"}}},{"id":"files","translations":{"cs":{"name":"Soubory","description":"Aplikace rozšiřující správu souborů nebo aplikaci Soubory"},"it":{"name":"File","description":"Applicazioni di gestione dei file ed estensione dell\'applicazione FIle"},"de":{"name":"Dateien","description":"Dateimanagement sowie Erweiterungs-Apps für die Dateien-App"},"hu":{"name":"Fájlok","description":"Fájl kezelő és kiegészítő alkalmazások"},"nl":{"name":"Bestanden","description":"Bestandebeheer en uitbreidingen van bestand apps"},"nb":{"name":"Filer","description":"Apper for filhåndtering og filer"},"fr":{"name":"Fichiers","description":"Applications de gestion de fichiers et extensions à l\'application Fichiers"},"ru":{"name":"Файлы","description":"Расширение: файлы и управление файлами"},"en":{"name":"Files","description":"File management and Files app extension apps"}}},{"id":"integration","translations":{"it":{"name":"Integrazione","description":"Applicazioni che collegano Nextcloud con altri servizi e piattaforme"},"hu":{"name":"Integráció","description":"Apps that connect Nextcloud with other services and platforms"},"nl":{"name":"Integratie","description":"Apps die Nextcloud verbinden met andere services en platformen"},"nb":{"name":"Integrasjon","description":"Apper som kobler Nextcloud med andre tjenester og plattformer"},"de":{"name":"Integration","description":"Apps die Nextcloud mit anderen Diensten und Plattformen verbinden"},"cs":{"name":"Propojení","description":"Aplikace propojující NextCloud s dalšími službami a platformami"},"fr":{"name":"Intégration","description":"Applications qui connectent Nextcloud avec d\'autres services et plateformes"},"ru":{"name":"Интеграция","description":"Приложения, соединяющие Nextcloud с другими службами и платформами"},"en":{"name":"Integration","description":"Apps that connect Nextcloud with other services and platforms"}}},{"id":"monitoring","translations":{"nb":{"name":"Overvåking","description":"Apper for statistikk, systemdiagnose og aktivitet"},"it":{"name":"Monitoraggio","description":"Applicazioni di statistiche, diagnostica di sistema e attività"},"de":{"name":"Überwachung","description":"Datenstatistiken-, Systemdiagnose- und Aktivitäten-Apps"},"hu":{"name":"Megfigyelés","description":"Data statistics, system diagnostics and activity apps"},"nl":{"name":"Monitoren","description":"Gegevensstatistiek, systeem diagnose en activiteit apps"},"cs":{"name":"Kontrola","description":"Datové statistiky, diagnózy systému a aktivity aplikací"},"fr":{"name":"Surveillance","description":"Applications de statistiques sur les données, de diagnostics systèmes et d\'activité."},"ru":{"name":"Мониторинг","description":"Статистика данных, диагностика системы и активность приложений"},"en":{"name":"Monitoring","description":"Data statistics, system diagnostics and activity apps"}}},{"id":"multimedia","translations":{"nb":{"name":"Multimedia","description":"Apper for lyd, film og bilde"},"it":{"name":"Multimedia","description":"Applicazioni per audio, video e immagini"},"de":{"name":"Multimedia","description":"Audio-, Video- und Bilder-Apps"},"hu":{"name":"Multimédia","description":"Hang, videó és kép alkalmazások"},"nl":{"name":"Multimedia","description":"Audio, video en afbeelding apps"},"en":{"name":"Multimedia","description":"Audio, video and picture apps"},"cs":{"name":"Multimédia","description":"Aplikace audia, videa a obrázků"},"fr":{"name":"Multimédia","description":"Applications audio, vidéo et image"},"ru":{"name":"Мультимедиа","description":"Приложение аудио, видео и изображения"}}},{"id":"office","translations":{"nb":{"name":"Kontorstøtte og tekst","description":"Apper for Kontorstøtte og tekstbehandling"},"it":{"name":"Ufficio e testo","description":"Applicazione per ufficio ed elaborazione di testi"},"de":{"name":"Büro & Text","description":"Büro- und Textverarbeitungs-Apps"},"hu":{"name":"Iroda és szöveg","description":"Irodai és szöveg feldolgozó alkalmazások"},"nl":{"name":"Office & tekst","description":"Office en tekstverwerkingsapps"},"cs":{"name":"Kancelář a text","description":"Aplikace pro kancelář a zpracování textu"},"fr":{"name":"Bureautique & texte","description":"Applications de bureautique et de traitement de texte"},"en":{"name":"Office & text","description":"Office and text processing apps"}}},{"id":"organization","translations":{"nb":{"name":"Organisering","description":"Apper for tidsstyring, oppgaveliste og kalender"},"it":{"name":"Organizzazione","description":"Applicazioni di gestione del tempo, elenco delle cose da fare e calendario"},"hu":{"name":"Szervezet","description":"Időbeosztás, teendő lista és naptár alkalmazások"},"nl":{"name":"Organisatie","description":"Tijdmanagement, takenlijsten en agenda apps"},"cs":{"name":"Organizace","description":"Aplikace pro správu času, plánování a kalendáře"},"de":{"name":"Organisation","description":"Time management, Todo list and calendar apps"},"fr":{"name":"Organisation","description":"Applications de gestion du temps, de listes de tâches et d\'agendas"},"ru":{"name":"Организация","description":"Приложения по управлению временем, список задач и календарь"},"en":{"name":"Organization","description":"Time management, Todo list and calendar apps"}}},{"id":"social","translations":{"nb":{"name":"Sosialt og kommunikasjon","description":"Apper for meldinger, kontakthåndtering og sosiale medier"},"it":{"name":"Sociale e comunicazione","description":"Applicazioni di messaggistica, gestione dei contatti e reti sociali"},"de":{"name":"Kommunikation","description":"Nachrichten-, Kontaktverwaltungs- und Social-Media-Apps"},"hu":{"name":"Közösségi és kommunikáció","description":"Üzenetküldő, kapcsolat kezelő és közösségi média alkalmazások"},"nl":{"name":"Sociaal & communicatie","description":"Messaging, contactbeheer en social media apps"},"cs":{"name":"Sociální sítě a komunikace","description":"Aplikace pro zasílání zpráv, správu kontaktů a sociální sítě"},"fr":{"name":"Social & communication","description":"Applications de messagerie, de gestion de contacts et de réseaux sociaux"},"ru":{"name":"Социальное и связь","description":"Общение, управление контактами и социальное медиа-приложение"},"en":{"name":"Social & communication","description":"Messaging, contact management and social media apps"}}},{"id":"tools","translations":{"nb":{"name":"Verktøy","description":"Alt annet"},"it":{"name":"Strumenti","description":"Tutto il resto"},"hu":{"name":"Eszközök","description":"Minden más"},"nl":{"name":"Tools","description":"De rest"},"de":{"name":"Werkzeuge","description":"Alles Andere"},"en":{"name":"Tools","description":"Everything else"},"cs":{"name":"Nástroje","description":"Vše ostatní"},"fr":{"name":"Outils","description":"Tout le reste"},"ru":{"name":"Приложения","description":"Что-то еще"}}}]', true)); - $this->assertSame($expected, $this->appSettingsController->listCategories()); + $this->assertEquals($expected, $this->appSettingsController->listCategories()); } public function testViewApps() { $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstore.experimental.enabled', false); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->navigationManager ->expects($this->once()) - ->method('setActiveEntry') - ->with('core_apps'); - - $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://apps.owncloud.com'); - - $expected = new TemplateResponse('settings', 'apps', ['experimentalEnabled' => false, 'category' => 'enabled', 'appstoreEnabled' => true], 'user'); - $expected->setContentSecurityPolicy($policy); - - $this->assertEquals($expected, $this->appSettingsController->viewApps()); - } - - public function testViewAppsNotEnabled() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstore.experimental.enabled', false); - $this->config - ->expects($this->at(1)) ->method('getSystemValue') ->with('appstoreenabled', true) ->will($this->returnValue(true)); @@ -259,21 +173,17 @@ class AppSettingsControllerTest extends TestCase { ->with('core_apps'); $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); - $expected = new TemplateResponse('settings', 'apps', ['experimentalEnabled' => false, 'category' => 'disabled', 'appstoreEnabled' => true], 'user'); + $expected = new TemplateResponse('settings', 'apps', ['category' => 'enabled', 'appstoreEnabled' => true], 'user'); $expected->setContentSecurityPolicy($policy); - $this->assertEquals($expected, $this->appSettingsController->viewApps('disabled')); + $this->assertEquals($expected, $this->appSettingsController->viewApps()); } public function testViewAppsAppstoreNotEnabled() { $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstore.experimental.enabled', false); - $this->config - ->expects($this->at(1)) + ->expects($this->once()) ->method('getSystemValue') ->with('appstoreenabled', true) ->will($this->returnValue(false)); @@ -283,9 +193,9 @@ class AppSettingsControllerTest extends TestCase { ->with('core_apps'); $policy = new ContentSecurityPolicy(); - $policy->addAllowedImageDomain('https://apps.owncloud.com'); + $policy->addAllowedImageDomain('https://usercontent.apps.nextcloud.com'); - $expected = new TemplateResponse('settings', 'apps', ['experimentalEnabled' => false, 'category' => 'enabled', 'appstoreEnabled' => false], 'user'); + $expected = new TemplateResponse('settings', 'apps', ['category' => 'enabled', 'appstoreEnabled' => false], 'user'); $expected->setContentSecurityPolicy($policy); $this->assertEquals($expected, $this->appSettingsController->viewApps()); diff --git a/tests/data/testapp.tar.gz b/tests/data/testapp.tar.gz Binary files differnew file mode 100644 index 00000000000..b9184c8c378 --- /dev/null +++ b/tests/data/testapp.tar.gz diff --git a/tests/data/testapp1.tar.gz b/tests/data/testapp1.tar.gz Binary files differnew file mode 100644 index 00000000000..f864edb2a6d --- /dev/null +++ b/tests/data/testapp1.tar.gz diff --git a/tests/enable_all.php b/tests/enable_all.php index afea5c89665..655597be7c8 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -10,7 +10,7 @@ require_once __DIR__.'/../lib/base.php'; function enableApp($app) { try { - OC_App::enable($app); + (new \OC_App())->enable($app); } catch (Exception $e) { echo $e; } diff --git a/tests/lib/App/AppStore/Fetcher/AppFetcherTest.php b/tests/lib/App/AppStore/Fetcher/AppFetcherTest.php new file mode 100644 index 00000000000..3b0418a7eba --- /dev/null +++ b/tests/lib/App/AppStore/Fetcher/AppFetcherTest.php @@ -0,0 +1,39 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\App\AppStore\Fetcher; + +use OC\App\AppStore\Fetcher\AppFetcher; + +class AppFetcherTest extends FetcherBase { + public function setUp() { + parent::setUp(); + $this->fileName = 'apps.json'; + $this->endpoint = 'https://apps.nextcloud.com/api/v1/platform/9.2.0/apps.json'; + + $this->fetcher = new AppFetcher( + $this->appData, + $this->clientService, + $this->timeFactory, + $this->config + ); + } +} diff --git a/tests/lib/App/AppStore/Fetcher/CategoryFetcherTest.php b/tests/lib/App/AppStore/Fetcher/CategoryFetcherTest.php new file mode 100644 index 00000000000..db4354119a0 --- /dev/null +++ b/tests/lib/App/AppStore/Fetcher/CategoryFetcherTest.php @@ -0,0 +1,38 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\App\AppStore\Fetcher; + +use OC\App\AppStore\Fetcher\CategoryFetcher; + +class CategoryFetcherTest extends FetcherBase { + public function setUp() { + parent::setUp(); + $this->fileName = 'categories.json'; + $this->endpoint = 'https://apps.nextcloud.com/api/v1/categories.json'; + + $this->fetcher = new CategoryFetcher( + $this->appData, + $this->clientService, + $this->timeFactory + ); + } +} diff --git a/tests/lib/App/AppStore/Fetcher/FetcherBase.php b/tests/lib/App/AppStore/Fetcher/FetcherBase.php new file mode 100644 index 00000000000..66df81f1b2e --- /dev/null +++ b/tests/lib/App/AppStore/Fetcher/FetcherBase.php @@ -0,0 +1,246 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\App\AppStore\Fetcher; + +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\Fetcher; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\IConfig; +use Test\TestCase; + +abstract class FetcherBase extends TestCase { + /** @var IAppData|\PHPUnit_Framework_MockObject_MockObject */ + protected $appData; + /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */ + protected $clientService; + /** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ + protected $timeFactory; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var Fetcher */ + protected $fetcher; + /** @var string */ + protected $fileName; + /** @var string */ + protected $endpoint; + + public function setUp() { + parent::setUp(); + $this->appData = $this->createMock(IAppData::class); + $this->clientService = $this->createMock(IClientService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->config = $this->createMock(IConfig::class); + } + + public function testGetWithAlreadyExistingFileAndUpToDateTimestamp() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('/') + ->willReturn($folder); + $folder + ->expects($this->once()) + ->method('getFile') + ->with($this->fileName) + ->willReturn($file); + $file + ->expects($this->once()) + ->method('getContent') + ->willReturn('{"timestamp":1200,"data":[{"id":"MyApp"}]}'); + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(1499); + + $expected = [ + [ + 'id' => 'MyApp', + ], + ]; + $this->assertSame($expected, $this->fetcher->get()); + } + + public function testGetWithNotExistingFileAndUpToDateTimestamp() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('/') + ->willReturn($folder); + $folder + ->expects($this->at(0)) + ->method('getFile') + ->with($this->fileName) + ->willThrowException(new NotFoundException()); + $folder + ->expects($this->at(1)) + ->method('newFile') + ->with($this->fileName) + ->willReturn($file); + $client = $this->createMock(IClient::class); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client + ->expects($this->once()) + ->method('get') + ->with($this->endpoint) + ->willReturn($response); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); + $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502}'; + $file + ->expects($this->at(0)) + ->method('putContent') + ->with($fileData); + $file + ->expects($this->at(1)) + ->method('getContent') + ->willReturn($fileData); + $this->timeFactory + ->expects($this->at(0)) + ->method('getTime') + ->willReturn(1502); + + $expected = [ + [ + 'id' => 'MyNewApp', + 'foo' => 'foo', + ], + [ + 'id' => 'bar', + ], + ]; + $this->assertSame($expected, $this->fetcher->get()); + } + + public function testGetWithAlreadyExistingFileAndOutdatedTimestamp() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('/') + ->willReturn($folder); + $folder + ->expects($this->once()) + ->method('getFile') + ->with($this->fileName) + ->willReturn($file); + $file + ->expects($this->at(0)) + ->method('getContent') + ->willReturn('{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}}'); + $this->timeFactory + ->expects($this->at(0)) + ->method('getTime') + ->willReturn(1501); + $client = $this->createMock(IClient::class); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client + ->expects($this->once()) + ->method('get') + ->with($this->endpoint) + ->willReturn($response); + $response + ->expects($this->once()) + ->method('getBody') + ->willReturn('[{"id":"MyNewApp", "foo": "foo"}, {"id":"bar"}]'); + $fileData = '{"data":[{"id":"MyNewApp","foo":"foo"},{"id":"bar"}],"timestamp":1502}'; + $file + ->expects($this->at(1)) + ->method('putContent') + ->with($fileData); + $file + ->expects($this->at(2)) + ->method('getContent') + ->willReturn($fileData); + $this->timeFactory + ->expects($this->at(1)) + ->method('getTime') + ->willReturn(1502); + + $expected = [ + [ + 'id' => 'MyNewApp', + 'foo' => 'foo', + ], + [ + 'id' => 'bar', + ], + ]; + $this->assertSame($expected, $this->fetcher->get()); + } + + public function testGetWithExceptionInClient() { + $folder = $this->createMock(ISimpleFolder::class); + $file = $this->createMock(ISimpleFile::class); + $this->appData + ->expects($this->once()) + ->method('getFolder') + ->with('/') + ->willReturn($folder); + $folder + ->expects($this->once()) + ->method('getFile') + ->with($this->fileName) + ->willReturn($file); + $file + ->expects($this->at(0)) + ->method('getContent') + ->willReturn('{"timestamp":1200,"data":{"MyApp":{"id":"MyApp"}}}'); + $this->timeFactory + ->expects($this->at(0)) + ->method('getTime') + ->willReturn(1501); + $client = $this->createMock(IClient::class); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $client + ->expects($this->once()) + ->method('get') + ->with($this->endpoint) + ->willThrowException(new \Exception()); + + $this->assertSame([], $this->fetcher->get()); + } +} diff --git a/tests/lib/App/AppStore/Version/VersionParserTest.php b/tests/lib/App/AppStore/Version/VersionParserTest.php new file mode 100644 index 00000000000..ebfa98ade39 --- /dev/null +++ b/tests/lib/App/AppStore/Version/VersionParserTest.php @@ -0,0 +1,99 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\App\AppStore\Version; + +use OC\App\AppStore\Version\Version; +use OC\App\AppStore\Version\VersionParser; +use Test\TestCase; + +class VersionParserTest extends TestCase { + /** @var VersionParser */ + private $versionParser; + + public function setUp() { + parent::setUp(); + $this->versionParser = new VersionParser(); + } + + /** + * @return array + */ + public function versionProvider() { + return [ + [ + '*', + new Version('', ''), + ], + [ + '<=8.1.2', + new Version('', '8.1.2'), + ], + [ + '<=9', + new Version('', '9'), + ], + [ + '>=9.3.2', + new Version('9.3.2', ''), + ], + [ + '>=8.1.2 <=9.3.2', + new Version('8.1.2', '9.3.2'), + ], + [ + '>=8.2 <=9.1', + new Version('8.2', '9.1'), + ], + [ + '>=9 <=11', + new Version('9', '11'), + ], + ]; + } + + /** + * @dataProvider versionProvider + * + * @param string $input + * @param Version $expected + */ + public function testGetVersion($input, + Version $expected) { + $this->assertEquals($expected, $this->versionParser->getVersion($input)); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Version cannot be parsed: BogusVersion + */ + public function testGetVersionException() { + $this->versionParser->getVersion('BogusVersion'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Version cannot be parsed: >=8.2 <=9.1a + */ + public function testGetVersionExceptionWithMultiple() { + $this->versionParser->getVersion('>=8.2 <=9.1a'); + } +} diff --git a/tests/lib/App/AppStore/Version/VersionTest.php b/tests/lib/App/AppStore/Version/VersionTest.php new file mode 100644 index 00000000000..969c96a57a8 --- /dev/null +++ b/tests/lib/App/AppStore/Version/VersionTest.php @@ -0,0 +1,37 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace Test\App\AppStore\Version; + +use OC\App\AppStore\Version\Version; +use Test\TestCase; + +class VersionTest extends TestCase { + public function testGetMinimumVersion() { + $version = new Version('9', '10'); + $this->assertSame('9', $version->getMinimumVersion()); + } + + public function testGetMaximumVersion() { + $version = new Version('9', '10'); + $this->assertSame('10', $version->getMaximumVersion()); + } +} diff --git a/tests/lib/App/DependencyAnalyzerTest.php b/tests/lib/App/DependencyAnalyzerTest.php index c41829b796b..fd44954eaf4 100644 --- a/tests/lib/App/DependencyAnalyzerTest.php +++ b/tests/lib/App/DependencyAnalyzerTest.php @@ -1,9 +1,10 @@ <?php - /** * @author Thomas Müller + * @author Lukas Reschke * @copyright 2014 Thomas Müller deepdiver@owncloud.com - * later. + * @copyright 2016 Lukas Reschke <lukas@statuscode.ch> + * * See the COPYING-README file. */ @@ -187,7 +188,7 @@ class DependencyAnalyzerTest extends TestCase { 'dependencies' => array() ); if (!is_null($oc)) { - $app['dependencies']['owncloud'] = $oc; + $app['dependencies'] = $oc; } $missing = $this->analyser->analyze($app); @@ -200,18 +201,216 @@ class DependencyAnalyzerTest extends TestCase { * @return array */ function providesOC() { - return array( + return [ // no version -> no missing dependency - array(array(), null), - array(array(), array('@attributes' => array('min-version' => '8', 'max-version' => '8'))), - array(array(), array('@attributes' => array('min-version' => '8.0', 'max-version' => '8.0'))), - array(array(), array('@attributes' => array('min-version' => '8.0.2', 'max-version' => '8.0.2'))), - array(array('Server version 8.0.3 or higher is required.'), array('@attributes' => array('min-version' => '8.0.3'))), - array(array('Server version 9 or higher is required.'), array('@attributes' => array('min-version' => '9'))), - array(array('Server version 10 or higher is required.'), array('@attributes' => array('min-version' => '9.1'))), - array(array('Server version 11 or higher is required.'), array('@attributes' => array('min-version' => '9.2'))), - [['Server version 8.0.1 or lower is required.'], ['@attributes' => ['max-version' => '8.0.1']]], - ); + [ + [], + null, + ], + [ + [], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '8', + 'max-version' => '8', + ], + ], + ], + ], + [ + [], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '8.0', + 'max-version' => '8.0', + ], + ], + ], + ], + [ + [], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '8.0.2', + 'max-version' => '8.0.2' + ], + ], + ], + ], + [ + [ + 'Server version 8.0.3 or higher is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '8.0.3' + ], + ], + ], + ], + [ + [ + 'Server version 9 or higher is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9' + ], + ], + ], + ], + [ + [ + 'Server version 10 or higher is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '10' + ], + ], + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9' + ], + ], + ], + ], + [ + [ + 'Server version 10 or higher is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + ], + ], + ], + ], + [ + [ + 'Server version 11 or higher is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.2', + ], + ], + ], + ], + [ + [ + 'Server version 8.0.1 or lower is required.', + ], + [ + 'nextcloud' => [ + '@attributes' => [ + 'max-version' => '8.0.1', + ], + ], + ], + ], + [ + [], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '8', + 'max-version' => '8', + ], + ], + ], + ], + [ + [], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '8.0', + 'max-version' => '8.0', + ], + ], + ], + ], + [ + [], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '8.0.2', + 'max-version' => '8.0.2' + ], + ], + ], + ], + [ + [ + 'Server version 8.0.3 or higher is required.', + ], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '8.0.3' + ], + ], + ], + ], + [ + [ + 'Server version 9 or higher is required.', + ], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9' + ], + ], + ], + ], + [ + [ + 'Server version 10 or higher is required.', + ], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + ], + ], + ], + ], + [ + [ + 'Server version 11 or higher is required.', + ], + [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9.2', + ], + ], + ], + ], + [ + [ + 'Server version 8.0.1 or lower is required.', + ], + [ + 'owncloud' => [ + '@attributes' => [ + 'max-version' => '8.0.1', + ], + ], + ], + ], + ]; } /** diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index b7263adb78b..971d86cf6a4 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -264,6 +264,40 @@ class AppTest extends \Test\TestCase { ), true ), + [ + '9.2.0.0', + [ + 'dependencies' => [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9.0', + 'max-version' => '9.1', + ], + ], + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + 'max-version' => '9.2', + ], + ], + ], + ], + true + ], + [ + '9.2.0.0', + [ + 'dependencies' => [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + 'max-version' => '9.2', + ], + ], + ], + ], + true + ], ); } diff --git a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php index d71d9468477..418a4d14f62 100644 --- a/tests/lib/Authentication/Token/DefaultTokenMapperTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenMapperTest.php @@ -130,6 +130,7 @@ class DefaultTokenMapperTest extends TestCase { $token->setName('Firefox on Android'); $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); $token->setLastActivity($this->time - 60 * 60 * 24 * 3); $token->setLastCheck($this->time - 10); diff --git a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php index 7f90cf051f4..5e4d4f94366 100644 --- a/tests/lib/Authentication/Token/DefaultTokenProviderTest.php +++ b/tests/lib/Authentication/Token/DefaultTokenProviderTest.php @@ -1,8 +1,8 @@ <?php - /** * @author Christoph Wurst <christoph@owncloud.com> * + * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> * @copyright Copyright (c) 2016, ownCloud, Inc. * @license AGPL-3.0 * @@ -25,7 +25,7 @@ namespace Test\Authentication\Token; use OC\Authentication\Token\DefaultToken; use OC\Authentication\Token\DefaultTokenProvider; use OC\Authentication\Token\IToken; -use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Mapper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\ILogger; @@ -35,13 +35,19 @@ use Test\TestCase; class DefaultTokenProviderTest extends TestCase { - /** @var DefaultTokenProvider */ + /** @var DefaultTokenProvider|\PHPUnit_Framework_MockObject_MockObject */ private $tokenProvider; + /** @var Mapper|\PHPUnit_Framework_MockObject_MockObject */ private $mapper; + /** @var ICrypto|\PHPUnit_Framework_MockObject_MockObject */ private $crypto; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ private $config; + /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ private $logger; + /** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ private $timeFactory; + /** @var int */ private $time; protected function setUp() { @@ -81,6 +87,7 @@ class DefaultTokenProviderTest extends TestCase { $toInsert->setName($name); $toInsert->setToken(hash('sha512', $token . '1f4h9s')); $toInsert->setType($type); + $toInsert->setRemember(IToken::DO_NOT_REMEMBER); $toInsert->setLastActivity($this->time); $this->config->expects($this->any()) @@ -95,7 +102,7 @@ class DefaultTokenProviderTest extends TestCase { ->method('insert') ->with($this->equalTo($toInsert)); - $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type); + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); $this->assertEquals($toInsert, $actual); } @@ -245,15 +252,128 @@ class DefaultTokenProviderTest extends TestCase { public function testInvalidateOldTokens() { $defaultSessionLifetime = 60 * 60 * 24; - $this->config->expects($this->once()) + $defaultRememberMeLifetime = 60 * 60 * 24 * 15; + $this->config->expects($this->exactly(2)) ->method('getSystemValue') - ->with('session_lifetime', $defaultSessionLifetime) - ->will($this->returnValue(150)); - $this->mapper->expects($this->once()) + ->will($this->returnValueMap([ + ['session_lifetime', $defaultSessionLifetime, 150], + ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300], + ])); + $this->mapper->expects($this->at(0)) ->method('invalidateOld') ->with($this->time - 150); + $this->mapper->expects($this->at(1)) + ->method('invalidateOld') + ->with($this->time - 300); $this->tokenProvider->invalidateOldTokens(); } + public function testRenewSessionTokenWithoutPassword() { + $token = $this->getMockBuilder(DefaultToken::class) + ->disableOriginalConstructor() + ->setMethods(['getUID', 'getLoginName', 'getPassword', 'getName']) + ->getMock(); + $token + ->expects($this->at(0)) + ->method('getUID') + ->willReturn('UserUid'); + $token + ->expects($this->at(1)) + ->method('getLoginName') + ->willReturn('UserLoginName'); + $token + ->expects($this->at(2)) + ->method('getPassword') + ->willReturn(null); + $token + ->expects($this->at(3)) + ->method('getName') + ->willReturn('MyTokenName'); + $this->config + ->expects($this->exactly(2)) + ->method('getSystemValue') + ->with('secret') + ->willReturn('MyInstanceSecret'); + $this->mapper + ->expects($this->at(0)) + ->method('getToken') + ->with(hash('sha512', 'oldId' . 'MyInstanceSecret')) + ->willReturn($token); + $newToken = new DefaultToken(); + $newToken->setUid('UserUid'); + $newToken->setLoginName('UserLoginName'); + $newToken->setName('MyTokenName'); + $newToken->setToken(hash('sha512', 'newId' . 'MyInstanceSecret')); + $newToken->setType(IToken::TEMPORARY_TOKEN); + $newToken->setLastActivity(1313131); + $this->mapper + ->expects($this->at(1)) + ->method('insert') + ->with($newToken); + + $this->tokenProvider->renewSessionToken('oldId', 'newId'); + } + + public function testRenewSessionTokenWithPassword() { + $token = $this->getMockBuilder(DefaultToken::class) + ->disableOriginalConstructor() + ->setMethods(['getUID', 'getLoginName', 'getPassword', 'getName']) + ->getMock(); + $token + ->expects($this->at(0)) + ->method('getUID') + ->willReturn('UserUid'); + $token + ->expects($this->at(1)) + ->method('getLoginName') + ->willReturn('UserLoginName'); + $token + ->expects($this->at(2)) + ->method('getPassword') + ->willReturn('EncryptedPassword'); + $token + ->expects($this->at(3)) + ->method('getPassword') + ->willReturn('EncryptedPassword'); + $token + ->expects($this->at(4)) + ->method('getName') + ->willReturn('MyTokenName'); + $this->crypto + ->expects($this->any(0)) + ->method('decrypt') + ->with('EncryptedPassword', 'oldIdMyInstanceSecret') + ->willReturn('ClearTextPassword'); + $this->crypto + ->expects($this->any(1)) + ->method('encrypt') + ->with('ClearTextPassword', 'newIdMyInstanceSecret') + ->willReturn('EncryptedPassword'); + $this->config + ->expects($this->exactly(4)) + ->method('getSystemValue') + ->with('secret') + ->willReturn('MyInstanceSecret'); + $this->mapper + ->expects($this->at(0)) + ->method('getToken') + ->with(hash('sha512', 'oldId' . 'MyInstanceSecret')) + ->willReturn($token); + $newToken = new DefaultToken(); + $newToken->setUid('UserUid'); + $newToken->setLoginName('UserLoginName'); + $newToken->setName('MyTokenName'); + $newToken->setToken(hash('sha512', 'newId' . 'MyInstanceSecret')); + $newToken->setType(IToken::TEMPORARY_TOKEN); + $newToken->setLastActivity(1313131); + $newToken->setPassword('EncryptedPassword'); + $this->mapper + ->expects($this->at(1)) + ->method('insert') + ->with($newToken); + + $this->tokenProvider->renewSessionToken('oldId', 'newId'); + } + } diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php index 72b70d817d2..52f3ca28500 100644 --- a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php +++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php @@ -233,8 +233,15 @@ class ManagerTest extends TestCase { ->with($this->user, $challenge) ->will($this->returnValue(true)); $this->session->expects($this->once()) + ->method('get') + ->with('two_factor_remember_login') + ->will($this->returnValue(false)); + $this->session->expects($this->at(1)) ->method('remove') ->with('two_factor_auth_uid'); + $this->session->expects($this->at(2)) + ->method('remove') + ->with('two_factor_remember_login'); $this->assertTrue($this->manager->verifyChallenge('email', $this->user, $challenge)); } @@ -304,11 +311,29 @@ class ManagerTest extends TestCase { ->method('getUID') ->will($this->returnValue('ferdinand')); - $this->session->expects($this->once()) + $this->session->expects($this->at(0)) + ->method('set') + ->with('two_factor_auth_uid', 'ferdinand'); + $this->session->expects($this->at(1)) + ->method('set') + ->with('two_factor_remember_login', true); + + $this->manager->prepareTwoFactorLogin($this->user, true); + } + + public function testPrepareTwoFactorLoginDontRemember() { + $this->user->expects($this->once()) + ->method('getUID') + ->will($this->returnValue('ferdinand')); + + $this->session->expects($this->at(0)) ->method('set') ->with('two_factor_auth_uid', 'ferdinand'); + $this->session->expects($this->at(1)) + ->method('set') + ->with('two_factor_remember_login', false); - $this->manager->prepareTwoFactorLogin($this->user); + $this->manager->prepareTwoFactorLogin($this->user, false); } } diff --git a/tests/lib/InstallerTest.php b/tests/lib/InstallerTest.php index e1c17b841a2..1212d3d7559 100644 --- a/tests/lib/InstallerTest.php +++ b/tests/lib/InstallerTest.php @@ -9,91 +9,572 @@ namespace Test; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\Archive\ZIP; use OC\Installer; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\ILogger; +use OCP\ITempManager; class InstallerTest extends TestCase { private static $appid = 'testapp'; private $appstore; + /** @var AppFetcher|\PHPUnit_Framework_MockObject_MockObject */ + private $appFetcher; + /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */ + private $clientService; + /** @var ITempManager|\PHPUnit_Framework_MockObject_MockObject */ + private $tempManager; + /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ + private $logger; + + /** @var Installer */ + private $installer; protected function setUp() { parent::setUp(); + $this->appFetcher = $this->createMock(AppFetcher::class); + $this->clientService = $this->createMock(IClientService::class); + $this->tempManager = $this->createMock(ITempManager::class); + $this->logger = $this->createMock(ILogger::class); + $this->installer = new Installer( + $this->appFetcher, + $this->clientService, + $this->tempManager, + $this->logger + ); + $config = \OC::$server->getConfig(); $this->appstore = $config->setSystemValue('appstoreenabled', true); $config->setSystemValue('appstoreenabled', true); - Installer::removeApp(self::$appid); + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + $installer->removeApp(self::$appid); } protected function tearDown() { - Installer::removeApp(self::$appid); + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + $installer->removeApp(self::$appid); \OC::$server->getConfig()->setSystemValue('appstoreenabled', $this->appstore); parent::tearDown(); } public function testInstallApp() { - $pathOfTestApp = __DIR__; - $pathOfTestApp .= '/../data/'; - $pathOfTestApp .= 'testapp.zip'; - - $tmp = \OC::$server->getTempManager()->getTemporaryFile('.zip'); - \OC_Helper::copyr($pathOfTestApp, $tmp); - - $data = array( - 'path' => $tmp, - 'source' => 'path', - 'appdata' => [ - 'id' => 'Bar', - 'level' => 100, - ] - ); + // Extract app + $pathOfTestApp = __DIR__ . '/../data/testapp.zip'; + $tar = new ZIP($pathOfTestApp); + $tar->extract(\OC_App::getInstallPath()); - Installer::installApp($data); + // Install app + $installer = new Installer( + \OC::$server->getAppFetcher(), + \OC::$server->getHTTPClientService(), + \OC::$server->getTempManager(), + \OC::$server->getLogger() + ); + $installer->installApp(self::$appid); $isInstalled = Installer::isInstalled(self::$appid); - $this->assertTrue($isInstalled); + $installer->removeApp(self::$appid); } - public function testUpdateApp() { - $pathOfOldTestApp = __DIR__; - $pathOfOldTestApp .= '/../data/'; - $pathOfOldTestApp .= 'testapp.zip'; - - $oldTmp = \OC::$server->getTempManager()->getTemporaryFile('.zip'); - \OC_Helper::copyr($pathOfOldTestApp, $oldTmp); - - $oldData = array( - 'path' => $oldTmp, - 'source' => 'path', - 'appdata' => [ - 'id' => 'Bar', - 'level' => 100, - ] - ); + public function updateArrayProvider() { + return [ + // Update available + [ + [ + [ + 'id' => 'files', + 'releases' => [ + [ + 'version' => '1111.0' + ], + ], + ], + ], + '1111.0', + ], + // No update available + [ + [ + [ + 'id' => 'files', + 'releases' => [ + [ + 'version' => '1.0' + ], + ], + ], + ], + false, + ], + ]; + } - $pathOfNewTestApp = __DIR__; - $pathOfNewTestApp .= '/../data/'; - $pathOfNewTestApp .= 'testapp2.zip'; + /** + * @dataProvider updateArrayProvider + * @param array $appArray + * @param string|bool $updateAvailable + */ + public function testIsUpdateAvailable(array $appArray, $updateAvailable) { + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); - $newTmp = \OC::$server->getTempManager()->getTemporaryFile('.zip'); - \OC_Helper::copyr($pathOfNewTestApp, $newTmp); + $this->assertSame($updateAvailable, Installer::isUpdateAvailable('files', $this->appFetcher)); + } - $newData = array( - 'path' => $newTmp, - 'source' => 'path', - 'appdata' => [ - 'id' => 'Bar', - 'level' => 100, - ] - ); + /** + * @expectedException \Exception + * @expectedExceptionMessage Certificate "4112" has been revoked + */ + public function testDownloadAppWithRevokedCertificate() { + $appArray = [ + [ + 'id' => 'news', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAQMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDAzMTMyNDM3WhcNMjcwMTA5MTMyNDM3WjASMRAwDgYD +VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApEt+ +KZGs+WqdZkHZflzqk+ophYWB8qB47XCzy+xdTGPFM84/9wXltRPbcQQWJJl5aOx0 +FPbsyTGhIt/IYZ2Vl0XrDRJjsaxzPcrofrwpJ2tqforXjGohl6mZUBA0ESzFiPzT +SAZe8E14+Jk8rbF/ecrkqcWf2cTMV3Qfu9YvJo8WVs4lHc95r1F+Nalh/OLkHkzb +fYPno2Z5cco6U7BXunFQG2gqy3wWQwmlhDxh5fwrCoFzPWm7WhwSyK+eMoSDz+Vp +3kmtyijsqnda0zA9bfNzgW26czbJaObbnkdtDC2nfoAWXndlS/5YRI8yHd9miB5C +u1OC8LUWToDGNa9+FOxBSj7Nk6iyjbVfRXcTqThdkVZdOOPaBRMsL9R4UYywCbhA +yGNiQ0ahfXD8MZSb08rlQg8tAtcUZW1sYQcbtMGnu8OyC5J7N1efzv5mys4+9hBS +5ECeyCuQTuOkF4H/XS2BMSFZWF2xh7wzhMLca+5yauDW4i8baFEv74QTeY1DADgI +Lz29NJ6z9xYzEnPesjNrwIcJwIjV52EkdLTi+EIf83UjXLQdwDbLxu76qxqP7K0I +oMmwbl7UNA0wzq7nmgRhvqhow5RoCaSJjTz0EYQVSa1xelwiKeJiSKj2G9Mgt5Ms +Miuy3C3VAGvQJ2ocILPGOt54oVeNRFLpnCo1e3sCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAkGYtg21rGpUVT/AokGUfI0PeyYAkcXKy2yuBAzfRk+uIXnRR0vK+OMpx +shBoYGR3JEGUHZcMTRh8wjAZ0wuyYlQONtJbFFF3bCfODXxCsw0Vm8/Ms+KCmE4Z +SyQafWEQf1sdqNw4VS4DYS2mlpDgAl+U9UY6HQKuT3+GFIxCsQSdS0GTaiYVKPVE +p/eKou739h+5dM4FEhIYZX+7PWlHmX6wPCFAjgNu3kiRGmF6LKmCNNXTySATEP86 +tczQMzLtVdTg5z8XMi//6TkAPxRPjYi8Vef/s2mLo7KystTmofxI/HZePSieJ9tj +gLgK8d8sKL60JMmKHN3boHrsThKBVA== +-----END CERTIFICATE-----', + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + + + $this->installer->downloadApp('news'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage App with id news has a certificate not issued by a trusted Code Signing Authority + */ + public function testDownloadAppWithNotNextcloudCertificate() { + $appArray = [ + [ + 'id' => 'news', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIID8TCCAdkCAhAAMA0GCSqGSIb3DQEBCwUAMG0xCzAJBgNVBAYTAlVTMQ8wDQYD +VQQIDAZCb3N0b24xFjAUBgNVBAoMDW93bkNsb3VkIEluYy4xNTAzBgNVBAMMLG93 +bkNsb3VkIENvZGUgU2lnbmluZyBJbnRlcm1lZGlhdGUgQXV0aG9yaXR5MB4XDTE2 +MDIwMzE3NTE0OVoXDTI2MDEzMTE3NTE0OVowDzENMAsGA1UEAwwEY29yZTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPHdSljnHI+ueQd27UyWPO9n4Lqt +bK0kdekiC3si7Mee7uXXJaGuqXJozHEZYB1LIFLdCU/itCxEk9hyLcyNzeT+nRT/ +zDuOYdbLgCj7/A5bX+u3jc29UlCYybSFchfMdvn7a0njCna4dE+73b4yEj16tS2h +S1EUygSzgicWlJqMD3Z9Qc+zLEpdhq9oDdDB8HURi2NW4KzIraVncSH+zF1QduOh +nERDnF8x48D3FLdTxGA0W/Kg4gYsq4NRvU6g3DJNdp4YfqRSFMmLFDCgzDuhan7D +wgRlI9NAeHbnyoUPtrDBUceI7shIbC/i87xk9ptqV0AyFonkJtK6lWwZjNkCAwEA +ATANBgkqhkiG9w0BAQsFAAOCAgEAAMgymqZE1YaHYlRGwvTE7gGDY3gmFOMaxQL4 +E5m0CnkBz4BdIPRsQFFdOv3l/MIWkw5ED3vUB925VpQZYFSiEuv5NbnlPaHZlIMI +n8AV/sTP5jue3LhtAN4EM63xNBhudAT6wVsvGwOuQOx9Xv+ptO8Po7sTuNYP0CMH +EOQN+/q8tYlSm2VW+dAlaJ+zVZwZldhVjL+lSH4E9ktWn3PmgNQeKfcnJISUbus6 +ZtsYDF/X96/Z2ZQvMXOKksgvU6XlvIxllcyebC9Bxe/h0D63GCO2tqN5CWQzIIqn +apUynPX8BlLaaExqYGERwlUi/yOGaUVPUjEPVehviOQYgAqxlrkJk1dWeCrwUori +CXpi+IUYkidfgiJ9F88M3ElpwqIaXp7G3/4oHBuE2u6M+L+1/vqPJeTCAWUxxpJE +yYmM+db6D4TySFpQPENNzPS8bpR6T8w2hRumkldC42HrnyJJbpjOieTXhXzjdPvZ +IEP9JGtkhB2du6nBF2MNAq2TqRXpcfQrQEbnQ13aV9bl+roTwwO+SOWK/wgvdOMI +STQ0Xk0sTGlmQjPYPkibVceaWMR3sX4cNt5c33YhJys5jxHoAh42km4nN9tfykR5 +crl5lBlKjXh2GP0+omSO3x1jX4+iQPCW2TWoyKkUdLu/hGHG2w8RrTeme+kATECH +YSu356M= +-----END CERTIFICATE-----', + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + + $this->installer->downloadApp('news'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage App with id news has a cert issued to passman + */ + public function testDownloadAppWithDifferentCN() { + $appArray = [ + [ + 'id' => 'news', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAYMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDE5MTkzNTEyWhcNMjcwMTI1MTkzNTEyWjASMRAwDgYD +VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Jw1 +8F0DefogaLaBudGbhK2zcFIBSzxhh7dRWguZKHGE+rG00BOvFLIAo37Bfmy9WKLc +3BFYvuFBowaVdaFOLxQJod0sOTmVMXhwoY5e3Xx+P+nsAw1/0gI10/LD1Vgl6i1u +gMocmnbEYhKwr0NbdiQiMI9UB9Ge/51wt4WtAxwK7yJFl3+5qzvJgfX75Wt+8L1e +Wk0LpVW23tUueJovjYZJXyAtohNaV3gwiST+QmKljCd4gwGX9abqfc76/lWtS+hI +rKptuICc55ffH30rqVhAgCMouF/Ml5Qru8tDen5dSNtmAXz89OlDNisP+9HL4WDZ +wvgps0mm/OYAUAQln24uXPDmAX/H2P5xIDHAa8avsqdgmHiqnLr4GYD8JYeb8GmB +zZ38hEMjCr2F1k1h9T1+SyfRiDPDqqv1mBtcvNVc1JmZvSikMxhtQbU0C4/o2SBG +RPCirknfPeKu8wBi6gvH4/SK0XTyuM8H58b9AKxzoo/wLbQ668+faLYyMSzCvsZD +eeZkiO85y87Ax57WRY93arccCMaUeks/cTriNw3JrvdDyb2SeQOX9JUp0orUlC64 +AzK2xhXCpmkprVBGizT5g3brrknX6VDX1gXFAmH/daCRJAIHPX0S/ol0z9w/hCEl +CpbiJPEphGtxqz4SfMv6IrIfneuDDKbF+w5MV/sCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAUKj+/GpnMn+0/u9SPHTNmX3U3Y/ldmud0CsU5ELzMf/3YPbC/qWziRik +ewM2WyG8cwT9ayt9DxWGfu/zLv+ddyl8Wje1e/FIkRKXK0WW6OMz3e8Y45ONzpmu +8ME75IpnMuZEqE/WayRg27dQT5QNnEe/uNLd4m9BfsQcHIx3OfHCu5Of6/BclgsJ +VWp31zY8kcT0QN1GQxfB3eXnMyELneKCP3OH9DBhr4FUFb0vRHc8/1rdADFvSsdX +hNm8iRq+s2n0F6OGBofYT8ZyCnDUSQAoKMTIHcz+dDGyP4BfPY5w0ZGUfuaYATvm +cR92p/PYCFXkAKP3OO0RPlf6dXNKTw== +-----END CERTIFICATE-----', + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + + $this->installer->downloadApp('news'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage App with id passman has invalid signature + */ + public function testDownloadAppWithInvalidSignature() { + $appArray = [ + [ + 'id' => 'passman', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAYMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDE5MTkzNTEyWhcNMjcwMTI1MTkzNTEyWjASMRAwDgYD +VQQDDAdwYXNzbWFuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Jw1 +8F0DefogaLaBudGbhK2zcFIBSzxhh7dRWguZKHGE+rG00BOvFLIAo37Bfmy9WKLc +3BFYvuFBowaVdaFOLxQJod0sOTmVMXhwoY5e3Xx+P+nsAw1/0gI10/LD1Vgl6i1u +gMocmnbEYhKwr0NbdiQiMI9UB9Ge/51wt4WtAxwK7yJFl3+5qzvJgfX75Wt+8L1e +Wk0LpVW23tUueJovjYZJXyAtohNaV3gwiST+QmKljCd4gwGX9abqfc76/lWtS+hI +rKptuICc55ffH30rqVhAgCMouF/Ml5Qru8tDen5dSNtmAXz89OlDNisP+9HL4WDZ +wvgps0mm/OYAUAQln24uXPDmAX/H2P5xIDHAa8avsqdgmHiqnLr4GYD8JYeb8GmB +zZ38hEMjCr2F1k1h9T1+SyfRiDPDqqv1mBtcvNVc1JmZvSikMxhtQbU0C4/o2SBG +RPCirknfPeKu8wBi6gvH4/SK0XTyuM8H58b9AKxzoo/wLbQ668+faLYyMSzCvsZD +eeZkiO85y87Ax57WRY93arccCMaUeks/cTriNw3JrvdDyb2SeQOX9JUp0orUlC64 +AzK2xhXCpmkprVBGizT5g3brrknX6VDX1gXFAmH/daCRJAIHPX0S/ol0z9w/hCEl +CpbiJPEphGtxqz4SfMv6IrIfneuDDKbF+w5MV/sCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAUKj+/GpnMn+0/u9SPHTNmX3U3Y/ldmud0CsU5ELzMf/3YPbC/qWziRik +ewM2WyG8cwT9ayt9DxWGfu/zLv+ddyl8Wje1e/FIkRKXK0WW6OMz3e8Y45ONzpmu +8ME75IpnMuZEqE/WayRg27dQT5QNnEe/uNLd4m9BfsQcHIx3OfHCu5Of6/BclgsJ +VWp31zY8kcT0QN1GQxfB3eXnMyELneKCP3OH9DBhr4FUFb0vRHc8/1rdADFvSsdX +hNm8iRq+s2n0F6OGBofYT8ZyCnDUSQAoKMTIHcz+dDGyP4BfPY5w0ZGUfuaYATvm +cR92p/PYCFXkAKP3OO0RPlf6dXNKTw== +-----END CERTIFICATE-----', + 'releases' => [ + [ + 'download' => 'https://example.com', + 'signature' => 'MySignature', + ], + [ + 'download' => 'https://nextcloud.com', + ], + ], + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + $realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz'); + copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile); + $this->tempManager + ->expects($this->at(0)) + ->method('getTemporaryFile') + ->with('.tar.gz') + ->willReturn($realTmpFile); + $client = $this->createMock(IClient::class); + $client + ->expects($this->once()) + ->method('get') + ->with('https://example.com', ['save_to' => $realTmpFile]); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $this->installer->downloadApp('passman'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Extracted app testapp has more than 1 folder + */ + public function testDownloadAppWithMoreThanOneFolderDownloaded() { + $appArray = [ + [ + 'id' => 'testapp', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD +VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x +FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j +ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe +svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr +l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 +6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ +me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 +KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ +l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z +yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 +dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg +gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj +nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G +z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx +89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ +Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 +cUg0V1uAxqUvKytKkMfcyPWsz/AINA== +-----END CERTIFICATE-----', + 'releases' => [ + [ + 'download' => 'https://example.com', + 'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz +VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ +jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt +Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU +bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg +ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB +D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC +oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a +GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67 +0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/ +YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=', + ], + [ + 'download' => 'https://nextcloud.com', + ], + ], + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + $realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz'); + copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile); + $this->tempManager + ->expects($this->at(0)) + ->method('getTemporaryFile') + ->with('.tar.gz') + ->willReturn($realTmpFile); + $realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); + mkdir($realTmpFolder . '/testfolder'); + $this->tempManager + ->expects($this->at(1)) + ->method('getTemporaryFolder') + ->willReturn($realTmpFolder); + $client = $this->createMock(IClient::class); + $client + ->expects($this->once()) + ->method('get') + ->with('https://example.com', ['save_to' => $realTmpFile]); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $this->installer->downloadApp('testapp'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage App for id testapp has a wrong app ID in info.xml: testapp1 + */ + public function testDownloadAppWithMismatchingIdentifier() { + $appArray = [ + [ + 'id' => 'testapp', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD +VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x +FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j +ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe +svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr +l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 +6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ +me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 +KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ +l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z +yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 +dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg +gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj +nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G +z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx +89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ +Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 +cUg0V1uAxqUvKytKkMfcyPWsz/AINA== +-----END CERTIFICATE-----', + 'releases' => [ + [ + 'download' => 'https://example.com', + 'signature' => 'h8H3tUy2dDlwrV/hY/ZxqYqe8Vue+IINluLtAt1HxX2cjz3vdoVHJRINRkMYYcdz +VlndvHyKdqJHDAACphR8tVV6EFrPermn7gEgWk7a51LbUM7sAN7RV7ijEooUo+TQ +jNW9Ch48Wg3jvebMwWNr5t5U4MEXTP5f0YX/kxvkJoUrG3a3spt7ziEuHaq8IPvt +Jj/JSDFhvRNpom7yNNcI1Ijoq8yC11sg7RJBNfrHdGPHPZVz2SyBiY9OcvgGSpUU +bfvzhIZDCl/RRi5fs39jLLupAP69Ez6+jylNXEMsNwM0YL5+egSXFtkCvgOw8UBg +ZqNZZojcS22acuvHRnoa6PDDhwHdCH+zpifXSOhSQvue5n6q+FVX6aeD1LnCQkYB +D2wvNyZWwdADJtvDj03DKhm21g+TPy63XC94q4IqvjQ94pV8U+qrBBfkQ62NGjaC +oOU6y5sEmQeAdVRpWVo0Hewmjp4Adoj5JRwuqCVEynTC6DXHs3HvHxYlmib1F05a +GqEhdDmOHsxNaeJ08Hlptq5yLv3+0wEdtriVjgAZNVduHG1F1FkhPIrDHaB6pd67 +0AFvO/pZgMSHDRHD+safBgaLb5dBZ895Qvudbq3RQevVnO+YZQYZkpmjoF/+TQ7/ +YwDVP+QmNRzx72jtqAN/Kc3CvQ9nkgYhU65B95aX0xA=', + ], + [ + 'download' => 'https://nextcloud.com', + ], + ], + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + $realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz'); + copy(__DIR__ . '/../data/testapp1.tar.gz', $realTmpFile); + $this->tempManager + ->expects($this->at(0)) + ->method('getTemporaryFile') + ->with('.tar.gz') + ->willReturn($realTmpFile); + $realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); + $this->tempManager + ->expects($this->at(1)) + ->method('getTemporaryFolder') + ->willReturn($realTmpFolder); + $client = $this->createMock(IClient::class); + $client + ->expects($this->once()) + ->method('get') + ->with('https://example.com', ['save_to' => $realTmpFile]); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $this->installer->downloadApp('testapp'); + } - Installer::installApp($oldData); - $oldVersionNumber = \OC_App::getAppVersion(self::$appid); + public function testDownloadAppSuccessful() { + $appArray = [ + [ + 'id' => 'testapp', + 'certificate' => '-----BEGIN CERTIFICATE----- +MIIEAjCCAuoCAhAbMA0GCSqGSIb3DQEBCwUAMHsxCzAJBgNVBAYTAkRFMRswGQYD +VQQIDBJCYWRlbi1XdWVydHRlbWJlcmcxFzAVBgNVBAoMDk5leHRjbG91ZCBHbWJI +MTYwNAYDVQQDDC1OZXh0Y2xvdWQgQ29kZSBTaWduaW5nIEludGVybWVkaWF0ZSBB +dXRob3JpdHkwHhcNMTYxMDMxMTgxNTI2WhcNMjcwMjA2MTgxNTI2WjASMRAwDgYD +VQQDEwd0ZXN0YXBwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqa0x +FcVa0YcO/ABqSNdbf7Bzp2PBBJzVM9gI4/HzzBKU/NY9/RibBBpNjAIWEFAbTI4j +ilFSoxHDQ8HrboFOeKCrOIdp9ATQ8SnYVNIQ12Ym3LA/XxcG0gG0H7DeS9C0uACe +svN8fwD1wnKnLLU9GBzO77jwYkneed85wwKG4waHd3965gxQWq0N5gnYS0TTn7Yr +l1veRiw+ryefXvfWI0cN1WBZJ/4XAkwVlpG1HP60AunIpcwn9bfG4XCka+7x26E4 +6Hw0Ot7D7j0yzVzimJDPB2h2buEtPVd6m+oNPueVvKGta+p6cEEaHlFVh2Pa9DI+ +me3nb6aXE2kABWXav3BmK18A5Rg4ZY4VFYvmHmxkOhT/ulGZRqy6TccL/optqs52 +KQ6P0e5dfmhLeoCvJObD+ZYKv+kJCRFtX1Hve/R4IHG6XSFKUfrRjyor9b6TX2L/ +l2vV0mFjmy4g3l05vWHg1Edtq7M29S/xNA3/hF29NjBq6NoMbLGcBtFced1iK07Z +yHLjXRZRfURP671Svqqg8pjxuDqkJ2vIj/Vpod4kF2jeiZYXcfmNKhEhxpkccSe0 +dI6p76Ne7XSUpf8yCPiSnWZLadqKZdEulcB4SlrZO2+/pycgqrqihofDrvDeWeeg +gQyvbZZKl4ylRNj6IRKnosKLVXNqMHQxLmxLHeUCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEALkKQwa40HfuP4Q6ShwBFJbXLyodIAXCT014kBVjReDKNl5oHtMXRjPxj +nj9doKu+3bLNuLCv9uU3H5+t/GFogReV3Av3z/fCqJ6wHv/KX+lacj31dWXZGD8G +z+RYibrxKkPN0V6q1mSvkg3hJOOE+/4FPIdc8PNlgratv3WS4dT8QwGSUavHW2Kx +89nIdnwtLEFpgML/bTG0dm8BH57xER8LCYixW1VmpV6A4IsoKVsnB7KUCRTK3iUJ +Zh8Xg8UMNrOtXc1Wx1Wmjaa4ZE9dY6/KkU2ny2UWyDHKU/9VE8QQ4HN93gxU4+H7 +cUg0V1uAxqUvKytKkMfcyPWsz/AINA== +-----END CERTIFICATE-----', + 'releases' => [ + [ + 'download' => 'https://example.com', + 'signature' => 'O5UWFRnSx4mSdEX83Uh9u7KW+Gl1OWU4uaFg6aYY19zc+lWP4rKCbAUH7Jo1Bohf +qxQbhXs4cMqGmoL8dW4zeFUqSJCRk52LA+ciLezjPFv275q+BxEgyWOylLnbhBaz ++v6lXLaeG0J/ry8wEdg+rwP8FCYPsvKlXSVbFjgubvCR/owKJJf5iL0B93noBwBN +jfbcxi7Kh16HAKy6f/gVZ6hf/4Uo7iEFMCPEHjidope+ejUpqbd8XhQg5/yh7TQ7 +VKR7pkdDG2eFr5c3CpaECdNg5ZIGRbQNJHBXHT/wliorWpYJtwtNAQJ4xC635gLP +4klkKN4XtSj8bJUaJC6aaksLFgRSeKXaYAHai/XP6BkeyNzlSbsmyZk8cZbySx8F +gVOzPok1c94UGT57FjeW5eqRjtmzbYivQdP89Ouz6et7PY69yOCqiRFQanrqzwoX +MPLX6f5V9tCJtlH6ztmEcDROfvuVc0U3rEhqx2hphoyo+MZrPFpdcJL8KkIdMKbY +7yQWrsV7QvAzygAOFsC0TlSNJbmMCljouUk9di4CUZ+xsQ6n6TZtE7gsdljlKjPS +3Ys+e3V1HUaVzv8SaSmKwjRoQxQxHWLtXpJS2Yq+i+gq7LuC+aStzxAzV/h2plDW +358picx/PobNDi71Q97+/CAOq+4wDOwhKwls7lwudIs=', + ], + [ + 'download' => 'https://nextcloud.com', + ], + ], + ], + ]; + $this->appFetcher + ->expects($this->once()) + ->method('get') + ->willReturn($appArray); + $realTmpFile = \OC::$server->getTempManager()->getTemporaryFile('.tar.gz'); + copy(__DIR__ . '/../data/testapp.tar.gz', $realTmpFile); + $this->tempManager + ->expects($this->at(0)) + ->method('getTemporaryFile') + ->with('.tar.gz') + ->willReturn($realTmpFile); + $realTmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); + $this->tempManager + ->expects($this->at(1)) + ->method('getTemporaryFolder') + ->willReturn($realTmpFolder); + $client = $this->createMock(IClient::class); + $client + ->expects($this->once()) + ->method('get') + ->with('https://example.com', ['save_to' => $realTmpFile]); + $this->clientService + ->expects($this->once()) + ->method('newClient') + ->willReturn($client); - Installer::updateApp($newData); - $newVersionNumber = \OC_App::getAppVersion(self::$appid); + $this->installer->downloadApp('testapp'); - $this->assertNotEquals($oldVersionNumber, $newVersionNumber); + $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); + $this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); } } diff --git a/tests/lib/OCSClientTest.php b/tests/lib/OCSClientTest.php deleted file mode 100644 index d4bfd77e871..00000000000 --- a/tests/lib/OCSClientTest.php +++ /dev/null @@ -1,1132 +0,0 @@ -<?php -/** - * @author Lukas Reschke <lukas@owncloud.com> - * - * @copyright Copyright (c) 2015, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -namespace Test; - -use OC\OCSClient; -use OCP\Http\Client\IClient; -use OCP\Http\Client\IClientService; -use OCP\Http\Client\IResponse; -use OCP\IConfig; -use OCP\ILogger; - -/** - * Class OCSClientTest - */ -class OCSClientTest extends \Test\TestCase { - /** @var OCSClient */ - private $ocsClient; - /** @var IConfig */ - private $config; - /** @var IClientService */ - private $clientService; - /** @var ILogger */ - private $logger; - - public function setUp() { - parent::setUp(); - - $this->config = $this->getMockBuilder('\OCP\IConfig') - ->disableOriginalConstructor()->getMock(); - $this->clientService = $this->createMock(IClientService::class); - $this->logger = $this->createMock(ILogger::class); - - $this->ocsClient = new OCSClient( - $this->clientService, - $this->config, - $this->logger - ); - } - - public function testIsAppStoreEnabledSuccess() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->assertTrue($this->ocsClient->isAppStoreEnabled()); - } - - public function testIsAppStoreEnabledFail() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(false)); - $this->assertFalse($this->ocsClient->isAppStoreEnabled()); - } - - public function testGetAppStoreUrl() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - $this->assertSame('https://api.owncloud.com/v1', self::invokePrivate($this->ocsClient, 'getAppStoreUrl')); - } - - public function testGetCategoriesDisabledAppStore() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(false)); - $this->assertNull($this->ocsClient->getCategories([8, 1, 0, 7])); - } - - public function testGetCategoriesExceptionClient() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/categories', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->throwException(new \Exception('TheErrorMessage'))); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get categories: TheErrorMessage', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getCategories([8, 1, 0, 7])); - } - - public function testGetCategoriesParseError() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('MyInvalidXml')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/categories', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get categories, content was no valid XML', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getCategories([8, 1, 0, 7])); - } - - public function testGetCategoriesSuccessful() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - <totalitems>6</totalitems> - </meta> - <data> - <category> - <id>920</id> - <name>ownCloud Multimedia</name> - </category> - <category> - <id>921</id> - <name>ownCloud PIM</name> - </category> - <category> - <id>922</id> - <name>ownCloud Productivity</name> - </category> - <category> - <id>923</id> - <name>ownCloud Game</name> - </category> - <category> - <id>924</id> - <name>ownCloud Tool</name> - </category> - <category> - <id>925</id> - <name>ownCloud other</name> - </category> - </data> - </ocs> - ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/categories', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $expected = [ - 920 => 'ownCloud Multimedia', - 921 => 'ownCloud PIM', - 922 => 'ownCloud Productivity', - 923 => 'ownCloud Game', - 924 => 'ownCloud Tool', - 925 => 'ownCloud other', - ]; - $this->assertSame($expected, $this->ocsClient->getCategories([8, 1, 0, 7])); - } - - public function testGetApplicationsDisabledAppStore() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(false)); - $this->assertSame([], $this->ocsClient->getApplications([], 1, 'approved', [8, 1, 0, 7])); - } - - public function testGetApplicationsExceptionClient() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', [8, 1, 0, 7]), - 'filter' => 'approved', - 'categories' => '815x1337', - 'sortmode' => 'new', - 'page' => 1, - 'pagesize' => 100, - 'approved' => 'approved', - ], - ] - ) - ->will($this->throwException(new \Exception('TheErrorMessage'))); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get applications: TheErrorMessage', - [ - 'app' => 'core', - ] - ); - - $this->assertSame([], $this->ocsClient->getApplications([815, 1337], 1, 'approved', [8, 1, 0, 7])); - } - - public function testGetApplicationsParseError() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('MyInvalidXml')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', [8, 1, 0, 7]), - 'filter' => 'approved', - 'categories' => '815x1337', - 'sortmode' => 'new', - 'page' => 1, - 'pagesize' => 100, - 'approved' => 'approved', - ], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get applications, content was no valid XML', - [ - 'app' => 'core', - ] - ); - - $this->assertSame([], $this->ocsClient->getApplications([815, 1337], 1, 'approved', [8, 1, 0, 7])); - } - - public function testGetApplicationsSuccessful() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - <totalitems>2</totalitems> - <itemsperpage>100</itemsperpage> - </meta> - <data> - <content details="summary"> - <id>168707</id> - <name>Calendar 8.0</name> - <version>0.6.4</version> - <label>recommended</label> - <changed>2015-02-09T15:23:56+01:00</changed> - <created>2015-01-26T04:35:19+01:00</created> - <typeid>921</typeid> - <typename>ownCloud PIM</typename> - <language></language> - <personid>owncloud</personid> - <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> - <downloads>5393</downloads> - <score>60</score> - <description>Calendar App for ownCloud</description> - <comments>7</comments> - <fans>10</fans> - <licensetype>16</licensetype> - <approved>0</approved> - <category>1</category> - <license>AGPL</license> - <preview1></preview1> - <detailpage>https://apps.owncloud.com/content/show.php?content=168707</detailpage> - <downloadtype1></downloadtype1> - <downloadway1>0</downloadway1> - <downloadprice1>0</downloadprice1> - <downloadlink1>http://apps.owncloud.com/content/download.php?content=168707&id=1</downloadlink1> - <downloadgpgsignature1></downloadgpgsignature1> - <downloadgpgfingerprint1></downloadgpgfingerprint1> - <downloadpackagename1></downloadpackagename1> - <downloadrepository1></downloadrepository1> - <downloadname1></downloadname1> - <downloadsize1>885</downloadsize1> - </content> - <content details="summary"> - <id>168708</id> - <name>Contacts 8.0</name> - <version>0.3.0.18</version> - <label>recommended</label> - <changed>2015-02-09T15:18:58+01:00</changed> - <created>2015-01-26T04:45:17+01:00</created> - <typeid>921</typeid> - <typename>ownCloud PIM</typename> - <language></language> - <personid>owncloud</personid> - <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> - <downloads>4237</downloads> - <score>58</score> - <description></description> - <comments>3</comments> - <fans>6</fans> - <licensetype>16</licensetype> - <approved>200</approved> - <category>1</category> - <license>AGPL</license> - <preview1></preview1> - <detailpage>https://apps.owncloud.com/content/show.php?content=168708</detailpage> - <downloadtype1></downloadtype1> - <downloadway1>0</downloadway1> - <downloadprice1>0</downloadprice1> - <downloadlink1>http://apps.owncloud.com/content/download.php?content=168708&id=1</downloadlink1> - <downloadgpgsignature1></downloadgpgsignature1> - <downloadgpgfingerprint1></downloadgpgfingerprint1> - <downloadpackagename1></downloadpackagename1> - <downloadrepository1></downloadrepository1> - <downloadname1></downloadname1> - <downloadsize1>1409</downloadsize1> - </content> - </data> - </ocs> ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data', - [ - 'timeout' => 20, - 'query' => [ - 'version' => implode('x', [8, 1, 0, 7]), - 'filter' => 'approved', - 'categories' => '815x1337', - 'sortmode' => 'new', - 'page' => 1, - 'pagesize' => 100, - 'approved' => 'approved', - ], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $expected = [ - [ - 'id' => '168707', - 'name' => 'Calendar 8.0', - 'label' => 'recommended', - 'version' => '0.6.4', - 'type' => '921', - 'typename' => 'ownCloud PIM', - 'personid' => 'owncloud', - 'license' => 'AGPL', - 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=168707', - 'preview' => '', - 'preview-full' => '', - 'changed' => 1423491836, - 'description' => 'Calendar App for ownCloud', - 'score' => '60', - 'downloads' => 5393, - 'level' => 0, - 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', - ], - [ - 'id' => '168708', - 'name' => 'Contacts 8.0', - 'label' => 'recommended', - 'version' => '0.3.0.18', - 'type' => '921', - 'typename' => 'ownCloud PIM', - 'personid' => 'owncloud', - 'license' => 'AGPL', - 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=168708', - 'preview' => '', - 'preview-full' => '', - 'changed' => 1423491538, - 'description' => '', - 'score' => '58', - 'downloads' => 4237, - 'level' => 200, - 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', - ], - ]; - $this->assertEquals($expected, $this->ocsClient->getApplications([815, 1337], 1, 'approved', [8, 1, 0, 7])); - } - - public function tesGetApplicationDisabledAppStore() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(false)); - $this->assertNull($this->ocsClient->getApplication('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationExceptionClient() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data/MyId', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->throwException(new \Exception('TheErrorMessage'))); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get application: TheErrorMessage', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getApplication('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationParseError() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('MyInvalidXml')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data/MyId', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get application, content was no valid XML', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getApplication('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationSuccessful() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - </meta> - <data> - <content details="full"> - <id>166053</id> - <name>Versioning</name> - <version>0.0.1</version> - <label>recommended</label> - <typeid>925</typeid> - <typename>ownCloud other</typename> - <language></language> - <personid>owncloud</personid> - <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> - <created>2014-07-07T16:34:40+02:00</created> - <changed>2014-07-07T16:34:40+02:00</changed> - <downloads>140</downloads> - <score>50</score> - <description>Placeholder for future updates</description> - <summary></summary> - <feedbackurl></feedbackurl> - <changelog></changelog> - <homepage></homepage> - <homepagetype></homepagetype> - <homepage2></homepage2> - <homepagetype2></homepagetype2> - <homepage3></homepage3> - <homepagetype3></homepagetype3> - <homepage4></homepage4> - <homepagetype4></homepagetype4> - <homepage5></homepage5> - <homepagetype5></homepagetype5> - <homepage6></homepage6> - <homepagetype6></homepagetype6> - <homepage7></homepage7> - <homepagetype7></homepagetype7> - <homepage8></homepage8> - <homepagetype8></homepagetype8> - <homepage9></homepage9> - <homepagetype9></homepagetype9> - <homepage10></homepage10> - <homepagetype10></homepagetype10> - <licensetype>16</licensetype> - <license>AGPL</license> - <donationpage></donationpage> - <comments>0</comments> - <commentspage>http://apps.owncloud.com/content/show.php?content=166053</commentspage> - <fans>0</fans> - <fanspage>http://apps.owncloud.com/content/show.php?action=fan&content=166053</fanspage> - <knowledgebaseentries>0</knowledgebaseentries> - <knowledgebasepage>http://apps.owncloud.com/content/show.php?action=knowledgebase&content=166053</knowledgebasepage> - <depend>ownCloud 7</depend> - <preview1></preview1> - <preview2></preview2> - <preview3></preview3> - <previewpic1></previewpic1> - <previewpic2></previewpic2> - <previewpic3></previewpic3> - <picsmall1></picsmall1> - <picsmall2></picsmall2> - <picsmall3></picsmall3> - <detailpage>https://apps.owncloud.com/content/show.php?content=166053</detailpage> - <downloadtype1></downloadtype1> - <downloadprice1>0</downloadprice1> - <downloadlink1>http://apps.owncloud.com/content/download.php?content=166053&id=1</downloadlink1> - <downloadname1></downloadname1> - <downloadgpgfingerprint1></downloadgpgfingerprint1> - <downloadgpgsignature1></downloadgpgsignature1> - <downloadpackagename1></downloadpackagename1> - <downloadrepository1></downloadrepository1> - <downloadsize1>1</downloadsize1> - <approved>200</approved> - </content> - </data> - </ocs> - ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data/166053', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $expected = [ - 'id' => 166053, - 'name' => 'Versioning', - 'version' => '0.0.1', - 'type' => '925', - 'label' => 'recommended', - 'typename' => 'ownCloud other', - 'personid' => 'owncloud', - 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', - 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=166053', - 'preview1' => '', - 'preview2' => '', - 'preview3' => '', - 'changed' => 1404743680, - 'description' => 'Placeholder for future updates', - 'score' => 50, - 'level' => 200, - ]; - $this->assertSame($expected, $this->ocsClient->getApplication(166053, [8, 1, 0, 7])); - } - - public function testGetApplicationSuccessfulWithOldId() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - </meta> - <data> - <content details="full"> - <id>1337</id> - <name>Versioning</name> - <version>0.0.1</version> - <label>recommended</label> - <typeid>925</typeid> - <typename>ownCloud other</typename> - <language></language> - <personid>owncloud</personid> - <profilepage>http://opendesktop.org/usermanager/search.php?username=owncloud</profilepage> - <created>2014-07-07T16:34:40+02:00</created> - <changed>2014-07-07T16:34:40+02:00</changed> - <downloads>140</downloads> - <score>50</score> - <description>Placeholder for future updates</description> - <summary></summary> - <feedbackurl></feedbackurl> - <changelog></changelog> - <homepage></homepage> - <homepagetype></homepagetype> - <homepage2></homepage2> - <homepagetype2></homepagetype2> - <homepage3></homepage3> - <homepagetype3></homepagetype3> - <homepage4></homepage4> - <homepagetype4></homepagetype4> - <homepage5></homepage5> - <homepagetype5></homepagetype5> - <homepage6></homepage6> - <homepagetype6></homepagetype6> - <homepage7></homepage7> - <homepagetype7></homepagetype7> - <homepage8></homepage8> - <homepagetype8></homepagetype8> - <homepage9></homepage9> - <homepagetype9></homepagetype9> - <homepage10></homepage10> - <homepagetype10></homepagetype10> - <licensetype>16</licensetype> - <license>AGPL</license> - <donationpage></donationpage> - <comments>0</comments> - <commentspage>http://apps.owncloud.com/content/show.php?content=166053</commentspage> - <fans>0</fans> - <fanspage>http://apps.owncloud.com/content/show.php?action=fan&content=166053</fanspage> - <knowledgebaseentries>0</knowledgebaseentries> - <knowledgebasepage>http://apps.owncloud.com/content/show.php?action=knowledgebase&content=166053</knowledgebasepage> - <depend>ownCloud 7</depend> - <preview1></preview1> - <preview2></preview2> - <preview3></preview3> - <previewpic1></previewpic1> - <previewpic2></previewpic2> - <previewpic3></previewpic3> - <picsmall1></picsmall1> - <picsmall2></picsmall2> - <picsmall3></picsmall3> - <detailpage>https://apps.owncloud.com/content/show.php?content=166053</detailpage> - <downloadtype1></downloadtype1> - <downloadprice1>0</downloadprice1> - <downloadlink1>http://apps.owncloud.com/content/download.php?content=166053&id=1</downloadlink1> - <downloadname1></downloadname1> - <downloadgpgfingerprint1></downloadgpgfingerprint1> - <downloadgpgsignature1></downloadgpgsignature1> - <downloadpackagename1></downloadpackagename1> - <downloadrepository1></downloadrepository1> - <downloadsize1>1</downloadsize1> - <approved>200</approved> - </content> - </data> - </ocs> - ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data/166053', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $expected = [ - 'id' => 166053, - 'name' => 'Versioning', - 'version' => '0.0.1', - 'type' => '925', - 'label' => 'recommended', - 'typename' => 'ownCloud other', - 'personid' => 'owncloud', - 'profilepage' => 'http://opendesktop.org/usermanager/search.php?username=owncloud', - 'detailpage' => 'https://apps.owncloud.com/content/show.php?content=166053', - 'preview1' => '', - 'preview2' => '', - 'preview3' => '', - 'changed' => 1404743680, - 'description' => 'Placeholder for future updates', - 'score' => 50, - 'level' => 200, - ]; - $this->assertSame($expected, $this->ocsClient->getApplication(166053, [8, 1, 0, 7])); - } - - public function testGetApplicationEmptyXml() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - </meta> - </ocs> - ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/data/MyId', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->assertSame(null, $this->ocsClient->getApplication('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationDownloadDisabledAppStore() { - $this->config - ->expects($this->once()) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(false)); - $this->assertNull($this->ocsClient->getApplicationDownload('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationDownloadExceptionClient() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/download/MyId/1', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->throwException(new \Exception('TheErrorMessage'))); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get application download URL: TheErrorMessage', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getApplicationDownload('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationDownloadParseError() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('MyInvalidXml')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/download/MyId/1', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $this->logger - ->expects($this->once()) - ->method('error') - ->with( - 'Could not get application download URL, content was no valid XML', - [ - 'app' => 'core', - ] - ); - - $this->assertNull($this->ocsClient->getApplicationDownload('MyId', [8, 1, 0, 7])); - } - - public function testGetApplicationDownloadUrlSuccessful() { - $this->config - ->expects($this->at(0)) - ->method('getSystemValue') - ->with('appstoreenabled', true) - ->will($this->returnValue(true)); - $this->config - ->expects($this->at(1)) - ->method('getSystemValue') - ->with('appstoreurl', 'https://api.owncloud.com/v1') - ->will($this->returnValue('https://api.owncloud.com/v1')); - - $response = $this->createMock(IResponse::class); - $response - ->expects($this->once()) - ->method('getBody') - ->will($this->returnValue('<?xml version="1.0"?> - <ocs> - <meta> - <status>ok</status> - <statuscode>100</statuscode> - <message></message> - </meta> - <data> - <content details="download"> - <downloadlink>https://apps.owncloud.com/CONTENT/content-files/166052-files_trashbin.zip</downloadlink> - <mimetype>application/zip</mimetype> - <gpgfingerprint></gpgfingerprint> - <gpgsignature></gpgsignature> - <packagename></packagename> - <repository></repository> - </content> - </data> - </ocs> - ')); - - $client = $this->createMock(IClient::class); - $client - ->expects($this->once()) - ->method('get') - ->with( - 'https://api.owncloud.com/v1/content/download/MyId/1', - [ - 'timeout' => 20, - 'query' => ['version' => '8x1x0x7'], - ] - ) - ->will($this->returnValue($response)); - - $this->clientService - ->expects($this->once()) - ->method('newClient') - ->will($this->returnValue($client)); - - $expected = [ - 'downloadlink' => 'https://apps.owncloud.com/CONTENT/content-files/166052-files_trashbin.zip', - ]; - $this->assertSame($expected, $this->ocsClient->getApplicationDownload('MyId', [8, 1, 0, 7])); - } -} diff --git a/tests/lib/Security/CSRF/CsrfTokenManagerTest.php b/tests/lib/Security/CSRF/CsrfTokenManagerTest.php index 6f7842fdfd9..f9dd8127e5a 100644 --- a/tests/lib/Security/CSRF/CsrfTokenManagerTest.php +++ b/tests/lib/Security/CSRF/CsrfTokenManagerTest.php @@ -137,15 +137,19 @@ class CsrfTokenManagerTest extends \Test\TestCase { } public function testIsTokenValidWithValidToken() { + $a = 'abc'; + $b = 'def'; + $xorB64 = 'BQcF'; + $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); $this->storageInterface ->expects($this->once()) ->method('hasToken') ->willReturn(true); - $token = new \OC\Security\CSRF\CsrfToken('XlQhHjgWCgBXAEI0Khl+IQEiCXN2LUcDHAQTQAc1HQs=:qgkUlg8l3m8WnkOG4XM9Az33pAt1vSVMx4hcJFsxdqc='); + $token = new \OC\Security\CSRF\CsrfToken($tokenVal); $this->storageInterface ->expects($this->once()) ->method('getToken') - ->willReturn('/3JKTq2ldmzcDr1f5zDJ7Wt0lEgqqfKF'); + ->willReturn($b); $this->assertSame(true, $this->csrfTokenManager->isTokenValid($token)); } diff --git a/tests/lib/Security/CSRF/CsrfTokenTest.php b/tests/lib/Security/CSRF/CsrfTokenTest.php index d19d1de916c..fbb92cd315a 100644 --- a/tests/lib/Security/CSRF/CsrfTokenTest.php +++ b/tests/lib/Security/CSRF/CsrfTokenTest.php @@ -36,7 +36,11 @@ class CsrfTokenTest extends \Test\TestCase { } public function testGetDecryptedValue() { - $csrfToken = new \OC\Security\CSRF\CsrfToken('XlQhHjgWCgBXAEI0Khl+IQEiCXN2LUcDHAQTQAc1HQs=:qgkUlg8l3m8WnkOG4XM9Az33pAt1vSVMx4hcJFsxdqc='); - $this->assertSame('/3JKTq2ldmzcDr1f5zDJ7Wt0lEgqqfKF', $csrfToken->getDecryptedValue()); + $a = 'abc'; + $b = 'def'; + $xorB64 = 'BQcF'; + $tokenVal = sprintf('%s:%s', $xorB64, base64_encode($a)); + $csrfToken = new \OC\Security\CSRF\CsrfToken($tokenVal); + $this->assertSame($b, $csrfToken->getDecryptedValue()); } } diff --git a/tests/lib/ServerTest.php b/tests/lib/ServerTest.php index 2e5900c4ce5..02fccee628e 100644 --- a/tests/lib/ServerTest.php +++ b/tests/lib/ServerTest.php @@ -23,6 +23,8 @@ */ namespace Test; +use OC\App\AppStore\Fetcher\AppFetcher; +use OC\App\AppStore\Fetcher\CategoryFetcher; /** * Class Server @@ -50,6 +52,7 @@ class ServerTest extends \Test\TestCase { ['AllConfig', '\OCP\IConfig'], ['AppConfig', '\OC\AppConfig'], ['AppConfig', '\OCP\IAppConfig'], + ['AppFetcher', AppFetcher::class], ['AppHelper', '\OC\AppHelper'], ['AppHelper', '\OCP\IHelper'], ['AppManager', '\OC\App\AppManager'], @@ -59,6 +62,7 @@ class ServerTest extends \Test\TestCase { ['AvatarManager', '\OC\AvatarManager'], ['AvatarManager', '\OCP\IAvatarManager'], + ['CategoryFetcher', CategoryFetcher::class], ['CapabilitiesManager', '\OC\CapabilitiesManager'], ['ContactsManager', '\OC\ContactsManager'], ['ContactsManager', '\OCP\Contacts\IManager'], @@ -122,8 +126,6 @@ class ServerTest extends \Test\TestCase { ['UserCache', '\OC\Cache\File'], ['UserCache', '\OCP\ICache'], - ['OcsClient', '\OC\OCSClient'], - ['PreviewManager', '\OC\PreviewManager'], ['PreviewManager', '\OCP\IPreview'], diff --git a/tests/lib/Share20/ManagerTest.php b/tests/lib/Share20/ManagerTest.php index c0c7b48b35d..bd85e3c73aa 100644 --- a/tests/lib/Share20/ManagerTest.php +++ b/tests/lib/Share20/ManagerTest.php @@ -2530,6 +2530,46 @@ class ManagerTest extends \Test\TestCase { $this->manager->moveShare($share, 'recipient'); } + + /** + * @dataProvider dataTestShareProviderExists + */ + public function testShareProviderExists($shareType, $expected) { + + $factory = $this->getMockBuilder('OCP\Share\IProviderFactory')->getMock(); + $factory->expects($this->any())->method('getProviderForType') + ->willReturnCallback(function ($id) { + if ($id === \OCP\Share::SHARE_TYPE_USER) { + return true; + } + throw new Exception\ProviderException(); + }); + + $manager = new Manager( + $this->logger, + $this->config, + $this->secureRandom, + $this->hasher, + $this->mountManager, + $this->groupManager, + $this->l, + $factory, + $this->userManager, + $this->rootFolder, + $this->eventDispatcher + ); + $this->assertSame($expected, + $manager->shareProviderExists($shareType) + ); + } + + public function dataTestShareProviderExists() { + return [ + [\OCP\Share::SHARE_TYPE_USER, true], + [42, false], + ]; + } + public function testGetSharesInFolder() { $factory = new DummyFactory2($this->createMock(IServerContainer::class)); diff --git a/tests/lib/StreamWrappersTest.php b/tests/lib/StreamWrappersTest.php index c0ecb5e738b..eb35fd54454 100644 --- a/tests/lib/StreamWrappersTest.php +++ b/tests/lib/StreamWrappersTest.php @@ -38,7 +38,7 @@ class StreamWrappersTest extends \Test\TestCase { public static function tearDownAfterClass() { if (self::$trashBinStatus) { - \OC_App::enable('files_trashbin'); + (new \OC_App())->enable('files_trashbin'); } } diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 268d8e10e5a..ee9ed737cf5 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -1,5 +1,4 @@ <?php - /** * Copyright (c) 2013 Robin Appelman <icewind@owncloud.com> * This file is licensed under the Affero General Public License version 3 or @@ -39,8 +38,16 @@ class SessionTest extends \Test\TestCase { protected $tokenProvider; /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ private $config; - /** @var Throttler */ + /** @var Throttler|\PHPUnit_Framework_MockObject_MockObject */ private $throttler; + /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */ + private $random; + /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + private $manager; + /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */ + private $session; + /** @var Session|\PHPUnit_Framework_MockObject_MockObject */ + private $userSession; protected function setUp() { parent::setUp(); @@ -52,6 +59,24 @@ class SessionTest extends \Test\TestCase { $this->tokenProvider = $this->createMock(IProvider::class); $this->config = $this->createMock(IConfig::class); $this->throttler = $this->createMock(Throttler::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->manager = $this->createMock(IUserManager::class); + $this->session = $this->createMock(ISession::class); + $this->userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([ + $this->manager, + $this->session, + $this->timeFactory, + $this->tokenProvider, + $this->config, + $this->random, + ]) + ->setMethods([ + 'setMagicInCookie', + ]) + ->getMock(); + + \OC_User::setIncognitoMode(false); } public function testGetUser() { @@ -100,12 +125,12 @@ class SessionTest extends \Test\TestCase { ->method('updateTokenActivity') ->with($token); - $manager->expects($this->any()) + $manager->expects($this->once()) ->method('get') ->with($expectedUser->getUID()) ->will($this->returnValue($expectedUser)); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $user = $userSession->getUser(); $this->assertSame($expectedUser, $user); $this->assertSame(10000, $token->getLastCheck()); @@ -127,7 +152,7 @@ class SessionTest extends \Test\TestCase { $manager = $this->createMock(Manager::class); $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods([ 'getUser' ]) @@ -154,7 +179,7 @@ class SessionTest extends \Test\TestCase { ->method('getUID') ->will($this->returnValue('foo')); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $userSession->setUser($user); } @@ -181,17 +206,10 @@ class SessionTest extends \Test\TestCase { }, 'foo')); $managerMethods = get_class_methods(Manager::class); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) + ->setMethods($mockedManagerMethods) ->setConstructorArgs([$this->config]) ->getMock(); @@ -213,7 +231,7 @@ class SessionTest extends \Test\TestCase { ->will($this->returnValue($user)); $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods([ 'prepareUserLogin' ]) @@ -238,18 +256,11 @@ class SessionTest extends \Test\TestCase { ->with('bar') ->will($this->throwException(new \OC\Authentication\Exceptions\InvalidTokenException())); - $managerMethods = get_class_methods('\OC\User\Manager'); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) + ->setMethods($mockedManagerMethods) ->setConstructorArgs([$this->config]) ->getMock(); @@ -267,28 +278,21 @@ class SessionTest extends \Test\TestCase { ->with('foo', 'bar') ->will($this->returnValue($user)); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $userSession->login('foo', 'bar'); } public function testLoginInvalidPassword() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $managerMethods = get_class_methods('\OC\User\Manager'); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) + ->setMethods($mockedManagerMethods) ->setConstructorArgs([$this->config]) ->getMock(); $backend = $this->createMock(\Test\Util\User\Dummy::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); @@ -317,7 +321,7 @@ class SessionTest extends \Test\TestCase { public function testLoginNonExisting() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); $manager = $this->createMock(Manager::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $session->expects($this->never()) ->method('set'); @@ -343,7 +347,7 @@ class SessionTest extends \Test\TestCase { public function testLoginWithDifferentTokenLoginName() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); $manager = $this->createMock(Manager::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $username = 'user123'; $token = new \OC\Authentication\Token\DefaultToken(); $token->setLoginName($username); @@ -375,7 +379,7 @@ class SessionTest extends \Test\TestCase { /** @var \OC\User\Session $userSession */ $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser']) ->getMock(); @@ -411,7 +415,7 @@ class SessionTest extends \Test\TestCase { /** @var Session $userSession */ $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser']) ->getMock(); @@ -434,7 +438,7 @@ class SessionTest extends \Test\TestCase { /** @var \OC\User\Session $userSession */ $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser']) ->getMock(); @@ -476,7 +480,7 @@ class SessionTest extends \Test\TestCase { /** @var \OC\User\Session $userSession */ $userSession = $this->getMockBuilder(Session::class) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods(['login', 'isTwoFactorEnforced']) ->getMock(); @@ -513,156 +517,216 @@ class SessionTest extends \Test\TestCase { public function testRememberLoginValidToken() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->exactly(1)) - ->method('set') - ->with($this->callback(function ($key) { - switch ($key) { - case 'user_id': - return true; - default: - return false; - } - }, 'foo')); - $session->expects($this->once()) - ->method('regenerateId'); - - $managerMethods = get_class_methods(Manager::class); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) + ->setMethods($mockedManagerMethods) ->setConstructorArgs([$this->config]) ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) + ->getMock(); - $backend = $this->createMock(\Test\Util\User\Dummy::class); + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue([$token])); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->random->expects($this->once()) + ->method('generate') + ->with(32) + ->will($this->returnValue('abcdefg123456')); + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('foo', 'login_token', 'abcdefg123456', 10000); + + $session->expects($this->once()) + ->method('getId') + ->will($this->returnValue($sessionId)); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->will($this->returnValue(true)); $user->expects($this->any()) ->method('getUID') ->will($this->returnValue('foo')); + $userSession->expects($this->once()) + ->method('setMagicInCookie'); $user->expects($this->once()) ->method('updateLastLoginTimestamp'); + $session->expects($this->once()) + ->method('set') + ->with('user_id', 'foo'); - $manager->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($user)); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); + $this->assertTrue($granted); + } + public function testRememberLoginInvalidSessionToken() { + $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->setMethods($mockedManagerMethods) + ->setConstructorArgs([$this->config]) + ->getMock(); $userSession = $this->getMockBuilder(Session::class) //override, otherwise tests will fail because of setcookie() ->setMethods(['setMagicInCookie']) - //there are passed as parameters to the constructor - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->getMock(); - $granted = $userSession->loginWithCookie('foo', $token); - - $this->assertSame($granted, true); - } + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; - public function testRememberLoginInvalidToken() { - $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->never()) - ->method('set'); $session->expects($this->once()) ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue([$token])); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->config->expects($this->once()) + ->method('setUserValue'); // TODO: mock new random value - $managerMethods = get_class_methods('\OC\User\Manager'); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } - $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) - ->setConstructorArgs([$this->config]) - ->getMock(); - - $backend = $this->createMock(\Test\Util\User\Dummy::class); - - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); + $session->expects($this->once()) + ->method('getId') + ->will($this->returnValue($sessionId)); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->will($this->throwException(new \OC\Authentication\Exceptions\InvalidTokenException())); - $user->expects($this->any()) + $user->expects($this->never()) ->method('getUID') ->will($this->returnValue('foo')); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); $user->expects($this->never()) ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertFalse($granted); + } + + public function testRememberLoginInvalidToken() { + $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->setMethods($mockedManagerMethods) + ->setConstructorArgs([$this->config]) + ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) + ->getMock(); + + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + + $session->expects($this->once()) + ->method('regenerateId'); $manager->expects($this->once()) ->method('get') ->with('foo') ->will($this->returnValue($user)); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue(['anothertoken'])); + $this->config->expects($this->never()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); - - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); - $granted = $userSession->loginWithCookie('foo', 'badToken'); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); - $this->assertSame($granted, false); + $this->assertFalse($granted); } public function testRememberLoginInvalidUser() { $session = $this->getMockBuilder(Memory::class)->setConstructorArgs([''])->getMock(); - $session->expects($this->never()) - ->method('set'); - $session->expects($this->once()) - ->method('regenerateId'); - - $managerMethods = get_class_methods('\OC\User\Manager'); - //keep following methods intact in order to ensure hooks are - //working - $doNotMock = array('__construct', 'emit', 'listen'); - foreach ($doNotMock as $methodName) { - $i = array_search($methodName, $managerMethods, true); - if ($i !== false) { - unset($managerMethods[$i]); - } - } + $managerMethods = get_class_methods(\OC\User\Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); $manager = $this->getMockBuilder(Manager::class) - ->setMethods($managerMethods) + ->setMethods($mockedManagerMethods) ->setConstructorArgs([$this->config]) ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->setMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) + ->getMock(); + $token = 'goodToken'; + $oldSessionId = 'sess321'; - $backend = $this->createMock(\Test\Util\User\Dummy::class); - - $user = $this->getMockBuilder(User::class)->setConstructorArgs(['foo', $backend])->getMock(); - - $user->expects($this->never()) - ->method('getUID'); - $user->expects($this->never()) - ->method('updateLastLoginTimestamp'); - + $session->expects($this->once()) + ->method('regenerateId'); $manager->expects($this->once()) ->method('get') ->with('foo') ->will($this->returnValue(null)); + $this->config->expects($this->never()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->will($this->returnValue(['anothertoken'])); + + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); - //prepare login token - $token = 'goodToken'; - \OC::$server->getConfig()->setUserValue('foo', 'login_token', $token, time()); - - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); - $granted = $userSession->loginWithCookie('foo', $token); + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); - $this->assertSame($granted, false); + $this->assertFalse($granted); } public function testActiveUserAfterSetSession() { @@ -684,7 +748,7 @@ class SessionTest extends \Test\TestCase { $session = new Memory(''); $session->set('user_id', 'foo'); $userSession = $this->getMockBuilder('\OC\User\Session') - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->setMethods([ 'validateSession' ]) @@ -705,7 +769,7 @@ class SessionTest extends \Test\TestCase { $session = $this->createMock(ISession::class); $token = $this->createMock(IToken::class); $user = $this->createMock(IUser::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $random = $this->createMock(ISecureRandom::class); $config = $this->createMock(IConfig::class); @@ -749,7 +813,7 @@ class SessionTest extends \Test\TestCase { $session = $this->createMock(ISession::class); $token = $this->createMock(IToken::class); $user = $this->createMock(IUser::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $random = $this->createMock(ISecureRandom::class); $config = $this->createMock(IConfig::class); @@ -796,7 +860,7 @@ class SessionTest extends \Test\TestCase { ->disableOriginalConstructor() ->getMock(); $session = $this->createMock(ISession::class); - $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config); + $userSession = new \OC\User\Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random); $request = $this->createMock(IRequest::class); $uid = 'user123'; @@ -826,7 +890,7 @@ class SessionTest extends \Test\TestCase { $user = $this->createMock(IUser::class); $userSession = $this->getMockBuilder('\OC\User\Session') ->setMethods(['logout']) - ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config]) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random]) ->getMock(); $request = $this->createMock(IRequest::class); @@ -855,7 +919,7 @@ class SessionTest extends \Test\TestCase { $timeFactory = $this->createMock(ITimeFactory::class); $tokenProvider = $this->createMock(IProvider::class); $userSession = $this->getMockBuilder('\OC\User\Session') - ->setConstructorArgs([$userManager, $session, $timeFactory, $tokenProvider, $this->config]) + ->setConstructorArgs([$userManager, $session, $timeFactory, $tokenProvider, $this->config, $this->random]) ->setMethods(['logout']) ->getMock(); @@ -902,7 +966,7 @@ class SessionTest extends \Test\TestCase { $timeFactory = $this->createMock(ITimeFactory::class); $tokenProvider = $this->createMock(IProvider::class); $userSession = $this->getMockBuilder('\OC\User\Session') - ->setConstructorArgs([$userManager, $session, $timeFactory, $tokenProvider, $this->config]) + ->setConstructorArgs([$userManager, $session, $timeFactory, $tokenProvider, $this->config, $this->random]) ->setMethods(['logout']) ->getMock(); @@ -936,7 +1000,7 @@ class SessionTest extends \Test\TestCase { $session = $this->createMock(ISession::class); $timeFactory = $this->createMock(ITimeFactory::class); $tokenProvider = $this->createMock(IProvider::class); - $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config); + $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config, $this->random); $password = '123456'; $sessionId = 'session1234'; @@ -961,7 +1025,7 @@ class SessionTest extends \Test\TestCase { $session = $this->createMock(ISession::class); $timeFactory = $this->createMock(ITimeFactory::class); $tokenProvider = $this->createMock(IProvider::class); - $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config); + $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config, $this->random); $session->expects($this->once()) ->method('getId') @@ -975,7 +1039,7 @@ class SessionTest extends \Test\TestCase { $session = $this->createMock(ISession::class); $timeFactory = $this->createMock(ITimeFactory::class); $tokenProvider = $this->createMock(IProvider::class); - $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config); + $userSession = new \OC\User\Session($userManager, $session, $timeFactory, $tokenProvider, $this->config, $this->random); $password = '123456'; $sessionId = 'session1234'; @@ -1015,7 +1079,7 @@ class SessionTest extends \Test\TestCase { $tokenProvider = new DefaultTokenProvider($mapper, $crypto, $this->config, $logger, $this->timeFactory); /** @var \OC\User\Session $userSession */ - $userSession = new Session($manager, $session, $this->timeFactory, $tokenProvider, $this->config); + $userSession = new Session($manager, $session, $this->timeFactory, $tokenProvider, $this->config, $this->random); $mapper->expects($this->any()) ->method('getToken') @@ -1065,7 +1129,7 @@ class SessionTest extends \Test\TestCase { $tokenProvider = new DefaultTokenProvider($mapper, $crypto, $this->config, $logger, $this->timeFactory); /** @var \OC\User\Session $userSession */ - $userSession = new Session($manager, $session, $this->timeFactory, $tokenProvider, $this->config); + $userSession = new Session($manager, $session, $this->timeFactory, $tokenProvider, $this->config, $this->random); $mapper->expects($this->any()) ->method('getToken') @@ -1092,4 +1156,27 @@ class SessionTest extends \Test\TestCase { $userSession->logClientIn('john', 'doe', $request, $this->throttler); } + + public function testCreateRememberMeToken() { + $user = $this->createMock(IUser::class); + $user + ->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('UserUid'); + $this->random + ->expects($this->once()) + ->method('generate') + ->with(32) + ->willReturn('LongRandomToken'); + $this->config + ->expects($this->once()) + ->method('setUserValue') + ->with('UserUid', 'login_token', 'LongRandomToken', 10000); + $this->userSession + ->expects($this->once()) + ->method('setMagicInCookie') + ->with('UserUid', 'LongRandomToken'); + + $this->userSession->createRememberMeToken($user); + } } diff --git a/version.php b/version.php index 96725a6bb4d..e6de2e2bde0 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(9, 2, 0, 4); +$OC_Version = array(9, 2, 0, 5); // The human readable string $OC_VersionString = '11.0 alpha'; |