]> source.dussan.org Git - nextcloud-server.git/commitdiff
Cache the carddav photo endpoint
authorRoeland Jago Douma <roeland@famdouma.nl>
Wed, 3 May 2017 12:22:02 +0000 (14:22 +0200)
committerRoeland Jago Douma <roeland@famdouma.nl>
Mon, 8 May 2017 09:20:49 +0000 (11:20 +0200)
Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
apps/dav/lib/AppInfo/Application.php
apps/dav/lib/CardDAV/ImageExportPlugin.php
apps/dav/lib/CardDAV/PhotoCache.php [new file with mode: 0644]
apps/dav/lib/Server.php

index b4f16f3cadf2cabeae625497d52a30d42dbf8bb4..d13f24d369a1dca98c02b1e828a48e4ec01d423f 100644 (file)
  */
 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);
        }
 
        /**
index 3ad7983451b9c9433e855392bf6444f989a43920..24bf1ada9f713cff549178f9f2aa7109e9215430 100644 (file)
@@ -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');
-                       $response->setStatus(200);
+               $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');
 
-                       $response->setBody($result['body']);
+               try {
+                       $file = $this->cache->get($hash, $size, $node);
+                       $response->setHeader('Content-Type', $file->getMimeType());
+                       $response->setHeader('Content-Disposition', 'inline');
+                       $response->setStatus(200);
 
-                       // 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 (file)
index 0000000..0f7194a
--- /dev/null
@@ -0,0 +1,227 @@
+<?php
+
+namespace OCA\DAV\CardDAV;
+
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use Sabre\CardDAV\Card;
+use Sabre\VObject\Property\Binary;
+use Sabre\VObject\Reader;
+
+class PhotoCache {
+
+       /** @var IAppData $appData */
+       protected $appData;
+
+       /**
+        * PhotoCache constructor.
+        *
+        * @param IAppData $appData
+        */
+       public function __construct(IAppData $appData) {
+               $this->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 '';
+       }
+}
index 5b0715b0dad017509f98c2c60a6bd10b5cdc7419..1984399ebf5af2a201f0d662e3887cb1609117ce 100644 (file)
@@ -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(