diff options
Diffstat (limited to 'apps/dav/lib')
-rw-r--r-- | apps/dav/lib/AppInfo/Application.php | 26 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/CardDavBackend.php | 36 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/ImageExportPlugin.php | 120 | ||||
-rw-r--r-- | apps/dav/lib/CardDAV/PhotoCache.php | 246 | ||||
-rw-r--r-- | apps/dav/lib/Server.php | 3 |
5 files changed, 319 insertions, 112 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index b4f16f3cadf..5d89324d4a9 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); } /** @@ -101,6 +112,19 @@ class Application extends App { } }); + $clearPhotoCache = function($event) { + if ($event instanceof GenericEvent) { + /** @var PhotoCache $p */ + $p = $this->getContainer()->query(PhotoCache::class); + $p->delete( + $event->getArgument('addressBookId'), + $event->getArgument('cardUri') + ); + } + }; + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $clearPhotoCache); + $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $clearPhotoCache); + $dispatcher->addListener('OC\AccountManager::userUpdated', function(GenericEvent $event) { $user = $event->getSubject(); $syncService = $this->getContainer()->query(SyncService::class); diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 5deb648fa7f..983220c6ba0 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -93,7 +93,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager, - EventDispatcherInterface $dispatcher = null) { + EventDispatcherInterface $dispatcher) { $this->db = $db; $this->principalBackend = $principalBackend; $this->userManager = $userManager; @@ -612,13 +612,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 1); $this->updateProperties($addressBookId, $cardUri, $cardData); - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); - } + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); return '"' . $etag . '"'; } @@ -664,13 +662,11 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 2); $this->updateProperties($addressBookId, $cardUri, $cardData); - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); - } + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri, + 'cardData' => $cardData])); return '"' . $etag . '"'; } @@ -696,12 +692,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { $this->addChange($addressBookId, $cardUri, 3); - if (!is_null($this->dispatcher)) { - $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri])); - } + $this->dispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', + new GenericEvent(null, [ + 'addressBookId' => $addressBookId, + 'cardUri' => $cardUri])); if ($ret === 1) { if ($cardId !== null) { diff --git a/apps/dav/lib/CardDAV/ImageExportPlugin.php b/apps/dav/lib/CardDAV/ImageExportPlugin.php index 3ad7983451b..747c879ecd4 100644 --- a/apps/dav/lib/CardDAV/ImageExportPlugin.php +++ b/apps/dav/lib/CardDAV/ImageExportPlugin.php @@ -22,25 +22,28 @@ namespace OCA\DAV\CardDAV; +use OCP\Files\NotFoundException; use OCP\ILogger; use Sabre\CardDAV\Card; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; -use Sabre\VObject\Parameter; -use Sabre\VObject\Property\Binary; -use Sabre\VObject\Reader; class ImageExportPlugin extends ServerPlugin { /** @var Server */ protected $server; - /** @var ILogger */ - private $logger; + /** @var PhotoCache */ + private $cache; - public function __construct(ILogger $logger) { - $this->logger = $logger; + /** + * ImageExportPlugin constructor. + * + * @param PhotoCache $cache + */ + public function __construct(PhotoCache $cache) { + $this->cache = $cache; } /** @@ -49,8 +52,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 +62,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 +72,8 @@ class ImageExportPlugin extends ServerPlugin { return true; } + $size = isset($queryParams['size']) ? (int)$queryParams['size'] : -1; + $path = $request->getPath(); $node = $this->server->tree->getNodeForPath($path); @@ -85,90 +89,28 @@ 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('Cache-Control', 'private, max-age=3600, must-revalidate'); + $response->setHeader('Etag', $node->getETag() ); + $response->setHeader('Pragma', 'public'); + + try { + $file = $this->cache->get($addressbook->getResourceId(), $node->getName(), $size, $node); + $response->setHeader('Content-Type', $file->getMimeType()); $response->setHeader('Content-Disposition', 'attachment'); $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; - } - function getPhoto(Card $node) { - // TODO: this is kind of expensive - load carddav data from database and parse it - // we might want to build up a cache one day - 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) { - $this->logger->logException($ex); - } return false; } - - private function readCard($cardData) { - return Reader::read($cardData); - } - - /** - * @param Binary $photo - * @return Parameter - */ - private function getType($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/CardDAV/PhotoCache.php b/apps/dav/lib/CardDAV/PhotoCache.php new file mode 100644 index 00000000000..c9625914263 --- /dev/null +++ b/apps/dav/lib/CardDAV/PhotoCache.php @@ -0,0 +1,246 @@ +<?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 int $addressBookId + * @param string $cardUri + * @param int $size + * @param Card $card + * + * @return ISimpleFile + * @throws NotFoundException + */ + public function get($addressBookId, $cardUri, $size, Card $card) { + $folder = $this->getFolder($addressBookId, $cardUri); + + 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()); + + $ratio = $photo->width() / $photo->height(); + if ($ratio < 1) { + $ratio = 1/$ratio; + } + $size = (int)($size * $ratio); + + if ($size !== -1) { + $photo->resize($size); + } + try { + $file = $folder->newFile($path); + $file->putContent($photo->data()); + } catch (NotPermittedException $e) { + + } + } + + return $file; + } + + + /** + * @param int $addressBookId + * @param string $cardUri + * @return ISimpleFolder + */ + private function getFolder($addressBookId, $cardUri) { + $hash = md5($addressBookId . ' ' . $cardUri); + 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 ''; + } + + /** + * @param int $addressBookId + * @param string $cardUri + */ + public function delete($addressBookId, $cardUri) { + $folder = $this->getFolder($addressBookId, $cardUri); + $folder->delete(); + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 5b0715b0dad..df5b0ea05b6 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(new PhotoCache(\OC::$server->getAppDataDir('dav-photocache')))); // system tags plugins $this->server->addPlugin(new SystemTagPlugin( |