diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php index 74a8b032e42fd..91167f5388c49 100644 --- a/apps/dav/lib/CardDAV/ImageExportPlugin.php +++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php @@ -9,6 +9,7 @@ use OCP\AppFramework\Http; use OCP\Files\NotFoundException; +use OCP\IConfig; use Sabre\CardDAV\Card; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -24,9 +25,11 @@ class ImageExportPlugin extends ServerPlugin { * ImageExportPlugin constructor. * * @param PhotoCache $cache + * @param IConfig $config */ public function __construct( private PhotoCache $cache, + private IConfig $config, ) { } @@ -79,7 +82,8 @@ public function httpGet(RequestInterface $request, ResponseInterface $response) /** @var AddressBook $addressbook */ $addressbook = $this->server->tree->getNodeForPath($addressbookpath); - $response->setHeader('Cache-Control', 'private, max-age=3600, must-revalidate'); + $maxAge = (int) $this->config->getAppValue('dav', 'contact_photo_cache_max_age', '3600'); + $response->setHeader('Cache-Control', 'private, max-age=' . $maxAge . ', must-revalidate'); $response->setHeader('Etag', $node->getETag()); try { diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 4ce5dd7bd4b54..2e4ac623eece6 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -228,7 +228,7 @@ public function __construct( $this->server->addPlugin(new VCFExportPlugin()); $this->server->addPlugin(new MultiGetExportPlugin()); $this->server->addPlugin(new HasPhotoPlugin()); - $this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class))); + $this->server->addPlugin(new ImageExportPlugin(\OCP\Server::get(PhotoCache::class), \OCP\Server::get(IConfig::class))); $this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class)); $this->server->addPlugin(\OCP\Server::get(CardDavValidatePlugin::class)); diff --git a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php index 2a766f1327b51..b2f7e568e9fa2 100644 --- a/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php +++ b/apps/dav/tests/unit/CardDAV/ImageExportPluginTest.php @@ -14,6 +14,7 @@ use OCP\AppFramework\Http; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; +use OCP\IConfig; use PHPUnit\Framework\MockObject\MockObject; use Sabre\CardDAV\Card; use Sabre\DAV\Node; @@ -29,6 +30,7 @@ class ImageExportPluginTest extends TestCase { private Server&MockObject $server; private Tree&MockObject $tree; private PhotoCache&MockObject $cache; + private IConfig&MockObject $config; private ImageExportPlugin $plugin; protected function setUp(): void { @@ -40,8 +42,12 @@ protected function setUp(): void { $this->tree = $this->createMock(Tree::class); $this->server->tree = $this->tree; $this->cache = $this->createMock(PhotoCache::class); + $this->config = $this->createMock(IConfig::class); + $this->config->method('getAppValue') + ->with('dav', 'contact_photo_cache_max_age', '3600') + ->willReturn('3600'); - $this->plugin = new ImageExportPlugin($this->cache); + $this->plugin = new ImageExportPlugin($this->cache, $this->config); $this->plugin->initialize($this->server); } @@ -171,4 +177,123 @@ public function testCard(?int $size, bool $photo): void { $result = $this->plugin->httpGet($this->request, $this->response); $this->assertFalse($result); } + + public function testCardWithCustomMaxAge(): void { + $config = $this->createMock(IConfig::class); + $config->method('getAppValue') + ->with('dav', 'contact_photo_cache_max_age', '3600') + ->willReturn('120'); + + $plugin = new ImageExportPlugin($this->cache, $config); + $plugin->initialize($this->server); + + $this->request->method('getQueryParameters') + ->willReturn(['photo' => null]); + $this->request->method('getPath') + ->willReturn('user/book/card'); + + $card = $this->createMock(Card::class); + $card->method('getETag') + ->willReturn('"myEtag"'); + $card->method('getName') + ->willReturn('card'); + $book = $this->createMock(AddressBook::class); + $book->method('getResourceId') + ->willReturn(1); + + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($path) use ($card, $book) { + if ($path === 'user/book/card') { + return $card; + } elseif ($path === 'user/book') { + return $book; + } + $this->fail(); + }); + + $file = $this->createMock(ISimpleFile::class); + $file->method('getMimeType') + ->willReturn('image/jpeg'); + $file->method('getContent') + ->willReturn('imgdata'); + + $this->cache->method('get') + ->with(1, 'card', -1, $card) + ->willReturn($file); + + $setHeaderCalls = [ + ['Cache-Control', 'private, max-age=120, must-revalidate'], + ['Etag', '"myEtag"'], + ['Content-Type', 'image/jpeg'], + ['Content-Disposition', 'attachment; filename=card.jpg'], + ]; + $this->response->expects($this->exactly(count($setHeaderCalls))) + ->method('setHeader') + ->willReturnCallback(function () use (&$setHeaderCalls): void { + $expected = array_shift($setHeaderCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->response->expects($this->once()) + ->method('setStatus') + ->with(200); + + $result = $plugin->httpGet($this->request, $this->response); + $this->assertFalse($result); + } + + public function testCardWithZeroMaxAge(): void { + $config = $this->createMock(IConfig::class); + $config->method('getAppValue') + ->with('dav', 'contact_photo_cache_max_age', '3600') + ->willReturn('0'); + + $plugin = new ImageExportPlugin($this->cache, $config); + $plugin->initialize($this->server); + + $this->request->method('getQueryParameters') + ->willReturn(['photo' => null]); + $this->request->method('getPath') + ->willReturn('user/book/card'); + + $card = $this->createMock(Card::class); + $card->method('getETag') + ->willReturn('"myEtag"'); + $card->method('getName') + ->willReturn('card'); + $book = $this->createMock(AddressBook::class); + $book->method('getResourceId') + ->willReturn(1); + + $this->tree->method('getNodeForPath') + ->willReturnCallback(function ($path) use ($card, $book) { + if ($path === 'user/book/card') { + return $card; + } elseif ($path === 'user/book') { + return $book; + } + $this->fail(); + }); + + $this->cache->method('get') + ->with(1, 'card', -1, $card) + ->willThrowException(new NotFoundException()); + + $setHeaderCalls = [ + ['Cache-Control', 'private, max-age=0, must-revalidate'], + ['Etag', '"myEtag"'], + ]; + $this->response->expects($this->exactly(count($setHeaderCalls))) + ->method('setHeader') + ->willReturnCallback(function () use (&$setHeaderCalls): void { + $expected = array_shift($setHeaderCalls); + $this->assertEquals($expected, func_get_args()); + }); + $this->response->expects($this->once()) + ->method('setStatus') + ->with(Http::STATUS_NO_CONTENT); + + $result = $plugin->httpGet($this->request, $this->response); + $this->assertFalse($result); + } }