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
);
}