Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Added

### Fixed

- Fix: OpenProject avatar remains stale [#1058](https://github.com/nextcloud/integration_openproject/pull/1058)

### Changed

### Removed
Expand Down
21 changes: 15 additions & 6 deletions lib/Controller/OpenProjectAPIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCSController;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
Expand Down Expand Up @@ -70,7 +70,7 @@ public function getOpenProjectUrl(): DataResponse {
*
* @param string $userId
* @param string $userName
* @return DataDisplayResponse|DataDownloadResponse
* @return DataDownloadResponse|Response
* @throws \OCP\Files\NotFoundException
* @throws \OCP\Files\NotPermittedException
* @throws \OCP\Lock\LockedException
Expand All @@ -81,11 +81,20 @@ public function getOpenProjectAvatar(string $userId = '', string $userName = '')
$result = $this->openprojectAPIService->getOpenProjectAvatar(
$userId, $userName, $this->userId
);
$response = new DataDownloadResponse(
$result['avatar'], 'avatar', $result['type'] ?? ''

$etag = '"' . $result['etag'] . '"';
$headers = [
'Cache-Control' => 'no-cache',
'ETag' => $etag,
];
$ifNoneMatch = $this->request->getHeader('If-None-Match');
if ($ifNoneMatch && $ifNoneMatch === $etag) {
return new Response(Http::STATUS_NOT_MODIFIED, $headers);
}

return new DataDownloadResponse(
$result['avatar'], 'avatar', $result['type'], Http::STATUS_OK, $headers
);
$response->cacheFor(60 * 60 * 24);
return $response;
}

/**
Expand Down
12 changes: 9 additions & 3 deletions lib/Service/OpenProjectAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ private function searchRequest(string $userId, array $filters, bool $onlyLinkabl
* @param string $openprojectUserId
* @param string $openprojectUserName
* @param string $nextcloudUserId
* @return array{avatar: string, type?: string}
* @return array{avatar: string, type: string, etag: string}
* @throws \OCP\Files\NotFoundException
* @throws \OCP\Files\NotPermittedException
* @throws \OCP\Lock\LockedException
Expand Down Expand Up @@ -387,12 +387,18 @@ public function getOpenProjectAvatar(
return [
'avatar' => $imageData,
'type' => $imageMimeType ,
'etag' => md5($imageData),
];
} catch (ServerException | ClientException | ConnectException | OpenprojectAvatarErrorException | Exception $e) {
$this->logger->debug('Error while getting OpenProject avatar for user ' . $openprojectUserId . ': ' . $e->getMessage(), ['app' => $this->appName]);
$avatar = $this->avatarManager->getGuestAvatar($openprojectUserName);
$avatarContent = $avatar->getFile(64)->getContent();
return ['avatar' => $avatarContent];
$avatarFile = $avatar->getFile(64);
$avatarContent = $avatarFile->getContent();
return [
'avatar' => $avatarContent,
'type' => $avatarFile->getMimeType(),
'etag' => md5($avatarContent),
];
}
}

Expand Down
125 changes: 84 additions & 41 deletions tests/lib/Controller/OpenProjectAPIControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,50 +205,67 @@ public function testGetNotificationsErrorResponse(string $authorizationMethod) {
}

/**
*
* @param string $authorizationMethod
* @return void
*
* @dataProvider getAuthorizationMethodDataProvider
* Data provider for testGetOpenProjectAvatar
*/
public function testGetOpenProjectAvatar(string $authorizationMethod) {
$service = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->onlyMethods(['getOpenProjectAvatar', 'getAccessToken'])
->getMock();
$service
->method('getAccessToken')
->willReturn('123');
$service->expects($this->once())
->method('getOpenProjectAvatar')
->with(
'id', 'name'
)
->willReturn(['avatar' => 'some image data', 'type' => 'image/png']);
$controller = $this->getOpenProjectAPIControllerMock([
'openProjectAPIService' => $service,
'config' => $this->getConfigMock($authorizationMethod),
]);
$response = $controller->getOpenProjectAvatar('id', 'name');
$this->assertSame('some image data', $response->render());
$this->assertMatchesRegularExpression(
'/attachment; filename="?avatar"?/',
$response->getHeaders()["Content-Disposition"]
);
$this->assertSame(
"image/png",
$response->getHeaders()["Content-Type"]
);
public function getOpenProjectAvatarDataProvider(): array {
return [
'OAuth: returns 200 OK when If-None-Match differs from ETag' => [
'authorizationMethod' => Application::AUTH_METHOD_OAUTH,
'etag' => 'some etag',
'ifNoneMatch' => '"different etag"',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_OK,
],
'OAuth: returns 200 OK if If-None-Match is empty' => [
'authorizationMethod' => Application::AUTH_METHOD_OAUTH,
'etag' => 'some etag',
'ifNoneMatch' => '',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_OK
],
'OAuth: returns 304 Not Modified when If-None-Match matches ETag' => [
'authorizationMethod' => Application::AUTH_METHOD_OAUTH,
'etag' => 'some etag',
'ifNoneMatch' => '"some etag"',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_NOT_MODIFIED,
],
'OIDC: returns 200 OK when If-None-Match differs from ETag' => [
'authorizationMethod' => Application::AUTH_METHOD_OIDC,
'etag' => 'some etag',
'ifNoneMatch' => '"different etag"',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_OK,
],
'OIDC: returns 200 OK if If-None-Match is empty' => [
'authorizationMethod' => Application::AUTH_METHOD_OIDC,
'etag' => 'some etag',
'ifNoneMatch' => '',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_OK
],
'OIDC: returns 304 Not Modified when If-None-Match matches ETag' => [
'authorizationMethod' => Application::AUTH_METHOD_OIDC,
'etag' => 'some etag',
'ifNoneMatch' => '"some etag"',
'contentType' => 'image/jpeg',
'expectedStatusCode' => Http::STATUS_NOT_MODIFIED,
]
];
}

/**
*
* @dataProvider getOpenProjectAvatarDataProvider
* @param string $authorizationMethod
* @param string $etag
* @param string $ifNoneMatch
* @param string $contentType
* @param int $expectedStatusCode
* @return void
*
* @dataProvider getAuthorizationMethodDataProvider
*/
public function testGetOpenProjectAvatarNoType(string $authorizationMethod) {
public function testGetOpenProjectAvatar(string $authorizationMethod, string $etag, string $ifNoneMatch, string $contentType, int $expectedStatusCode): void {
$service = $this->getMockBuilder(OpenProjectAPIService::class)
->disableOriginalConstructor()
->onlyMethods(['getOpenProjectAvatar', 'getAccessToken'])
Expand All @@ -261,18 +278,44 @@ public function testGetOpenProjectAvatarNoType(string $authorizationMethod) {
->with(
'id', 'name'
)
->willReturn(['avatar' => 'some image data']);
->willReturn(['avatar' => 'some image data', 'type' => $contentType, 'etag' => $etag]);
$request = $this->createMock(IRequest::class);
$request->expects($this->once())->method('getHeader')->with('If-None-Match')->willReturn($ifNoneMatch);
$controller = $this->getOpenProjectAPIControllerMock([
'openProjectAPIService' => $service,
'config' => $this->getConfigMock($authorizationMethod),
'request' => $request,
]);
$response = $controller->getOpenProjectAvatar('id', 'name');
$this->assertSame('some image data', $response->render());
$this->assertMatchesRegularExpression(
'/attachment; filename="?avatar"?/',
$response->getHeaders()["Content-Disposition"]
$etag = '"' . $etag . '"';
$headers = $response->getHeaders();
$this->assertSame(
"no-cache",
$headers["Cache-Control"]
);
$this->assertSame(
$etag,
$headers["ETag"]
);
$this->assertSame(
$expectedStatusCode,
$response->getStatus()
);
$this->assertEmpty($response->getHeaders()["Content-Type"]);
if ($etag === $ifNoneMatch) {
$this->assertEmpty($response->render());
$this->assertArrayNotHasKey("Content-Disposition", $headers);
$this->assertArrayNotHasKey("Content-Type", $headers);
} else {
$this->assertSame('some image data', $response->render());
$this->assertMatchesRegularExpression(
'/attachment; filename="?avatar"?/',
$headers["Content-Disposition"]
);
$this->assertSame(
$contentType,
$headers["Content-Type"]
);
}
}

/**
Expand Down
Loading