Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>tags/v12.0.0beta2
@@ -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); | |||
} | |||
/** |
@@ -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) { |
@@ -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 ''; | |||
} | |||
} |
@@ -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( |