From dd430c2fd72e2d77d69ab93090b5fb6ea6ba76a6 Mon Sep 17 00:00:00 2001 From: Roeland Jago Douma Date: Wed, 3 May 2017 14:22:02 +0200 Subject: [PATCH] Cache the carddav photo endpoint Signed-off-by: Roeland Jago Douma --- apps/dav/lib/AppInfo/Application.php | 13 +- apps/dav/lib/CardDAV/ImageExportPlugin.php | 57 ++++-- apps/dav/lib/CardDAV/PhotoCache.php | 227 +++++++++++++++++++++ apps/dav/lib/Server.php | 3 +- 4 files changed, 281 insertions(+), 19 deletions(-) create mode 100644 apps/dav/lib/CardDAV/PhotoCache.php diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index b4f16f3cad..d13f24d369 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -24,11 +24,13 @@ */ namespace OCA\DAV\AppInfo; +use OC\AppFramework\Utility\SimpleContainer; use OCA\DAV\CalDAV\Activity\Backend; use OCA\DAV\CalDAV\Activity\Provider\Event; use OCA\DAV\CalDAV\BirthdayService; use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\ContactsManager; +use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\CardDAV\SyncService; use OCA\DAV\HookManager; use \OCP\AppFramework\App; @@ -44,10 +46,19 @@ class Application extends App { public function __construct() { parent::__construct('dav'); + $container = $this->getContainer(); + $server = $container->getServer(); + + $container->registerService(PhotoCache::class, function(SimpleContainer $s) use ($server) { + return new PhotoCache( + $server->getAppDataDir('dav-photocache') + ); + }); + /* * Register capabilities */ - $this->getContainer()->registerCapability(Capabilities::class); + $container->registerCapability(Capabilities::class); } /** diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php index 3ad7983451..24bf1ada9f 100644 --- a/apps/dav/lib/CardDAV/ImageExportPlugin.php +++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php @@ -22,6 +22,7 @@ namespace OCA\DAV\CardDAV; +use OCP\Files\NotFoundException; use OCP\ILogger; use Sabre\CardDAV\Card; use Sabre\DAV\Server; @@ -38,9 +39,18 @@ class ImageExportPlugin extends ServerPlugin { protected $server; /** @var ILogger */ private $logger; + /** @var PhotoCache */ + private $cache; - public function __construct(ILogger $logger) { + /** + * ImageExportPlugin constructor. + * + * @param ILogger $logger + * @param PhotoCache $cache + */ + public function __construct(ILogger $logger, PhotoCache $cache) { $this->logger = $logger; + $this->cache = $cache; } /** @@ -49,8 +59,7 @@ class ImageExportPlugin extends ServerPlugin { * @param Server $server * @return void */ - function initialize(Server $server) { - + public function initialize(Server $server) { $this->server = $server; $this->server->on('method:GET', [$this, 'httpGet'], 90); } @@ -60,9 +69,9 @@ class ImageExportPlugin extends ServerPlugin { * * @param RequestInterface $request * @param ResponseInterface $response - * @return bool|void + * @return bool */ - function httpGet(RequestInterface $request, ResponseInterface $response) { + public function httpGet(RequestInterface $request, ResponseInterface $response) { $queryParams = $request->getQueryParameters(); // TODO: in addition to photo we should also add logo some point in time @@ -70,6 +79,11 @@ class ImageExportPlugin extends ServerPlugin { return true; } + $size = -1; + if (isset($queryParams['size'])) { + $size = (int)$queryParams['size']; + } + $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); @@ -85,22 +99,31 @@ class ImageExportPlugin extends ServerPlugin { $aclPlugin->checkPrivileges($path, '{DAV:}read'); } - if ($result = $this->getPhoto($node)) { - // Allow caching - $response->setHeader('Cache-Control', 'private, max-age=3600, must-revalidate'); - $response->setHeader('Etag', $node->getETag() ); - $response->setHeader('Pragma', 'public'); + // Fetch addressbook + $addressbookpath = explode('/', $path); + array_pop($addressbookpath); + $addressbookpath = implode('/', $addressbookpath); + /** @var AddressBook $addressbook */ + $addressbook = $this->server->tree->getNodeForPath($addressbookpath); - $response->setHeader('Content-Type', $result['Content-Type']); - $response->setHeader('Content-Disposition', 'attachment'); + $hash = md5($addressbook->getResourceId() . $node->getName()); + + $response->setHeader('Cache-Control', 'private, max-age=3600, must-revalidate'); + $response->setHeader('Etag', $node->getETag() ); + $response->setHeader('Pragma', 'public'); + + try { + $file = $this->cache->get($hash, $size, $node); + $response->setHeader('Content-Type', $file->getMimeType()); + $response->setHeader('Content-Disposition', 'inline'); $response->setStatus(200); - $response->setBody($result['body']); - - // Returning false to break the event chain - return false; + $response->setBody($file->getContent()); + } catch (NotFoundException $e) { + $response->setStatus(404); } - return true; + + return false; } function getPhoto(Card $node) { diff --git a/apps/dav/lib/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php new file mode 100644 index 0000000000..0f7194a537 --- /dev/null +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -0,0 +1,227 @@ +appData = $appData; + } + + /** + * @param string $hash + * @param int $size + * @param Card $card + * + * @return ISimpleFile + * @throws NotFoundException + */ + public function get($hash, $size, Card $card) { + $folder = $this->getFolder($hash); + + if ($this->isEmpty($folder)) { + $this->init($folder, $card); + } + + if (!$this->hasPhoto($folder)) { + throw new NotFoundException(); + } + + if ($size !== -1) { + $size = 2 ** ceil(log($size) / log(2)); + } + + return $this->getFile($folder, $size); + } + + /** + * @param ISimpleFolder $folder + * @return bool + */ + private function isEmpty(ISimpleFolder $folder) { + return $folder->getDirectoryListing() === []; + } + + /** + * @param ISimpleFolder $folder + * @param Card $card + */ + private function init(ISimpleFolder $folder, Card $card) { + $data = $this->getPhoto($card); + + if ($data === false) { + $folder->newFile('nophoto'); + } else { + switch ($data['Content-Type']) { + case 'image/png': + $ext = 'png'; + break; + case 'image/jpeg': + $ext = 'jpg'; + break; + case 'image/gif': + $ext = 'gif'; + break; + } + $file = $folder->newFile('photo.' . $ext); + $file->putContent($data['body']); + } + } + + private function hasPhoto(ISimpleFolder $folder) { + return !$folder->fileExists('nophoto'); + } + + private function getFile(ISimpleFolder $folder, $size) { + $ext = $this->getExtension($folder); + + if ($size === -1) { + $path = 'photo.' . $ext; + } else { + $path = 'photo.' . $size . '.' . $ext; + } + + try { + $file = $folder->getFile($path); + } catch (NotFoundException $e) { + if ($size <= 0) { + throw new NotFoundException; + } + + $photo = new \OC_Image(); + /** @var ISimpleFile $file */ + $file = $folder->getFile('photo.' . $ext); + $photo->loadFromData($file->getContent()); + if ($size !== -1) { + $photo->resize($size); + } + try { + $file = $folder->newFile($path); + $file->putContent($photo->data()); + } catch (NotPermittedException $e) { + + } + } + + return $file; + } + + + /** + * @param $hash + * @return ISimpleFolder + */ + private function getFolder($hash) { + try { + return $this->appData->getFolder($hash); + } catch (NotFoundException $e) { + return $this->appData->newFolder($hash); + } + } + + /** + * Get the extension of the avatar. If there is no avatar throw Exception + * + * @param ISimpleFolder $folder + * @return string + * @throws NotFoundException + */ + private function getExtension(ISimpleFolder $folder) { + if ($folder->fileExists('photo.jpg')) { + return 'jpg'; + } elseif ($folder->fileExists('photo.png')) { + return 'png'; + } elseif ($folder->fileExists('photo.gif')) { + return 'gif'; + } + throw new NotFoundException; + } + + private function getPhoto(Card $node) { + try { + $vObject = $this->readCard($node->get()); + if (!$vObject->PHOTO) { + return false; + } + + $photo = $vObject->PHOTO; + $type = $this->getType($photo); + + $val = $photo->getValue(); + if ($photo->getValueType() === 'URI') { + $parsed = \Sabre\URI\parse($val); + //only allow data:// + if ($parsed['scheme'] !== 'data') { + return false; + } + if (substr_count($parsed['path'], ';') === 1) { + list($type,) = explode(';', $parsed['path']); + } + $val = file_get_contents($val); + } + + $allowedContentTypes = [ + 'image/png', + 'image/jpeg', + 'image/gif', + ]; + + if(!in_array($type, $allowedContentTypes, true)) { + $type = 'application/octet-stream'; + } + + return [ + 'Content-Type' => $type, + 'body' => $val + ]; + } catch(\Exception $ex) { + + } + return false; + } + + /** + * @param string $cardData + * @return \Sabre\VObject\Document + */ + private function readCard($cardData) { + return Reader::read($cardData); + } + + /** + * @param Binary $photo + * @return string + */ + private function getType(Binary $photo) { + $params = $photo->parameters(); + if (isset($params['TYPE']) || isset($params['MEDIATYPE'])) { + /** @var Parameter $typeParam */ + $typeParam = isset($params['TYPE']) ? $params['TYPE'] : $params['MEDIATYPE']; + $type = $typeParam->getValue(); + + if (strpos($type, 'image/') === 0) { + return $type; + } else { + return 'image/' . strtolower($type); + } + } + return ''; + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 5b0715b0da..1984399ebf 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -30,6 +30,7 @@ namespace OCA\DAV; use OCA\DAV\CalDAV\Schedule\IMipPlugin; use OCA\DAV\CardDAV\ImageExportPlugin; +use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\Comments\CommentsPlugin; use OCA\DAV\Connector\Sabre\Auth; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; @@ -137,7 +138,7 @@ class Server { // addressbook plugins $this->server->addPlugin(new \OCA\DAV\CardDAV\Plugin()); $this->server->addPlugin(new VCFExportPlugin()); - $this->server->addPlugin(new ImageExportPlugin(\OC::$server->getLogger())); + $this->server->addPlugin(new ImageExportPlugin(\OC::$server->getLogger(), new PhotoCache(\OC::$server->getAppDataDir('dav-photocache')))); // system tags plugins $this->server->addPlugin(new SystemTagPlugin(