diff --git a/.drone.yml b/.drone.yml index aa718998203d5..e9cacb376fc9d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -857,6 +857,31 @@ trigger: - pull_request - push +--- +kind: pipeline +name: integration-avatar + +steps: +- name: submodules + image: docker:git + commands: + - git submodule update --init +- name: integration-auth + image: nextcloudci/integration-php7.3:integration-php7.3-2 + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - ./occ maintenance:install --admin-pass=admin --data-dir=/dev/shm/nc_int + - cd build/integration + - ./run.sh features/avatar.feature + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push + --- kind: pipeline name: integration-maintenance-mode diff --git a/build/integration/data/coloured-pattern.png b/build/integration/data/coloured-pattern.png new file mode 100644 index 0000000000000..cf43787f3fda7 Binary files /dev/null and b/build/integration/data/coloured-pattern.png differ diff --git a/build/integration/data/green-square-256.png b/build/integration/data/green-square-256.png new file mode 100644 index 0000000000000..9f14b707ca369 Binary files /dev/null and b/build/integration/data/green-square-256.png differ diff --git a/build/integration/features/avatar.feature b/build/integration/features/avatar.feature new file mode 100644 index 0000000000000..5ff33c23e1676 --- /dev/null +++ b/build/integration/features/avatar.feature @@ -0,0 +1,407 @@ +Feature: avatar + + Background: + Given using api version "2" + Given user "user0" exists + Given user "user1" exists + + Scenario: get default generic user avatar + When user "user0" gets avatar for type "user" and id "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get default generic user avatar as an anonymous user + When user "anonymous" gets avatar for type "user" and id "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get default generic guest avatar + When user "user0" gets avatar for type "guest" and id "guest0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get default generic guest avatar as an anonymous user + When user "anonymous" gets avatar for type "guest" and id "guest0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get generic unknown avatar + When user "user0" gets avatar for type "unknown" and id "user0" with size "128" with 404 + + + + Scenario: set generic user avatar + When user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + Then user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + And user "anonymous" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + + Scenario: set generic user avatar as another user + When user "user1" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" with "404" + Then user "user0" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: set generic user avatar as an anonymous user + When user "anonymous" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" with "404" + Then user "user0" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: set non squared image as generic user avatar + When user "user0" sets avatar for type "user" and id "user0" from file "data/coloured-pattern.png" with "400" + Then user "user0" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: set not an image as generic user avatar + When user "user0" sets avatar for type "user" and id "user0" from file "data/textfile.txt" with "400" + Then user "user0" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: set generic guest avatar + When user "user0" sets avatar for type "guest" and id "guest0" from file "data/green-square-256.png" with "404" + Then user "user0" gets avatar for type "guest" and id "guest0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: set generic unknown avatar + When user "user0" sets avatar for type "unknown" and id "user0" from file "data/green-square-256.png" with "404" + + + + Scenario: delete generic user avatar + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + And user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + And user "anonymous" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + When user "user0" deletes avatar for type "user" and id "user0" + Then user "user0" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + And user "anonymous" gets avatar for type "user" and id "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: delete generic user avatar as another user + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + And user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + When user "user1" deletes avatar for type "user" and id "user0" with "404" + Then user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + + Scenario: delete generic user avatar as an anonymous user + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + And user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + When user "anonymous" deletes avatar for type "user" and id "user0" with "404" + Then user "user0" gets avatar for type "user" and id "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + + Scenario: delete generic guest avatar + When user "user0" deletes avatar for type "guest" and id "guest0" with "404" + Then user "user0" gets avatar for type "guest" and id "guest0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: delete generic unknown avatar + When user "user0" deletes avatar for type "unknown" and id "user0" with "404" + + + + Scenario: get generic user avatar with a larger size than the original one + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + When user "user0" gets avatar for type "user" and id "user0" with size "512" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 512 + And last avatar is a single "#00FF00" color + + Scenario: get generic user avatar with a smaller size than the original one + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + When user "user0" gets avatar for type "user" and id "user0" with size "128" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#00FF00" color + + + + Scenario: get user avatar after setting generic user avatar + Given user "user0" sets avatar for type "user" and id "user0" from file "data/green-square-256.png" + When user "user0" gets avatar for user "user0" with size "256" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + + Scenario: get generic user avatar after setting user avatar + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/coloured-pattern.png" + And logged in user crops temporary avatar + | x | 384 | + | y | 256 | + | w | 128 | + | h | 128 | + When user "user0" gets avatar for type "user" and id "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#FF0000" color + + + + Scenario: get default user avatar + When user "user0" gets avatar for user "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get default user avatar as an anonymous user + When user "anonymous" gets avatar for user "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + + + Scenario: get temporary user avatar before cropping it + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/green-square-256.png" + When logged in user gets temporary avatar + Then The following headers should be set + | Content-Type | image/png | + # "last avatar" also includes the last temporary avatar + And last avatar is a square of size 256 + And last avatar is a single "#00FF00" color + + Scenario: get user avatar before cropping it + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/green-square-256.png" + # Avatar needs to be cropped to finish setting it even if it is squared + When user "user0" gets avatar for user "user0" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + + + Scenario: set user avatar from file + Given Logging in using web as "user0" + When logged in user posts temporary avatar from file "data/coloured-pattern.png" + And logged in user crops temporary avatar + | x | 384 | + | y | 256 | + | w | 128 | + | h | 128 | + Then logged in user gets temporary avatar with 404 + And user "user0" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#FF0000" color + And user "anonymous" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#FF0000" color + + Scenario: set user avatar from internal path + Given user "user0" uploads file "data/coloured-pattern.png" to "/internal-coloured-pattern.png" + And Logging in using web as "user0" + When logged in user posts temporary avatar from internal path "internal-coloured-pattern.png" + And logged in user crops temporary avatar + | x | 704 | + | y | 320 | + | w | 64 | + | h | 64 | + Then logged in user gets temporary avatar with 404 + And user "user0" gets avatar for user "user0" with size "64" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 64 + And last avatar is a single "#00FF00" color + And user "anonymous" gets avatar for user "user0" with size "64" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 64 + And last avatar is a single "#00FF00" color + + Scenario: cropped user avatar needs to be squared + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/coloured-pattern.png" + When logged in user crops temporary avatar with 400 + | x | 384 | + | y | 256 | + | w | 192 | + | h | 128 | + + + + Scenario: delete user avatar + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/coloured-pattern.png" + And logged in user crops temporary avatar + | x | 384 | + | y | 256 | + | w | 128 | + | h | 128 | + And user "user0" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#FF0000" color + And user "anonymous" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 128 + And last avatar is a single "#FF0000" color + When logged in user deletes the user avatar + Then user "user0" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + And user "anonymous" gets avatar for user "user0" + And The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 0 | + And last avatar is a square of size 128 + And last avatar is not a single color + + + + Scenario: get user avatar with a larger size than the original one + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/coloured-pattern.png" + And logged in user crops temporary avatar + | x | 384 | + | y | 256 | + | w | 128 | + | h | 128 | + When user "user0" gets avatar for user "user0" with size "192" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 192 + And last avatar is a single "#FF0000" color + + Scenario: get user avatar with a smaller size than the original one + Given Logging in using web as "user0" + And logged in user posts temporary avatar from file "data/coloured-pattern.png" + And logged in user crops temporary avatar + | x | 384 | + | y | 256 | + | w | 128 | + | h | 128 | + When user "user0" gets avatar for user "user0" with size "96" + Then The following headers should be set + | Content-Type | image/png | + | X-NC-IsCustomAvatar | 1 | + And last avatar is a square of size 96 + And last avatar is a single "#FF0000" color + + + + Scenario: get default guest avatar + When user "user0" gets avatar for guest "guest0" + Then The following headers should be set + | Content-Type | image/png | + And last avatar is a square of size 128 + And last avatar is not a single color + + Scenario: get default guest avatar as an anonymous user + When user "anonymous" gets avatar for guest "guest0" + Then The following headers should be set + | Content-Type | image/png | + And last avatar is a square of size 128 + And last avatar is not a single color diff --git a/build/integration/features/bootstrap/Avatar.php b/build/integration/features/bootstrap/Avatar.php new file mode 100644 index 0000000000000..eda591a1e5408 --- /dev/null +++ b/build/integration/features/bootstrap/Avatar.php @@ -0,0 +1,376 @@ + + * + * @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 . + * + */ + +use Behat\Gherkin\Node\TableNode; +use PHPUnit\Framework\Assert; + +require __DIR__ . '/../../vendor/autoload.php'; + +trait Avatar { + + /** @var string **/ + private $lastAvatar; + + /** @AfterScenario **/ + public function cleanupLastAvatar() { + $this->lastAvatar = null; + } + + private function getLastAvatar() { + $this->lastAvatar = ''; + + $body = $this->response->getBody(); + while (!$body->eof()) { + $this->lastAvatar .= $body->read(8192); + } + $body->close(); + } + /** + * @When user :user gets avatar for type :type and id :id + * + * @param string $user + * @param string $type + * @param string $id + */ + public function userGetsAvatarForTypeAndId(string $user, string $type, string $id) { + $this->userGetsAvatarForTypeAndIdWithSize($user, $type, $id, '128'); + } + + /** + * @When user :user gets avatar for type :type and id :id with size :size + * + * @param string $user + * @param string $type + * @param string $id + * @param string $size + */ + public function userGetsAvatarForTypeAndIdWithSize(string $user, string $type, string $id, string $size) { + $this->userGetsAvatarForTypeAndIdWithSizeWith($user, $type, $id, $size, '200'); + } + + /** + * @When user :user gets avatar for type :type and id :id with size :size with :statusCode + * + * @param string $user + * @param string $type + * @param string $id + * @param string $size + * @param string $statusCode + */ + public function userGetsAvatarForTypeAndIdWithSizeWith(string $user, string $type, string $id, string $size, string $statusCode) { + $this->asAn($user); + $this->sendingToWith('GET', '/core/avatar/' . $type . '/' . $id . '/' . $size, null); + $this->theHTTPStatusCodeShouldBe($statusCode); + + if ($statusCode !== '200') { + return; + } + + $this->getLastAvatar(); + } + + /** + * @When user :user sets avatar for type :type and id :id from file :source + * + * @param string $user + * @param string $type + * @param string $id + * @param string $source + */ + public function userSetsAvatarForTypeAndIdFromFile(string $user, string $type, string $id, string $source) { + $this->userSetsAvatarForTypeAndIdFromFileWith($user, $type, $id, $source, '200'); + } + + /** + * @When user :user sets avatar for type :type and id :id from file :source with :statusCode + * + * @param string $user + * @param string $type + * @param string $id + * @param string $source + * @param string $statusCode + */ + public function userSetsAvatarForTypeAndIdFromFileWith(string $user, string $type, string $id, string $source, string $statusCode) { + $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r')); + + $this->asAn($user); + $this->sendingToWith('POST', '/core/avatar/' . $type . '/' . $id, + [ + 'multipart' => [ + [ + 'name' => 'files[]', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe($statusCode); + } + + /** + * @When user :user deletes avatar for type :type and id :id + * + * @param string $user + * @param string $type + * @param string $id + */ + public function userDeletesAvatarForTypeAndId(string $user, string $type, string $id) { + $this->userDeletesAvatarForTypeAndIdWith($user, $type, $id, '200'); + } + + /** + * @When user :user deletes avatar for type :type and id :id with :statusCode + * + * @param string $user + * @param string $type + * @param string $id + * @param string $statusCode + */ + public function userDeletesAvatarForTypeAndIdWith(string $user, string $type, string $id, string $statusCode) { + $this->asAn($user); + $this->sendingToWith('DELETE', '/core/avatar/' . $type . '/' . $id, null); + $this->theHTTPStatusCodeShouldBe($statusCode); + } + + /** + * @When user :user gets avatar for user :userAvatar + * + * @param string $user + * @param string $userAvatar + */ + public function userGetsAvatarForUser(string $user, string $userAvatar) { + $this->userGetsAvatarForUserWithSize($user, $userAvatar, '128'); + } + + /** + * @When user :user gets avatar for user :userAvatar with size :size + * + * @param string $user + * @param string $userAvatar + * @param string $size + */ + public function userGetsAvatarForUserWithSize(string $user, string $userAvatar, string $size) { + $this->asAn($user); + $this->sendingToDirectUrl('GET', '/index.php/avatar/' . $userAvatar . '/' . $size); + $this->theHTTPStatusCodeShouldBe('200'); + + $this->getLastAvatar(); + } + + /** + * @When user :user gets avatar for guest :guestAvatar + * + * @param string $user + * @param string $guestAvatar + */ + public function userGetsAvatarForGuest(string $user, string $guestAvatar) { + $this->asAn($user); + $this->sendingToDirectUrl('GET', '/index.php/avatar/guest/' . $guestAvatar . '/128'); + $this->theHTTPStatusCodeShouldBe('201'); + + $this->getLastAvatar(); + } + + /** + * @When logged in user gets temporary avatar + */ + public function loggedInUserGetsTemporaryAvatar() { + $this->loggedInUserGetsTemporaryAvatarWith('200'); + } + + /** + * @When logged in user gets temporary avatar with :statusCode + * + * @param string $statusCode + */ + public function loggedInUserGetsTemporaryAvatarWith(string $statusCode) { + $this->sendingAToWithRequesttoken('GET', '/index.php/avatar/tmp'); + $this->theHTTPStatusCodeShouldBe($statusCode); + + $this->getLastAvatar(); + } + + /** + * @When logged in user posts temporary avatar from file :source + * + * @param string $source + */ + public function loggedInUserPostsTemporaryAvatarFromFile(string $source) { + $file = \GuzzleHttp\Psr7\stream_for(fopen($source, 'r')); + + $this->sendingAToWithRequesttoken('POST', '/index.php/avatar', + [ + 'multipart' => [ + [ + 'name' => 'files[]', + 'contents' => $file + ] + ] + ]); + $this->theHTTPStatusCodeShouldBe('200'); + } + + /** + * @When logged in user posts temporary avatar from internal path :path + * + * @param string $path + */ + public function loggedInUserPostsTemporaryAvatarFromInternalPath(string $path) { + $this->sendingAToWithRequesttoken('POST', '/index.php/avatar?path=' . $path); + $this->theHTTPStatusCodeShouldBe('200'); + } + + /** + * @When logged in user crops temporary avatar + * + * @param TableNode $crop + */ + public function loggedInUserCropsTemporaryAvatar(TableNode $crop) { + $this->loggedInUserCropsTemporaryAvatarWith('200', $crop); + } + + /** + * @When logged in user crops temporary avatar with :statusCode + * + * @param string $statusCode + * @param TableNode $crop + */ + public function loggedInUserCropsTemporaryAvatarWith(string $statusCode, TableNode $crop) { + $parameters = []; + foreach ($crop->getRowsHash() as $key => $value) { + $parameters[] = 'crop[' . $key . ']=' . $value; + } + + $this->sendingAToWithRequesttoken('POST', '/index.php/avatar/cropped?' . implode('&', $parameters)); + $this->theHTTPStatusCodeShouldBe($statusCode); + } + + /** + * @When logged in user deletes the user avatar + */ + public function loggedInUserDeletesTheUserAvatar() { + $this->sendingAToWithRequesttoken('DELETE', '/index.php/avatar'); + $this->theHTTPStatusCodeShouldBe('200'); + } + + /** + * @Then last avatar is a square of size :size + * + * @param string size + */ + public function lastAvatarIsASquareOfSize(string $size) { + list($width, $height) = getimagesizefromstring($this->lastAvatar); + + Assert::assertEquals($width, $height, 'Avatar is not a square'); + Assert::assertEquals($size, $width); + } + + /** + * @Then last avatar is not a single color + */ + public function lastAvatarIsNotASingleColor() { + Assert::assertEquals(null, $this->getColorFromLastAvatar()); + } + + /** + * @Then last avatar is a single :color color + * + * @param string $color + * @param string $size + */ + public function lastAvatarIsASingleColor(string $color) { + $expectedColor = $this->hexStringToRgbColor($color); + $colorFromLastAvatar = $this->getColorFromLastAvatar(); + + Assert::assertTrue($this->isSameColor($expectedColor, $colorFromLastAvatar), + $this->rgbColorToHexString($colorFromLastAvatar) . ' does not match expected ' . $color); + } + + private function hexStringToRgbColor($hexString) { + // Strip initial "#" + $hexString = substr($hexString, 1); + + $rgbColorInt = hexdec($hexString); + + // RGBA hex strings are not supported; the given string is assumed to be + // an RGB hex string. + return [ + 'red' => ($rgbColorInt >> 16) & 0xFF, + 'green' => ($rgbColorInt >> 8) & 0xFF, + 'blue' => $rgbColorInt & 0xFF, + 'alpha' => 0 + ]; + } + + private function rgbColorToHexString($rgbColor) { + $rgbColorInt = ($rgbColor['red'] << 16) + ($rgbColor['green'] << 8) + ($rgbColor['blue']); + + return '#' . str_pad(strtoupper(dechex($rgbColorInt)), 6, '0', STR_PAD_LEFT); + } + + private function getColorFromLastAvatar() { + $image = imagecreatefromstring($this->lastAvatar); + + $firstPixelColorIndex = imagecolorat($image, 0, 0); + $firstPixelColor = imagecolorsforindex($image, $firstPixelColorIndex); + + for ($i = 0; $i < imagesx($image); $i++) { + for ($j = 0; $j < imagesx($image); $j++) { + $currentPixelColorIndex = imagecolorat($image, $i, $j); + $currentPixelColor = imagecolorsforindex($image, $currentPixelColorIndex); + + // The colors are compared with a small allowed delta, as even + // on solid color images the resizing can cause some small + // artifacts that slightly modify the color of certain pixels. + if (!$this->isSameColor($firstPixelColor, $currentPixelColor)) { + imagedestroy($image); + + return null; + } + } + } + + imagedestroy($image); + + return $firstPixelColor; + } + + private function isSameColor(array $firstColor, array $secondColor, int $allowedDelta = 1) { + if ($this->isSameColorComponent($firstColor['red'], $secondColor['red'], $allowedDelta) && + $this->isSameColorComponent($firstColor['green'], $secondColor['green'], $allowedDelta) && + $this->isSameColorComponent($firstColor['blue'], $secondColor['blue'], $allowedDelta) && + $this->isSameColorComponent($firstColor['alpha'], $secondColor['alpha'], $allowedDelta)) { + return true; + } + + return false; + } + + private function isSameColorComponent(int $firstColorComponent, int $secondColorComponent, int $allowedDelta) { + if ($firstColorComponent >= ($secondColorComponent - $allowedDelta) && + $firstColorComponent <= ($secondColorComponent + $allowedDelta)) { + return true; + } + + return false; + } +} diff --git a/build/integration/features/bootstrap/BasicStructure.php b/build/integration/features/bootstrap/BasicStructure.php index eed0f173ced46..b47840b75cadb 100644 --- a/build/integration/features/bootstrap/BasicStructure.php +++ b/build/integration/features/bootstrap/BasicStructure.php @@ -44,6 +44,7 @@ trait BasicStructure { use Auth; + use Avatar; use Download; use Mail; use Trashbin; @@ -178,7 +179,7 @@ public function sendingToWith($verb, $url, $body) { $options = []; if ($this->currentUser === 'admin') { $options['auth'] = $this->adminUser; - } else { + } elseif (strpos($this->currentUser, 'anonymous') !== 0) { $options['auth'] = [$this->currentUser, $this->regularUser]; } $options['headers'] = [ @@ -187,6 +188,8 @@ public function sendingToWith($verb, $url, $body) { if ($body instanceof TableNode) { $fd = $body->getRowsHash(); $options['form_params'] = $fd; + } elseif ($body) { + $options = array_merge($options, $body); } // TODO: Fix this hack! @@ -218,7 +221,7 @@ public function sendingToWithDirectUrl($verb, $url, $body) { $options = []; if ($this->currentUser === 'admin') { $options['auth'] = $this->adminUser; - } else { + } elseif (strpos($this->currentUser, 'anonymous') !== 0) { $options['auth'] = [$this->currentUser, $this->regularUser]; } if ($body instanceof TableNode) { @@ -307,21 +310,31 @@ public function loggingInUsingWebAs($user) { * @When Sending a :method to :url with requesttoken * @param string $method * @param string $url + * @param TableNode|array|null $body */ - public function sendingAToWithRequesttoken($method, $url) { + public function sendingAToWithRequesttoken($method, $url, $body = null) { $baseUrl = substr($this->baseUrl, 0, -5); + $options = [ + 'cookies' => $this->cookieJar, + 'headers' => [ + 'requesttoken' => $this->requestToken + ], + ]; + + if ($body instanceof TableNode) { + $fd = $body->getRowsHash(); + $options['form_params'] = $fd; + } elseif ($body) { + $options = array_merge($options, $body); + } + $client = new Client(); try { $this->response = $client->request( $method, $baseUrl . $url, - [ - 'cookies' => $this->cookieJar, - 'headers' => [ - 'requesttoken' => $this->requestToken - ] - ] + $options ); } catch (ClientException $e) { $this->response = $e->getResponse(); diff --git a/core/Controller/GenericAvatarController.php b/core/Controller/GenericAvatarController.php new file mode 100644 index 0000000000000..d98912633881f --- /dev/null +++ b/core/Controller/GenericAvatarController.php @@ -0,0 +1,242 @@ + + * + * @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 . + * + */ + +namespace OC\Core\Controller; + +use OCP\AppFramework\OCSController; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\Response; +use OCP\AvatarProviderException; +use OCP\Files\NotFoundException; +use OCP\IAvatarManager; +use OCP\IL10N; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class GenericAvatarController extends OCSController { + + /** @var IAvatarManager */ + protected $avatarManager; + + /** @var IL10N */ + protected $l; + + /** @var LoggerInterface */ + protected $logger; + + public function __construct($appName, + IRequest $request, + IAvatarManager $avatarManager, + IL10N $l10n, + LoggerInterface $logger) { + parent::__construct($appName, $request); + + $this->avatarManager = $avatarManager; + $this->l = $l10n; + $this->logger = $logger; + } + + /** + * @PublicPage + * + * @param string $avatarType + * @param string $avatarId + * @param int $size + * @return DataResponse|FileDisplayResponse + */ + public function getAvatar(string $avatarType, string $avatarId, int $size): Response { + $size = $this->sanitizeSize($size); + + try { + $avatarProvider = $this->avatarManager->getAvatarProvider($avatarType); + $avatar = $avatarProvider->getAvatar($avatarId); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (AvatarProviderException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$avatarProvider->canBeAccessedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatarFile = $avatar->getFile($size); + $response = new FileDisplayResponse( + $avatarFile, + Http::STATUS_OK, + [ + 'Content-Type' => $avatarFile->getMimeType(), + 'X-NC-IsCustomAvatar' => $avatar->isCustomAvatar() ? '1' : '0', + ] + ); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + $cache = $avatarProvider->getCacheTimeToLive($avatar); + if ($cache !== null) { + $response->cacheFor($cache); + } + + return $response; + } + + /** + * Returns the closest value to the predefined set of sizes + * + * @param int $size the size to sanitize + * @return int the sanitized size + */ + private function sanitizeSize(int $size): int { + $validSizes = [64, 128, 256, 512]; + + if ($size < $validSizes[0]) { + return $validSizes[0]; + } + + if ($size > $validSizes[count($validSizes) - 1]) { + return $validSizes[count($validSizes) - 1]; + } + + for ($i = 0; $i < count($validSizes) - 1; $i++) { + if ($size >= $validSizes[$i] && $size <= $validSizes[$i + 1]) { + $middlePoint = ($validSizes[$i] + $validSizes[$i + 1]) / 2; + if ($size < $middlePoint) { + return $validSizes[$i]; + } + return $validSizes[$i + 1]; + } + } + + return $size; + } + + /** + * @PublicPage + * + * @param string $path + * @return DataResponse + */ + public function setAvatar(string $avatarType, string $avatarId): DataResponse { + $files = $this->request->getUploadedFile('files'); + + if (is_null($files)) { + return new DataResponse( + ['data' => ['message' => $this->l->t('No file provided')]], + Http::STATUS_BAD_REQUEST + ); + } + + if ( + $files['error'][0] !== 0 || + !is_uploaded_file($files['tmp_name'][0]) || + \OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0]) + ) { + return new DataResponse( + ['data' => ['message' => $this->l->t('Invalid file provided')]], + Http::STATUS_BAD_REQUEST + ); + } + + if ($files['size'][0] > 20 * 1024 * 1024) { + return new DataResponse( + ['data' => ['message' => $this->l->t('File is too big')]], + Http::STATUS_BAD_REQUEST + ); + } + + $content = file_get_contents($files['tmp_name'][0]); + unlink($files['tmp_name'][0]); + + $image = new \OC_Image(); + $image->loadFromData($content); + + try { + $avatarProvider = $this->avatarManager->getAvatarProvider($avatarType); + $avatar = $avatarProvider->getAvatar($avatarId); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (AvatarProviderException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$avatarProvider->canBeModifiedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatar->set($image); + return new DataResponse( + ['status' => 'success'] + ); + } catch (\OC\NotSquareException $e) { + return new DataResponse( + ['data' => ['message' => $this->l->t('Crop is not square')]], + Http::STATUS_BAD_REQUEST + ); + } catch (\Exception $e) { + $this->logger->error('Error when setting avatar', ['app' => 'core', 'exception' => $e]); + return new DataResponse( + ['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], + Http::STATUS_BAD_REQUEST + ); + } + } + + /** + * @PublicPage + * + * @return DataResponse + */ + public function deleteAvatar(string $avatarType, string $avatarId): DataResponse { + try { + $avatarProvider = $this->avatarManager->getAvatarProvider($avatarType); + $avatar = $avatarProvider->getAvatar($avatarId); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (AvatarProviderException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + if (!$avatarProvider->canBeModifiedByCurrentUser($avatar)) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $avatar->remove(); + return new DataResponse(); + } catch (\Exception $e) { + $this->logger->error('Error when deleting avatar', ['app' => 'core', 'exception' => $e]); + return new DataResponse( + ['data' => ['message' => $this->l->t('An error occurred. Please contact your admin.')]], + Http::STATUS_BAD_REQUEST + ); + } + } +} diff --git a/core/routes.php b/core/routes.php index 9fa378dc1d8ed..07fa69c732067 100644 --- a/core/routes.php +++ b/core/routes.php @@ -103,6 +103,9 @@ ['root' => '/core', 'name' => 'AppPassword#getAppPassword', 'url' => '/getapppassword', 'verb' => 'GET'], ['root' => '/core', 'name' => 'AppPassword#rotateAppPassword', 'url' => '/apppassword/rotate', 'verb' => 'POST'], ['root' => '/core', 'name' => 'AppPassword#deleteAppPassword', 'url' => '/apppassword', 'verb' => 'DELETE'], + ['root' => '/core', 'name' => 'GenericAvatar#getAvatar', 'url' => '/avatar/{avatarType}/{avatarId}/{size}', 'verb' => 'GET'], + ['root' => '/core', 'name' => 'GenericAvatar#setAvatar', 'url' => '/avatar/{avatarType}/{avatarId}', 'verb' => 'POST'], + ['root' => '/core', 'name' => 'GenericAvatar#deleteAvatar', 'url' => '/avatar/{avatarType}/{avatarId}', 'verb' => 'DELETE'], ['root' => '/collaboration', 'name' => 'CollaborationResources#searchCollections', 'url' => '/resources/collections/search/{filter}', 'verb' => 'GET'], ['root' => '/collaboration', 'name' => 'CollaborationResources#listCollection', 'url' => '/resources/collections/{collectionId}', 'verb' => 'GET'], diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 0400e68109056..1da2055002701 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -105,6 +105,7 @@ 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php', 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorProviderDisabled' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderDisabled.php', 'OCP\\AutoloadNotAllowedException' => $baseDir . '/lib/public/AutoloadNotAllowedException.php', + 'OCP\\AvatarProviderException' => $baseDir . '/lib/public/AvatarProviderException.php', 'OCP\\BackgroundJob' => $baseDir . '/lib/public/BackgroundJob.php', 'OCP\\BackgroundJob\\IJob' => $baseDir . '/lib/public/BackgroundJob/IJob.php', 'OCP\\BackgroundJob\\IJobList' => $baseDir . '/lib/public/BackgroundJob/IJobList.php', @@ -363,6 +364,7 @@ 'OCP\\IAppConfig' => $baseDir . '/lib/public/IAppConfig.php', 'OCP\\IAvatar' => $baseDir . '/lib/public/IAvatar.php', 'OCP\\IAvatarManager' => $baseDir . '/lib/public/IAvatarManager.php', + 'OCP\\IAvatarProvider' => $baseDir . '/lib/public/IAvatarProvider.php', 'OCP\\ICache' => $baseDir . '/lib/public/ICache.php', 'OCP\\ICacheFactory' => $baseDir . '/lib/public/ICacheFactory.php', 'OCP\\ICertificate' => $baseDir . '/lib/public/ICertificate.php', @@ -721,7 +723,9 @@ 'OC\\Avatar\\Avatar' => $baseDir . '/lib/private/Avatar/Avatar.php', 'OC\\Avatar\\AvatarManager' => $baseDir . '/lib/private/Avatar/AvatarManager.php', 'OC\\Avatar\\GuestAvatar' => $baseDir . '/lib/private/Avatar/GuestAvatar.php', + 'OC\\Avatar\\GuestAvatarProvider' => $baseDir . '/lib/private/Avatar/GuestAvatarProvider.php', 'OC\\Avatar\\UserAvatar' => $baseDir . '/lib/private/Avatar/UserAvatar.php', + 'OC\\Avatar\\UserAvatarProvider' => $baseDir . '/lib/private/Avatar/UserAvatarProvider.php', 'OC\\BackgroundJob\\Job' => $baseDir . '/lib/private/BackgroundJob/Job.php', 'OC\\BackgroundJob\\JobList' => $baseDir . '/lib/private/BackgroundJob/JobList.php', 'OC\\BackgroundJob\\Legacy\\QueuedJob' => $baseDir . '/lib/private/BackgroundJob/Legacy/QueuedJob.php', @@ -875,6 +879,7 @@ 'OC\\Core\\Controller\\CollaborationResourcesController' => $baseDir . '/core/Controller/CollaborationResourcesController.php', 'OC\\Core\\Controller\\ContactsMenuController' => $baseDir . '/core/Controller/ContactsMenuController.php', 'OC\\Core\\Controller\\CssController' => $baseDir . '/core/Controller/CssController.php', + 'OC\\Core\\Controller\\GenericAvatarController' => $baseDir . '/core/Controller/GenericAvatarController.php', 'OC\\Core\\Controller\\GuestAvatarController' => $baseDir . '/core/Controller/GuestAvatarController.php', 'OC\\Core\\Controller\\JsController' => $baseDir . '/core/Controller/JsController.php', 'OC\\Core\\Controller\\LoginController' => $baseDir . '/core/Controller/LoginController.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b9b4f2f307b8c..11532cdc3ae4e 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -134,6 +134,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorException' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/TwoFactorException.php', 'OCP\\Authentication\\TwoFactorAuth\\TwoFactorProviderDisabled' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderDisabled.php', 'OCP\\AutoloadNotAllowedException' => __DIR__ . '/../../..' . '/lib/public/AutoloadNotAllowedException.php', + 'OCP\\AvatarProviderException' => __DIR__ . '/../../..' . '/lib/public/AvatarProviderException.php', 'OCP\\BackgroundJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob.php', 'OCP\\BackgroundJob\\IJob' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJob.php', 'OCP\\BackgroundJob\\IJobList' => __DIR__ . '/../../..' . '/lib/public/BackgroundJob/IJobList.php', @@ -392,6 +393,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\IAppConfig' => __DIR__ . '/../../..' . '/lib/public/IAppConfig.php', 'OCP\\IAvatar' => __DIR__ . '/../../..' . '/lib/public/IAvatar.php', 'OCP\\IAvatarManager' => __DIR__ . '/../../..' . '/lib/public/IAvatarManager.php', + 'OCP\\IAvatarProvider' => __DIR__ . '/../../..' . '/lib/public/IAvatarProvider.php', 'OCP\\ICache' => __DIR__ . '/../../..' . '/lib/public/ICache.php', 'OCP\\ICacheFactory' => __DIR__ . '/../../..' . '/lib/public/ICacheFactory.php', 'OCP\\ICertificate' => __DIR__ . '/../../..' . '/lib/public/ICertificate.php', @@ -750,7 +752,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Avatar\\Avatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/Avatar.php', 'OC\\Avatar\\AvatarManager' => __DIR__ . '/../../..' . '/lib/private/Avatar/AvatarManager.php', 'OC\\Avatar\\GuestAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatar.php', + 'OC\\Avatar\\GuestAvatarProvider' => __DIR__ . '/../../..' . '/lib/private/Avatar/GuestAvatarProvider.php', 'OC\\Avatar\\UserAvatar' => __DIR__ . '/../../..' . '/lib/private/Avatar/UserAvatar.php', + 'OC\\Avatar\\UserAvatarProvider' => __DIR__ . '/../../..' . '/lib/private/Avatar/UserAvatarProvider.php', 'OC\\BackgroundJob\\Job' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/Job.php', 'OC\\BackgroundJob\\JobList' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/JobList.php', 'OC\\BackgroundJob\\Legacy\\QueuedJob' => __DIR__ . '/../../..' . '/lib/private/BackgroundJob/Legacy/QueuedJob.php', @@ -904,6 +908,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Controller\\CollaborationResourcesController' => __DIR__ . '/../../..' . '/core/Controller/CollaborationResourcesController.php', 'OC\\Core\\Controller\\ContactsMenuController' => __DIR__ . '/../../..' . '/core/Controller/ContactsMenuController.php', 'OC\\Core\\Controller\\CssController' => __DIR__ . '/../../..' . '/core/Controller/CssController.php', + 'OC\\Core\\Controller\\GenericAvatarController' => __DIR__ . '/../../..' . '/core/Controller/GenericAvatarController.php', 'OC\\Core\\Controller\\GuestAvatarController' => __DIR__ . '/../../..' . '/core/Controller/GuestAvatarController.php', 'OC\\Core\\Controller\\JsController' => __DIR__ . '/../../..' . '/core/Controller/JsController.php', 'OC\\Core\\Controller\\LoginController' => __DIR__ . '/../../..' . '/core/Controller/LoginController.php', diff --git a/lib/private/AppFramework/Bootstrap/Coordinator.php b/lib/private/AppFramework/Bootstrap/Coordinator.php index 06a17e5242b57..7d6957fafae19 100644 --- a/lib/private/AppFramework/Bootstrap/Coordinator.php +++ b/lib/private/AppFramework/Bootstrap/Coordinator.php @@ -29,6 +29,8 @@ namespace OC\AppFramework\Bootstrap; +use OC\Avatar\GuestAvatarProvider; +use OC\Avatar\UserAvatarProvider; use OC\Support\CrashReport\Registry; use OC_App; use OCP\AppFramework\App; @@ -79,6 +81,11 @@ public function __construct(IServerContainer $container, } public function runInitialRegistration(): void { + if ($this->registrationContext === null) { + $this->registrationContext = new RegistrationContext($this->logger); + } + + $this->registerCore(); $this->registerApps(OC_App::getEnabledApps()); } @@ -86,13 +93,15 @@ public function runLazyRegistration(string $appId): void { $this->registerApps([$appId]); } + private function registerCore(): void { + $this->registrationContext->registerAvatarProvider('', 'user', UserAvatarProvider::class); + $this->registrationContext->registerAvatarProvider('', 'guest', GuestAvatarProvider::class); + } + /** * @param string[] $appIds */ private function registerApps(array $appIds): void { - if ($this->registrationContext === null) { - $this->registrationContext = new RegistrationContext($this->logger); - } $apps = []; foreach ($appIds as $appId) { /* diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 12fca23c51fd8..d4c32dc7caa60 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -72,6 +72,9 @@ class RegistrationContext { /** @var array[] */ private $initialStates = []; + /** @var string[] */ + private $avatarProviders = []; + /** @var ILogger */ private $logger; @@ -174,6 +177,14 @@ public function registerInitialStateProvider(string $class): void { $class ); } + + public function registerAvatarProvider(string $type, string $class): void { + $this->context->registerAvatarProvider( + $this->appId, + $type, + $class + ); + } }; } @@ -260,6 +271,10 @@ public function registerInitialState(string $appId, string $class): void { ]; } + public function registerAvatarProvider(string $appId, string $type, string $class): void { + $this->avatarProviders[$type] = $class; + } + /** * @param App[] $apps */ @@ -437,4 +452,11 @@ public function getAlternativeLogins(): array { public function getInitialStates(): array { return $this->initialStates; } + + /** + * @erturn array[] + */ + public function getAvatarProviders(): array { + return $this->avatarProviders; + } } diff --git a/lib/private/Avatar/Avatar.php b/lib/private/Avatar/Avatar.php index 02fc04eae3663..0877a97613eb5 100644 --- a/lib/private/Avatar/Avatar.php +++ b/lib/private/Avatar/Avatar.php @@ -43,14 +43,14 @@ use OC_Image; use OCP\Files\NotFoundException; use OCP\IAvatar; -use OCP\ILogger; +use Psr\Log\LoggerInterface; /** * This class gets and sets users avatars. */ abstract class Avatar implements IAvatar { - /** @var ILogger */ + /** @var LoggerInterface */ protected $logger; /** @@ -72,9 +72,9 @@ abstract class Avatar implements IAvatar { /** * The base avatar constructor. * - * @param ILogger $logger The logger + * @param LoggerInterface $logger The logger */ - public function __construct(ILogger $logger) { + public function __construct(LoggerInterface $logger) { $this->logger = $logger; } diff --git a/lib/private/Avatar/AvatarManager.php b/lib/private/Avatar/AvatarManager.php index 5102396224d87..899e6f5619b1b 100644 --- a/lib/private/Avatar/AvatarManager.php +++ b/lib/private/Avatar/AvatarManager.php @@ -34,57 +34,61 @@ namespace OC\Avatar; -use OC\User\Manager; +use OC\AppFramework\Bootstrap\Coordinator; use OC\User\NoUserException; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; use OCP\IAvatar; use OCP\IAvatarManager; +use OCP\IAvatarProvider; use OCP\IConfig; -use OCP\IL10N; -use OCP\ILogger; +use OCP\IServerContainer; +use Psr\Log\LoggerInterface; /** * This class implements methods to access Avatar functionality */ class AvatarManager implements IAvatarManager { - /** @var Manager */ - private $userManager; - /** @var IAppData */ private $appData; - /** @var IL10N */ - private $l; - - /** @var ILogger */ + /** @var LoggerInterface */ private $logger; /** @var IConfig */ private $config; + /** @var IServerContainer */ + private $serverContainer; + + /** @var Coordinator */ + private $bootstrapCoordinator; + + /** @var IAvatarProvider[] */ + private $providers = []; + /** * AvatarManager constructor. * - * @param Manager $userManager * @param IAppData $appData - * @param IL10N $l - * @param ILogger $logger + * @param LoggerInterface $logger * @param IConfig $config + * @param IServerContainer $serverContainer + * @param Coordinator $bootstrapCoordinator */ public function __construct( - Manager $userManager, IAppData $appData, - IL10N $l, - ILogger $logger, - IConfig $config) { - $this->userManager = $userManager; + LoggerInterface $logger, + IConfig $config, + IServerContainer $serverContainer, + Coordinator $bootstrapCoordinator) { $this->appData = $appData; - $this->l = $l; $this->logger = $logger; $this->config = $config; + $this->serverContainer = $serverContainer; + $this->bootstrapCoordinator = $bootstrapCoordinator; } /** @@ -96,21 +100,7 @@ public function __construct( * @throws NotFoundException In case there is no user folder yet */ public function getAvatar(string $userId) : IAvatar { - $user = $this->userManager->get($userId); - if ($user === null) { - throw new \Exception('user does not exist'); - } - - // sanitize userID - fixes casing issue (needed for the filesystem stuff that is done below) - $userId = $user->getUID(); - - try { - $folder = $this->appData->getFolder($userId); - } catch (NotFoundException $e) { - $folder = $this->appData->newFolder($userId); - } - - return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); + return $this->getAvatarProvider('user')->getAvatar($userId); } /** @@ -150,6 +140,26 @@ public function deleteUserAvatar(string $userId): void { * @return IAvatar */ public function getGuestAvatar(string $name): IAvatar { - return new GuestAvatar($name, $this->logger); + return $this->getAvatarProvider('guest')->getAvatar($name); + } + + public function getAvatarProvider(string $type): IAvatarProvider { + $context = $this->bootstrapCoordinator->getRegistrationContext(); + + if ($context === null) { + throw new \RuntimeException("Avatar provider requested before the apps had been fully registered"); + } + + $providerClasses = $context->getAvatarProviders(); + + if (!array_key_exists($type, $providerClasses)) { + throw new \InvalidArgumentException('Unknown avatar type: ' . $type); + } + + if (!array_key_exists($type, $this->providers)) { + $this->providers[$type] = $this->serverContainer->get($providerClasses[$type]); + } + + return $this->providers[$type]; } } diff --git a/lib/private/Avatar/GuestAvatar.php b/lib/private/Avatar/GuestAvatar.php index cc7e21b9fe647..c208353d72644 100644 --- a/lib/private/Avatar/GuestAvatar.php +++ b/lib/private/Avatar/GuestAvatar.php @@ -27,7 +27,7 @@ namespace OC\Avatar; use OCP\Files\SimpleFS\InMemoryFile; -use OCP\ILogger; +use Psr\Log\LoggerInterface; /** * This class represents a guest user's avatar. @@ -44,9 +44,9 @@ class GuestAvatar extends Avatar { * GuestAvatar constructor. * * @param string $userDisplayName The guest user display name - * @param ILogger $logger The logger + * @param LoggerInterface $logger The logger */ - public function __construct(string $userDisplayName, ILogger $logger) { + public function __construct(string $userDisplayName, LoggerInterface $logger) { parent::__construct($logger); $this->userDisplayName = $userDisplayName; } diff --git a/lib/private/Avatar/GuestAvatarProvider.php b/lib/private/Avatar/GuestAvatarProvider.php new file mode 100644 index 0000000000000..0768fece2980e --- /dev/null +++ b/lib/private/Avatar/GuestAvatarProvider.php @@ -0,0 +1,94 @@ + + * + * @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 . + * + */ + +namespace OC\Avatar; + +use OCP\IAvatar; +use OCP\IAvatarProvider; +use Psr\Log\LoggerInterface; + +class GuestAvatarProvider implements IAvatarProvider { + + /** @var LoggerInterface */ + private $logger; + + public function __construct( + LoggerInterface $logger) { + $this->logger = $logger; + } + + /** + * Returns a GuestAvatar instance for the given guest name + * + * @param string $id the guest name, e.g. "Albert" + * @returns IAvatar + */ + public function getAvatar(string $id): IAvatar { + return new GuestAvatar($id, $this->logger); + } + + /** + * Returns whether the current user can access the given avatar or not + * + * @param IAvatar $avatar ignored + * @return bool true, as all users can access guest avatars + */ + public function canBeAccessedByCurrentUser(IAvatar $avatar): bool { + return true; + } + + /** + * Returns whether the current user can modify the given avatar or not + * + * @param IAvatar $avatar ignored + * @return bool false, as guest avatars can not be modified even by the + * guest of the avatar + */ + public function canBeModifiedByCurrentUser(IAvatar $avatar): bool { + return false; + } + + /** + * Returns the latest value of the avatar version + * + * @param IAvatar $avatar ignored + * @return int 0, as versions are not supported by guest avatars + */ + public function getVersion(IAvatar $avatar): int { + return 0; + } + + /** + * Returns the cache duration for guest avatars in seconds + * + * @param IAvatar $avatar ignored, same duration for all guest avatars + * @return int|null the cache duration + */ + public function getCacheTimeToLive(IAvatar $avatar): ?int { + // Cache for 30 minutes + return 60 * 30; + } +} diff --git a/lib/private/Avatar/UserAvatar.php b/lib/private/Avatar/UserAvatar.php index f7ace429f7d98..f6469b3f14964 100644 --- a/lib/private/Avatar/UserAvatar.php +++ b/lib/private/Avatar/UserAvatar.php @@ -39,7 +39,7 @@ use OCP\IConfig; use OCP\IImage; use OCP\IL10N; -use OCP\ILogger; +use Psr\Log\LoggerInterface; /** * This class represents a registered user's avatar. @@ -64,13 +64,13 @@ class UserAvatar extends Avatar { * @param ISimpleFolder $folder The avatar files folder * @param IL10N $l The localization helper * @param User $user The user this class manages the avatar for - * @param ILogger $logger The logger + * @param LoggerInterface $logger The logger */ public function __construct( ISimpleFolder $folder, IL10N $l, $user, - ILogger $logger, + LoggerInterface $logger, IConfig $config) { parent::__construct($logger); $this->folder = $folder; @@ -79,6 +79,10 @@ public function __construct( $this->config = $config; } + public function getUser(): User { + return $this->user; + } + /** * Check if an avatar exists for the user * diff --git a/lib/private/Avatar/UserAvatarProvider.php b/lib/private/Avatar/UserAvatarProvider.php new file mode 100644 index 0000000000000..65d71a5d2096b --- /dev/null +++ b/lib/private/Avatar/UserAvatarProvider.php @@ -0,0 +1,159 @@ + + * + * @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 . + * + */ + +namespace OC\Avatar; + +use OC\Files\AppData\Factory as AppDataFactory; +use OCP\AvatarProviderException; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\IAvatar; +use OCP\IAvatarProvider; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory as L10NFactory; +use Psr\Log\LoggerInterface; + +class UserAvatarProvider implements IAvatarProvider { + + /** @var IUserManager */ + private $userManager; + + /** @var IAppData */ + private $appData; + + /** @var IL10N */ + private $l; + + /** @var LoggerInterface */ + private $logger; + + /** @var IConfig */ + private $config; + + /** @var IUser|null */ + protected $currentUser; + + public function __construct( + IUserManager $userManager, + AppDataFactory $appDataFactory, + L10NFactory $lFactory, + LoggerInterface $logger, + IConfig $config, + IUserSession $userSession) { + $this->userManager = $userManager; + $this->appData = $appDataFactory->get('avatar'); + $this->l = $lFactory->get('lib'); + $this->logger = $logger; + $this->config = $config; + $this->currentUser = $userSession->getUser(); + } + + /** + * Returns a UserAvatar instance for the given user id + * + * @param string $id the user id + * @returns IAvatar + * @throws AvatarProviderException if the user name does not exist + * @throws NotFoundException if there is no user folder yet + */ + public function getAvatar(string $id): IAvatar { + $user = $this->userManager->get($id); + if ($user === null) { + throw new AvatarProviderException('user ' . $id . ' does not exist'); + } + + // sanitize userID - fixes casing issue (needed for the filesystem stuff that is done below) + $userId = $user->getUID(); + + try { + $folder = $this->appData->getFolder($userId); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder($userId); + } + + return new UserAvatar($folder, $this->l, $user, $this->logger, $this->config); + } + + /** + * Returns whether the current user can access the given avatar or not + * + * @param IAvatar $avatar ignored + * @return bool true, as all users can access user avatars + */ + public function canBeAccessedByCurrentUser(IAvatar $avatar): bool { + return true; + } + + /** + * Returns whether the current user can modify the given avatar or not + * + * @param IAvatar $avatar the avatar to check + * @return bool true if the current user is the user of the avatar, false + * otherwise + * @throws \InvalidArgumentException if the given avatar is not a UserAvatar + */ + public function canBeModifiedByCurrentUser(IAvatar $avatar): bool { + if (!($avatar instanceof UserAvatar)) { + throw new \InvalidArgumentException(); + } + + if (!$this->currentUser) { + return false; + } + + return $avatar->getUser()->getUID() === $this->currentUser->getUID(); + } + + /** + * Returns the latest value of the avatar version + * + * @param IAvatar $avatar the avatar to check + * @return int the latest value of the avatar version + * @throws \InvalidArgumentException if the given avatar is not a UserAvatar + */ + public function getVersion(IAvatar $avatar): int { + if (!($avatar instanceof UserAvatar)) { + throw new \InvalidArgumentException(); + } + + return (int) $this->config->getUserValue($avatar->getUser()->getUID(), 'avatar', 'version', 0); + } + + /** + * Returns the cache duration for user avatars in seconds + * + * @param IAvatar $avatar ignored, same duration for all user avatars + * @return int|null the cache duration + */ + public function getCacheTimeToLive(IAvatar $avatar): ?int { + // Cache for 1 day + return 60 * 60 * 24; + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index b426c9c454dc6..31b85b5dc1629 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -60,6 +60,7 @@ use OC\App\AppStore\Bundles\BundleFetcher; use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher; +use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Http\Request; use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Events\LoginFailed; @@ -716,11 +717,11 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerService(AvatarManager::class, function (Server $c) { return new AvatarManager( - $c->get(\OC\User\Manager::class), $c->getAppDataDir('avatar'), - $c->getL10N('lib'), - $c->get(ILogger::class), - $c->get(\OCP\IConfig::class) + $c->get(LoggerInterface::class), + $c->get(\OCP\IConfig::class), + $c->get(IServerContainer::class), + $c->get(Coordinator::class) ); }); $this->registerAlias(IAvatarManager::class, AvatarManager::class); diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index aaf5ef00bfaf7..328b5d248166c 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -180,4 +180,18 @@ public function registerAlternativeLogin(string $class): void; * @since 21.0.0 */ public function registerInitialStateProvider(string $class): void; + + /** + * Register an avatar provider + * + * It is allowed to register more than one provider per app. + * + * @param string $class + * @psalm-param class-string<\OCP\IAvatarProvider> $class + * + * @return void + * + * @since 21.0.0 + */ + public function registerAvatarProvider(string $type, string $class): void; } diff --git a/lib/public/AvatarProviderException.php b/lib/public/AvatarProviderException.php new file mode 100644 index 0000000000000..87380d15fa2ff --- /dev/null +++ b/lib/public/AvatarProviderException.php @@ -0,0 +1,44 @@ + + * + * @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 . + * + */ + +namespace OCP; + +/** + * Generic exception thrown when an AvatarProvider can not perform an action + * + * @since 21.0.0 + */ +class AvatarProviderException extends \RuntimeException { + + /** + * @param string $message + * @param int $code + * @param \Exception $previous + */ + public function __construct(string $message = "", int $code = 0, \Exception $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/lib/public/IAvatarManager.php b/lib/public/IAvatarManager.php index 75ea886c16a7c..611c8a4e8415b 100644 --- a/lib/public/IAvatarManager.php +++ b/lib/public/IAvatarManager.php @@ -56,4 +56,14 @@ public function getAvatar(string $user) : IAvatar; * @since 16.0.0 */ public function getGuestAvatar(string $name): IAvatar; + + /** + * Returns an avatar provider instance of the given type + * + * @param string $type the type of the avatar + * @return IAvatarProvider + * @throws \InvalidArgumentException if the type is not known + * @since 21.0.0 + */ + public function getAvatarProvider(string $type): IAvatarProvider; } diff --git a/lib/public/IAvatarProvider.php b/lib/public/IAvatarProvider.php new file mode 100644 index 0000000000000..a20c0c2b06b8f --- /dev/null +++ b/lib/public/IAvatarProvider.php @@ -0,0 +1,113 @@ + + * + * @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 . + * + */ + +namespace OCP; + +/** + * This class acts as a factory for avatar instances + * + * @since 21.0.0 + */ +interface IAvatarProvider { + + /** + * Returns an IAvatar instance for the given id + * + * @param string $id the identifier of the avatar + * @return IAvatar the avatar instance + * @throws AvatarProviderException if an error occurred while getting the + * avatar + * @since 21.0.0 + */ + public function getAvatar(string $id): IAvatar; + + /** + * Returns whether the current user can access the given avatar or not + * + * Clients of IAvatarProvider should not try to access the avatar if not + * allowed, but they can ignore it if it makes sense. + * + * Implementers of IAvatarProvider may not throw \InvalidArgumentException + * if the behaviour does not depend on specific avatar instances. + * + * @param IAvatar $avatar the avatar to check + * @return bool true if the current user can access the avatar, false + * otherwise + * @throws \InvalidArgumentException if the given avatar is not supported by + * this provider + * @since 21.0.0 + */ + public function canBeAccessedByCurrentUser(IAvatar $avatar): bool; + + /** + * Returns whether the current user can modify the given avatar or not + * + * Clients of IAvatarProvider should not try to modify the avatar (including + * deletion) if not allowed, but they can ignore it if it makes sense. + * + * Implementers of IAvatarProvider may not throw \InvalidArgumentException + * if the behaviour does not depend on specific avatar instances. + * + * @param IAvatar $avatar the avatar to check + * @return bool true if the current user can modify the avatar, false + * otherwise + * @throws \InvalidArgumentException if the given avatar is not supported by + * this provider + * @since 21.0.0 + */ + public function canBeModifiedByCurrentUser(IAvatar $avatar): bool; + + /** + * Returns the latest value of the avatar version + * + * Implementers of IAvatarProvider may not throw \InvalidArgumentException + * if the behaviour does not depend on specific avatar instances (for + * example, if versions are not supported and the same version is always + * returned). + * + * @param IAvatar $avatar the avatar to check + * @return int the latest value of the avatar version + * @throws \InvalidArgumentException if the given avatar is not supported by + * this provider + * @since 21.0.0 + */ + public function getVersion(IAvatar $avatar): int; + + /** + * Returns the cache duration in seconds + * + * Implementers of IAvatarProvider may not throw \InvalidArgumentException + * if the behaviour does not depend on specific avatar instances. + * + * @param IAvatar $avatar the specific avatar, returned by this provider, to + * get the cache for + * @return int|null the cache duration, or null for no cache + * @throws \InvalidArgumentException if the given avatar is not supported by + * this provider + * @since 21.0.0 + */ + public function getCacheTimeToLive(IAvatar $avatar): ?int; +} diff --git a/tests/Core/Controller/GenericAvatarControllerTest.php b/tests/Core/Controller/GenericAvatarControllerTest.php new file mode 100644 index 0000000000000..005aa2c39aa01 --- /dev/null +++ b/tests/Core/Controller/GenericAvatarControllerTest.php @@ -0,0 +1,109 @@ + + * + * @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 . + * + */ + +namespace Tests\Core\Controller; + +use OC\Core\Controller\GenericAvatarController; +use OCP\IAvatarManager; +use OCP\IL10N; +use OCP\IRequest; +use Psr\Log\LoggerInterface; + +class GenericAvatarControllerTest extends \Test\TestCase { + + /** @var GenericAvatarController */ + private $genericAvatarController; + + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */ + private $avatarManager; + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->getMockBuilder(IRequest::class)->getMock(); + $this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock(); + $this->l = $this->getMockBuilder(IL10N::class)->getMock(); + $this->l->method('t')->willReturnArgument(0); + $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + $this->genericAvatarController = new GenericAvatarController( + 'core', + $this->request, + $this->avatarManager, + $this->l, + $this->logger + ); + } + + public function dataSanitizeSize(): array { + return [ + [-1, 64], + [32, 64], + + [64, 64], + [65, 64], + + [95, 64], + [96, 128], + + [127, 128], + [128, 128], + [129, 128], + + [191, 128], + [192, 256], + + [255, 256], + [256, 256], + [257, 256], + + [383, 256], + [384, 512], + + [511, 512], + [512, 512], + + [8192, 512], + + ]; + } + + /** + * @dataProvider dataSanitizeSize + * + * @param int $inputSize + * @param int $expectedSize + */ + public function testSanitizeSize(int $inputSize, int $expectedSize) { + $this->assertEquals($expectedSize, $this->invokePrivate($this->genericAvatarController, 'sanitizeSize', [$inputSize])); + } +} diff --git a/tests/lib/Avatar/GuestAvatarTest.php b/tests/lib/Avatar/GuestAvatarTest.php index 1c424234f100c..b8e6d8ae2e8a6 100644 --- a/tests/lib/Avatar/GuestAvatarTest.php +++ b/tests/lib/Avatar/GuestAvatarTest.php @@ -26,8 +26,8 @@ use OC\Avatar\GuestAvatar; use OCP\Files\SimpleFS\InMemoryFile; -use OCP\ILogger; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; /** @@ -48,8 +48,8 @@ class GuestAvatarTest extends TestCase { * @return void */ public function setupGuestAvatar() { - /* @var MockObject|ILogger $logger */ - $logger = $this->getMockBuilder(ILogger::class)->getMock(); + /* @var MockObject|LoggerInterface $logger */ + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); $this->guestAvatar = new GuestAvatar('einstein', $logger); } diff --git a/tests/lib/Avatar/AvatarManagerTest.php b/tests/lib/Avatar/UserAvatarProviderTest.php similarity index 60% rename from tests/lib/Avatar/AvatarManagerTest.php rename to tests/lib/Avatar/UserAvatarProviderTest.php index 5a061cd10e9e4..40c15366946fa 100644 --- a/tests/lib/Avatar/AvatarManagerTest.php +++ b/tests/lib/Avatar/UserAvatarProviderTest.php @@ -24,55 +24,81 @@ namespace Test\Avatar; -use OC\Avatar\AvatarManager; use OC\Avatar\UserAvatar; -use OC\User\Manager; +use OC\Avatar\UserAvatarProvider; +use OC\Files\AppData\AppData; +use OC\Files\AppData\Factory as AppDataFactory; use OCP\Files\IAppData; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; use OCP\IL10N; -use OCP\ILogger; use OCP\IUser; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory as L10NFactory; +use Psr\Log\LoggerInterface; /** - * Class AvatarManagerTest + * Class UserAvatarProviderTest */ -class AvatarManagerTest extends \Test\TestCase { - /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */ +class UserAvatarProviderTest extends \Test\TestCase { + /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ private $userManager; /** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */ private $appData; /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ private $l10n; - /** @var ILogger|\PHPUnit\Framework\MockObject\MockObject */ + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ private $logger; /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ private $config; - /** @var AvatarManager | \PHPUnit\Framework\MockObject\MockObject */ - private $avatarManager; + /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */ + private $currentUser; + /** @var UserAvatarProvider | \PHPUnit\Framework\MockObject\MockObject */ + private $userAvatarProvider; protected function setUp(): void { parent::setUp(); - $this->userManager = $this->createMock(Manager::class); - $this->appData = $this->createMock(IAppData::class); + $this->userManager = $this->createMock(IUserManager::class); + // The specific subclass rather than the interface needs to be mocked so + // PHPUnit does not complain about the returned type from the mocked + // "AppDataFactory::get". + $this->appData = $this->createMock(AppData::class); $this->l10n = $this->createMock(IL10N::class); - $this->logger = $this->createMock(ILogger::class); + $this->logger = $this->createMock(LoggerInterface::class); $this->config = $this->createMock(IConfig::class); + $this->currentUser = $this->createMock(IUser::class); - $this->avatarManager = new AvatarManager( + $appDataFactory = $this->createMock(AppDataFactory::class); + $appDataFactory + ->method('get') + ->with('avatar') + ->willReturn($this->appData); + $l10nFactory = $this->createMock(L10NFactory::class); + $l10nFactory + ->method('get') + ->with('lib') + ->willReturn($this->l10n); + $userSession = $this->createMock(IUserSession::class); + $userSession + ->method('getUser') + ->willReturn($this->currentUser); + + $this->userAvatarProvider = new UserAvatarProvider( $this->userManager, - $this->appData, - $this->l10n, + $appDataFactory, + $l10nFactory, $this->logger, - $this->config + $this->config, + $userSession ); } public function testGetAvatarInvalidUser() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('user does not exist'); + $this->expectExceptionMessage('user invalidUser does not exist'); $this->userManager ->expects($this->once()) @@ -80,7 +106,7 @@ public function testGetAvatarInvalidUser() { ->with('invalidUser') ->willReturn(null); - $this->avatarManager->getAvatar('invalidUser'); + $this->userAvatarProvider->getAvatar('invalidUser'); } public function testGetAvatarValidUser() { @@ -102,7 +128,7 @@ public function testGetAvatarValidUser() { ->willReturn($folder); $expected = new UserAvatar($folder, $this->l10n, $user, $this->logger, $this->config); - $this->assertEquals($expected, $this->avatarManager->getAvatar('valid-user')); + $this->assertEquals($expected, $this->userAvatarProvider->getAvatar('valid-user')); } public function testGetAvatarValidUserDifferentCasing() { @@ -124,6 +150,6 @@ public function testGetAvatarValidUserDifferentCasing() { ->willReturn($folder); $expected = new UserAvatar($folder, $this->l10n, $user, $this->logger, $this->config); - $this->assertEquals($expected, $this->avatarManager->getAvatar('vaLid-USER')); + $this->assertEquals($expected, $this->userAvatarProvider->getAvatar('vaLid-USER')); } } diff --git a/tests/lib/Avatar/UserAvatarTest.php b/tests/lib/Avatar/UserAvatarTest.php index cf0edad950277..d0de36fd09a59 100644 --- a/tests/lib/Avatar/UserAvatarTest.php +++ b/tests/lib/Avatar/UserAvatarTest.php @@ -16,7 +16,7 @@ use OCP\Files\SimpleFS\ISimpleFile; use OCP\IConfig; use OCP\IL10N; -use OCP\ILogger; +use Psr\Log\LoggerInterface; class UserAvatarTest extends \Test\TestCase { /** @var Folder | \PHPUnit\Framework\MockObject\MockObject */ @@ -286,7 +286,7 @@ private function getUserAvatar($user) { $this->folder, $l, $user, - $this->createMock(ILogger::class), + $this->createMock(LoggerInterface::class), $this->config ); }