From b68d8adc0faa5165a47714684a7fe26bccf01622 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Fri, 25 Aug 2017 17:10:01 +0200 Subject: [PATCH 01/14] Add new controller for avatar fetching --- appinfo/routes.php | 5 + composer.json | 4 +- composer.lock | 114 +++++++++++++++++++++- lib/Controller/AvatarsController.php | 137 +++++++++++++++++++++++++++ 4 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 lib/Controller/AvatarsController.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 54750fd41d..3cf0c76a34 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -74,6 +74,11 @@ 'url' => '/api/accounts/{accountId}/folders/{folderId}/messages/{id}/move', 'verb' => 'POST' ], + [ + 'name' => 'avatars#show', + 'url' => '/avatars', + 'verb' => 'GET' + ], [ 'name' => 'proxy#redirect', 'url' => '/redirect', diff --git a/composer.json b/composer.json index 86ee5a62d1..3f5e65b96c 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,9 @@ "pear-pear.horde.org/Horde_Smtp": "^1.9.5@stable", "cerdic/css-tidy": "v1.5.5", "ezyang/htmlpurifier": "4.9.3", - "kwi/urllinker": "dev-default" + "kwi/urllinker": "dev-default", + "gravatarphp/gravatar": "^1.0", + "mpclarkson/icon-scraper": "dev-master" }, "require-dev": { "christophwurst/nextcloud": "^12.0.0", diff --git a/composer.lock b/composer.lock index 4823272e69..5e4f01e48d 100644 --- a/composer.lock +++ b/composer.lock @@ -86,6 +86,60 @@ ], "time": "2017-06-03T02:28:16+00:00" }, + { + "name": "gravatarphp/gravatar", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/gravatarphp/gravatar.git", + "reference": "21b760c50bcdddd8e65d3a4b3981b7ec9b3d44aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gravatarphp/gravatar/zipball/21b760c50bcdddd8e65d3a4b3981b7ec9b3d44aa", + "reference": "21b760c50bcdddd8e65d3a4b3981b7ec9b3d44aa", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^1.11", + "henrikbjorn/phpspec-code-coverage": "^2.0", + "phpspec/phpspec": "^2.5", + "sllh/php-cs-fixer-styleci-bridge": "^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Gravatar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Gravatar URL builder which is most commonly called as a Gravatar library", + "homepage": "http://gravatarphp.com", + "keywords": [ + "avatar", + "gravatar", + "image", + "profile" + ], + "time": "2016-06-25T11:20:11+00:00" + }, { "name": "kwi/urllinker", "version": "dev-default", @@ -129,7 +183,62 @@ ], "description": "Autolink URLs in text or html", "homepage": "http://www.kwi.dk/projects/php/UrlLinker/", - "time": "2014-12-28T22:05:00+00:00" + "time": "2014-12-28T23:05:00+00:00" + }, + { + "name": "mpclarkson/icon-scraper", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/mpclarkson/icon-scraper.git", + "reference": "48b96f54efa2ecd6773c5d66143b62facb909dd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mpclarkson/icon-scraper/zipball/48b96f54efa2ecd6773c5d66143b62facb909dd8", + "reference": "48b96f54efa2ecd6773c5d66143b62facb909dd8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mpclarkson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Chris Shiflett", + "homepage": "http://shiflett.org/" + }, + { + "name": "Arthur Hoaro", + "homepage": "http://hoa.ro" + }, + { + "name": "Matthew Clarkson", + "homepage": "http://hilenium.com" + } + ], + "description": "PHP Library to get the apple-touch-icons and favicon from a website.", + "homepage": "https://github.com/mpclarkson/icon-scraper", + "keywords": [ + "apple-touch-icon", + "favicon", + "finder", + "icon", + "scraper" + ], + "time": "2017-08-11T02:44:17+00:00" }, { "name": "pear-pear.horde.org/Horde_Crypt_Blowfish", @@ -2131,7 +2240,8 @@ "pear-pear.horde.org/horde_text_flowed": 0, "pear-pear.horde.org/horde_util": 0, "pear-pear.horde.org/horde_smtp": 0, - "kwi/urllinker": 20 + "kwi/urllinker": 20, + "mpclarkson/icon-scraper": 20 }, "prefer-stable": false, "prefer-lowest": false, diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php new file mode 100644 index 0000000000..01f2c9b7cd --- /dev/null +++ b/lib/Controller/AvatarsController.php @@ -0,0 +1,137 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Controller; + +use Exception; +use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http; +use OCP\AppFramework\Controller; +use OCP\Http\Client\IClientService; +use OCP\IRequest; +use OCP\ISession; +use OCA\Mail\Service\ContactsIntegration; +use Gravatar\Gravatar; +use Mpclarkson\IconScraper\Scraper; + +class AvatarsController extends Controller { + + /** @var ISession */ + private $session; + + /** @var IClientService */ + private $clientService; + + /** @var string */ + private $referrer; + + /** @var string */ + private $hostname; + + /** @var ContactsIntegration */ + private $contactsIntegration; + + /** + * @param string $appName + * @param IRequest $request + * @param ISession $session + * @param IClientService $clientService + * @param string $referrer + * @param string $hostname + */ + public function __construct($appName, IRequest $request, + ContactsIntegration $contactsIntegration, + ISession $session, IClientService $clientService, $referrer, $hostname) { + parent::__construct($appName, $request); + $this->session = $session; + $this->clientService = $clientService; + $this->referrer = $referrer; + $this->hostname = $hostname; + $this->contactsIntegration = $contactsIntegration; + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $email + * @return Response + */ + public function show($email) { + // close the session to allow parallel downloads + $this->session->close(); + + // 1) load the photo from the address book + $result = $this->contactsIntegration->getPhoto($email); + if ($result != null) { + return [ + 'source' => 'addressbook', + 'url' => $result + ]; + } + + // 2: Federated cloud + + // 3: Gravatar + $gravatar = new Gravatar(['size' => 128], true); + $gravatarUrl = $gravatar->avatar($email, ['d' => 404]); + + $client = $this->clientService->newClient(); + $foundGravatar = true; + try { + $response = $client->get($gravatarUrl); + } + catch (\Exception $exception) { + $foundGravatar = false; + } + + if ($foundGravatar) { + return [ + 'source' => 'gravatar', + 'url' => $gravatarUrl + ]; + } + + // 4 Favicon + $lastAt = strrpos($email, '@'); + $domain = "http://"; + $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); + $scraper = new Scraper(); + $icons = $scraper->get($domain); + + if (count($icons) > 0) { + usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); + + return [ + 'source' => 'favicon', + 'url' => $icons[0]->getHref() + ]; + } + + // No avatar found. Return 204 (no content) + return [ + 'source' => 'none', + 'url' => null + ]; + } + +} From 97430c78577c06bbbf0e7a65d09f0f8dccea66d5 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 27 Aug 2017 10:20:29 +0200 Subject: [PATCH 02/14] Show gravatar images in message list --- js/models/messagecollection.js | 14 +++ lib/Controller/AvatarsController.php | 85 ++------------- lib/Service/AvatarService.php | 149 +++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 77 deletions(-) create mode 100644 lib/Service/AvatarService.php diff --git a/js/models/messagecollection.js b/js/models/messagecollection.js index 1316e6b31a..1b2caa7dcb 100644 --- a/js/models/messagecollection.js +++ b/js/models/messagecollection.js @@ -21,6 +21,20 @@ define(function(require) { model: Message, comparator: function(message) { return message.get('dateInt') * -1; + }, + initialize: function() { + this.on('add', function(message) { + var url = OC.generateUrl('apps/mail/avatars?email={email}', { + email: message.get('fromEmail') + }); + + Promise.resolve($.ajax(url)).then(function(avatar) { + if (avatar.source != 'none'){ + message.set('senderImage', avatar.url); + } + }); + }); + } }); diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php index 01f2c9b7cd..7f826967eb 100644 --- a/lib/Controller/AvatarsController.php +++ b/lib/Controller/AvatarsController.php @@ -26,47 +26,29 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http; use OCP\AppFramework\Controller; -use OCP\Http\Client\IClientService; use OCP\IRequest; use OCP\ISession; -use OCA\Mail\Service\ContactsIntegration; -use Gravatar\Gravatar; -use Mpclarkson\IconScraper\Scraper; +use OCA\Mail\Service\AvatarService; class AvatarsController extends Controller { /** @var ISession */ private $session; - /** @var IClientService */ - private $clientService; + /** @var AvatarService */ + private $avatarService; - /** @var string */ - private $referrer; - - /** @var string */ - private $hostname; - - /** @var ContactsIntegration */ - private $contactsIntegration; - /** * @param string $appName * @param IRequest $request * @param ISession $session + * @param AvatarService $avatarService * @param IClientService $clientService - * @param string $referrer - * @param string $hostname */ - public function __construct($appName, IRequest $request, - ContactsIntegration $contactsIntegration, - ISession $session, IClientService $clientService, $referrer, $hostname) { + public function __construct($appName, IRequest $request, ISession $session, AvatarService $avatarService) { parent::__construct($appName, $request); $this->session = $session; - $this->clientService = $clientService; - $this->referrer = $referrer; - $this->hostname = $hostname; - $this->contactsIntegration = $contactsIntegration; + $this->avatarService = $avatarService; } /** @@ -80,58 +62,7 @@ public function show($email) { // close the session to allow parallel downloads $this->session->close(); - // 1) load the photo from the address book - $result = $this->contactsIntegration->getPhoto($email); - if ($result != null) { - return [ - 'source' => 'addressbook', - 'url' => $result - ]; - } - - // 2: Federated cloud - - // 3: Gravatar - $gravatar = new Gravatar(['size' => 128], true); - $gravatarUrl = $gravatar->avatar($email, ['d' => 404]); - - $client = $this->clientService->newClient(); - $foundGravatar = true; - try { - $response = $client->get($gravatarUrl); - } - catch (\Exception $exception) { - $foundGravatar = false; - } - - if ($foundGravatar) { - return [ - 'source' => 'gravatar', - 'url' => $gravatarUrl - ]; - } - - // 4 Favicon - $lastAt = strrpos($email, '@'); - $domain = "http://"; - $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); - $scraper = new Scraper(); - $icons = $scraper->get($domain); - - if (count($icons) > 0) { - usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); - - return [ - 'source' => 'favicon', - 'url' => $icons[0]->getHref() - ]; - } - - // No avatar found. Return 204 (no content) - return [ - 'source' => 'none', - 'url' => null - ]; + // Get the data from the service + return $this->avatarService->findByEmail($email); } - } diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php new file mode 100644 index 0000000000..9adf9d8bb6 --- /dev/null +++ b/lib/Service/AvatarService.php @@ -0,0 +1,149 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service; + +use Exception; +use OCA\Mail\Service\ContactsIntegration; +use OCP\IUserManager; +use OCP\IAvatarManager; +use OCP\IURLGenerator; +use OCP\Http\Client\IClientService; +use Gravatar\Gravatar; +use Mpclarkson\IconScraper\Scraper; + +class AvatarService { + /** @var IClientService */ + private $clientService; + + /** @var ContactsIntegration */ + private $contactsIntegration; + + /** @var IUserManager */ + private $userManager; + + /** @var IAvatarManager */ + protected $avatarManager; + + /** @var IURLGenerator */ + private $urlGenerator; + + public function __construct(IAvatarManager $avatarManager, ContactsIntegration $contactsIntegration, IUserManager $userManager, IURLGenerator $urlGenerator, IClientService $clientService) { + $this->clientService = $clientService; + $this->contactsIntegration = $contactsIntegration; + $this->userManager = $userManager; + $this->avatarManager = $avatarManager; + $this->urlGenerator = $urlGenerator; + } + + public function findByEmail($email) { + // 1) load the photo from the address book + $result = $this->findAddressbookEntry($email); + if(!$result) $result = $this->findGravatarEntry($email); + if(!$result) $result = $this->findFavicon($email); + if(!$result) $result = $this->emptyEntry($email); + + return $result; + } + + private function findAddressbookEntry($email) { + $result = $this->contactsIntegration->getPhoto($email); + if ($result != null) { + return [ + 'email' => $email, + 'source' => 'addressbook', + 'url' => $result + ]; + } + + return null; + } + + private function findGravatarEntry($email) { + $gravatar = new Gravatar(['size' => 128], true); + $gravatarUrl = $gravatar->avatar($email, ['d' => 404]); + + $client = $this->clientService->newClient(); + $foundGravatar = true; + try { + $response = $client->get($gravatarUrl); + } + catch (\Exception $exception) { + $foundGravatar = false; + } + + if ($foundGravatar) { + return [ + 'email' => $email, + 'source' => 'gravatar', + 'url' => $this->urlGenerator->linkToRoute('mail.proxy.proxy', [ 'src' => $gravatarUrl ]) + ]; + } + + return null; + } + + private function findFavicon($email) { + $lastAt = strrpos($email, '@'); + $domain = "http://"; + $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); + $scraper = new Scraper(); + $icons = $scraper->get($domain); + + if (count($icons) > 0) { + usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); + + return [ + 'email' => $email, + 'source' => 'favicon', + 'url' => $this->urlGenerator->linkToRoute('mail.proxy.proxy', [ 'src' => $icons[0]->getHref()]) + ]; + } + + return null; + } + + private function emptyEntry($email) { + // No avatar found. Return 204 (no content) + return [ + 'email' => $email, + 'source' => 'none', + 'url' => null + ]; + } +} + +__halt_compiler(); + + public function __construct( + IAvatarManager $avatarManager, + ContactsIntegration $contactsIntegration, + IUserManager $userManager, + IURLGenerator $urlGenerator, + IClientService $clientService) { + $this->clientService = $clientService; + $this->contactsIntegration = $contactsIntegration; + $this->userManager = $userManager; + $this->avatarManager = $avatarManager; + $this->urlGenerator = $urlGenerator; + } + +} From 96ea8e144abfd224c19167655f65f55c840e1b3e Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Mon, 28 Aug 2017 14:51:32 +0200 Subject: [PATCH 03/14] Introduce caching for avatars --- appinfo/database.xml | 60 ++++++ appinfo/info.xml | 2 +- appinfo/routes.php | 9 +- js/models/messagecollection.js | 15 +- lib/AppInfo/Application.php | 7 +- lib/Controller/AvatarsController.php | 31 ++- lib/Controller/MessagesController.php | 19 +- lib/Db/Alias.php | 19 +- lib/Db/Avatar.php | 46 +++++ lib/Db/AvatarMapper.php | 45 +++++ lib/Http/AvatarDownloadResponse.php | 55 ++++++ lib/Service/AvatarService.php | 186 +++++++++--------- .../AvatarSource/AddressbookSource.php | 45 +++++ lib/Service/AvatarSource/FaviconSource.php | 77 ++++++++ lib/Service/AvatarSource/GravatarSource.php | 66 +++++++ lib/Service/AvatarSource/NoneSource.php | 35 ++++ lib/Storage/AvatarStorage.php | 44 +++++ 17 files changed, 640 insertions(+), 121 deletions(-) create mode 100644 lib/Db/Avatar.php create mode 100644 lib/Db/AvatarMapper.php create mode 100644 lib/Http/AvatarDownloadResponse.php create mode 100644 lib/Service/AvatarSource/AddressbookSource.php create mode 100644 lib/Service/AvatarSource/FaviconSource.php create mode 100644 lib/Service/AvatarSource/GravatarSource.php create mode 100644 lib/Service/AvatarSource/NoneSource.php create mode 100644 lib/Storage/AvatarStorage.php diff --git a/appinfo/database.xml b/appinfo/database.xml index 140c670ae7..fca2bea2d5 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -237,4 +237,64 @@ + + + *dbprefix*mail_avatars + + + id + integer + 1 + 0 + true + 4 + + + user_id + text + + true + 64 + + + email + text + + true + 64 + + + source + text + + true + 255 + + + url + text + + true + 64 + + + updated_at + integer + 0 + true + + + + mail_avatar_email_userid_index + + email + ascending + + + user_id + ascending + + + +
diff --git a/appinfo/info.xml b/appinfo/info.xml index 6534d86790..554085af36 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -5,7 +5,7 @@ Mail IMAP web client Easy to use email client which connects to your mail server via IMAP and SMTP. - 0.7.3 + 0.7.4 agpl Christoph Wurst Jan-Christoph Borchardt diff --git a/appinfo/routes.php b/appinfo/routes.php index 3cf0c76a34..d761ce949a 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -75,8 +75,13 @@ 'verb' => 'POST' ], [ - 'name' => 'avatars#show', - 'url' => '/avatars', + 'name' => 'avatars#url', + 'url' => '/avatars/url', + 'verb' => 'GET' + ], + [ + 'name' => 'avatars#file', + 'url' => '/avatars/file', 'verb' => 'GET' ], [ diff --git a/js/models/messagecollection.js b/js/models/messagecollection.js index 1b2caa7dcb..c9677c5693 100644 --- a/js/models/messagecollection.js +++ b/js/models/messagecollection.js @@ -23,18 +23,21 @@ define(function(require) { return message.get('dateInt') * -1; }, initialize: function() { - this.on('add', function(message) { - var url = OC.generateUrl('apps/mail/avatars?email={email}', { + this.on('add', this._fetchAvatar); + }, + + _fetchAvatar: function(message) { + if (message.get('avatarInDatabase') === false) { + var url = OC.generateUrl('apps/mail/avatars/url?email={email}', { email: message.get('fromEmail') }); Promise.resolve($.ajax(url)).then(function(avatar) { - if (avatar.source != 'none'){ + if (avatar.source !== 'none'){ message.set('senderImage', avatar.url); } - }); - }); - + }).catch(console.error.bind(this)); + } } }); diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 5b41b78414..ad8ee73285 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -28,6 +28,7 @@ use OCA\Mail\Service\Attachment\AttachmentService; use OCA\Mail\Service\MailManager; use OCA\Mail\Service\MailTransmission; +use OCA\Mail\Storage\AvatarStorage; use OCP\AppFramework\App; use OCP\Util; @@ -57,9 +58,13 @@ private function initializeAppContainer() { $user = $container->query("UserId"); return $container->getServer()->getUserFolder($user); }); + $container->registerParameter("testSmtp", $testSmtp); $container->registerParameter("referrer", isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null); $container->registerParameter("hostname", Util::getServerHostName()); - } + $container->registerService('avatarStorage', function($c) { + return new AvatarStorage($c->query('ServerContainer')->getUserFolder()->getParent()); + }); + } } diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php index 7f826967eb..901224dc5d 100644 --- a/lib/Controller/AvatarsController.php +++ b/lib/Controller/AvatarsController.php @@ -21,14 +21,13 @@ namespace OCA\Mail\Controller; -use Exception; use OCP\AppFramework\Http\Response; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Http; use OCP\AppFramework\Controller; use OCP\IRequest; use OCP\ISession; + use OCA\Mail\Service\AvatarService; +use OCA\Mail\Http\AvatarDownloadResponse; class AvatarsController extends Controller { @@ -38,17 +37,22 @@ class AvatarsController extends Controller { /** @var AvatarService */ private $avatarService; + /** @var string */ + private $currentUserId; + /** * @param string $appName * @param IRequest $request * @param ISession $session * @param AvatarService $avatarService * @param IClientService $clientService + * @param string $UserId */ - public function __construct($appName, IRequest $request, ISession $session, AvatarService $avatarService) { + public function __construct($appName, IRequest $request, ISession $session, AvatarService $avatarService, $userId) { parent::__construct($appName, $request); $this->session = $session; $this->avatarService = $avatarService; + $this->currentUserId = $userId; } /** @@ -58,11 +62,20 @@ public function __construct($appName, IRequest $request, ISession $session, Avat * @param string $email * @return Response */ - public function show($email) { - // close the session to allow parallel downloads - $this->session->close(); - + public function url($email) { // Get the data from the service - return $this->avatarService->findByEmail($email); + return $this->avatarService->rewriteUrl($this->avatarService->fetch($email, $this->currentUserId)); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param string $email + * @return Response + */ + public function file($email) { + // Return file (or fail) + return new AvatarDownloadResponse($this->avatarService->loadFile($email)); } } diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index ad4a488b76..871d8a6529 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -33,6 +33,7 @@ use OCA\Mail\Http\HtmlResponse; use OCA\Mail\Model\IMAPMessage; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\AvatarService; use OCA\Mail\Service\ContactsIntegration; use OCA\Mail\Service\IAccount; use OCA\Mail\Service\IMailBox; @@ -80,6 +81,9 @@ class MessagesController extends Controller { /** @var IAccount[] */ private $accounts = []; + /** @var AvatarService */ + private $avatarService; + /** * @param string $appName * @param IRequest $request @@ -101,7 +105,8 @@ public function __construct($appName, Logger $logger, IL10N $l10n, IMimeTypeDetector $mimeTypeDetector, - IURLGenerator $urlGenerator) { + IURLGenerator $urlGenerator, + AvatarService $avatarService) { parent::__construct($appName, $request); $this->accountService = $accountService; $this->currentUserId = $UserId; @@ -111,6 +116,7 @@ public function __construct($appName, $this->l10n = $l10n; $this->mimeTypeDetector = $mimeTypeDetector; $this->urlGenerator = $urlGenerator; + $this->avatarService = $avatarService; } /** @@ -152,7 +158,10 @@ public function index($accountId, $folderId, $cursor = null, $filter=null, $ids= } } - $j['senderImage'] = $ci->getPhoto($j['fromEmail']); + $avatar = $this->avatarService->loadFromCache($j['fromEmail'], $this->currentUserId); + $j['senderImage'] = $avatar === null ? '' : $this->avatarService->rewriteUrl($avatar)->getUrl(); + $j['avatarInDatabase'] = $avatar !== null; + return $j; }, $messages); @@ -494,7 +503,11 @@ private function loadMultiple($accountId, $folderId, $ids) { */ private function enhanceMessage($accountId, $folderId, $id, IMAPMessage $m, $mailBox) { $json = $m->getFullMessage($mailBox->getSpecialRole()); - $json['senderImage'] = $this->contactsIntegration->getPhoto($m->getFrom()->first()->toHorde()->bare_address); + + $avatar = $this->avatarService->loadFromCache($m->getFrom()->first()->toHorde()->bare_address, $this->currentUserId); + $json['senderImage'] = $avatar === null ? '' : $this->avatarService->rewriteUrl($avatar)->getUrl(); + $json['avatarInDatabase'] = $avatar !== null; + if (isset($json['hasHtmlBody'])) { $json['htmlBodyUrl'] = $this->buildHtmlBodyUrl($accountId, $folderId, $id); } diff --git a/lib/Db/Alias.php b/lib/Db/Alias.php index fd67631c5a..62bbfd9101 100644 --- a/lib/Db/Alias.php +++ b/lib/Db/Alias.php @@ -1,13 +1,22 @@ + * * Mail * - * This file is licensed under the Affero General Public License version 3 or - * later. See the COPYING file. + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see * - * @author Tahaa Karim - * @copyright Tahaa Karim 2016 */ namespace OCA\Mail\Db; @@ -23,7 +32,7 @@ * @method string getAlias() */ class Alias extends Entity { - + public $accountId; public $name; public $alias; diff --git a/lib/Db/Avatar.php b/lib/Db/Avatar.php new file mode 100644 index 0000000000..b6a74eb9c6 --- /dev/null +++ b/lib/Db/Avatar.php @@ -0,0 +1,46 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * @method void setUserId(string $userId) + * @method string getUserId() + * @method void setEmail(string $email) + * @method string getEmail() + * @method void setSource(string $source) + * @method string getSource() + * @method void setUrl(string $url) + * @method string getUrl() + * @method void setUpdatedAt(string $updatedAt) + * @method string getUpdatedAt() + */ +class Avatar extends Entity { + + public $userId; + public $email; + public $source; + public $url; + public $updatedAt; + +} diff --git a/lib/Db/AvatarMapper.php b/lib/Db/AvatarMapper.php new file mode 100644 index 0000000000..13041bbaee --- /dev/null +++ b/lib/Db/AvatarMapper.php @@ -0,0 +1,45 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Db; + +use OCP\AppFramework\Db\Mapper; +use OCP\IDBConnection; + +class AvatarMapper extends Mapper { + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'mail_avatars'); + } + + /** + * @param int $email + * @param string $currentUserId + * @return Avatar + */ + public function find($email, $currentUserId) { + $sql = 'select * from *PREFIX*mail_avatars where user_id = ? and email = ?'; + return $this->findEntities($sql, [$currentUserId, $email]); + } +} diff --git a/lib/Http/AvatarDownloadResponse.php b/lib/Http/AvatarDownloadResponse.php new file mode 100644 index 0000000000..9a01e4842e --- /dev/null +++ b/lib/Http/AvatarDownloadResponse.php @@ -0,0 +1,55 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Http; + +use OCP\AppFramework\Http\DownloadResponse; + +class AvatarDownloadResponse extends DownloadResponse { + + private $content; + + /** + * Creates a response that prompts the user to download a file which + * contains the passed string + * Additionally the response will be cacheable by browsers. Since the content is + * generally not sensitive content (e.g. Logos in mails) this should not be a problem. + * @param IFile $content the content that should be written into the file + */ + public function __construct($content){ + parent::__construct('avatar', 'application/octet-stream'); + $this->content = $content; + + $expires = new \DateTime('now + 2 hours'); + $this->addHeader('Expires', $expires->format(\DateTime::RFC1123)); + $this->addHeader('Cache-Control', 'private'); + $this->addHeader('Pragma', 'cache'); + } + + /** + * Simply sets the headers and returns the file contents + * @return string the file contents + */ + public function render(){ + return $this->content; + } + +} diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index 9adf9d8bb6..8923dcc4e7 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -21,129 +21,127 @@ namespace OCA\Mail\Service; -use Exception; +use OCA\Mail\Db\Avatar; +use OCA\Mail\Db\AvatarMapper; +use OCA\Mail\Service\AvatarSource\AddressbookSource; +use OCA\Mail\Service\AvatarSource\FaviconSource; +use OCA\Mail\Service\AvatarSource\GravatarSource; +use OCA\Mail\Service\AvatarSource\NoneSource; use OCA\Mail\Service\ContactsIntegration; -use OCP\IUserManager; -use OCP\IAvatarManager; +use OCA\Mail\Storage\AvatarStorage; + use OCP\IURLGenerator; -use OCP\Http\Client\IClientService; -use Gravatar\Gravatar; -use Mpclarkson\IconScraper\Scraper; class AvatarService { - /** @var IClientService */ - private $clientService; - /** @var ContactsIntegration */ private $contactsIntegration; - /** @var IUserManager */ - private $userManager; - - /** @var IAvatarManager */ - protected $avatarManager; - /** @var IURLGenerator */ private $urlGenerator; - public function __construct(IAvatarManager $avatarManager, ContactsIntegration $contactsIntegration, IUserManager $userManager, IURLGenerator $urlGenerator, IClientService $clientService) { - $this->clientService = $clientService; - $this->contactsIntegration = $contactsIntegration; - $this->userManager = $userManager; - $this->avatarManager = $avatarManager; - $this->urlGenerator = $urlGenerator; - } + /** @var AvatarMapper */ + private $mapper; - public function findByEmail($email) { - // 1) load the photo from the address book - $result = $this->findAddressbookEntry($email); - if(!$result) $result = $this->findGravatarEntry($email); - if(!$result) $result = $this->findFavicon($email); - if(!$result) $result = $this->emptyEntry($email); + /** @var AvatarStorage */ + private $storage; - return $result; - } + /** @var AddressbookSource */ + private $addressbookSource; - private function findAddressbookEntry($email) { - $result = $this->contactsIntegration->getPhoto($email); - if ($result != null) { - return [ - 'email' => $email, - 'source' => 'addressbook', - 'url' => $result - ]; - } + /** @var FaviconSource */ + private $faviconSource; - return null; + /** @var GravatarSource */ + private $gravatarSource; + + /** @var NoneSource */ + private $noneSource; + + public function __construct(AvatarMapper $avatarMapper, + ContactsIntegration $contactsIntegration, + IURLGenerator $urlGenerator, + AvatarStorage $avatarStorage, + AddressbookSource $addressbookSource, + FaviconSource $faviconSource, + GravatarSource $gravatarSource, + NoneSource $noneSource) { + $this->mapper = $avatarMapper; + $this->contactsIntegration = $contactsIntegration; + $this->urlGenerator = $urlGenerator; + $this->storage = $avatarStorage; + $this->addressbookSource = $addressbookSource; + $this->faviconSource = $faviconSource; + $this->gravatarSource = $gravatarSource; + $this->noneSource = $noneSource; } - private function findGravatarEntry($email) { - $gravatar = new Gravatar(['size' => 128], true); - $gravatarUrl = $gravatar->avatar($email, ['d' => 404]); + public function rewriteUrl(Avatar $avatar) { + if ($avatar === null) { + return null; + } + + $copy = new Avatar(); + $copy->setUserId($avatar->getUserId()); + $copy->setEmail($avatar->getEmail()); + $copy->setSource($avatar->getSource()); - $client = $this->clientService->newClient(); - $foundGravatar = true; - try { - $response = $client->get($gravatarUrl); + if ($avatar->getSource() === 'addressbook') { + $result = $this->contactsIntegration->getPhoto($avatar->getEmail()); + if ($result != null) { + $copy->setUrl($result); + } } - catch (\Exception $exception) { - $foundGravatar = false; + elseif ($avatar->getUrl() !== '') { + $copy->setUrl($this->urlGenerator->linkToRoute('mail.avatars.file', [ 'email' => $avatar->getEmail() ])); } - if ($foundGravatar) { - return [ - 'email' => $email, - 'source' => 'gravatar', - 'url' => $this->urlGenerator->linkToRoute('mail.proxy.proxy', [ 'src' => $gravatarUrl ]) - ]; - } + return $copy; + } - return null; + public function loadFile($email) { + return $this->storage->read($email); } - private function findFavicon($email) { - $lastAt = strrpos($email, '@'); - $domain = "http://"; - $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); - $scraper = new Scraper(); - $icons = $scraper->get($domain); - - if (count($icons) > 0) { - usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); - - return [ - 'email' => $email, - 'source' => 'favicon', - 'url' => $this->urlGenerator->linkToRoute('mail.proxy.proxy', [ 'src' => $icons[0]->getHref()]) - ]; + public function loadFromCache($email, $userId) { + // Find avatars (but not older than a week + $result = $this->mapper->find($email, $userId); + if (count($result) > 0 && time() - $result[0]->getUpdatedAt() < 604800) { + return $result[0]; } return null; } - private function emptyEntry($email) { - // No avatar found. Return 204 (no content) - return [ - 'email' => $email, - 'source' => 'none', - 'url' => null - ]; - } -} + public function fetch($email, $userId) { + // Try to fetch old data first + $avatar = $this->loadFromCache($email, $userId); + if (!is_null($avatar)) { + return $avatar; + } -__halt_compiler(); + // our avatar is too old or does not exist + if (is_null($avatar)) { + $avatar = new Avatar(); + $avatar->setUserId($userId); + $avatar->setEmail($email); + } - public function __construct( - IAvatarManager $avatarManager, - ContactsIntegration $contactsIntegration, - IUserManager $userManager, - IURLGenerator $urlGenerator, - IClientService $clientService) { - $this->clientService = $clientService; - $this->contactsIntegration = $contactsIntegration; - $this->userManager = $userManager; - $this->avatarManager = $avatarManager; - $this->urlGenerator = $urlGenerator; - } + // 1) load the photo from the address book + $result = $this->addressbookSource->fetch($email); + if(!$result) $result = $this->gravatarSource->fetch($email); + if(!$result) $result = $this->faviconSource->fetch($email); + if(!$result) $result = $this->noneSource->fetch($email); + + $avatar->setSource($result['source']); + $avatar->setUrl($result['url']); + $avatar->setUpdatedAt(time()); + + if ($avatar->getId() === null) { + $this->mapper->insert($avatar); + } else { + $this->mapper->update($avatar); + } + return $avatar; + } } diff --git a/lib/Service/AvatarSource/AddressbookSource.php b/lib/Service/AvatarSource/AddressbookSource.php new file mode 100644 index 0000000000..b35c147e15 --- /dev/null +++ b/lib/Service/AvatarSource/AddressbookSource.php @@ -0,0 +1,45 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service\AvatarSource; + +use OCA\Mail\Service\ContactsIntegration; + +class AddressbookSource { + /** @var ContactsIntegration */ + private $contactsIntegration; + + public function __construct(ContactsIntegration $contactsIntegration) { + $this->contactsIntegration = $contactsIntegration; + } + + public function fetch($email) { + if ($this->contactsIntegration->getPhoto($email) === null) { + return null; + } + + return [ + 'email' => $email, + 'source' => 'addressbook', + 'url' => '' + ]; + } +} diff --git a/lib/Service/AvatarSource/FaviconSource.php b/lib/Service/AvatarSource/FaviconSource.php new file mode 100644 index 0000000000..c411997214 --- /dev/null +++ b/lib/Service/AvatarSource/FaviconSource.php @@ -0,0 +1,77 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service\AvatarSource; + +use OCA\Mail\Storage\AvatarStorage; +use OCP\Http\Client\IClientService; + +use Mpclarkson\IconScraper\Scraper; + +class FaviconSource { + /** @var IClientService */ + private $clientService; + + /** @var AvatarStorage */ + private $storage; + + public function __construct(IClientService $clientService, AvatarStorage $avatarStorage) { + $this->clientService = $clientService; + $this->storage = $avatarStorage; + } + + public function fetch($email) { + $lastAt = strrpos($email, '@'); + $domain = "http://"; + $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); + $scraper = new Scraper(); + $icons = $scraper->get($domain); + + if (count($icons) === 0) return null; + + usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); + + $client = $this->clientService->newClient(); + foreach ($icons as $icon) { + try { + $response = $client->get($icon->getHref()); + + // Don't save 0 byte images + $body = $response->getBody(); + if (strlen($body) === 0) { + continue; + } + $this->storage->save($email, $body); + + return [ + 'email' => $email, + 'source' => 'favicon', + 'url' => $icon->getHref() + ]; + } + catch (\Exception $exception){ + continue; + } + } + + return null; + } +} diff --git a/lib/Service/AvatarSource/GravatarSource.php b/lib/Service/AvatarSource/GravatarSource.php new file mode 100644 index 0000000000..958354fba3 --- /dev/null +++ b/lib/Service/AvatarSource/GravatarSource.php @@ -0,0 +1,66 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service\AvatarSource; + +use OCA\Mail\Storage\AvatarStorage; +use OCP\Http\Client\IClientService; + +use Gravatar\Gravatar; + +class GravatarSource { + /** @var IClientService */ + private $clientService; + + /** @var AvatarStorage */ + private $storage; + + public function __construct(IClientService $clientService, AvatarStorage $avatarStorage) { + $this->clientService = $clientService; + $this->storage = $avatarStorage; + } + + public function fetch($email) { + $gravatar = (new Gravatar(['size' => 128], true))->avatar($email, ['d' => 404]); + + $client = $this->clientService->newClient(); + + try { + $response = $client->get($gravatar); + + // Don't save 0 byte images + $body = $response->getBody(); + if (strlen() === 0) { + return null; + } + $this->storage->save($email, $body); + + return [ + 'email' => $email, + 'source' => 'gravatar', + 'url' => $gravatar + ]; + } + catch (\Exception $exception) { + return null; + } + } +} diff --git a/lib/Service/AvatarSource/NoneSource.php b/lib/Service/AvatarSource/NoneSource.php new file mode 100644 index 0000000000..58d630c07e --- /dev/null +++ b/lib/Service/AvatarSource/NoneSource.php @@ -0,0 +1,35 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service\AvatarSource; + +class NoneSource { + public function __construct() { + } + + public function fetch($email) { + return [ + 'email' => $email, + 'source' => 'none', + 'url' => '' + ]; + } +} diff --git a/lib/Storage/AvatarStorage.php b/lib/Storage/AvatarStorage.php new file mode 100644 index 0000000000..0d9f4b8883 --- /dev/null +++ b/lib/Storage/AvatarStorage.php @@ -0,0 +1,44 @@ +nodeExists('mail')) { + $storage->newFolder('mail'); + } + $node = $storage->get('mail'); + + if (!$node->nodeExists('avatars')) { + $node->newFolder('avatars'); + } + + $this->storage = $node->get('avatars'); + } + + public function save($email, $content) { + // check if file exists and write to it if possible + $filename = $this->getFilename($email); + + try { + $file = $this->storage->get($filename); + } catch(\OCP\Files\NotFoundException $e) { + $this->storage->newFile($filename); + $file = $this->storage->get($filename); + } + + // the id can be accessed by $file->getId(); + $file->putContent($content); + } + + public function read($email) { + return $this->storage->get($this->getFilename($email))->getContent(); + } + + private function getFilename($email) { + return md5($email); + } +} From 7a6fd92f998a2f8d4eac4d8221c5a660af4a9451 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Mon, 28 Aug 2017 15:28:52 +0200 Subject: [PATCH 04/14] Try to fix tests --- lib/Service/AvatarService.php | 2 ++ tests/Controller/MessagesControllerTest.php | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index 8923dcc4e7..c575cde435 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -84,6 +84,8 @@ public function rewriteUrl(Avatar $avatar) { $copy->setUserId($avatar->getUserId()); $copy->setEmail($avatar->getEmail()); $copy->setSource($avatar->getSource()); + $copy->setUpdatedAt($avatar->getUpdatedAt()); + $copy->setUrl(''); if ($avatar->getSource() === 'addressbook') { $result = $this->contactsIntegration->getPhoto($avatar->getEmail()); diff --git a/tests/Controller/MessagesControllerTest.php b/tests/Controller/MessagesControllerTest.php index 0336bb98e7..c12d6fecf0 100644 --- a/tests/Controller/MessagesControllerTest.php +++ b/tests/Controller/MessagesControllerTest.php @@ -48,6 +48,7 @@ class MessagesControllerTest extends PHPUnit_Framework_TestCase { private $attachment; private $mimeTypeDetector; private $urlGenerator; + private $avatarService; protected function setUp() { parent::setUp(); @@ -73,6 +74,9 @@ protected function setUp() { $this->l10n = $this->getMockBuilder('\OCP\IL10N')->getMock(); $this->mimeTypeDetector = $this->getMockBuilder('OCP\Files\IMimeTypeDetector')->getMock(); $this->urlGenerator = $this->getMockBuilder('\OCP\IURLGenerator')->getMock(); + $this->avatarService = $this->getMockBuilder('\OCA\Mail\Service\AvatarService') + ->disableOriginalConstructor() + ->getMock(); $this->controller = new MessagesController( $this->appName, @@ -84,7 +88,8 @@ protected function setUp() { $this->logger, $this->l10n, $this->mimeTypeDetector, - $this->urlGenerator); + $this->urlGenerator, + $this->avatarService); $this->account = $this->getMockBuilder('\OCA\Mail\Account') ->disableOriginalConstructor() From 0a49e4fd65acefc50b00c10418f2342c58d9f45e Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Mon, 28 Aug 2017 15:43:55 +0200 Subject: [PATCH 05/14] Fix tiny code style issue --- js/models/messagecollection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/messagecollection.js b/js/models/messagecollection.js index c9677c5693..cb29c5b0ff 100644 --- a/js/models/messagecollection.js +++ b/js/models/messagecollection.js @@ -33,7 +33,7 @@ define(function(require) { }); Promise.resolve($.ajax(url)).then(function(avatar) { - if (avatar.source !== 'none'){ + if (avatar.source !== 'none') { message.set('senderImage', avatar.url); } }).catch(console.error.bind(this)); From 67a9e4b78b23f21b1b0f5fb72ab86fe37a80e090 Mon Sep 17 00:00:00 2001 From: Jakob Sack Date: Sun, 22 Oct 2017 22:36:16 +0200 Subject: [PATCH 06/14] Use AppData for storing avatars --- composer.lock | 133 +-- lib/AppInfo/Application.php | 2 +- lib/Service/AvatarSource/GravatarSource.php | 2 +- lib/Storage/AvatarStorage.php | 28 +- package-lock.json | 936 +++++++++++++++++++- package.json | 26 +- 6 files changed, 1014 insertions(+), 113 deletions(-) diff --git a/composer.lock b/composer.lock index 5e4f01e48d..f75e9dec22 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "28522020e1dcad59038ae96380ca6d6e", + "content-hash": "101e0fa1f925af45b392e64d2e44ac4c", "packages": [ { "name": "cerdic/css-tidy", @@ -273,10 +273,10 @@ }, { "name": "pear-pear.horde.org/Horde_Date", - "version": "2.4.0", + "version": "2.4.1", "dist": { "type": "file", - "url": "https://pear.horde.org/get/Horde_Date-2.4.0.tgz", + "url": "https://pear.horde.org/get/Horde_Date-2.4.1.tgz", "reference": null, "shasum": null }, @@ -287,7 +287,7 @@ "php": "<8.0.0.0" }, "replace": { - "pear-horde/horde_date": "== 2.4.0.0" + "pear-horde/horde_date": "== 2.4.1.0" }, "type": "pear-library", "autoload": { @@ -335,10 +335,10 @@ }, { "name": "pear-pear.horde.org/Horde_Idna", - "version": "1.1.0", + "version": "1.1.1", "dist": { "type": "file", - "url": "https://pear.horde.org/get/Horde_Idna-1.1.0.tgz", + "url": "https://pear.horde.org/get/Horde_Idna-1.1.1.tgz", "reference": null, "shasum": null }, @@ -348,7 +348,7 @@ "php": "<8.0.0.0" }, "replace": { - "pear-horde/horde_idna": "== 1.1.0.0" + "pear-horde/horde_idna": "== 1.1.1.0" }, "type": "pear-library", "autoload": { @@ -366,10 +366,10 @@ }, { "name": "pear-pear.horde.org/Horde_Imap_Client", - "version": "2.29.14", + "version": "2.29.15", "dist": { "type": "file", - "url": "https://pear.horde.org/get/Horde_Imap_Client-2.29.14.tgz", + "url": "https://pear.horde.org/get/Horde_Imap_Client-2.29.15.tgz", "reference": null, "shasum": null }, @@ -388,7 +388,7 @@ "php": "<8.0.0.0" }, "replace": { - "pear-horde/horde_imap_client": "== 2.29.14.0" + "pear-horde/horde_imap_client": "== 2.29.15.0" }, "type": "pear-library", "autoload": { @@ -406,10 +406,10 @@ }, { "name": "pear-pear.horde.org/Horde_ListHeaders", - "version": "1.2.4", + "version": "1.2.5", "dist": { "type": "file", - "url": "https://pear.horde.org/get/Horde_ListHeaders-1.2.4.tgz", + "url": "https://pear.horde.org/get/Horde_ListHeaders-1.2.5.tgz", "reference": null, "shasum": null }, @@ -420,7 +420,7 @@ "php": "<8.0.0.0" }, "replace": { - "pear-horde/horde_listheaders": "== 1.2.4.0" + "pear-horde/horde_listheaders": "== 1.2.5.0" }, "type": "pear-library", "autoload": { @@ -896,6 +896,7 @@ "shasum": "" }, "type": "library", + "notification-url": "https://packagist.org/downloads/", "license": [ "AGPLv3" ], @@ -906,9 +907,6 @@ } ], "description": "Composer package containing Nextcloud's public API (classes, interfaces)", - "support": { - "source": "https://github.com/ChristophWurst/nextcloud_composer/tree/master" - }, "time": "2017-09-20T09:46:46+00:00" }, { @@ -967,37 +965,40 @@ }, { "name": "myclabs/deep-copy", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" }, "type": "library", "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" - } + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ "clone", "copy", @@ -1005,20 +1006,20 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "phpdocumentor/reflection-common", - "version": "1.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", "shasum": "" }, "require": { @@ -1059,26 +1060,26 @@ "reflection", "static analysis" ], - "time": "2015-12-27T11:43:31+00:00" + "time": "2017-09-11T18:02:19+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", + "version": "3.2.2", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", "shasum": "" }, "require": { "php": ">=5.5", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", + "phpdocumentor/type-resolver": "^0.3.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -1104,24 +1105,24 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30T07:12:33+00:00" + "time": "2017-08-08T06:39:58+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.2.1", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", + "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^5.5 || ^7.0", "phpdocumentor/reflection-common": "^1.0" }, "require-dev": { @@ -1151,26 +1152,26 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25T06:54:22+00:00" + "time": "2017-06-03T08:32:36+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.7.0", + "version": "v1.7.2", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", + "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", "sebastian/comparator": "^1.1|^2.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, @@ -1181,7 +1182,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -1214,7 +1215,7 @@ "spy", "stub" ], - "time": "2017-03-02T20:05:34+00:00" + "time": "2017-09-04T11:05:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1467,16 +1468,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.22", + "version": "5.7.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "10df877596c9906d4110b5b905313829043f2ada" + "reference": "78532d5269d984660080d8e0f4c99c5c2ea65ffe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/10df877596c9906d4110b5b905313829043f2ada", - "reference": "10df877596c9906d4110b5b905313829043f2ada", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/78532d5269d984660080d8e0f4c99c5c2ea65ffe", + "reference": "78532d5269d984660080d8e0f4c99c5c2ea65ffe", "shasum": "" }, "require": { @@ -1545,20 +1546,20 @@ "testing", "xunit" ], - "time": "2017-09-24T07:23:38+00:00" + "time": "2017-10-15T06:13:55+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", "shasum": "" }, "require": { @@ -1604,7 +1605,7 @@ "mock", "xunit" ], - "time": "2016-12-08T20:27:08+00:00" + "time": "2017-06-30T09:13:00+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -2121,20 +2122,20 @@ }, { "name": "symfony/yaml", - "version": "v3.3.2", + "version": "v3.3.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063" + "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/9752a30000a8ca9f4b34b5227d15d0101b96b063", - "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063", + "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46", + "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "require-dev": { "symfony/console": "~2.8|~3.0" @@ -2172,7 +2173,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-02T22:05:06+00:00" + "time": "2017-10-05T14:43:42+00:00" }, { "name": "webmozart/assert", diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index ad8ee73285..343201925e 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -64,7 +64,7 @@ private function initializeAppContainer() { $container->registerParameter("hostname", Util::getServerHostName()); $container->registerService('avatarStorage', function($c) { - return new AvatarStorage($c->query('ServerContainer')->getUserFolder()->getParent()); + return new AvatarStorage(); }); } } diff --git a/lib/Service/AvatarSource/GravatarSource.php b/lib/Service/AvatarSource/GravatarSource.php index 958354fba3..697e709f5c 100644 --- a/lib/Service/AvatarSource/GravatarSource.php +++ b/lib/Service/AvatarSource/GravatarSource.php @@ -48,7 +48,7 @@ public function fetch($email) { // Don't save 0 byte images $body = $response->getBody(); - if (strlen() === 0) { + if (strlen($body) === 0) { return null; } $this->storage->save($email, $body); diff --git a/lib/Storage/AvatarStorage.php b/lib/Storage/AvatarStorage.php index 0d9f4b8883..b8e186de9e 100644 --- a/lib/Storage/AvatarStorage.php +++ b/lib/Storage/AvatarStorage.php @@ -1,41 +1,37 @@ nodeExists('mail')) { - $storage->newFolder('mail'); - } - $node = $storage->get('mail'); - - if (!$node->nodeExists('avatars')) { - $node->newFolder('avatars'); + public function __construct(IAppData $appData) { + try { + $this->storage = $appData->getFolder('avatars'); + } catch (NotFoundException $e) { + $this->storage = $appData->newFolder('avatar'); } - - $this->storage = $node->get('avatars'); } public function save($email, $content) { // check if file exists and write to it if possible $filename = $this->getFilename($email); - try { - $file = $this->storage->get($filename); - } catch(\OCP\Files\NotFoundException $e) { + if (!$this->storage->fileExists($filename)) { $this->storage->newFile($filename); - $file = $this->storage->get($filename); } + $file = $this->storage->getFile($filename); + // the id can be accessed by $file->getId(); $file->putContent($content); } public function read($email) { - return $this->storage->get($this->getFilename($email))->getContent(); + return $this->storage->getFile($this->getFilename($email))->getContent(); } private function getFilename($email) { diff --git a/package-lock.json b/package-lock.json index 1ef5d97cf7..a50d37ccce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -198,12 +198,9 @@ "dev": true }, "autosize": { - "version": "1.18.13", - "resolved": "https://registry.npmjs.org/autosize/-/autosize-1.18.13.tgz", - "integrity": "sha1-7ljP8O2dzVDB/xB+culUAxyRl78=", - "requires": { - "jquery": "2.2.3" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.0.tgz", + "integrity": "sha1-egWZsbqE1zvXWJsNnaOHAVLGkjc=" }, "aws-sign2": { "version": "0.6.0", @@ -235,9 +232,9 @@ } }, "backbone.marionette": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/backbone.marionette/-/backbone.marionette-3.4.2.tgz", - "integrity": "sha1-QrYc0fdgsRSAY6DqauuYvlpiUsY=", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/backbone.marionette/-/backbone.marionette-3.4.4.tgz", + "integrity": "sha1-+i2Aa9qynaadiUEg05ZMpnUDIcI=", "requires": { "backbone.radio": "2.0.0" } @@ -600,6 +597,7 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", + "fsevents": "1.1.2", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -2009,6 +2007,905 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, + "fsevents": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", + "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "dev": true, + "optional": true, + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.36" + }, + "dependencies": { + "abbrev": { + "version": "1.1.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "dev": true + }, + "aproba": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.2.9" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "0.4.2", + "bundled": true, + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.7", + "bundled": true, + "dev": true, + "requires": { + "balanced-match": "0.4.2", + "concat-map": "0.0.1" + } + }, + "buffer-shims": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "caseless": { + "version": "0.12.0", + "bundled": true, + "dev": true, + "optional": true + }, + "co": { + "version": "4.6.0", + "bundled": true, + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "2.6.8", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true, + "dev": true, + "optional": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true, + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.15" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.1" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true, + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true, + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.0", + "sshpk": "1.13.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "dev": true + }, + "ini": { + "version": "1.3.4", + "bundled": true, + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "dev": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true, + "dev": true, + "optional": true + }, + "jodid25519": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true, + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.0.2", + "json-schema": "0.2.3", + "verror": "1.3.6" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "bundled": true, + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "bundled": true, + "dev": true, + "requires": { + "mime-db": "1.27.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "dev": true, + "requires": { + "brace-expansion": "1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "node-pre-gyp": { + "version": "0.6.36", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.1", + "semver": "5.3.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true, + "dev": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true, + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true, + "dev": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true, + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "bundled": true, + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.2.9", + "bundled": true, + "dev": true, + "requires": { + "buffer-shims": "1.0.0", + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "string_decoder": "1.0.1", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.15", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.0.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.2", + "tunnel-agent": "0.6.0", + "uuid": "3.0.1" + } + }, + "rimraf": { + "version": "2.6.1", + "bundled": true, + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.0.1", + "bundled": true, + "dev": true + }, + "semver": { + "version": "5.3.0", + "bundled": true, + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "dev": true, + "optional": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jodid25519": "1.0.2", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true, + "dev": true, + "optional": true + } + } + }, + "string_decoder": { + "version": "1.0.1", + "bundled": true, + "dev": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true, + "dev": true, + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.8", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.2.9", + "rimraf": "2.6.1", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "dev": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true, + "dev": true, + "optional": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "dev": true + }, + "uuid": { + "version": "3.0.1", + "bundled": true, + "dev": true, + "optional": true + }, + "verror": { + "version": "1.3.6", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "extsprintf": "1.0.2" + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "dev": true + } + } + }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -2375,9 +3272,9 @@ } }, "handlebars": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", - "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", "requires": { "async": "1.5.2", "optimist": "0.6.1", @@ -2396,9 +3293,9 @@ } }, "handlebars-loader": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/handlebars-loader/-/handlebars-loader-1.5.0.tgz", - "integrity": "sha1-nrszqoBm/Acz+xZ5tEFjitkRhLs=", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/handlebars-loader/-/handlebars-loader-1.6.0.tgz", + "integrity": "sha512-FsM1ALql8+RNJuTgD1qPrVT8GpB209hPUKml/uIoR6aJIoeoLB+l/jt8xBnpfzN74aaQ9kMXaZajhIE6PNHusg==", "requires": { "async": "0.2.10", "fastparse": "1.1.1", @@ -2851,7 +3748,7 @@ "escodegen": "1.8.1", "esprima": "2.7.3", "glob": "5.0.15", - "handlebars": "4.0.10", + "handlebars": "4.0.11", "js-yaml": "3.6.1", "mkdirp": "0.5.1", "nopt": "3.0.6", @@ -3900,6 +4797,13 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", + "dev": true, + "optional": true + }, "native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", diff --git a/package.json b/package.json index b5065c9343..27c20186f8 100644 --- a/package.json +++ b/package.json @@ -54,22 +54,22 @@ "autosize": "^4.0.0", "backbone": "^1.3.3", "backbone.babysitter": "^0.1.12", - "backbone.marionette": "^3.0.0", + "backbone.marionette": "^3.4.4", "backbone.radio": "^2.0.0", - "backbone.wreqr": "^1.3.7", - "davclient.js": "0.1.0", - "domready": "1.0.8", + "backbone.wreqr": "^1.4.0", + "davclient.js": "^0.1.0", + "domready": "^1.0.8", "es6-promise": "^4.1.1", "exports-loader": "^0.6.4", - "handlebars": "^4.0.10", - "handlebars-loader": "^1.5.0", - "ical.js": "^1.2.1", - "jquery": "2.2.3", - "jquery-ui": "1.10.4", - "jquery-visibility": "~1.0.11", - "js-storage": "1.0.1", + "handlebars": "^4.0.11", + "handlebars-loader": "^1.6.0", + "ical.js": "^1.2.2", + "jquery": "^2.2.3", + "jquery-ui": "^1.10.4", + "jquery-visibility": "^1.0.11", + "js-storage": "^1.0.1", "karma-sourcemap-loader": "^0.3.7", - "requirejs": "^2.3.3", - "underscore": "~1.8.3" + "requirejs": "^2.3.5", + "underscore": "^1.8.3" } } From 703a417ec9504105d661ee71c2e2950e3ab47bb2 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 6 Nov 2017 14:55:05 +0100 Subject: [PATCH 07/14] Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- appinfo/database.xml | 59 ------ appinfo/routes.php | 6 +- lib/AppInfo/Application.php | 9 +- lib/Contracts/IAvatarService.php | 44 ++++ lib/Controller/AvatarsController.php | 76 ++++--- lib/Controller/MessagesController.php | 15 +- lib/Db/Avatar.php | 46 ----- lib/Db/AvatarMapper.php | 45 ---- lib/Http/AvatarDownloadResponse.php | 15 +- .../AddressbookSource.php | 31 +-- lib/Service/Avatar/Cache.php | 97 +++++++++ lib/Service/Avatar/CompositeAvatarSource.php | 71 +++++++ lib/Service/Avatar/Downloader.php | 58 ++++++ lib/Service/Avatar/FaviconSource.php | 88 ++++++++ .../GravatarSource.php | 53 ++--- lib/Service/Avatar/IAvatarSource.php | 35 ++++ lib/Service/AvatarService.php | 153 +++++--------- lib/Service/AvatarSource/FaviconSource.php | 77 ------- lib/Service/AvatarSource/NoneSource.php | 35 ---- lib/Storage/AvatarStorage.php | 40 ---- tests/Controller/AvatarControllerTest.php | 110 ++++++++++ .../Service/AvatarServiceIntegrationTest.php | 67 ++++++ .../Service/Avatar/AddressbookSourceTest.php | 72 +++++++ tests/Service/Avatar/CacheTest.php | 127 ++++++++++++ .../Avatar/CompositeAvatarSourceTest.php | 96 +++++++++ tests/Service/Avatar/DownloaderTest.php | 85 ++++++++ tests/Service/Avatar/FaviconSourceTest.php | 127 ++++++++++++ tests/Service/Avatar/GravatarSouceTest.php | 89 ++++++++ tests/Service/AvatarServiceTest.php | 193 ++++++++++++++++++ 29 files changed, 1523 insertions(+), 496 deletions(-) create mode 100644 lib/Contracts/IAvatarService.php delete mode 100644 lib/Db/Avatar.php delete mode 100644 lib/Db/AvatarMapper.php rename lib/Service/{AvatarSource => Avatar}/AddressbookSource.php (65%) create mode 100644 lib/Service/Avatar/Cache.php create mode 100644 lib/Service/Avatar/CompositeAvatarSource.php create mode 100644 lib/Service/Avatar/Downloader.php create mode 100644 lib/Service/Avatar/FaviconSource.php rename lib/Service/{AvatarSource => Avatar}/GravatarSource.php (56%) create mode 100644 lib/Service/Avatar/IAvatarSource.php delete mode 100644 lib/Service/AvatarSource/FaviconSource.php delete mode 100644 lib/Service/AvatarSource/NoneSource.php delete mode 100644 lib/Storage/AvatarStorage.php create mode 100644 tests/Controller/AvatarControllerTest.php create mode 100644 tests/Integration/Service/AvatarServiceIntegrationTest.php create mode 100644 tests/Service/Avatar/AddressbookSourceTest.php create mode 100644 tests/Service/Avatar/CacheTest.php create mode 100644 tests/Service/Avatar/CompositeAvatarSourceTest.php create mode 100644 tests/Service/Avatar/DownloaderTest.php create mode 100644 tests/Service/Avatar/FaviconSourceTest.php create mode 100644 tests/Service/Avatar/GravatarSouceTest.php create mode 100644 tests/Service/AvatarServiceTest.php diff --git a/appinfo/database.xml b/appinfo/database.xml index fca2bea2d5..44c1e14444 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -238,63 +238,4 @@ - - *dbprefix*mail_avatars - - - id - integer - 1 - 0 - true - 4 - - - user_id - text - - true - 64 - - - email - text - - true - 64 - - - source - text - - true - 255 - - - url - text - - true - 64 - - - updated_at - integer - 0 - true - - - - mail_avatar_email_userid_index - - email - ascending - - - user_id - ascending - - - -
diff --git a/appinfo/routes.php b/appinfo/routes.php index d761ce949a..678482081d 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -76,12 +76,12 @@ ], [ 'name' => 'avatars#url', - 'url' => '/avatars/url', + 'url' => '/api/avatars/url/{email}', 'verb' => 'GET' ], [ - 'name' => 'avatars#file', - 'url' => '/avatars/file', + 'name' => 'avatars#image', + 'url' => '/api/avatars/image/{email}', 'verb' => 'GET' ], [ diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 343201925e..3ef9e3d81c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,12 +23,13 @@ namespace OCA\Mail\AppInfo; use OCA\Mail\Contracts\IAttachmentService; +use OCA\Mail\Contracts\IAvatarService; use OCA\Mail\Contracts\IMailManager; use OCA\Mail\Contracts\IMailTransmission; use OCA\Mail\Service\Attachment\AttachmentService; +use OCA\Mail\Service\AvatarService; use OCA\Mail\Service\MailManager; use OCA\Mail\Service\MailTransmission; -use OCA\Mail\Storage\AvatarStorage; use OCP\AppFramework\App; use OCP\Util; @@ -46,6 +47,7 @@ private function initializeAppContainer() { $transport = $container->getServer()->getConfig()->getSystemValue('app.mail.transport', 'smtp'); $testSmtp = $transport === 'smtp'; + $container->registerAlias(IAvatarService::class, AvatarService::class); $container->registerAlias(IMailManager::class, MailManager::class); $container->registerAlias(IAttachmentService::class, AttachmentService::class); $container->registerAlias(IMailTransmission::class, MailTransmission::class); @@ -62,9 +64,6 @@ private function initializeAppContainer() { $container->registerParameter("testSmtp", $testSmtp); $container->registerParameter("referrer", isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null); $container->registerParameter("hostname", Util::getServerHostName()); - - $container->registerService('avatarStorage', function($c) { - return new AvatarStorage(); - }); } + } diff --git a/lib/Contracts/IAvatarService.php b/lib/Contracts/IAvatarService.php new file mode 100644 index 0000000000..0af36d309b --- /dev/null +++ b/lib/Contracts/IAvatarService.php @@ -0,0 +1,44 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Contracts; + +interface IAvatarService { + + /** + * Try to find an avatar URL for the given email address + * + * @param string $email + * @param string $uid + * @return string|null URL, if one can be found + */ + public function getAvatarUrl($email, $uid); + + /** + * @param string $email + * @param string $uid + * @return mixed|null image data + */ + public function getAvatarImage($email, $uid); +} diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php index 901224dc5d..374b939397 100644 --- a/lib/Controller/AvatarsController.php +++ b/lib/Controller/AvatarsController.php @@ -1,6 +1,7 @@ * @author Jakob Sack * * Mail @@ -21,61 +22,90 @@ namespace OCA\Mail\Controller; -use OCP\AppFramework\Http\Response; +use OCA\Mail\Contracts\IAvatarService; +use OCA\Mail\Http\AvatarDownloadResponse; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; use OCP\IRequest; -use OCP\ISession; - -use OCA\Mail\Service\AvatarService; -use OCA\Mail\Http\AvatarDownloadResponse; class AvatarsController extends Controller { - /** @var ISession */ - private $session; - - /** @var AvatarService */ + /** @var IAvatarService */ private $avatarService; /** @var string */ - private $currentUserId; + private $uid; /** * @param string $appName * @param IRequest $request - * @param ISession $session - * @param AvatarService $avatarService - * @param IClientService $clientService + * @param IAvatarService $avatarService * @param string $UserId */ - public function __construct($appName, IRequest $request, ISession $session, AvatarService $avatarService, $userId) { + public function __construct($appName, IRequest $request, IAvatarService $avatarService, $UserId) { parent::__construct($appName, $request); - $this->session = $session; $this->avatarService = $avatarService; - $this->currentUserId = $userId; + $this->uid = $UserId; } /** * @NoAdminRequired - * @NoCSRFRequired * * @param string $email * @return Response */ public function url($email) { - // Get the data from the service - return $this->avatarService->rewriteUrl($this->avatarService->fetch($email, $this->currentUserId)); + if (is_null($email) || empty($email)) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + $avatarUrl = $this->avatarService->getAvatarUrl($email, $this->uid); + if (is_null($avatarUrl)) { + // No avatar found + $response = new JSONResponse([], Http::STATUS_NOT_FOUND); + + // Debounce this a bit + // (cache for one day) + $response->cacheFor(24 * 60 * 60); + + return $response; + } + + $response = new JSONResponse([ + 'url' => $avatarUrl, + ]); + + // Let the browser cache this for a week + $response->cacheFor(7 * 24 * 60 * 60); + + return $response; } /** * @NoAdminRequired - * @NoCSRFRequired * * @param string $email * @return Response */ - public function file($email) { - // Return file (or fail) - return new AvatarDownloadResponse($this->avatarService->loadFile($email)); + public function image($email) { + if (is_null($email) || empty($email)) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + $imageData = $this->avatarService->getAvatarImage($email, $this->uid); + + if (is_null($imageData)) { + // This could happen if the cache invalidated meanwhile + $response = new Response(); + $response->setStatus(Http::STATUS_NOT_FOUND); + // Clear cache + $response->cacheFor(0); + return $response; + } + + return new AvatarDownloadResponse($imageData); } + } diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index edf942ae1f..4bb9e92e16 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -81,9 +81,6 @@ class MessagesController extends Controller { /** @var IAccount[] */ private $accounts = []; - /** @var AvatarService */ - private $avatarService; - /** * @param string $appName * @param IRequest $request @@ -105,8 +102,7 @@ public function __construct($appName, Logger $logger, IL10N $l10n, IMimeTypeDetector $mimeTypeDetector, - IURLGenerator $urlGenerator, - AvatarService $avatarService) { + IURLGenerator $urlGenerator) { parent::__construct($appName, $request); $this->accountService = $accountService; $this->currentUserId = $UserId; @@ -116,7 +112,6 @@ public function __construct($appName, $this->l10n = $l10n; $this->mimeTypeDetector = $mimeTypeDetector; $this->urlGenerator = $urlGenerator; - $this->avatarService = $avatarService; } /** @@ -158,10 +153,6 @@ public function index($accountId, $folderId, $cursor = null, $filter=null, $ids= } } - $avatar = $this->avatarService->loadFromCache($j['fromEmail'], $this->currentUserId); - $j['senderImage'] = $avatar === null ? '' : $this->avatarService->rewriteUrl($avatar)->getUrl(); - $j['avatarInDatabase'] = $avatar !== null; - return $j; }, $messages); @@ -504,10 +495,6 @@ private function loadMultiple($accountId, $folderId, $ids) { private function enhanceMessage($accountId, $folderId, $id, IMAPMessage $m, $mailBox) { $json = $m->getFullMessage($mailBox->getSpecialRole()); - $avatar = $this->avatarService->loadFromCache($m->getFrom()->first()->toHorde()->bare_address, $this->currentUserId); - $json['senderImage'] = $avatar === null ? '' : $this->avatarService->rewriteUrl($avatar)->getUrl(); - $json['avatarInDatabase'] = $avatar !== null; - if (isset($json['hasHtmlBody'])) { $json['htmlBodyUrl'] = $this->buildHtmlBodyUrl($accountId, $folderId, $id); } diff --git a/lib/Db/Avatar.php b/lib/Db/Avatar.php deleted file mode 100644 index b6a74eb9c6..0000000000 --- a/lib/Db/Avatar.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\Mail\Db; - -use OCP\AppFramework\Db\Entity; - -/** - * @method void setUserId(string $userId) - * @method string getUserId() - * @method void setEmail(string $email) - * @method string getEmail() - * @method void setSource(string $source) - * @method string getSource() - * @method void setUrl(string $url) - * @method string getUrl() - * @method void setUpdatedAt(string $updatedAt) - * @method string getUpdatedAt() - */ -class Avatar extends Entity { - - public $userId; - public $email; - public $source; - public $url; - public $updatedAt; - -} diff --git a/lib/Db/AvatarMapper.php b/lib/Db/AvatarMapper.php deleted file mode 100644 index 13041bbaee..0000000000 --- a/lib/Db/AvatarMapper.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\Mail\Db; - -use OCP\AppFramework\Db\Mapper; -use OCP\IDBConnection; - -class AvatarMapper extends Mapper { - - /** - * @param IDBConnection $db - */ - public function __construct(IDBConnection $db) { - parent::__construct($db, 'mail_avatars'); - } - - /** - * @param int $email - * @param string $currentUserId - * @return Avatar - */ - public function find($email, $currentUserId) { - $sql = 'select * from *PREFIX*mail_avatars where user_id = ? and email = ?'; - return $this->findEntities($sql, [$currentUserId, $email]); - } -} diff --git a/lib/Http/AvatarDownloadResponse.php b/lib/Http/AvatarDownloadResponse.php index 9a01e4842e..a95e19359f 100644 --- a/lib/Http/AvatarDownloadResponse.php +++ b/lib/Http/AvatarDownloadResponse.php @@ -28,27 +28,20 @@ class AvatarDownloadResponse extends DownloadResponse { private $content; /** - * Creates a response that prompts the user to download a file which - * contains the passed string - * Additionally the response will be cacheable by browsers. Since the content is - * generally not sensitive content (e.g. Logos in mails) this should not be a problem. - * @param IFile $content the content that should be written into the file + * @param string $content */ - public function __construct($content){ + public function __construct($content) { parent::__construct('avatar', 'application/octet-stream'); $this->content = $content; - $expires = new \DateTime('now + 2 hours'); - $this->addHeader('Expires', $expires->format(\DateTime::RFC1123)); - $this->addHeader('Cache-Control', 'private'); - $this->addHeader('Pragma', 'cache'); + $this->cacheFor(2 * 60 * 60); } /** * Simply sets the headers and returns the file contents * @return string the file contents */ - public function render(){ + public function render() { return $this->content; } diff --git a/lib/Service/AvatarSource/AddressbookSource.php b/lib/Service/Avatar/AddressbookSource.php similarity index 65% rename from lib/Service/AvatarSource/AddressbookSource.php rename to lib/Service/Avatar/AddressbookSource.php index b35c147e15..e6c87eccb3 100644 --- a/lib/Service/AvatarSource/AddressbookSource.php +++ b/lib/Service/Avatar/AddressbookSource.php @@ -1,6 +1,7 @@ * @author Jakob Sack * * Mail @@ -19,27 +20,33 @@ * */ -namespace OCA\Mail\Service\AvatarSource; +namespace OCA\Mail\Service\Avatar; use OCA\Mail\Service\ContactsIntegration; -class AddressbookSource { +/** + * This class is just a think wrapper around the contacts integration to use it + * as avatar source + */ +class AddressbookSource implements IAvatarSource { + /** @var ContactsIntegration */ private $contactsIntegration; + /** + * @param ContactsIntegration $contactsIntegration + */ public function __construct(ContactsIntegration $contactsIntegration) { $this->contactsIntegration = $contactsIntegration; } - public function fetch($email) { - if ($this->contactsIntegration->getPhoto($email) === null) { - return null; - } - - return [ - 'email' => $email, - 'source' => 'addressbook', - 'url' => '' - ]; + /** + * @param string $email + * @param string $uid + * @return string|null + */ + public function fetch($email, $uid) { + return $this->contactsIntegration->getPhoto($email); } + } diff --git a/lib/Service/Avatar/Cache.php b/lib/Service/Avatar/Cache.php new file mode 100644 index 0000000000..2017b2e969 --- /dev/null +++ b/lib/Service/Avatar/Cache.php @@ -0,0 +1,97 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +use OCP\ICache; +use OCP\ICacheFactory; + +class Cache { + + // Cache for one week + const CACHE_TTL = 7 * 24 * 60 * 60; + + /** @var ICache */ + private $cache; + + /** + * @param ICacheFactory $cacheFactory + */ + public function __construct(ICacheFactory $cacheFactory) { + $this->cache = $cacheFactory->create('mail.avatars'); + } + + /** + * @param string $email + * @param string $uid + * @return string + */ + private function buildUrlKey($email, $uid) { + return base64_encode(json_encode([$email, $uid])); + } + + /** + * @param string $url + * @param string $uid + * @return string + */ + private function buildImageKey($url, $uid) { + return base64_encode(json_encode([$url, $uid])); + } + + /** + * @param string $email + * @return string|null avatar URL + */ + public function getUrl($email, $uid) { + return $this->cache->get($this->buildUrlKey($email, $uid)); + } + + /** + * @param string $email + * @param string $url + */ + public function addUrl($email, $uid, $url) { + $this->cache->set($this->buildUrlKey($email, $uid), $url, self::CACHE_TTL); + } + + /** + * @param string $url + * @param string $uid + * @return string|null cached image data + */ + public function getImage($url, $uid) { + return $this->cache->get($this->buildImageKey($url, $uid)); + } + + /** + * @param string $url + * @param string $uid + * @param string $image base64 encoded image data + */ + public function addImage($url, $uid, $image) { + $this->cache->set($this->buildImageKey($url, $uid), $image, self::CACHE_TTL); + } + +} diff --git a/lib/Service/Avatar/CompositeAvatarSource.php b/lib/Service/Avatar/CompositeAvatarSource.php new file mode 100644 index 0000000000..20899d7f7a --- /dev/null +++ b/lib/Service/Avatar/CompositeAvatarSource.php @@ -0,0 +1,71 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +/** + * Composition of all avatar sources for easier usage + */ +class CompositeAvatarSource implements IAvatarSource { + + /** @var IAvatarSource[] */ + private $sources; + + /** + * @param AddressbookSource $addressbookSource + * @param FaviconSource $faviconSource + * @param GravatarSource $gravatarSource + */ + public function __construct(AddressbookSource $addressbookSource, FaviconSource $faviconSource, GravatarSource $gravatarSource) { + // This determines the priority of known sources + $this->sources = [ + $addressbookSource, + $gravatarSource, + $faviconSource, + ]; + } + + /** + * Find avatar URL with the help of avatar sources and return the first + * valid result. + * + * @param string $email + * @param string $uid + * @return string|null + */ + public function fetch($email, $uid) { + foreach ($this->sources as $source) { + $avatar = $source->fetch($email, $uid); + + if (is_null($avatar)) { + continue; + } + + return $avatar; + } + + return null; + } + +} diff --git a/lib/Service/Avatar/Downloader.php b/lib/Service/Avatar/Downloader.php new file mode 100644 index 0000000000..d7a0a30604 --- /dev/null +++ b/lib/Service/Avatar/Downloader.php @@ -0,0 +1,58 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +use Exception; +use OCP\Http\Client\IClientService; + +class Downloader { + + /** @var IClientService */ + private $clientService; + + /** + * @param IClientService $clientService + */ + public function __construct(IClientService $clientService) { + $this->clientService = $clientService; + } + + /** + * @param string $url + * @return string|null + */ + public function download($url) { + $client = $this->clientService->newClient(); + + try { + $resp = $client->get($url); + } catch (Exception $e) { + return null; + } + + return $resp->getBody(); + } + +} diff --git a/lib/Service/Avatar/FaviconSource.php b/lib/Service/Avatar/FaviconSource.php new file mode 100644 index 0000000000..91fda81dff --- /dev/null +++ b/lib/Service/Avatar/FaviconSource.php @@ -0,0 +1,88 @@ + + * @author Jakob Sack + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Service\Avatar; + +use Exception; +use Horde_Mail_Rfc822_Address; +use Mpclarkson\IconScraper\Scraper; +use OCP\Http\Client\IClientService; + +class FaviconSource implements IAvatarSource { + + /** @var IClientService */ + private $clientService; + + /** @var Scraper */ + private $scraper; + + /** + * @param IClientService $clientService + * @param Scraper $scraper + */ + public function __construct(IClientService $clientService, Scraper $scraper) { + $this->clientService = $clientService; + $this->scraper = $scraper; + } + + /** + * @param string $email + * @param string $uid + * @return string|null + */ + public function fetch($email, $uid) { + $horde = new Horde_Mail_Rfc822_Address($email); + // TODO: fall back to insecure HTTP? + $domain = 'https://' . $horde->host; + + $icons = $this->scraper->get($domain); + + if (empty($icons)) { + return null; + } + + usort($icons, function ($a, $b) { + return $b->getHeight() - $a->getHeight(); + }); + + $client = $this->clientService->newClient(); + foreach ($icons as $icon) { + $url = $icon->getHref(); + try { + $response = $client->get($url); + } catch (Exception $exception) { + continue; + } + + // Don't save 0 byte images + $body = $response->getBody(); + if (strlen($body) === 0) { + continue; + } + + return $url; + } + + return null; + } + +} diff --git a/lib/Service/AvatarSource/GravatarSource.php b/lib/Service/Avatar/GravatarSource.php similarity index 56% rename from lib/Service/AvatarSource/GravatarSource.php rename to lib/Service/Avatar/GravatarSource.php index 697e709f5c..008392bc70 100644 --- a/lib/Service/AvatarSource/GravatarSource.php +++ b/lib/Service/Avatar/GravatarSource.php @@ -1,6 +1,7 @@ * @author Jakob Sack * * Mail @@ -19,48 +20,48 @@ * */ -namespace OCA\Mail\Service\AvatarSource; +namespace OCA\Mail\Service\Avatar; -use OCA\Mail\Storage\AvatarStorage; +use Exception; +use Gravatar\Gravatar; use OCP\Http\Client\IClientService; -use Gravatar\Gravatar; +class GravatarSource implements IAvatarSource { -class GravatarSource { /** @var IClientService */ private $clientService; - /** @var AvatarStorage */ - private $storage; - - public function __construct(IClientService $clientService, AvatarStorage $avatarStorage) { + /** + * @param IClientService $clientService + */ + public function __construct(IClientService $clientService) { $this->clientService = $clientService; - $this->storage = $avatarStorage; } - public function fetch($email) { - $gravatar = (new Gravatar(['size' => 128], true))->avatar($email, ['d' => 404]); + /** + * @param string $email + * @param string $uid + * @return string|null + */ + public function fetch($email, $uid) { + $gravatar = new Gravatar(['size' => 128], true); + $avatar = $gravatar->avatar($email, ['d' => 404], true); $client = $this->clientService->newClient(); try { - $response = $client->get($gravatar); - - // Don't save 0 byte images - $body = $response->getBody(); - if (strlen($body) === 0) { - return null; - } - $this->storage->save($email, $body); - - return [ - 'email' => $email, - 'source' => 'gravatar', - 'url' => $gravatar - ]; + $response = $client->get($avatar); + } catch (Exception $exception) { + return null; } - catch (\Exception $exception) { + + // Don't save 0 byte images + $body = $response->getBody(); + if (strlen($body) === 0) { return null; } + + return $avatar; } + } diff --git a/lib/Service/Avatar/IAvatarSource.php b/lib/Service/Avatar/IAvatarSource.php new file mode 100644 index 0000000000..8b4895cc77 --- /dev/null +++ b/lib/Service/Avatar/IAvatarSource.php @@ -0,0 +1,35 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +interface IAvatarSource { + + /** + * @param string $email sender email address + * @param string $uid User ID + * @return string|null avatar URL if one can be found + */ + public function fetch($email, $uid); +} diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index c575cde435..c9c979aa51 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -21,129 +21,82 @@ namespace OCA\Mail\Service; -use OCA\Mail\Db\Avatar; -use OCA\Mail\Db\AvatarMapper; -use OCA\Mail\Service\AvatarSource\AddressbookSource; -use OCA\Mail\Service\AvatarSource\FaviconSource; -use OCA\Mail\Service\AvatarSource\GravatarSource; -use OCA\Mail\Service\AvatarSource\NoneSource; -use OCA\Mail\Service\ContactsIntegration; -use OCA\Mail\Storage\AvatarStorage; - +use OCA\Mail\Contracts\IAvatarService; +use OCA\Mail\Service\Avatar\Cache as AvatarCache; +use OCA\Mail\Service\Avatar\CompositeAvatarSource; +use OCA\Mail\Service\Avatar\Downloader; +use OCA\Mail\Service\Avatar\IAvatarSource; use OCP\IURLGenerator; -class AvatarService { - /** @var ContactsIntegration */ - private $contactsIntegration; - - /** @var IURLGenerator */ - private $urlGenerator; - - /** @var AvatarMapper */ - private $mapper; +class AvatarService implements IAvatarService { - /** @var AvatarStorage */ - private $storage; + /** @var AvatarCache */ + private $cache; - /** @var AddressbookSource */ - private $addressbookSource; + /** @var Downloader */ + private $downloader; - /** @var FaviconSource */ - private $faviconSource; + /** @var IAvatarSource */ + private $source; - /** @var GravatarSource */ - private $gravatarSource; - - /** @var NoneSource */ - private $noneSource; + /** @var IURLGenerator */ + private $urlGenerator; - public function __construct(AvatarMapper $avatarMapper, - ContactsIntegration $contactsIntegration, - IURLGenerator $urlGenerator, - AvatarStorage $avatarStorage, - AddressbookSource $addressbookSource, - FaviconSource $faviconSource, - GravatarSource $gravatarSource, - NoneSource $noneSource) { - $this->mapper = $avatarMapper; - $this->contactsIntegration = $contactsIntegration; + public function __construct(CompositeAvatarSource $source, Downloader $downloader, AvatarCache $cache, IURLGenerator $urlGenerator) { + $this->source = $source; + $this->cache = $cache; $this->urlGenerator = $urlGenerator; - $this->storage = $avatarStorage; - $this->addressbookSource = $addressbookSource; - $this->faviconSource = $faviconSource; - $this->gravatarSource = $gravatarSource; - $this->noneSource = $noneSource; + $this->downloader = $downloader; } - public function rewriteUrl(Avatar $avatar) { - if ($avatar === null) { - return null; + /** + * @param string $email + * @param string $uid + * @return string|null + */ + public function getAvatarUrl($email, $uid) { + $cachedUrl = $this->cache->getUrl($email, $uid); + if (!is_null($cachedUrl)) { + return $cachedUrl; } - $copy = new Avatar(); - $copy->setUserId($avatar->getUserId()); - $copy->setEmail($avatar->getEmail()); - $copy->setSource($avatar->getSource()); - $copy->setUpdatedAt($avatar->getUpdatedAt()); - $copy->setUrl(''); - - if ($avatar->getSource() === 'addressbook') { - $result = $this->contactsIntegration->getPhoto($avatar->getEmail()); - if ($result != null) { - $copy->setUrl($result); - } - } - elseif ($avatar->getUrl() !== '') { - $copy->setUrl($this->urlGenerator->linkToRoute('mail.avatars.file', [ 'email' => $avatar->getEmail() ])); + $url = $this->source->fetch($email, $uid); + if (is_null($url)) { + // Cannot locate any avatar -> nothing to do here + return null; } - return $copy; - } + // Cache for the next call + $this->cache->addUrl($email, $uid, $url); - public function loadFile($email) { - return $this->storage->read($email); + return $url; } - public function loadFromCache($email, $userId) { - // Find avatars (but not older than a week - $result = $this->mapper->find($email, $userId); - if (count($result) > 0 && time() - $result[0]->getUpdatedAt() < 604800) { - return $result[0]; + /** + * @param string $email + * @param string $uid + * @return mixed|null image data + */ + public function getAvatarImage($email, $uid) { + $url = $this->cache->getUrl($email, $uid); + if (is_null($url)) { + return null; } - return null; - } - - public function fetch($email, $userId) { - // Try to fetch old data first - $avatar = $this->loadFromCache($email, $userId); - if (!is_null($avatar)) { - return $avatar; + $cachedImage = $this->cache->getImage($url, $uid); + if (!is_null($cachedImage)) { + return base64_decode($cachedImage); } - // our avatar is too old or does not exist - if (is_null($avatar)) { - $avatar = new Avatar(); - $avatar->setUserId($userId); - $avatar->setEmail($email); + $image = $this->downloader->download($url); + if (is_null($image)) { + return null; } - // 1) load the photo from the address book - $result = $this->addressbookSource->fetch($email); - if(!$result) $result = $this->gravatarSource->fetch($email); - if(!$result) $result = $this->faviconSource->fetch($email); - if(!$result) $result = $this->noneSource->fetch($email); - - $avatar->setSource($result['source']); - $avatar->setUrl($result['url']); - $avatar->setUpdatedAt(time()); + // Cache for the next call + $this->cache->addImage($url, $uid, base64_encode($image)); - if ($avatar->getId() === null) { - $this->mapper->insert($avatar); - } else { - $this->mapper->update($avatar); - } - - return $avatar; + return $image; } + } diff --git a/lib/Service/AvatarSource/FaviconSource.php b/lib/Service/AvatarSource/FaviconSource.php deleted file mode 100644 index c411997214..0000000000 --- a/lib/Service/AvatarSource/FaviconSource.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\Mail\Service\AvatarSource; - -use OCA\Mail\Storage\AvatarStorage; -use OCP\Http\Client\IClientService; - -use Mpclarkson\IconScraper\Scraper; - -class FaviconSource { - /** @var IClientService */ - private $clientService; - - /** @var AvatarStorage */ - private $storage; - - public function __construct(IClientService $clientService, AvatarStorage $avatarStorage) { - $this->clientService = $clientService; - $this->storage = $avatarStorage; - } - - public function fetch($email) { - $lastAt = strrpos($email, '@'); - $domain = "http://"; - $domain .= $lastAt === false ? $email : substr($email, $lastAt + 1); - $scraper = new Scraper(); - $icons = $scraper->get($domain); - - if (count($icons) === 0) return null; - - usort($icons, function ($a, $b) { return $b->getHeight() - $a->getHeight(); }); - - $client = $this->clientService->newClient(); - foreach ($icons as $icon) { - try { - $response = $client->get($icon->getHref()); - - // Don't save 0 byte images - $body = $response->getBody(); - if (strlen($body) === 0) { - continue; - } - $this->storage->save($email, $body); - - return [ - 'email' => $email, - 'source' => 'favicon', - 'url' => $icon->getHref() - ]; - } - catch (\Exception $exception){ - continue; - } - } - - return null; - } -} diff --git a/lib/Service/AvatarSource/NoneSource.php b/lib/Service/AvatarSource/NoneSource.php deleted file mode 100644 index 58d630c07e..0000000000 --- a/lib/Service/AvatarSource/NoneSource.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * Mail - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see - * - */ - -namespace OCA\Mail\Service\AvatarSource; - -class NoneSource { - public function __construct() { - } - - public function fetch($email) { - return [ - 'email' => $email, - 'source' => 'none', - 'url' => '' - ]; - } -} diff --git a/lib/Storage/AvatarStorage.php b/lib/Storage/AvatarStorage.php deleted file mode 100644 index b8e186de9e..0000000000 --- a/lib/Storage/AvatarStorage.php +++ /dev/null @@ -1,40 +0,0 @@ -storage = $appData->getFolder('avatars'); - } catch (NotFoundException $e) { - $this->storage = $appData->newFolder('avatar'); - } - } - - public function save($email, $content) { - // check if file exists and write to it if possible - $filename = $this->getFilename($email); - - if (!$this->storage->fileExists($filename)) { - $this->storage->newFile($filename); - } - - $file = $this->storage->getFile($filename); - - // the id can be accessed by $file->getId(); - $file->putContent($content); - } - - public function read($email) { - return $this->storage->getFile($this->getFilename($email))->getContent(); - } - - private function getFilename($email) { - return md5($email); - } -} diff --git a/tests/Controller/AvatarControllerTest.php b/tests/Controller/AvatarControllerTest.php new file mode 100644 index 0000000000..6c4e32ffaa --- /dev/null +++ b/tests/Controller/AvatarControllerTest.php @@ -0,0 +1,110 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Controller; + +use OCA\Mail\Contracts\IAvatarService; +use OCA\Mail\Controller\AvatarsController; +use OCA\Mail\Http\AvatarDownloadResponse; +use OCA\Mail\Tests\TestCase; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; +use OCP\IRequest; +use PHPUnit_Framework_MockObject_MockObject; + +class AvatarControllerTest extends TestCase { + + /** @var IAvatarService|PHPUnit_Framework_MockObject_MockObject */ + private $avatarService; + + /** @var AvatarsController */ + private $controller; + + protected function setUp() { + parent::setUp(); + + $request = $this->createMock(IRequest::class); + $this->avatarService = $this->createMock(IAvatarService::class); + + $this->controller = new AvatarsController('mail', $request, $this->avatarService, 'jane'); + } + + public function testGetUrl() { + $email = 'john@doe.com'; + $this->avatarService->expects($this->once()) + ->method('getAvatarUrl') + ->with($email, 'jane') + ->willReturn('https://doe.com/favicon.ico'); + + $resp = $this->controller->url($email); + + $expected = new JSONResponse(['url' => 'https://doe.com/favicon.ico']); + $expected->cacheFor(7 * 24 * 60 * 60); + $this->assertEquals($expected, $resp); + } + + public function testGetUrlNoAvatarFound() { + $email = 'john@doe.com'; + $this->avatarService->expects($this->once()) + ->method('getAvatarUrl') + ->with($email, 'jane') + ->willReturn(null); + + $resp = $this->controller->url($email); + + $expected = new JSONResponse([], Http::STATUS_NOT_FOUND); + $expected->cacheFor(24 * 60 * 60); + $this->assertEquals($expected, $resp); + } + + public function testGetImage() { + $email = 'john@doe.com'; + $this->avatarService->expects($this->once()) + ->method('getAvatarImage') + ->with($email, 'jane') + ->willReturn('data'); + + $resp = $this->controller->image($email); + + $expected = new AvatarDownloadResponse('data'); + $this->assertEquals($expected, $resp); + } + + public function testGetImageNotFound() { + $email = 'john@doe.com'; + $this->avatarService->expects($this->once()) + ->method('getAvatarImage') + ->with($email, 'jane') + ->willReturn(null); + + $resp = $this->controller->image($email); + + $expected = new Response(); + $expected->setStatus(Http::STATUS_NOT_FOUND); + $expected->cacheFor(0); + $this->assertEquals($expected, $resp); + } + +} diff --git a/tests/Integration/Service/AvatarServiceIntegrationTest.php b/tests/Integration/Service/AvatarServiceIntegrationTest.php new file mode 100644 index 0000000000..246f5ab88d --- /dev/null +++ b/tests/Integration/Service/AvatarServiceIntegrationTest.php @@ -0,0 +1,67 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Integration\Service; + +use OC; +use OCA\Mail\Contracts\IAvatarService; +use OCA\Mail\Tests\TestCase; +use OCP\ICache; +use OCP\ICacheFactory; + +class AvatarServiceIntegrationTest extends TestCase { + + /** @var IAvatarService */ + private $service; + + private function clearCache() { + /* @var $cacheFactory ICacheFactory */ + $cacheFactory = OC::$server->query(ICacheFactory::class); + /* @var $cache ICache */ + $cache = $cacheFactory->create('mail.avatars'); + $cache->clear(); + } + + protected function setUp() { + parent::setUp(); + + $this->clearCache(); + $this->service = OC::$server->query(IAvatarService::class); + } + + public function testJansGravatar() { + $avatar = $this->service->getAvatarUrl('hey@jancborchardt.net', 'john'); + $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar); + $image = $this->service->getAvatarImage('hey@jancborchardt.net', 'john'); + $this->assertNotNull($image); + } + + public function testJansNicknameFavicon() { + $avatar = $this->service->getAvatarUrl('janli@nextcloud.com', 'jane'); + $this->assertEquals('https://nextcloud.com/media/screenshot-150x150.png?x16328', $avatar); + $image = $this->service->getAvatarImage('janli@nextcloud.com', 'jane'); + $this->assertNotNull($image); + } + +} diff --git a/tests/Service/Avatar/AddressbookSourceTest.php b/tests/Service/Avatar/AddressbookSourceTest.php new file mode 100644 index 0000000000..67edb0c2fc --- /dev/null +++ b/tests/Service/Avatar/AddressbookSourceTest.php @@ -0,0 +1,72 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use OCA\Mail\Service\Avatar\AddressbookSource; +use OCA\Mail\Service\ContactsIntegration; +use OCA\Mail\Tests\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +class AddressbookSourceTest extends TestCase { + + /** @var ContactsIntegration|PHPUnit_Framework_MockObject_MockObject */ + private $ci; + + /** @var AddressbookSource */ + private $source; + + protected function setUp() { + parent::setUp(); + + $this->ci = $this->createMock(ContactsIntegration::class); + + $this->source = new AddressbookSource($this->ci); + } + + public function testFetch() { + $email = 'john@doe.com'; + $uid = 'jane'; + $this->ci->expects($this->once()) + ->method('getPhoto') + ->willReturn('https://next.cloud/photo'); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertSame('https://next.cloud/photo', $avatar); + } + + public function testFetchNoneFound() { + $email = 'john@doe.com'; + $uid = 'jane'; + $this->ci->expects($this->once()) + ->method('getPhoto') + ->willReturn(null); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertNull($avatar); + } + +} diff --git a/tests/Service/Avatar/CacheTest.php b/tests/Service/Avatar/CacheTest.php new file mode 100644 index 0000000000..8fa4908700 --- /dev/null +++ b/tests/Service/Avatar/CacheTest.php @@ -0,0 +1,127 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use OCA\Mail\Service\Avatar\Cache; +use OCA\Mail\Tests\TestCase; +use OCP\ICache; +use OCP\ICacheFactory; +use PHPUnit_Framework_MockObject_MockObject; + +class CacheTest extends TestCase { + + const BLACK_DOT_BASE64 = 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + + /** @var ICacheFactory|PHPUnit_Framework_MockObject_MockObject */ + private $cacheFactory; + + /** @var ICache|PHPUnit_Framework_MockObject_MockObject */ + private $cacheImpl; + + /** @var Cache */ + private $cache; + + protected function setUp() { + parent::setUp(); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cacheImpl = $this->createMock(ICache::class); + $this->cacheFactory->expects($this->once()) + ->method('create') + ->with('mail.avatars') + ->willReturn($this->cacheImpl); + $this->cache = new Cache($this->cacheFactory); + } + + public function testGetNonCachedUrl() { + $email = 'john@doe.com'; + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('get') + ->with(base64_encode(json_encode([$email, $uid]))) + ->willReturn(null); + + $url = $this->cache->getUrl($email, $uid); + + $this->assertNull($url); + } + + public function testGetCachedUrl() { + $email = 'john@doe.com'; + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('get') + ->with(base64_encode(json_encode([$email, $uid]))) + ->willReturn('https://doe.com/favicon.ico'); + + $url = $this->cache->getUrl($email, $uid); + + $this->assertEquals('https://doe.com/favicon.ico', $url); + } + + public function testSetUrl() { + $email = 'john@doe.com'; + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('set') + ->with(base64_encode(json_encode([$email, $uid])), 'https://doe.com/favicon.ico', 7 * 24 * 60 * 60); + + $this->cache->addUrl($email, $uid, 'https://doe.com/favicon.ico'); + } + + public function testGetImage() { + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('get') + ->with(base64_encode(json_encode(['https://doe.com/favicon.ico', $uid]))) + ->willReturn(self::BLACK_DOT_BASE64); + + $image = $this->cache->getImage('https://doe.com/favicon.ico', $uid); + + $this->assertEquals(self::BLACK_DOT_BASE64, $image); + } + + public function testGetNonCachedImage() { + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('get') + ->with(base64_encode(json_encode(['https://doe.com/favicon.ico', $uid]))) + ->willReturn(null); + + $image = $this->cache->getImage('https://doe.com/favicon.ico', $uid); + + $this->assertNull($image); + } + + public function testAddImage() { + $uid = 'jane'; + $this->cacheImpl->expects($this->once()) + ->method('set') + ->with(base64_encode(json_encode(['https://doe.com/favicon.ico', $uid])), self::BLACK_DOT_BASE64, 7 * 24 * 60 * 60); + + $this->cache->addImage('https://doe.com/favicon.ico', $uid, self::BLACK_DOT_BASE64); + } + +} diff --git a/tests/Service/Avatar/CompositeAvatarSourceTest.php b/tests/Service/Avatar/CompositeAvatarSourceTest.php new file mode 100644 index 0000000000..8e2312feac --- /dev/null +++ b/tests/Service/Avatar/CompositeAvatarSourceTest.php @@ -0,0 +1,96 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use OCA\Mail\Service\Avatar\AddressbookSource; +use OCA\Mail\Service\Avatar\CompositeAvatarSource; +use OCA\Mail\Service\Avatar\FaviconSource; +use OCA\Mail\Service\Avatar\GravatarSource; +use OCA\Mail\Tests\TestCase; +use PHPUnit_Framework_MockObject_MockObject; + +class CompositeAvatarSourceTest extends TestCase { + + /** @var AddressbookSource|PHPUnit_Framework_MockObject_MockObject */ + private $addressbookSource; + + /** @var GravatarSource|PHPUnit_Framework_MockObject_MockObject */ + private $gravatarSource; + + /** @var FaviconSource|PHPUnit_Framework_MockObject_MockObject */ + private $faviconSource; + + /** @var CompositeAvatarSource */ + private $source; + + protected function setUp() { + parent::setUp(); + + $this->addressbookSource = $this->createMock(AddressbookSource::class); + $this->gravatarSource = $this->createMock(GravatarSource::class); + $this->faviconSource = $this->createMock(FaviconSource::class); + + $this->source = new CompositeAvatarSource($this->addressbookSource, $this->faviconSource, $this->gravatarSource); + } + + public function testFetchNoneFound() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->addressbookSource->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn(null); + $this->gravatarSource->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn(null); + $this->faviconSource->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn(null); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertNull($avatar); + } + + public function testFetchFromGravatar() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->addressbookSource->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn(null); + $this->gravatarSource->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn('https://gravatar.com'); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertEquals('https://gravatar.com', $avatar); + } + +} diff --git a/tests/Service/Avatar/DownloaderTest.php b/tests/Service/Avatar/DownloaderTest.php new file mode 100644 index 0000000000..c6a3f27848 --- /dev/null +++ b/tests/Service/Avatar/DownloaderTest.php @@ -0,0 +1,85 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use Exception; +use OCA\Mail\Service\Avatar\Downloader; +use OCA\Mail\Tests\TestCase; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use PHPUnit_Framework_MockObject_MockObject; + +class DownloaderTest extends TestCase { + + /** @var IClientService|PHPUnit_Framework_MockObject_MockObject */ + private $clientService; + + /** @var Downloader */ + private $downloader; + + protected function setUp() { + parent::setUp(); + + $this->clientService = $this->createMock(IClientService::class); + + $this->downloader = new Downloader($this->clientService); + } + + public function testDownload() { + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client->expects($this->once()) + ->method('get') + ->with('https://domain.tld/favicon.ico') + ->willReturn($response); + $response->expects($this->once()) + ->method('getBody') + ->willReturn('data'); + + $data = $this->downloader->download('https://domain.tld/favicon.ico'); + + $this->assertEquals('data', $data); + } + + public function testDownloadError() { + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $client->expects($this->once()) + ->method('get') + ->with('https://domain.tld/favicon.ico') + ->willThrowException(new Exception()); + + $data = $this->downloader->download('https://domain.tld/favicon.ico'); + + $this->assertNull($data); + } + +} diff --git a/tests/Service/Avatar/FaviconSourceTest.php b/tests/Service/Avatar/FaviconSourceTest.php new file mode 100644 index 0000000000..98228a58dd --- /dev/null +++ b/tests/Service/Avatar/FaviconSourceTest.php @@ -0,0 +1,127 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use Mpclarkson\IconScraper\Icon; +use Mpclarkson\IconScraper\Scraper; +use OCA\Mail\Service\Avatar\FaviconSource; +use OCA\Mail\Tests\TestCase; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use PHPUnit_Framework_MockObject_MockObject; + +class FaviconSourceTest extends TestCase { + + /** @var IClientService|PHPUnit_Framework_MockObject_MockObject */ + private $clientService; + + /** @var Scraper|PHPUnit_Framework_MockObject_MockObject */ + private $scraper; + + /** @var FaviconSource */ + private $source; + + protected function setUp() { + parent::setUp(); + + $this->clientService = $this->createMock(IClientService::class); + $this->scraper = $this->createMock(Scraper::class); + + $this->source = new FaviconSource($this->clientService, $this->scraper); + } + + public function testFetchNoIconsFound() { + $email = 'hey@jancborchardt.net'; + $uid = 'john'; + $this->scraper->expects($this->once()) + ->method('get') + ->with('jancborchardt.net') + ->willReturn([]); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertNull($avatar); + } + + public function testFetchSingleIcon() { + $email = 'hey@jancborchardt.net'; + $uid = 'john'; + $icon = $this->createMock(Icon::class); + $this->scraper->expects($this->once()) + ->method('get') + ->with('jancborchardt.net') + ->willReturn([$icon]); + $icon->expects($this->once()) + ->method('getHref') + ->willReturn('https://domain.tld/favicon.ico'); + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client->expects($this->once()) + ->method('get') + ->with('https://domain.tld/favicon.ico') + ->willReturn($response); + $response->expects($this->once()) + ->method('getBody') + ->willReturn('data'); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertSame('https://domain.tld/favicon.ico', $avatar); + } + + public function testFetchEmptyIcon() { + $email = 'hey@jancborchardt.net'; + $uid = 'john'; + $icon = $this->createMock(Icon::class); + $this->scraper->expects($this->once()) + ->method('get') + ->with('jancborchardt.net') + ->willReturn([$icon]); + $icon->expects($this->once()) + ->method('getHref') + ->willReturn('https://domain.tld/favicon.ico'); + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client->expects($this->once()) + ->method('get') + ->with('https://domain.tld/favicon.ico') + ->willReturn($response); + $response->expects($this->once()) + ->method('getBody') + ->willReturn(''); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertNull($avatar); + } + +} diff --git a/tests/Service/Avatar/GravatarSouceTest.php b/tests/Service/Avatar/GravatarSouceTest.php new file mode 100644 index 0000000000..e2f41baaf3 --- /dev/null +++ b/tests/Service/Avatar/GravatarSouceTest.php @@ -0,0 +1,89 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service\Avatar; + +use Exception; +use OCA\Mail\Service\Avatar\GravatarSource; +use OCA\Mail\Tests\TestCase; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use PHPUnit_Framework_MockObject_MockObject; + +class GravatarSourceTest extends TestCase { + + /** @var IClientService|PHPUnit_Framework_MockObject_MockObject */ + private $clientService; + + /** @var GravatarSource */ + private $source; + + protected function setUp() { + parent::setUp(); + + $this->clientService = $this->createMock(IClientService::class); + + $this->source = new GravatarSource($this->clientService); + } + + public function testFetchExisting() { + $email = 'hey@jancborchardt.net'; + $uid = 'john'; + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $response = $this->createMock(IResponse::class); + $client->expects($this->once()) + ->method('get') + ->with('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404') + ->willReturn($response); + $response->expects($this->once()) + ->method('getBody') + ->willReturn('data'); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar); + } + + public function testFetchHttpError() { + $email = 'hey@jancborchardt.net'; + $uid = 'john'; + $client = $this->createMock(IClient::class); + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + $client->expects($this->once()) + ->method('get') + ->with('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404') + ->willThrowException(new Exception()); + + $avatar = $this->source->fetch($email, $uid); + + $this->assertNull($avatar); + } + +} diff --git a/tests/Service/AvatarServiceTest.php b/tests/Service/AvatarServiceTest.php new file mode 100644 index 0000000000..99a61a7aa9 --- /dev/null +++ b/tests/Service/AvatarServiceTest.php @@ -0,0 +1,193 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Service; + +use OCA\Mail\Service\Avatar\Cache; +use OCA\Mail\Service\Avatar\CompositeAvatarSource; +use OCA\Mail\Service\Avatar\Downloader; +use OCA\Mail\Service\Avatar\IAvatarSource; +use OCA\Mail\Service\AvatarService; +use OCA\Mail\Tests\TestCase; +use OCP\IURLGenerator; +use PHPUnit_Framework_MockObject_MockObject; + +class AvatarServiceTest extends TestCase { + + const BLACK_DOT_BASE64 = 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + + /** @var IAvatarSource|PHPUnit_Framework_MockObject_MockObject */ + private $source; + + /** @var Downloader|PHPUnit_Framework_MockObject_MockObject */ + private $downloader; + + /** @var Cache|PHPUnit_Framework_MockObject_MockObject */ + private $cache; + + /** @var IURLGenerator|PHPUnit_Framework_MockObject_MockObject */ + private $urlGenerator; + + /** @var AvatarService */ + private $avatarService; + + protected function setUp() { + parent::setUp(); + + $this->source = $this->createMock(CompositeAvatarSource::class); + $this->downloader = $this->createMock(Downloader::class); + $this->cache = $this->createMock(Cache::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->avatarService = new AvatarService($this->source, $this->downloader, $this->cache, $this->urlGenerator); + } + + public function testGetCachedAvatarUrl() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn('https://doe.com/favicon.ico'); + + $url = $this->avatarService->getAvatarUrl($email, $uid); + + $this->assertEquals('https://doe.com/favicon.ico', $url); + } + + public function testGetAvatarUrlNoAvatarFound() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn(null); + $this->source->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn(null); + $this->cache->expects($this->never()) + ->method('addUrl'); + + $url = $this->avatarService->getAvatarUrl($email, $uid); + + $this->assertNull($url); + } + + public function testGetAvatarUrl() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn(null); + $this->source->expects($this->once()) + ->method('fetch') + ->with($email, $uid) + ->willReturn('https://doe.com/favicon.ico'); + $this->cache->expects($this->once()) + ->method('addUrl') + ->with($email, $uid, 'https://doe.com/favicon.ico'); + + $url = $this->avatarService->getAvatarUrl($email, $uid); + + $this->assertEquals('https://doe.com/favicon.ico', $url); + } + + public function testGetCachedAvatarImage() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn('https://doe.com/favicon.ico'); + $this->cache->expects($this->once()) + ->method('getImage') + ->with('https://doe.com/favicon.ico', $uid) + ->willReturn(self::BLACK_DOT_BASE64); + + $image = $this->avatarService->getAvatarImage($email, $uid); + + $this->assertEquals(base64_decode(self::BLACK_DOT_BASE64), $image); + } + + public function testGetAvatarImageNoUrlCached() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn(null); + + $image = $this->avatarService->getAvatarImage($email, $uid); + + $this->assertNull($image); + } + + public function testGetAvatarImageDownloadImage() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn('https://doe.com/favicon.ico'); + $this->cache->expects($this->once()) + ->method('getImage') + ->with('https://doe.com/favicon.ico', $uid) + ->willReturn(null); + $this->downloader->expects($this->once()) + ->method('download') + ->with('https://doe.com/favicon.ico') + ->willReturn(base64_decode(self::BLACK_DOT_BASE64)); + $this->cache->expects($this->once()) + ->method('addImage') + ->with('https://doe.com/favicon.ico', $uid, self::BLACK_DOT_BASE64); + + $image = $this->avatarService->getAvatarImage($email, $uid); + + $this->assertEquals(base64_decode(self::BLACK_DOT_BASE64), $image); + } + + public function testGetAvatarImageDownloadImageFails() { + $email = 'jane@doe.com'; + $uid = 'john'; + $this->cache->expects($this->once()) + ->method('getUrl') + ->with($email, $uid) + ->willReturn('https://doe.com/favicon.ico'); + $this->cache->expects($this->once()) + ->method('getImage') + ->with('https://doe.com/favicon.ico', $uid) + ->willReturn(null); + $this->downloader->expects($this->once()) + ->method('download') + ->with('https://doe.com/favicon.ico') + ->willReturn(null); + + $image = $this->avatarService->getAvatarImage($email, $uid); + + $this->assertNull($image); + } + +} From cab769512ace86ed84964b2e152fa1517ac6be7d Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 6 Nov 2017 15:09:52 +0100 Subject: [PATCH 08/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- lib/Controller/MessagesController.php | 1 - lib/Service/Avatar/AddressbookSource.php | 3 +-- lib/Service/Avatar/CompositeAvatarSource.php | 5 ++--- lib/Service/Avatar/FaviconSource.php | 3 +-- lib/Service/Avatar/GravatarSource.php | 3 +-- lib/Service/Avatar/IAvatarSource.php | 3 +-- lib/Service/AvatarService.php | 6 ++++++ tests/Service/Avatar/AddressbookSourceTest.php | 6 ++---- .../Service/Avatar/CompositeAvatarSourceTest.php | 10 +++++----- tests/Service/Avatar/FaviconSourceTest.php | 15 ++++++--------- tests/Service/Avatar/GravatarSouceTest.php | 6 ++---- 11 files changed, 27 insertions(+), 34 deletions(-) diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index 4bb9e92e16..5e9fc633ee 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -33,7 +33,6 @@ use OCA\Mail\Http\HtmlResponse; use OCA\Mail\Model\IMAPMessage; use OCA\Mail\Service\AccountService; -use OCA\Mail\Service\AvatarService; use OCA\Mail\Service\ContactsIntegration; use OCA\Mail\Service\IAccount; use OCA\Mail\Service\IMailBox; diff --git a/lib/Service/Avatar/AddressbookSource.php b/lib/Service/Avatar/AddressbookSource.php index e6c87eccb3..4a751ab1f2 100644 --- a/lib/Service/Avatar/AddressbookSource.php +++ b/lib/Service/Avatar/AddressbookSource.php @@ -42,10 +42,9 @@ public function __construct(ContactsIntegration $contactsIntegration) { /** * @param string $email - * @param string $uid * @return string|null */ - public function fetch($email, $uid) { + public function fetch($email) { return $this->contactsIntegration->getPhoto($email); } diff --git a/lib/Service/Avatar/CompositeAvatarSource.php b/lib/Service/Avatar/CompositeAvatarSource.php index 20899d7f7a..e569d1315b 100644 --- a/lib/Service/Avatar/CompositeAvatarSource.php +++ b/lib/Service/Avatar/CompositeAvatarSource.php @@ -51,12 +51,11 @@ public function __construct(AddressbookSource $addressbookSource, FaviconSource * valid result. * * @param string $email - * @param string $uid * @return string|null */ - public function fetch($email, $uid) { + public function fetch($email) { foreach ($this->sources as $source) { - $avatar = $source->fetch($email, $uid); + $avatar = $source->fetch($email); if (is_null($avatar)) { continue; diff --git a/lib/Service/Avatar/FaviconSource.php b/lib/Service/Avatar/FaviconSource.php index 91fda81dff..4adfd0cb50 100644 --- a/lib/Service/Avatar/FaviconSource.php +++ b/lib/Service/Avatar/FaviconSource.php @@ -46,10 +46,9 @@ public function __construct(IClientService $clientService, Scraper $scraper) { /** * @param string $email - * @param string $uid * @return string|null */ - public function fetch($email, $uid) { + public function fetch($email) { $horde = new Horde_Mail_Rfc822_Address($email); // TODO: fall back to insecure HTTP? $domain = 'https://' . $horde->host; diff --git a/lib/Service/Avatar/GravatarSource.php b/lib/Service/Avatar/GravatarSource.php index 008392bc70..99e5dbbcd4 100644 --- a/lib/Service/Avatar/GravatarSource.php +++ b/lib/Service/Avatar/GravatarSource.php @@ -40,10 +40,9 @@ public function __construct(IClientService $clientService) { /** * @param string $email - * @param string $uid * @return string|null */ - public function fetch($email, $uid) { + public function fetch($email) { $gravatar = new Gravatar(['size' => 128], true); $avatar = $gravatar->avatar($email, ['d' => 404], true); diff --git a/lib/Service/Avatar/IAvatarSource.php b/lib/Service/Avatar/IAvatarSource.php index 8b4895cc77..e3dae61a8b 100644 --- a/lib/Service/Avatar/IAvatarSource.php +++ b/lib/Service/Avatar/IAvatarSource.php @@ -28,8 +28,7 @@ interface IAvatarSource { /** * @param string $email sender email address - * @param string $uid User ID * @return string|null avatar URL if one can be found */ - public function fetch($email, $uid); + public function fetch($email); } diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index c9c979aa51..1595a47ff3 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -42,6 +42,12 @@ class AvatarService implements IAvatarService { /** @var IURLGenerator */ private $urlGenerator; + /** + * @param CompositeAvatarSource $source + * @param Downloader $downloader + * @param AvatarCache $cache + * @param IURLGenerator $urlGenerator + */ public function __construct(CompositeAvatarSource $source, Downloader $downloader, AvatarCache $cache, IURLGenerator $urlGenerator) { $this->source = $source; $this->cache = $cache; diff --git a/tests/Service/Avatar/AddressbookSourceTest.php b/tests/Service/Avatar/AddressbookSourceTest.php index 67edb0c2fc..f85c483d23 100644 --- a/tests/Service/Avatar/AddressbookSourceTest.php +++ b/tests/Service/Avatar/AddressbookSourceTest.php @@ -47,24 +47,22 @@ protected function setUp() { public function testFetch() { $email = 'john@doe.com'; - $uid = 'jane'; $this->ci->expects($this->once()) ->method('getPhoto') ->willReturn('https://next.cloud/photo'); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertSame('https://next.cloud/photo', $avatar); } public function testFetchNoneFound() { $email = 'john@doe.com'; - $uid = 'jane'; $this->ci->expects($this->once()) ->method('getPhoto') ->willReturn(null); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertNull($avatar); } diff --git a/tests/Service/Avatar/CompositeAvatarSourceTest.php b/tests/Service/Avatar/CompositeAvatarSourceTest.php index 8e2312feac..41c0a6c7da 100644 --- a/tests/Service/Avatar/CompositeAvatarSourceTest.php +++ b/tests/Service/Avatar/CompositeAvatarSourceTest.php @@ -60,15 +60,15 @@ public function testFetchNoneFound() { $uid = 'john'; $this->addressbookSource->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->gravatarSource->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->faviconSource->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn(null); $avatar = $this->source->fetch($email, $uid); @@ -81,11 +81,11 @@ public function testFetchFromGravatar() { $uid = 'john'; $this->addressbookSource->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->gravatarSource->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn('https://gravatar.com'); $avatar = $this->source->fetch($email, $uid); diff --git a/tests/Service/Avatar/FaviconSourceTest.php b/tests/Service/Avatar/FaviconSourceTest.php index 98228a58dd..0668e966f7 100644 --- a/tests/Service/Avatar/FaviconSourceTest.php +++ b/tests/Service/Avatar/FaviconSourceTest.php @@ -55,24 +55,22 @@ protected function setUp() { public function testFetchNoIconsFound() { $email = 'hey@jancborchardt.net'; - $uid = 'john'; $this->scraper->expects($this->once()) ->method('get') - ->with('jancborchardt.net') + ->with('https://jancborchardt.net') ->willReturn([]); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertNull($avatar); } public function testFetchSingleIcon() { $email = 'hey@jancborchardt.net'; - $uid = 'john'; $icon = $this->createMock(Icon::class); $this->scraper->expects($this->once()) ->method('get') - ->with('jancborchardt.net') + ->with('https://jancborchardt.net') ->willReturn([$icon]); $icon->expects($this->once()) ->method('getHref') @@ -90,18 +88,17 @@ public function testFetchSingleIcon() { ->method('getBody') ->willReturn('data'); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertSame('https://domain.tld/favicon.ico', $avatar); } public function testFetchEmptyIcon() { $email = 'hey@jancborchardt.net'; - $uid = 'john'; $icon = $this->createMock(Icon::class); $this->scraper->expects($this->once()) ->method('get') - ->with('jancborchardt.net') + ->with('https://jancborchardt.net') ->willReturn([$icon]); $icon->expects($this->once()) ->method('getHref') @@ -119,7 +116,7 @@ public function testFetchEmptyIcon() { ->method('getBody') ->willReturn(''); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertNull($avatar); } diff --git a/tests/Service/Avatar/GravatarSouceTest.php b/tests/Service/Avatar/GravatarSouceTest.php index e2f41baaf3..ea5a1d7f1c 100644 --- a/tests/Service/Avatar/GravatarSouceTest.php +++ b/tests/Service/Avatar/GravatarSouceTest.php @@ -50,7 +50,6 @@ protected function setUp() { public function testFetchExisting() { $email = 'hey@jancborchardt.net'; - $uid = 'john'; $client = $this->createMock(IClient::class); $this->clientService->expects($this->once()) ->method('newClient') @@ -64,14 +63,13 @@ public function testFetchExisting() { ->method('getBody') ->willReturn('data'); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar); } public function testFetchHttpError() { $email = 'hey@jancborchardt.net'; - $uid = 'john'; $client = $this->createMock(IClient::class); $this->clientService->expects($this->once()) ->method('newClient') @@ -81,7 +79,7 @@ public function testFetchHttpError() { ->with('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404') ->willThrowException(new Exception()); - $avatar = $this->source->fetch($email, $uid); + $avatar = $this->source->fetch($email); $this->assertNull($avatar); } From 1eaef459c5655b9eb109ec08e01ce3baa0a3d956 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 6 Nov 2017 15:18:18 +0100 Subject: [PATCH 09/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- lib/Service/AvatarService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index 1595a47ff3..ff3ef71e6a 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -66,7 +66,7 @@ public function getAvatarUrl($email, $uid) { return $cachedUrl; } - $url = $this->source->fetch($email, $uid); + $url = $this->source->fetch($email); if (is_null($url)) { // Cannot locate any avatar -> nothing to do here return null; @@ -81,7 +81,7 @@ public function getAvatarUrl($email, $uid) { /** * @param string $email * @param string $uid - * @return mixed|null image data + * @return string|null image data */ public function getAvatarImage($email, $uid) { $url = $this->cache->getUrl($email, $uid); From b180201ec79977c3e45d3c0669498d1ce7d1ddfb Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 6 Nov 2017 16:33:55 +0100 Subject: [PATCH 10/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- js/models/messagecollection.js | 18 ++++++++---------- lib/Controller/AvatarsController.php | 16 ++++++++++++++-- lib/Service/AvatarService.php | 2 +- tests/Service/AvatarServiceTest.php | 8 ++++---- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/js/models/messagecollection.js b/js/models/messagecollection.js index cb29c5b0ff..b7c9508420 100644 --- a/js/models/messagecollection.js +++ b/js/models/messagecollection.js @@ -27,17 +27,15 @@ define(function(require) { }, _fetchAvatar: function(message) { - if (message.get('avatarInDatabase') === false) { - var url = OC.generateUrl('apps/mail/avatars/url?email={email}', { - email: message.get('fromEmail') - }); + var url = OC.generateUrl('/apps/mail/api/avatars/url/{email}', { + email: message.get('fromEmail') + }); - Promise.resolve($.ajax(url)).then(function(avatar) { - if (avatar.source !== 'none') { - message.set('senderImage', avatar.url); - } - }).catch(console.error.bind(this)); - } + Promise.resolve($.ajax(url)).then(function() { + message.set('senderImage', OC.generateUrl('/apps/mail/api/avatars/image/{email}', { + email: message.get('fromEmail') + })); + }).catch(console.error.bind(this)); } }); diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php index 374b939397..7760235179 100644 --- a/lib/Controller/AvatarsController.php +++ b/lib/Controller/AvatarsController.php @@ -22,12 +22,14 @@ namespace OCA\Mail\Controller; +use OC; use OCA\Mail\Contracts\IAvatarService; use OCA\Mail\Http\AvatarDownloadResponse; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; +use OCP\Files\IMimeTypeDetector; use OCP\IRequest; class AvatarsController extends Controller { @@ -38,15 +40,20 @@ class AvatarsController extends Controller { /** @var string */ private $uid; + /** @var IMimeTypeDetector */ + private $mimeDetector; + /** * @param string $appName * @param IRequest $request * @param IAvatarService $avatarService + * @param IMimeTypeDetector $mimeDetector * @param string $UserId */ - public function __construct($appName, IRequest $request, IAvatarService $avatarService, $UserId) { + public function __construct($appName, IRequest $request, IAvatarService $avatarService, IMimeTypeDetector $mimeDetector, $UserId) { parent::__construct($appName, $request); $this->avatarService = $avatarService; + $this->mimeDetector = $mimeDetector; $this->uid = $UserId; } @@ -85,6 +92,7 @@ public function url($email) { /** * @NoAdminRequired + * @NoCSRFRequired * * @param string $email * @return Response @@ -105,7 +113,11 @@ public function image($email) { return $response; } - return new AvatarDownloadResponse($imageData); + // TODO: limit to known MIME types + $mime = $this->mimeDetector->detectString($imageData); + $resp = new AvatarDownloadResponse($imageData); + $resp->addHeader('Content-Type', $mime); + return $resp; } } diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index ff3ef71e6a..d70aad19bf 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -84,7 +84,7 @@ public function getAvatarUrl($email, $uid) { * @return string|null image data */ public function getAvatarImage($email, $uid) { - $url = $this->cache->getUrl($email, $uid); + $url = $this->getAvatarUrl($email, $uid); if (is_null($url)) { return null; } diff --git a/tests/Service/AvatarServiceTest.php b/tests/Service/AvatarServiceTest.php index 99a61a7aa9..a6081498eb 100644 --- a/tests/Service/AvatarServiceTest.php +++ b/tests/Service/AvatarServiceTest.php @@ -81,11 +81,11 @@ public function testGetAvatarUrlNoAvatarFound() { $uid = 'john'; $this->cache->expects($this->once()) ->method('getUrl') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->source->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->cache->expects($this->never()) ->method('addUrl'); @@ -100,11 +100,11 @@ public function testGetAvatarUrl() { $uid = 'john'; $this->cache->expects($this->once()) ->method('getUrl') - ->with($email, $uid) + ->with($email) ->willReturn(null); $this->source->expects($this->once()) ->method('fetch') - ->with($email, $uid) + ->with($email) ->willReturn('https://doe.com/favicon.ico'); $this->cache->expects($this->once()) ->method('addUrl') From 3bc14145ca9bd97981bca5d23fa3e447f52fcadc Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 13 Nov 2017 14:16:23 +0100 Subject: [PATCH 11/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- js/models/messagecollection.js | 15 ---- js/views/messagesitem.js | 32 ++++++- lib/Contracts/IAvatarService.php | 4 +- lib/Controller/AvatarsController.php | 55 ++++++------ lib/Http/AvatarDownloadResponse.php | 5 +- lib/Http/CacheHeaders.php | 44 ++++++++++ lib/Http/JSONResponse.php | 32 +++++++ lib/Service/Avatar/AddressbookSource.php | 15 +++- lib/Service/Avatar/Avatar.php | 85 +++++++++++++++++++ lib/Service/Avatar/AvatarFactory.php | 50 +++++++++++ lib/Service/Avatar/Cache.php | 29 +++++-- lib/Service/Avatar/CompositeAvatarSource.php | 12 ++- lib/Service/Avatar/FaviconSource.php | 17 ++-- lib/Service/Avatar/GravatarSource.php | 14 +-- lib/Service/Avatar/IAvatarSource.php | 7 +- lib/Service/AvatarService.php | 58 +++++++++---- tests/Controller/AvatarControllerTest.php | 30 +++++-- tests/Http/AvatarDownloadResponseTest.php | 39 +++++++++ .../Service/AvatarServiceIntegrationTest.php | 15 +++- .../Service/Avatar/AddressbookSourceTest.php | 15 +++- tests/Service/Avatar/CacheTest.php | 34 +++++--- .../Avatar/CompositeAvatarSourceTest.php | 27 +++--- tests/Service/Avatar/FaviconSourceTest.php | 33 +++++-- tests/Service/Avatar/GravatarSouceTest.php | 17 +++- tests/Service/AvatarServiceTest.php | 78 +++++++++++------ 25 files changed, 592 insertions(+), 170 deletions(-) create mode 100644 lib/Http/CacheHeaders.php create mode 100644 lib/Http/JSONResponse.php create mode 100644 lib/Service/Avatar/Avatar.php create mode 100644 lib/Service/Avatar/AvatarFactory.php create mode 100644 tests/Http/AvatarDownloadResponseTest.php diff --git a/js/models/messagecollection.js b/js/models/messagecollection.js index b7c9508420..1316e6b31a 100644 --- a/js/models/messagecollection.js +++ b/js/models/messagecollection.js @@ -21,21 +21,6 @@ define(function(require) { model: Message, comparator: function(message) { return message.get('dateInt') * -1; - }, - initialize: function() { - this.on('add', this._fetchAvatar); - }, - - _fetchAvatar: function(message) { - var url = OC.generateUrl('/apps/mail/api/avatars/url/{email}', { - email: message.get('fromEmail') - }); - - Promise.resolve($.ajax(url)).then(function() { - message.set('senderImage', OC.generateUrl('/apps/mail/api/avatars/image/{email}', { - email: message.get('fromEmail') - })); - }).catch(console.error.bind(this)); } }); diff --git a/js/views/messagesitem.js b/js/views/messagesitem.js index 3688d1435b..b877207450 100644 --- a/js/views/messagesitem.js +++ b/js/views/messagesitem.js @@ -13,6 +13,7 @@ define(function(require) { var $ = require('jquery'); var _ = require('underscore'); + var OC = require('OC'); var Marionette = require('backbone.marionette'); var Radio = require('radio'); var MessageTemplate = require('templates/message-list-item.html'); @@ -74,7 +75,30 @@ define(function(require) { }); $('.action.delete').tooltip({placement: 'left'}); + + this._fetchAvatar(); }, + + /** + * @private + * @returns {undefined} + */ + _fetchAvatar: function() { + var url = OC.generateUrl('/apps/mail/api/avatars/url/{email}', { + email: this.model.get('fromEmail') + }); + + Promise.resolve($.ajax(url)).then(function(avatar) { + if (avatar.isExternal) { + this.model.set('senderImage', OC.generateUrl('/apps/mail/api/avatars/image/{email}', { + email: this.model.get('fromEmail') + })); + } else { + this.model.set('senderImage', avatar.url); + } + }.bind(this)); + }, + toggleMessageStar: function(event) { event.stopPropagation(); @@ -83,12 +107,12 @@ define(function(require) { // directly change star state in the interface for quick feedback if (starred) { this.getUI('star') - .removeClass('icon-starred') - .addClass('icon-star'); + .removeClass('icon-starred') + .addClass('icon-star'); } else { this.getUI('star') - .removeClass('icon-star') - .addClass('icon-starred'); + .removeClass('icon-star') + .addClass('icon-starred'); } Radio.message.trigger('flag', this.model, 'flagged', !starred); diff --git a/lib/Contracts/IAvatarService.php b/lib/Contracts/IAvatarService.php index 0af36d309b..fc685d6ce9 100644 --- a/lib/Contracts/IAvatarService.php +++ b/lib/Contracts/IAvatarService.php @@ -33,12 +33,12 @@ interface IAvatarService { * @param string $uid * @return string|null URL, if one can be found */ - public function getAvatarUrl($email, $uid); + public function getAvatar($email, $uid); /** * @param string $email * @param string $uid - * @return mixed|null image data + * @return array|null image data */ public function getAvatarImage($email, $uid); } diff --git a/lib/Controller/AvatarsController.php b/lib/Controller/AvatarsController.php index 7760235179..36c303fdaf 100644 --- a/lib/Controller/AvatarsController.php +++ b/lib/Controller/AvatarsController.php @@ -22,14 +22,13 @@ namespace OCA\Mail\Controller; -use OC; use OCA\Mail\Contracts\IAvatarService; use OCA\Mail\Http\AvatarDownloadResponse; +use OCA\Mail\Http\JSONResponse; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; -use OCP\Files\IMimeTypeDetector; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; class AvatarsController extends Controller { @@ -40,25 +39,26 @@ class AvatarsController extends Controller { /** @var string */ private $uid; - /** @var IMimeTypeDetector */ - private $mimeDetector; + /** @var ITimeFactory */ + private $timeFactory; /** * @param string $appName * @param IRequest $request * @param IAvatarService $avatarService - * @param IMimeTypeDetector $mimeDetector * @param string $UserId + * @param ITimeFactory $timeFactory */ - public function __construct($appName, IRequest $request, IAvatarService $avatarService, IMimeTypeDetector $mimeDetector, $UserId) { + public function __construct($appName, IRequest $request, IAvatarService $avatarService, $UserId, ITimeFactory $timeFactory) { parent::__construct($appName, $request); $this->avatarService = $avatarService; - $this->mimeDetector = $mimeDetector; $this->uid = $UserId; + $this->timeFactory = $timeFactory; } /** * @NoAdminRequired + * @NoCSRFRequired * * @param string $email * @return Response @@ -68,24 +68,22 @@ public function url($email) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } - $avatarUrl = $this->avatarService->getAvatarUrl($email, $this->uid); - if (is_null($avatarUrl)) { + $avatar = $this->avatarService->getAvatar($email, $this->uid); + if (is_null($avatar)) { // No avatar found $response = new JSONResponse([], Http::STATUS_NOT_FOUND); // Debounce this a bit // (cache for one day) - $response->cacheFor(24 * 60 * 60); + $response->setCacheHeaders(24 * 60 * 60, $this->timeFactory); return $response; } - $response = new JSONResponse([ - 'url' => $avatarUrl, - ]); + $response = new JSONResponse($avatar); // Let the browser cache this for a week - $response->cacheFor(7 * 24 * 60 * 60); + $response->setCacheHeaders(7 * 24 * 60 * 60, $this->timeFactory); return $response; } @@ -103,21 +101,28 @@ public function image($email) { } $imageData = $this->avatarService->getAvatarImage($email, $this->uid); + list($avatar, $image) = $imageData; - if (is_null($imageData)) { + if (is_null($imageData) || !$avatar->isExternal()) { // This could happen if the cache invalidated meanwhile - $response = new Response(); - $response->setStatus(Http::STATUS_NOT_FOUND); - // Clear cache - $response->cacheFor(0); - return $response; + return $this->noAvatarFoundResponse(); } - // TODO: limit to known MIME types - $mime = $this->mimeDetector->detectString($imageData); - $resp = new AvatarDownloadResponse($imageData); - $resp->addHeader('Content-Type', $mime); + $resp = new AvatarDownloadResponse($image); + $resp->addHeader('Content-Type', $avatar->getMime()); + + // Let the browser cache this for a week + $resp->setCacheHeaders(7 * 24 * 60 * 60, $this->timeFactory); + return $resp; } + private function noAvatarFoundResponse() { + $response = new Response(); + $response->setStatus(Http::STATUS_NOT_FOUND); + // Clear cache + $response->cacheFor(0); + return $response; + } + } diff --git a/lib/Http/AvatarDownloadResponse.php b/lib/Http/AvatarDownloadResponse.php index a95e19359f..6cc494834f 100644 --- a/lib/Http/AvatarDownloadResponse.php +++ b/lib/Http/AvatarDownloadResponse.php @@ -25,6 +25,9 @@ class AvatarDownloadResponse extends DownloadResponse { + use CacheHeaders; + + /** @var string */ private $content; /** @@ -33,8 +36,6 @@ class AvatarDownloadResponse extends DownloadResponse { public function __construct($content) { parent::__construct('avatar', 'application/octet-stream'); $this->content = $content; - - $this->cacheFor(2 * 60 * 60); } /** diff --git a/lib/Http/CacheHeaders.php b/lib/Http/CacheHeaders.php new file mode 100644 index 0000000000..b3dece80d4 --- /dev/null +++ b/lib/Http/CacheHeaders.php @@ -0,0 +1,44 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Http; + +use DateInterval; +use DateTime; +use OCP\AppFramework\Utility\ITimeFactory; + +trait CacheHeaders { + + public function setCacheHeaders($cacheFor, ITimeFactory $timeFactory) { + $this->cacheFor(7 * 24 * 60 * 60); + + $expires = new DateTime(); + $expires->setTimestamp($timeFactory->getTime()); + $expires->add(new DateInterval('PT' . $cacheFor . 'S')); + $this->addHeader('Expires', $expires->format(DateTime::RFC1123)); + + $this->addHeader('Pragma', 'cache'); + } + +} diff --git a/lib/Http/JSONResponse.php b/lib/Http/JSONResponse.php new file mode 100644 index 0000000000..1e6976d655 --- /dev/null +++ b/lib/Http/JSONResponse.php @@ -0,0 +1,32 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Http; + +use OCP\AppFramework\Http\JSONResponse as Base; + +class JSONResponse extends Base { + + use CacheHeaders; +} diff --git a/lib/Service/Avatar/AddressbookSource.php b/lib/Service/Avatar/AddressbookSource.php index 4a751ab1f2..44b920f92e 100644 --- a/lib/Service/Avatar/AddressbookSource.php +++ b/lib/Service/Avatar/AddressbookSource.php @@ -41,11 +41,18 @@ public function __construct(ContactsIntegration $contactsIntegration) { } /** - * @param string $email - * @return string|null + * @param string $email sender email address + * @param AvatarFactory $factory + * @return Avatar|null avatar URL if one can be found */ - public function fetch($email) { - return $this->contactsIntegration->getPhoto($email); + public function fetch($email, AvatarFactory $factory) { + $url = $this->contactsIntegration->getPhoto($email); + + if (is_null($url)) { + return null; + } + + return $factory->createInternal($url, null); } } diff --git a/lib/Service/Avatar/Avatar.php b/lib/Service/Avatar/Avatar.php new file mode 100644 index 0000000000..25296b962e --- /dev/null +++ b/lib/Service/Avatar/Avatar.php @@ -0,0 +1,85 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +use JsonSerializable; + +class Avatar implements JsonSerializable { + + /** @var string */ + private $url; + + /** @var string|null */ + private $mime; + + /** @var bool */ + private $isExternal; + + /** + * @param string $url + * @param string|null $mime + * @param bool $isExternal + */ + public function __construct($url, $mime = null, $isExternal = true) { + $this->url = $url; + $this->mime = $mime; + $this->isExternal = $isExternal; + } + + /** + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * Get the MIME type of this avatar + * + * @return string|null + */ + public function getMime() { + return $this->mime; + } + + /** + * @return boolean + */ + public function isExternal() { + return $this->isExternal; + } + + /** + * @return array + */ + public function jsonSerialize() { + return [ + 'isExternal' => $this->isExternal, + 'mime' => $this->mime, + 'url' => $this->url, + ]; + } + +} diff --git a/lib/Service/Avatar/AvatarFactory.php b/lib/Service/Avatar/AvatarFactory.php new file mode 100644 index 0000000000..eeb8f5d81c --- /dev/null +++ b/lib/Service/Avatar/AvatarFactory.php @@ -0,0 +1,50 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Service\Avatar; + +class AvatarFactory { + + /** + * Create a new avatar whose URL points to an internal endpoint + * + * @param string $url + * @return Avatar + */ + public function createInternal($url) { + return new Avatar($url, null, false); + } + + /** + * Create a new avatar whose URL points to an external endpoint + * + * @param string $url + * @param string $mime + * @return Avatar + */ + public function createExternal($url, $mime) { + return new Avatar($url, $mime); + } + +} diff --git a/lib/Service/Avatar/Cache.php b/lib/Service/Avatar/Cache.php index 2017b2e969..8455671d3b 100644 --- a/lib/Service/Avatar/Cache.php +++ b/lib/Service/Avatar/Cache.php @@ -35,11 +35,15 @@ class Cache { /** @var ICache */ private $cache; + /** @var AvatarFactory */ + private $avatarFactory; + /** * @param ICacheFactory $cacheFactory */ - public function __construct(ICacheFactory $cacheFactory) { + public function __construct(ICacheFactory $cacheFactory, AvatarFactory $avatarFactory) { $this->cache = $cacheFactory->create('mail.avatars'); + $this->avatarFactory = $avatarFactory; } /** @@ -62,18 +66,29 @@ private function buildImageKey($url, $uid) { /** * @param string $email - * @return string|null avatar URL + * @return Avatar|null avatar URL */ - public function getUrl($email, $uid) { - return $this->cache->get($this->buildUrlKey($email, $uid)); + public function get($email, $uid) { + $cached = $this->cache->get($this->buildUrlKey($email, $uid)); + + if (is_null($cached)) { + return null; + } + + if ($cached['isExternal']) { + return $this->avatarFactory->createExternal($cached['url'], $cached['mime']); + } else { + return $this->avatarFactory->createInternal($cached['url'], $cached['mime']); + } } /** * @param string $email - * @param string $url + * @param string $uid + * @param Avatar $avatar */ - public function addUrl($email, $uid, $url) { - $this->cache->set($this->buildUrlKey($email, $uid), $url, self::CACHE_TTL); + public function add($email, $uid, Avatar $avatar) { + $this->cache->set($this->buildUrlKey($email, $uid), $avatar->jsonSerialize(), self::CACHE_TTL); } /** diff --git a/lib/Service/Avatar/CompositeAvatarSource.php b/lib/Service/Avatar/CompositeAvatarSource.php index e569d1315b..344e63c775 100644 --- a/lib/Service/Avatar/CompositeAvatarSource.php +++ b/lib/Service/Avatar/CompositeAvatarSource.php @@ -47,15 +47,13 @@ public function __construct(AddressbookSource $addressbookSource, FaviconSource } /** - * Find avatar URL with the help of avatar sources and return the first - * valid result. - * - * @param string $email - * @return string|null + * @param string $email sender email address + * @param AvatarFactory $factory + * @return Avatar|null avatar URL if one can be found */ - public function fetch($email) { + public function fetch($email, AvatarFactory $factory) { foreach ($this->sources as $source) { - $avatar = $source->fetch($email); + $avatar = $source->fetch($email, $factory); if (is_null($avatar)) { continue; diff --git a/lib/Service/Avatar/FaviconSource.php b/lib/Service/Avatar/FaviconSource.php index 4adfd0cb50..9861592ddb 100644 --- a/lib/Service/Avatar/FaviconSource.php +++ b/lib/Service/Avatar/FaviconSource.php @@ -25,6 +25,7 @@ use Exception; use Horde_Mail_Rfc822_Address; use Mpclarkson\IconScraper\Scraper; +use OCP\Files\IMimeTypeDetector; use OCP\Http\Client\IClientService; class FaviconSource implements IAvatarSource { @@ -35,20 +36,25 @@ class FaviconSource implements IAvatarSource { /** @var Scraper */ private $scraper; + /** @var IMimeTypeDetector */ + private $mimeDetector; + /** * @param IClientService $clientService * @param Scraper $scraper */ - public function __construct(IClientService $clientService, Scraper $scraper) { + public function __construct(IClientService $clientService, Scraper $scraper, IMimeTypeDetector $mimeDetector) { $this->clientService = $clientService; $this->scraper = $scraper; + $this->mimeDetector = $mimeDetector; } /** - * @param string $email - * @return string|null + * @param string $email sender email address + * @param AvatarFactory $factory + * @return Avatar|null avatar URL if one can be found */ - public function fetch($email) { + public function fetch($email, AvatarFactory $factory) { $horde = new Horde_Mail_Rfc822_Address($email); // TODO: fall back to insecure HTTP? $domain = 'https://' . $horde->host; @@ -77,8 +83,9 @@ public function fetch($email) { if (strlen($body) === 0) { continue; } + $mime = $this->mimeDetector->detectString($body); - return $url; + return $factory->createExternal($url, $mime); } return null; diff --git a/lib/Service/Avatar/GravatarSource.php b/lib/Service/Avatar/GravatarSource.php index 99e5dbbcd4..1c41325979 100644 --- a/lib/Service/Avatar/GravatarSource.php +++ b/lib/Service/Avatar/GravatarSource.php @@ -39,17 +39,18 @@ public function __construct(IClientService $clientService) { } /** - * @param string $email - * @return string|null + * @param string $email sender email address + * @param AvatarFactory $factory + * @return Avatar|null avatar URL if one can be found */ - public function fetch($email) { + public function fetch($email, AvatarFactory $factory) { $gravatar = new Gravatar(['size' => 128], true); - $avatar = $gravatar->avatar($email, ['d' => 404], true); + $avatarUrl = $gravatar->avatar($email, ['d' => 404], true); $client = $this->clientService->newClient(); try { - $response = $client->get($avatar); + $response = $client->get($avatarUrl); } catch (Exception $exception) { return null; } @@ -60,7 +61,8 @@ public function fetch($email) { return null; } - return $avatar; + // TODO: check whether it's really always a jpeg + return $factory->createExternal($avatarUrl, 'image/jpeg'); } } diff --git a/lib/Service/Avatar/IAvatarSource.php b/lib/Service/Avatar/IAvatarSource.php index e3dae61a8b..4be65d5979 100644 --- a/lib/Service/Avatar/IAvatarSource.php +++ b/lib/Service/Avatar/IAvatarSource.php @@ -24,11 +24,14 @@ namespace OCA\Mail\Service\Avatar; +use OCP\Files\IMimeTypeDetector; + interface IAvatarSource { /** * @param string $email sender email address - * @return string|null avatar URL if one can be found + * @param AvatarFactory $factory + * @return Avatar|null avatar URL if one can be found */ - public function fetch($email); + public function fetch($email, AvatarFactory $factory); } diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index d70aad19bf..f794bc5563 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -22,6 +22,8 @@ namespace OCA\Mail\Service; use OCA\Mail\Contracts\IAvatarService; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\Cache as AvatarCache; use OCA\Mail\Service\Avatar\CompositeAvatarSource; use OCA\Mail\Service\Avatar\Downloader; @@ -42,67 +44,89 @@ class AvatarService implements IAvatarService { /** @var IURLGenerator */ private $urlGenerator; + /** @var AvatarFactory */ + private $avatarFactory; + /** * @param CompositeAvatarSource $source * @param Downloader $downloader * @param AvatarCache $cache * @param IURLGenerator $urlGenerator + * @param AvatarFactory $avatarFactory */ - public function __construct(CompositeAvatarSource $source, Downloader $downloader, AvatarCache $cache, IURLGenerator $urlGenerator) { + public function __construct(CompositeAvatarSource $source, Downloader $downloader, AvatarCache $cache, IURLGenerator $urlGenerator, AvatarFactory $avatarFactory) { $this->source = $source; $this->cache = $cache; $this->urlGenerator = $urlGenerator; $this->downloader = $downloader; + $this->avatarFactory = $avatarFactory; + } + + /** + * @param Avatar $avatar + */ + private function hasAllowedMime(Avatar $avatar) { + if ($avatar->isExternal()) { + $mime = $avatar->getMime(); + + return in_array($mime, [ + 'image/jpeg', + 'image/png', + ]); + } else { + // We trust internal URLs by default + return true; + } } /** * @param string $email * @param string $uid - * @return string|null + * @return Avatar|null */ - public function getAvatarUrl($email, $uid) { - $cachedUrl = $this->cache->getUrl($email, $uid); - if (!is_null($cachedUrl)) { - return $cachedUrl; + public function getAvatar($email, $uid) { + $cachedAvatar = $this->cache->get($email, $uid); + if (!is_null($cachedAvatar)) { + return $cachedAvatar; } - $url = $this->source->fetch($email); - if (is_null($url)) { + $avatar = $this->source->fetch($email, $this->avatarFactory); + if (is_null($avatar) || !$this->hasAllowedMime($avatar)) { // Cannot locate any avatar -> nothing to do here return null; } // Cache for the next call - $this->cache->addUrl($email, $uid, $url); + $this->cache->add($email, $uid, $avatar); - return $url; + return $avatar; } /** * @param string $email * @param string $uid - * @return string|null image data + * @return array|null image data */ public function getAvatarImage($email, $uid) { - $url = $this->getAvatarUrl($email, $uid); - if (is_null($url)) { + $avatar = $this->getAvatar($email, $uid); + if (is_null($avatar)) { return null; } - $cachedImage = $this->cache->getImage($url, $uid); + $cachedImage = $this->cache->getImage($avatar->getUrl(), $uid); if (!is_null($cachedImage)) { return base64_decode($cachedImage); } - $image = $this->downloader->download($url); + $image = $this->downloader->download($avatar->getUrl()); if (is_null($image)) { return null; } // Cache for the next call - $this->cache->addImage($url, $uid, base64_encode($image)); + $this->cache->addImage($avatar->getUrl(), $uid, base64_encode($image)); - return $image; + return [$avatar, $image]; } } diff --git a/tests/Controller/AvatarControllerTest.php b/tests/Controller/AvatarControllerTest.php index 6c4e32ffaa..0026e2c56c 100644 --- a/tests/Controller/AvatarControllerTest.php +++ b/tests/Controller/AvatarControllerTest.php @@ -27,10 +27,12 @@ use OCA\Mail\Contracts\IAvatarService; use OCA\Mail\Controller\AvatarsController; use OCA\Mail\Http\AvatarDownloadResponse; +use OCA\Mail\Http\JSONResponse; +use OCA\Mail\Service\Avatar\Avatar; use OCA\Mail\Tests\TestCase; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; use PHPUnit_Framework_MockObject_MockObject; @@ -39,6 +41,9 @@ class AvatarControllerTest extends TestCase { /** @var IAvatarService|PHPUnit_Framework_MockObject_MockObject */ private $avatarService; + /** @var ITimeFactory|PHPUnit_Framework_MockObject_MockObject */ + private $timeFactory; + /** @var AvatarsController */ private $controller; @@ -47,35 +52,40 @@ protected function setUp() { $request = $this->createMock(IRequest::class); $this->avatarService = $this->createMock(IAvatarService::class); + $this->timeFactory = $this->createMocK(ITimeFactory::class); + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(10000); - $this->controller = new AvatarsController('mail', $request, $this->avatarService, 'jane'); + $this->controller = new AvatarsController('mail', $request, $this->avatarService, 'jane', $this->timeFactory); } public function testGetUrl() { $email = 'john@doe.com'; + $avatar = new Avatar('https://doe.com/favicon.ico'); $this->avatarService->expects($this->once()) - ->method('getAvatarUrl') + ->method('getAvatar') ->with($email, 'jane') - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn($avatar); $resp = $this->controller->url($email); - $expected = new JSONResponse(['url' => 'https://doe.com/favicon.ico']); - $expected->cacheFor(7 * 24 * 60 * 60); + $expected = new JSONResponse($avatar); + $expected->setCacheHeaders(7 * 24 * 60 * 60, $this->timeFactory); $this->assertEquals($expected, $resp); } public function testGetUrlNoAvatarFound() { $email = 'john@doe.com'; $this->avatarService->expects($this->once()) - ->method('getAvatarUrl') + ->method('getAvatar') ->with($email, 'jane') ->willReturn(null); $resp = $this->controller->url($email); $expected = new JSONResponse([], Http::STATUS_NOT_FOUND); - $expected->cacheFor(24 * 60 * 60); + $expected->setCacheHeaders(24 * 60 * 60, $this->timeFactory); $this->assertEquals($expected, $resp); } @@ -84,11 +94,13 @@ public function testGetImage() { $this->avatarService->expects($this->once()) ->method('getAvatarImage') ->with($email, 'jane') - ->willReturn('data'); + ->willReturn([new Avatar('johne@doe.com', 'image/jpeg'), 'data']); $resp = $this->controller->image($email); $expected = new AvatarDownloadResponse('data'); + $expected->addHeader('Content-Type', 'image/jpeg'); + $expected->setCacheHeaders(7 * 24 * 60 * 60, $this->timeFactory); $this->assertEquals($expected, $resp); } diff --git a/tests/Http/AvatarDownloadResponseTest.php b/tests/Http/AvatarDownloadResponseTest.php new file mode 100644 index 0000000000..07b7a3b206 --- /dev/null +++ b/tests/Http/AvatarDownloadResponseTest.php @@ -0,0 +1,39 @@ + + * + * @author 2017 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Mail\Tests\Http; + +use OCA\Mail\Http\AvatarDownloadResponse; +use OCA\Mail\Tests\TestCase; + +class AvatarDownloadResponseTest extends TestCase { + + public function testResponse() { + $response = new AvatarDownloadResponse('data'); + + $this->assertEquals('application/octet-stream', $response->getHeaders()['Content-Type']); + $this->assertEquals('data', $response->render()); + } + +} diff --git a/tests/Integration/Service/AvatarServiceIntegrationTest.php b/tests/Integration/Service/AvatarServiceIntegrationTest.php index 246f5ab88d..970148ce67 100644 --- a/tests/Integration/Service/AvatarServiceIntegrationTest.php +++ b/tests/Integration/Service/AvatarServiceIntegrationTest.php @@ -51,17 +51,24 @@ protected function setUp() { } public function testJansGravatar() { - $avatar = $this->service->getAvatarUrl('hey@jancborchardt.net', 'john'); - $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar); + $avatar = $this->service->getAvatar('hey@jancborchardt.net', 'john'); + $this->assertNotNull($avatar); + $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar->getUrl()); $image = $this->service->getAvatarImage('hey@jancborchardt.net', 'john'); $this->assertNotNull($image); } public function testJansNicknameFavicon() { - $avatar = $this->service->getAvatarUrl('janli@nextcloud.com', 'jane'); - $this->assertEquals('https://nextcloud.com/media/screenshot-150x150.png?x16328', $avatar); + $avatar = $this->service->getAvatar('janli@nextcloud.com', 'jane'); + $this->assertNotNull($avatar); + $this->assertEquals('https://nextcloud.com/media/screenshot-150x150.png?x16328', $avatar->getUrl()); $image = $this->service->getAvatarImage('janli@nextcloud.com', 'jane'); $this->assertNotNull($image); } + public function testChristophsFavicon() { + $avatar = $this->service->getAvatar('christoph@winzerhof-wurst.at', 'jan'); + $this->assertNull($avatar); // There is none + } + } diff --git a/tests/Service/Avatar/AddressbookSourceTest.php b/tests/Service/Avatar/AddressbookSourceTest.php index f85c483d23..8ca6a1ef86 100644 --- a/tests/Service/Avatar/AddressbookSourceTest.php +++ b/tests/Service/Avatar/AddressbookSourceTest.php @@ -25,6 +25,8 @@ namespace OCA\Mail\Tests\Service\Avatar; use OCA\Mail\Service\Avatar\AddressbookSource; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\ContactsIntegration; use OCA\Mail\Tests\TestCase; use PHPUnit_Framework_MockObject_MockObject; @@ -47,22 +49,29 @@ protected function setUp() { public function testFetch() { $email = 'john@doe.com'; + $avatarFactory = $this->createMock(AvatarFactory::class); $this->ci->expects($this->once()) ->method('getPhoto') ->willReturn('https://next.cloud/photo'); + $avatar = new Avatar('https://next.cloud/photo'); + $avatarFactory->expects($this->once()) + ->method('createInternal') + ->with('https://next.cloud/photo') + ->willReturn($avatar); - $avatar = $this->source->fetch($email); + $actualAvatar = $this->source->fetch($email, $avatarFactory); - $this->assertSame('https://next.cloud/photo', $avatar); + $this->assertSame($avatar, $actualAvatar); } public function testFetchNoneFound() { $email = 'john@doe.com'; + $avatarFactory = $this->createMock(AvatarFactory::class); $this->ci->expects($this->once()) ->method('getPhoto') ->willReturn(null); - $avatar = $this->source->fetch($email); + $avatar = $this->source->fetch($email, $avatarFactory); $this->assertNull($avatar); } diff --git a/tests/Service/Avatar/CacheTest.php b/tests/Service/Avatar/CacheTest.php index 8fa4908700..06235916d9 100644 --- a/tests/Service/Avatar/CacheTest.php +++ b/tests/Service/Avatar/CacheTest.php @@ -24,6 +24,8 @@ namespace OCA\Mail\Tests\Service\Avatar; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\Cache; use OCA\Mail\Tests\TestCase; use OCP\ICache; @@ -40,6 +42,9 @@ class CacheTest extends TestCase { /** @var ICache|PHPUnit_Framework_MockObject_MockObject */ private $cacheImpl; + /** @var AvatarFactory|PHPUnit_Framework_MockObject_MockObject */ + private $avatarFactory; + /** @var Cache */ private $cache; @@ -52,10 +57,11 @@ protected function setUp() { ->method('create') ->with('mail.avatars') ->willReturn($this->cacheImpl); - $this->cache = new Cache($this->cacheFactory); + $this->avatarFactory = $this->createMock(AvatarFactory::class); + $this->cache = new Cache($this->cacheFactory, $this->avatarFactory); } - public function testGetNonCachedUrl() { + public function testGetNonCachedAvatar() { $email = 'john@doe.com'; $uid = 'jane'; $this->cacheImpl->expects($this->once()) @@ -63,32 +69,38 @@ public function testGetNonCachedUrl() { ->with(base64_encode(json_encode([$email, $uid]))) ->willReturn(null); - $url = $this->cache->getUrl($email, $uid); + $cachedAvatar = $this->cache->get($email, $uid); - $this->assertNull($url); + $this->assertNull($cachedAvatar); } - public function testGetCachedUrl() { + public function testGetCachedAvatar() { $email = 'john@doe.com'; $uid = 'jane'; $this->cacheImpl->expects($this->once()) ->method('get') ->with(base64_encode(json_encode([$email, $uid]))) - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn(['isExternal' => true, 'mime' => 'image/jpeg', 'url' => 'https://…']); + $expected = new Avatar('https://…', 'image/jpeg'); + $this->avatarFactory->expects($this->once()) + ->method('createExternal') + ->with('https://…', 'image/jpeg') + ->willReturn($expected); - $url = $this->cache->getUrl($email, $uid); + $cachedAvatar = $this->cache->get($email, $uid); - $this->assertEquals('https://doe.com/favicon.ico', $url); + $this->assertEquals($expected, $cachedAvatar); } - public function testSetUrl() { + public function testSetAvatar() { $email = 'john@doe.com'; $uid = 'jane'; + $avatar = new Avatar('https://…', 'image/jpeg'); $this->cacheImpl->expects($this->once()) ->method('set') - ->with(base64_encode(json_encode([$email, $uid])), 'https://doe.com/favicon.ico', 7 * 24 * 60 * 60); + ->with(base64_encode(json_encode([$email, $uid])), ['isExternal' => true, 'mime' => 'image/jpeg', 'url' => 'https://…'], 7 * 24 * 60 * 60); - $this->cache->addUrl($email, $uid, 'https://doe.com/favicon.ico'); + $this->cache->add($email, $uid, $avatar); } public function testGetImage() { diff --git a/tests/Service/Avatar/CompositeAvatarSourceTest.php b/tests/Service/Avatar/CompositeAvatarSourceTest.php index 41c0a6c7da..e3511c5ee7 100644 --- a/tests/Service/Avatar/CompositeAvatarSourceTest.php +++ b/tests/Service/Avatar/CompositeAvatarSourceTest.php @@ -25,6 +25,8 @@ namespace OCA\Mail\Tests\Service\Avatar; use OCA\Mail\Service\Avatar\AddressbookSource; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\CompositeAvatarSource; use OCA\Mail\Service\Avatar\FaviconSource; use OCA\Mail\Service\Avatar\GravatarSource; @@ -57,40 +59,41 @@ protected function setUp() { public function testFetchNoneFound() { $email = 'jane@doe.com'; - $uid = 'john'; + $avatarFactory = $this->createMock(AvatarFactory::class); $this->addressbookSource->expects($this->once()) ->method('fetch') - ->with($email) + ->with($email, $avatarFactory) ->willReturn(null); $this->gravatarSource->expects($this->once()) ->method('fetch') - ->with($email) + ->with($email, $avatarFactory) ->willReturn(null); $this->faviconSource->expects($this->once()) ->method('fetch') - ->with($email) + ->with($email, $avatarFactory) ->willReturn(null); - $avatar = $this->source->fetch($email, $uid); + $actualAvatar = $this->source->fetch($email, $avatarFactory); - $this->assertNull($avatar); + $this->assertNull($actualAvatar); } public function testFetchFromGravatar() { $email = 'jane@doe.com'; - $uid = 'john'; + $avatar = new Avatar('https://gravatar.com', 'image/jpeg'); + $avatarFactory = $this->createMock(AvatarFactory::class); $this->addressbookSource->expects($this->once()) ->method('fetch') - ->with($email) + ->with($email, $avatarFactory) ->willReturn(null); $this->gravatarSource->expects($this->once()) ->method('fetch') - ->with($email) - ->willReturn('https://gravatar.com'); + ->with($email, $avatarFactory) + ->willReturn($avatar); - $avatar = $this->source->fetch($email, $uid); + $actualAvatar = $this->source->fetch($email, $avatarFactory); - $this->assertEquals('https://gravatar.com', $avatar); + $this->assertEquals($avatar, $actualAvatar); } } diff --git a/tests/Service/Avatar/FaviconSourceTest.php b/tests/Service/Avatar/FaviconSourceTest.php index 0668e966f7..3fe86370d8 100644 --- a/tests/Service/Avatar/FaviconSourceTest.php +++ b/tests/Service/Avatar/FaviconSourceTest.php @@ -26,8 +26,11 @@ use Mpclarkson\IconScraper\Icon; use Mpclarkson\IconScraper\Scraper; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\FaviconSource; use OCA\Mail\Tests\TestCase; +use OCP\Files\IMimeTypeDetector; use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\Http\Client\IResponse; @@ -41,6 +44,9 @@ class FaviconSourceTest extends TestCase { /** @var Scraper|PHPUnit_Framework_MockObject_MockObject */ private $scraper; + /** @var IMimeTypeDetector|PHPUnit_Framework_MockObject_MockObject */ + private $mimeDetector; + /** @var FaviconSource */ private $source; @@ -49,18 +55,20 @@ protected function setUp() { $this->clientService = $this->createMock(IClientService::class); $this->scraper = $this->createMock(Scraper::class); + $this->mimeDetector = $this->createMock(IMimeTypeDetector::class); - $this->source = new FaviconSource($this->clientService, $this->scraper); + $this->source = new FaviconSource($this->clientService, $this->scraper, $this->mimeDetector); } public function testFetchNoIconsFound() { $email = 'hey@jancborchardt.net'; + $avatarFactory = $this->createMock(AvatarFactory::class); $this->scraper->expects($this->once()) ->method('get') ->with('https://jancborchardt.net') ->willReturn([]); - $avatar = $this->source->fetch($email); + $avatar = $this->source->fetch($email, $avatarFactory); $this->assertNull($avatar); } @@ -68,6 +76,8 @@ public function testFetchNoIconsFound() { public function testFetchSingleIcon() { $email = 'hey@jancborchardt.net'; $icon = $this->createMock(Icon::class); + $avatarFactory = $this->createMock(AvatarFactory::class); + $avatar = new Avatar('https://domain.tld/favicon.ico'); $this->scraper->expects($this->once()) ->method('get') ->with('https://jancborchardt.net') @@ -87,15 +97,24 @@ public function testFetchSingleIcon() { $response->expects($this->once()) ->method('getBody') ->willReturn('data'); - - $avatar = $this->source->fetch($email); - - $this->assertSame('https://domain.tld/favicon.ico', $avatar); + $this->mimeDetector->expects($this->once()) + ->method('detectString') + ->with('data') + ->willReturn('image/png'); + $avatarFactory->expects($this->once()) + ->method('createExternal') + ->with('https://domain.tld/favicon.ico', 'image/png') + ->willReturn($avatar); + + $actualAvatar = $this->source->fetch($email, $avatarFactory); + + $this->assertSame($avatar, $actualAvatar); } public function testFetchEmptyIcon() { $email = 'hey@jancborchardt.net'; $icon = $this->createMock(Icon::class); + $avatarFactory = $this->createMock(AvatarFactory::class); $this->scraper->expects($this->once()) ->method('get') ->with('https://jancborchardt.net') @@ -116,7 +135,7 @@ public function testFetchEmptyIcon() { ->method('getBody') ->willReturn(''); - $avatar = $this->source->fetch($email); + $avatar = $this->source->fetch($email, $avatarFactory); $this->assertNull($avatar); } diff --git a/tests/Service/Avatar/GravatarSouceTest.php b/tests/Service/Avatar/GravatarSouceTest.php index ea5a1d7f1c..db4274e5ea 100644 --- a/tests/Service/Avatar/GravatarSouceTest.php +++ b/tests/Service/Avatar/GravatarSouceTest.php @@ -25,6 +25,8 @@ namespace OCA\Mail\Tests\Service\Avatar; use Exception; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\GravatarSource; use OCA\Mail\Tests\TestCase; use OCP\Http\Client\IClient; @@ -51,6 +53,7 @@ protected function setUp() { public function testFetchExisting() { $email = 'hey@jancborchardt.net'; $client = $this->createMock(IClient::class); + $avatarFactory = $this->createMock(AvatarFactory::class); $this->clientService->expects($this->once()) ->method('newClient') ->willReturn($client); @@ -62,15 +65,21 @@ public function testFetchExisting() { $response->expects($this->once()) ->method('getBody') ->willReturn('data'); + $avatar = new Avatar('https://next.cloud/photo'); + $avatarFactory->expects($this->once()) + ->method('createExternal') + ->with('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404') + ->willReturn($avatar); - $avatar = $this->source->fetch($email); + $actualAvatar = $this->source->fetch($email, $avatarFactory); - $this->assertEquals('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404', $avatar); + $this->assertEquals($avatar, $actualAvatar); } public function testFetchHttpError() { $email = 'hey@jancborchardt.net'; $client = $this->createMock(IClient::class); + $avatarFactory = $this->createMock(AvatarFactory::class); $this->clientService->expects($this->once()) ->method('newClient') ->willReturn($client); @@ -79,9 +88,9 @@ public function testFetchHttpError() { ->with('https://secure.gravatar.com/avatar/2fd3f4d5d762955e5b603794a888fa97?size=128&d=404') ->willThrowException(new Exception()); - $avatar = $this->source->fetch($email); + $actualAvatar = $this->source->fetch($email, $avatarFactory); - $this->assertNull($avatar); + $this->assertNull($actualAvatar); } } diff --git a/tests/Service/AvatarServiceTest.php b/tests/Service/AvatarServiceTest.php index a6081498eb..bf4bc60ceb 100644 --- a/tests/Service/AvatarServiceTest.php +++ b/tests/Service/AvatarServiceTest.php @@ -24,6 +24,8 @@ namespace OCA\Mail\Tests\Service; +use OCA\Mail\Service\Avatar\Avatar; +use OCA\Mail\Service\Avatar\AvatarFactory; use OCA\Mail\Service\Avatar\Cache; use OCA\Mail\Service\Avatar\CompositeAvatarSource; use OCA\Mail\Service\Avatar\Downloader; @@ -49,6 +51,9 @@ class AvatarServiceTest extends TestCase { /** @var IURLGenerator|PHPUnit_Framework_MockObject_MockObject */ private $urlGenerator; + /** @var AvatarFactory|PHPUnit_Framework_MockObject_MockObject */ + private $avatarFactory; + /** @var AvatarService */ private $avatarService; @@ -59,69 +64,92 @@ protected function setUp() { $this->downloader = $this->createMock(Downloader::class); $this->cache = $this->createMock(Cache::class); $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->avatarFactory = $this->createMock(AvatarFactory::class); - $this->avatarService = new AvatarService($this->source, $this->downloader, $this->cache, $this->urlGenerator); + $this->avatarService = new AvatarService($this->source, $this->downloader, $this->cache, $this->urlGenerator, $this->avatarFactory); } public function testGetCachedAvatarUrl() { $email = 'jane@doe.com'; $uid = 'john'; $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email, $uid) ->willReturn('https://doe.com/favicon.ico'); - $url = $this->avatarService->getAvatarUrl($email, $uid); + $url = $this->avatarService->getAvatar($email, $uid); $this->assertEquals('https://doe.com/favicon.ico', $url); } - public function testGetAvatarUrlNoAvatarFound() { + public function testGetAvatarNoAvatarFound() { $email = 'jane@doe.com'; $uid = 'john'; $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email) ->willReturn(null); $this->source->expects($this->once()) ->method('fetch') - ->with($email) + ->with($email, $this->avatarFactory) ->willReturn(null); $this->cache->expects($this->never()) - ->method('addUrl'); + ->method('add'); - $url = $this->avatarService->getAvatarUrl($email, $uid); + $url = $this->avatarService->getAvatar($email, $uid); $this->assertNull($url); } - public function testGetAvatarUrl() { + public function testGetAvatarMimeNotAllowed() { $email = 'jane@doe.com'; $uid = 'john'; $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email) ->willReturn(null); + $avatar = new Avatar('http://…', 'application/xml'); $this->source->expects($this->once()) ->method('fetch') + ->with($email, $this->avatarFactory) + ->willReturn($avatar); + $this->cache->expects($this->never()) + ->method('add'); + + $url = $this->avatarService->getAvatar($email, $uid); + + $this->assertNull($url); + } + + public function testGetAvatar() { + $email = 'jane@doe.com'; + $uid = 'john'; + $avatar = new Avatar('https://doe.com/favicon.ico', 'image/png'); + $this->cache->expects($this->once()) + ->method('get') ->with($email) - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn(null); + $this->source->expects($this->once()) + ->method('fetch') + ->with($email, $this->avatarFactory) + ->willReturn($avatar); $this->cache->expects($this->once()) - ->method('addUrl') - ->with($email, $uid, 'https://doe.com/favicon.ico'); + ->method('add') + ->with($email, $uid, $avatar); - $url = $this->avatarService->getAvatarUrl($email, $uid); + $actualAvatar = $this->avatarService->getAvatar($email, $uid); - $this->assertEquals('https://doe.com/favicon.ico', $url); + $this->assertEquals($avatar, $actualAvatar); } public function testGetCachedAvatarImage() { $email = 'jane@doe.com'; $uid = 'john'; + $avatar = new Avatar('https://doe.com/favicon.ico', 'image/png'); $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email, $uid) - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn($avatar); $this->cache->expects($this->once()) ->method('getImage') ->with('https://doe.com/favicon.ico', $uid) @@ -136,7 +164,7 @@ public function testGetAvatarImageNoUrlCached() { $email = 'jane@doe.com'; $uid = 'john'; $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email, $uid) ->willReturn(null); @@ -148,10 +176,11 @@ public function testGetAvatarImageNoUrlCached() { public function testGetAvatarImageDownloadImage() { $email = 'jane@doe.com'; $uid = 'john'; + $avatar = new Avatar('https://doe.com/favicon.ico', 'image/jpg'); $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email, $uid) - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn($avatar); $this->cache->expects($this->once()) ->method('getImage') ->with('https://doe.com/favicon.ico', $uid) @@ -164,18 +193,19 @@ public function testGetAvatarImageDownloadImage() { ->method('addImage') ->with('https://doe.com/favicon.ico', $uid, self::BLACK_DOT_BASE64); - $image = $this->avatarService->getAvatarImage($email, $uid); + $data = $this->avatarService->getAvatarImage($email, $uid); - $this->assertEquals(base64_decode(self::BLACK_DOT_BASE64), $image); + $this->assertEquals([$avatar, base64_decode(self::BLACK_DOT_BASE64)], $data); } public function testGetAvatarImageDownloadImageFails() { $email = 'jane@doe.com'; $uid = 'john'; + $avatar = new Avatar('https://doe.com/favicon.ico', 'image/jpg'); $this->cache->expects($this->once()) - ->method('getUrl') + ->method('get') ->with($email, $uid) - ->willReturn('https://doe.com/favicon.ico'); + ->willReturn($avatar); $this->cache->expects($this->once()) ->method('getImage') ->with('https://doe.com/favicon.ico', $uid) From a03e1d4093b182bb84f81ac9fd934ffca756329d Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 13 Nov 2017 14:20:55 +0100 Subject: [PATCH 12/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- lib/Service/AvatarService.php | 2 +- tests/Service/AvatarServiceTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index f794bc5563..a086ef6b6d 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -115,7 +115,7 @@ public function getAvatarImage($email, $uid) { $cachedImage = $this->cache->getImage($avatar->getUrl(), $uid); if (!is_null($cachedImage)) { - return base64_decode($cachedImage); + return [$avatar, base64_decode($cachedImage)]; } $image = $this->downloader->download($avatar->getUrl()); diff --git a/tests/Service/AvatarServiceTest.php b/tests/Service/AvatarServiceTest.php index bf4bc60ceb..6a642d628c 100644 --- a/tests/Service/AvatarServiceTest.php +++ b/tests/Service/AvatarServiceTest.php @@ -155,9 +155,9 @@ public function testGetCachedAvatarImage() { ->with('https://doe.com/favicon.ico', $uid) ->willReturn(self::BLACK_DOT_BASE64); - $image = $this->avatarService->getAvatarImage($email, $uid); + $data = $this->avatarService->getAvatarImage($email, $uid); - $this->assertEquals(base64_decode(self::BLACK_DOT_BASE64), $image); + $this->assertEquals([$avatar, base64_decode(self::BLACK_DOT_BASE64)], $data); } public function testGetAvatarImageNoUrlCached() { From 6e9b17c4eaea84ce58fe3f5e4917b8fdd97b4e45 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Mon, 13 Nov 2017 14:31:58 +0100 Subject: [PATCH 13/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- js/views/messagesitem.js | 2 ++ lib/Service/AvatarService.php | 1 + 2 files changed, 3 insertions(+) diff --git a/js/views/messagesitem.js b/js/views/messagesitem.js index b877207450..d770dcd422 100644 --- a/js/views/messagesitem.js +++ b/js/views/messagesitem.js @@ -1,3 +1,5 @@ +/* global Promise */ + /** * Mail * diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php index a086ef6b6d..d3d5656fce 100644 --- a/lib/Service/AvatarService.php +++ b/lib/Service/AvatarService.php @@ -72,6 +72,7 @@ private function hasAllowedMime(Avatar $avatar) { return in_array($mime, [ 'image/jpeg', 'image/png', + 'image/x-icon', ]); } else { // We trust internal URLs by default From 50216e41b63f75bf88ffd185228143adbc21eb10 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Tue, 14 Nov 2017 12:17:57 +0100 Subject: [PATCH 14/14] fixup! Cleanup, refactor and test new AvatarService Signed-off-by: Christoph Wurst --- package-lock.json | 907 ---------------------------------------------- 1 file changed, 907 deletions(-) diff --git a/package-lock.json b/package-lock.json index b993ae0b16..12c4773043 100644 --- a/package-lock.json +++ b/package-lock.json @@ -600,7 +600,6 @@ "requires": { "anymatch": "1.3.2", "async-each": "1.0.1", - "fsevents": "1.1.2", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -2015,905 +2014,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.7.0", - "node-pre-gyp": "0.6.36" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.36", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, "generate-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", @@ -4782,13 +3882,6 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, - "nan": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", - "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=", - "dev": true, - "optional": true - }, "native-promise-only": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz",