diff --git a/css/icons.scss b/css/icons.scss index 084df4e7b..853289d5a 100644 --- a/css/icons.scss +++ b/css/icons.scss @@ -41,6 +41,7 @@ @include icon-black-white('twitter', 'contacts', 2); // “twitter (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/twitter?style=brands) @include icon-black-white('diaspora', 'contacts', 2); // “diaspora (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/diaspora?style=brands) @include icon-black-white('xing', 'contacts', 2); // “xing (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/xing?style=brands) +@include icon-black-white('gravatar', 'contacts', 2); // “gravatar (fab)” by svgrepo.com is licensed under public domain CCO 1.0. (https://www.svgrepo.com/page/licensing) .icon-up-force-white { // using #fffffe to trick the accessibility dark theme icon invert diff --git a/img/gravatar.svg b/img/gravatar.svg new file mode 100644 index 000000000..9b20de269 --- /dev/null +++ b/img/gravatar.svg @@ -0,0 +1 @@ +Gravatar icon \ No newline at end of file diff --git a/img/license.txt b/img/license.txt index 1022843e4..a1c9cbaa4 100644 --- a/img/license.txt +++ b/img/license.txt @@ -6,3 +6,5 @@ * “twitter (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/twitter?style=brands) * “diaspora (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/diaspora?style=brands) * “xing (fab)” by fontawesome.com is licensed under CC BY 4.0. (https://fontawesome.com/icons/xing?style=brands) +* “gravatar (fab)” by svgrepo.com is licensed under public domain CCO 1.0. (https://www.svgrepo.com/page/licensing) + diff --git a/lib/Service/Social/CompositeSocialProvider.php b/lib/Service/Social/CompositeSocialProvider.php index 40d937e6e..751f5cc68 100644 --- a/lib/Service/Social/CompositeSocialProvider.php +++ b/lib/Service/Social/CompositeSocialProvider.php @@ -37,17 +37,19 @@ public function __construct(InstagramProvider $instagramProvider, TwitterProvider $twitterProvider, TumblrProvider $tumblrProvider, DiasporaProvider $diasporaProvider, - XingProvider $xingProvider) { + XingProvider $xingProvider, + GravatarProvider $gravatarProvider) { // This determines the priority of known providers $this->providers = [ - 'instagram' => $instagramProvider, - 'mastodon' => $mastodonProvider, - 'twitter' => $twitterProvider, - 'facebook' => $facebookProvider, - 'tumblr' => $tumblrProvider, - 'diaspora' => $diasporaProvider, - 'xing' => $xingProvider, + $instagramProvider->name => $instagramProvider, + $mastodonProvider->name => $mastodonProvider, + $twitterProvider->name => $twitterProvider, + $facebookProvider->name => $facebookProvider, + $tumblrProvider->name => $tumblrProvider, + $diasporaProvider->name => $diasporaProvider, + $xingProvider->name => $xingProvider, + $gravatarProvider->name => $gravatarProvider ]; } @@ -60,40 +62,28 @@ public function getSupportedNetworks() : array { return array_keys($this->providers); } - /** * generate download url for a social entry * - * @param array socialEntries all social data from the contact - * @param String network the choice which network to use (fallback: take first available) + * @param String network the choice which network to use * - * @returns String the url to the requested information or null in case of errors + * @return ISocialProvider if provider of 'network' is found, otherwise null */ - public function getSocialConnector(array $socialEntries, string $network) : ?string { + public function getSocialConnector(string $network) : ?ISocialProvider { $connector = null; - $selection = $this->providers; // check if dedicated network selected if (isset($this->providers[$network])) { - $selection = [$network => $this->providers[$network]]; + $connector = $this->providers[$network]; } + return $connector; + } - // check selected providers in order - foreach ($selection as $type => $socialProvider) { - - // search for this network in user's profile - foreach ($socialEntries as $socialEntry) { - if (strtolower($type) === strtolower($socialEntry['type'])) { - $profileId = $socialProvider->cleanupId($socialEntry['value']); - if (!is_null($profileId)) { - $connector = $socialProvider->getImageUrl($profileId); - } - break; - } - } - if ($connector) { - break; - } - } - return ($connector); + /** + * generate download url for a social entry + * + * @return ISocialProvider[] all social providers + */ + public function getSocialConnectors() : array { + return array_values($this->providers); } } diff --git a/lib/Service/Social/DiasporaProvider.php b/lib/Service/Social/DiasporaProvider.php index 73f85792c..c0af278f6 100644 --- a/lib/Service/Social/DiasporaProvider.php +++ b/lib/Service/Social/DiasporaProvider.php @@ -30,31 +30,48 @@ class DiasporaProvider implements ISocialProvider { /** @var IClientService */ private $httpClient; - /** @var boolean */ + /** @var bool */ private $looping; + /** @var string */ + public $name = "diaspora"; + public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); $this->looping = false; } - + /** - * Returns the profile-id + * Returns if this provider supports this contact * - * @param {string} the value from the contact's x-socialprofile + * @param {array} contact info * - * @return string + * @return bool */ - public function cleanupId(string $candidate):string { - try { - if (strpos($candidate, 'http') !== 0) { - $user_server = explode('@', $candidate); - $candidate = 'https://' . array_pop($user_server) . '/public/' . array_pop($user_server) . '.atom'; + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfileIds($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns all possible profile-picture urls + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + + foreach ($profileIds as $profileId) { + $url = $this->getImageUrl($profileId); + if (isset($url)) { + $urls[] = $url; } - } catch (Exception $e) { - $candidate = null; } - return $candidate; + + return $urls; } /** @@ -64,7 +81,7 @@ public function cleanupId(string $candidate):string { * * @return string|null */ - public function getImageUrl(string $profileUrl):?string { + protected function getImageUrl(string $profileUrl):?string { try { $result = $this->httpClient->get($profileUrl); $htmlResult = $result->getBody(); @@ -82,8 +99,51 @@ public function getImageUrl(string $profileUrl):?string { } } return null; - } catch (Exception $e) { + } catch (\Exception $e) { return null; } } + + /** + * Returns all possible profile ids for contact + * + * @param {array} contact information + * + * @return array + */ + protected function getProfileIds($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profileIds = []; + + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profileId = $this->cleanupId($profile['value']); + if (isset($profileId)) { + $profileIds[] = $profileId; + } + } + } + } + return $profileIds; + } + + /** + * Returns the profile-id + * + * @param {string} the value from the contact's x-socialprofile + * + * @return string + */ + protected function cleanupId(string $candidate):?string { + try { + if (strpos($candidate, 'http') !== 0) { + $user_server = explode('@', $candidate); + $candidate = 'https://' . array_pop($user_server) . '/public/' . array_pop($user_server) . '.atom'; + } + } catch (\Exception $e) { + $candidate = null; + } + return $candidate; + } } diff --git a/lib/Service/Social/FacebookProvider.php b/lib/Service/Social/FacebookProvider.php index 770ac45ae..e5d34397a 100644 --- a/lib/Service/Social/FacebookProvider.php +++ b/lib/Service/Social/FacebookProvider.php @@ -30,10 +30,43 @@ class FacebookProvider implements ISocialProvider { /** @var IClientService */ private $httpClient; + /** @var string */ + public $name = "facebook"; + public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); } - + + /** + * Returns if this provider supports this contact + * + * @param {array} contact info + * + * @return bool + */ + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfiles($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns the profile-picture url + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + foreach ($profileIds as $profileId) { + $recipe = 'https://graph.facebook.com/{socialId}/picture?width=720'; + $connector = str_replace("{socialId}", $profileId, $recipe); + $urls[] = $connector; + } + return $urls; + } + /** * Returns the profile-id * @@ -41,7 +74,7 @@ public function __construct(IClientService $httpClient) { * * @return string */ - public function cleanupId(string $candidate):string { + protected function cleanupId(string $candidate):string { $candidate = basename($candidate); if (!is_numeric($candidate)) { $candidate = $this->findFacebookId($candidate); @@ -50,16 +83,39 @@ public function cleanupId(string $candidate):string { } /** - * Returns the profile-picture url + * Returns all possible profile ids for contact * - * @param {string} profileId the profile-id + * @param {array} contact information * - * @return string + * @return array of string profile ids */ - public function getImageUrl(string $profileId):string { - $recipe = 'https://graph.facebook.com/{socialId}/picture?width=720'; - $connector = str_replace("{socialId}", $profileId, $recipe); - return $connector; + protected function getProfiles(array $contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profiles = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profiles[] = $profile['value']; + } + } + } + return $profiles; + } + + /** + * Returns all possible profile ids for contact + * + * @param {array} contact information + * + * @return array of string profile ids + */ + protected function getProfileIds(array $contact):array { + $profiles = $this->getProfiles($contact); + $profileIds = []; + foreach ($profiles as $profile) { + $profileIds[] = $this->cleanupId($profile); + } + return $profileIds; } /** diff --git a/lib/Service/Social/GravatarProvider.php b/lib/Service/Social/GravatarProvider.php new file mode 100644 index 000000000..4b9d9a842 --- /dev/null +++ b/lib/Service/Social/GravatarProvider.php @@ -0,0 +1,65 @@ + + * + * @author leith + * + * @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\Contacts\Service\Social; + +class GravatarProvider implements ISocialProvider { + /** @var string */ + public $name = "gravatar"; + + public function __construct() { + } + + /** + * Returns if this provider supports this contact + * + * @param {array} contact info + * + * @return bool + */ + public function supportsContact(array $contact):bool { + $emails = $contact['EMAIL']; + return isset($emails) && count($emails); + } + + /** + * Returns the profile-picture url + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $urls = []; + $emails = $contact['EMAIL']; + if (isset($emails)) { + foreach ($emails as $email) { + $hash = md5(strtolower(trim($email['value']))); + $recipe = 'https://www.gravatar.com/avatar/{hash}?s=720&d=404'; + $connector = str_replace("{hash}", $hash, $recipe); + $urls[] = $connector; + } + } + return $urls; + } +} diff --git a/lib/Service/Social/ISocialProvider.php b/lib/Service/Social/ISocialProvider.php index 1415f4d09..a21fe32bb 100644 --- a/lib/Service/Social/ISocialProvider.php +++ b/lib/Service/Social/ISocialProvider.php @@ -24,22 +24,21 @@ namespace OCA\Contacts\Service\Social; interface ISocialProvider { - /** - * Returns the profile-id + * Returns true if provider supports the contact * - * @param {string} the value from the contact's x-socialprofile + * @param {array} contact details * - * @return string + * @return boolean */ - public function cleanupId(string $candidate):?string ; + public function supportsContact(array $contact):bool ; /** - * Returns the profile-picture url + * Returns all possible profile-picture urls * - * @param {string} profileId the profile-id + * @param {array} contact information * - * @return string|null + * @return array */ - public function getImageUrl(string $profileId):?string ; + public function getImageUrls(array $contact):array ; } diff --git a/lib/Service/Social/InstagramProvider.php b/lib/Service/Social/InstagramProvider.php index b7687e715..a43d12c3d 100644 --- a/lib/Service/Social/InstagramProvider.php +++ b/lib/Service/Social/InstagramProvider.php @@ -30,10 +30,44 @@ class InstagramProvider implements ISocialProvider { /** @var IClientService */ private $httpClient; + /** @var string */ + public $name = "instagram"; + public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); } - + + /** + * Returns if this provider supports this contact + * + * @param {array} contact info + * + * @return bool + */ + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfiles($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns the profile-picture url + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + foreach ($profileIds as $profileId) { + $recipe = 'https://www.instagram.com/{socialId}/?__a=1'; + $connector = str_replace("{socialId}", $profileId, $recipe); + $connector = $this->getFromJson($connector, 'graphql->user->profile_pic_url_hd'); + $urls[] = $connector; + } + return $urls; + } + /** * Returns the profile-id * @@ -41,25 +75,47 @@ public function __construct(IClientService $httpClient) { * * @return string */ - public function cleanupId(string $candidate):string { + protected function cleanupId(string $candidate):string { $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); return basename($candidate); } + + /** + * Returns all possible profile urls for contact + * + * @param {array} contact information + * + * @return array of string profile urls + */ + protected function getProfiles($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profiles = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profiles[] = $profile['value']; + } + } + } + return $profiles; + } /** - * Returns the profile-picture url + * Returns all possible profile ids for contact * - * @param {string} profileId the profile-id + * @param {array} contact information * - * @return string|null + * @return array of string profile ids */ - public function getImageUrl(string $profileId):?string { - $recipe = 'https://www.instagram.com/{socialId}/?__a=1'; - $connector = str_replace("{socialId}", $profileId, $recipe); - $connector = $this->getFromJson($connector, 'graphql->user->profile_pic_url_hd'); - return $connector; + protected function getProfileIds($contact):array { + $socialprofiles = $this->getProfiles($contact); + $profileIds = []; + foreach ($socialprofiles as $profile) { + $profileIds[] = $this->cleanupId($profile); + } + return $profileIds; } - + /** * extracts desired value from a json * @@ -81,7 +137,7 @@ protected function getFromJson(string $url, string $desired) : ?string { $jsonResult = $jsonResult[$loc]; } return $jsonResult; - } catch (Exception $e) { + } catch (\Exception $e) { return null; } } diff --git a/lib/Service/Social/MastodonProvider.php b/lib/Service/Social/MastodonProvider.php index 74b68e96d..a212433c7 100644 --- a/lib/Service/Social/MastodonProvider.php +++ b/lib/Service/Social/MastodonProvider.php @@ -30,34 +30,49 @@ class MastodonProvider implements ISocialProvider { /** @var IClientService */ private $httpClient; + /** @var string */ + public $name = "mastodon"; + public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); } - + /** - * Returns the profile-id + * Returns if this provider supports this contact * - * @param {string} the value from the contact's x-socialprofile + * @param {array} contact info * - * @return string + * @return bool */ - public function cleanupId(string $candidate):?string { - $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); - try { - if (strpos($candidate, 'http') !== 0) { - $user_server = explode('@', $candidate); - $candidate = 'https://' . array_pop($user_server) . '/@' . array_pop($user_server); + public function supportsContact(array $contact):bool { + $profiles = $this->getProfileIds($contact); + return isset($profiles) && count($profiles) > 0; + } + + /** + * Returns all possible profile-picture urls + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + + foreach ($profileIds as $profileId) { + $url = $this->getImageUrl($profileId); + if (isset($url)) { + $urls[] = $url; } - } catch (Exception $e) { - $candidate = null; } - return $candidate; + return $urls; } /** * Returns the profile-picture url * - * @param {string} profileUrl link to the profile + * @param {array} contact information * * @return string|null */ @@ -72,8 +87,51 @@ public function getImageUrl(string $profileUrl):?string { return $img->getAttribute("data-original"); } return null; - } catch (Exception $e) { + } catch (\Exception $e) { return null; } } + + /** + * Returns all possible profile ids for contact + * + * @param {array} contact information + * + * @return array of possible profileIds + */ + protected function getProfileIds($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profileIds = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profileId = $this->cleanupId($profile['value']); + if (isset($profileId)) { + $profileIds[] = $profileId; + } + } + } + } + return $profileIds; + } + + /** + * Returns the profile-id + * + * @param {string} the value from the contact's x-socialprofile + * + * @return string + */ + protected function cleanupId(string $candidate):?string { + $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); + try { + if (strpos($candidate, 'http') !== 0) { + $user_server = explode('@', $candidate); + $candidate = 'https://' . array_pop($user_server) . '/@' . array_pop($user_server); + } + } catch (\Exception $e) { + $candidate = null; + } + return $candidate; + } } diff --git a/lib/Service/Social/TumblrProvider.php b/lib/Service/Social/TumblrProvider.php index a830b945b..cd3d577f7 100644 --- a/lib/Service/Social/TumblrProvider.php +++ b/lib/Service/Social/TumblrProvider.php @@ -24,9 +24,42 @@ namespace OCA\Contacts\Service\Social; class TumblrProvider implements ISocialProvider { + /** @var string */ + public $name = "tumblr"; + public function __construct() { } - + + /** + * Returns if this provider supports this contact + * + * @param {array} contact info + * + * @return bool + */ + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfileIds($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns the profile-picture url + * + * @param {string} profileId the profile-id + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + foreach ($profileIds as $profileId) { + $recipe = 'https://api.tumblr.com/v2/blog/{socialId}/avatar/512'; + $connector = str_replace("{socialId}", $profileId, $recipe); + $urls[] = $connector; + } + return $urls; + } + /** * Returns the profile-id * @@ -34,7 +67,7 @@ public function __construct() { * * @return string */ - public function cleanupId(string $candidate):?string { + protected function cleanupId(string $candidate):?string { $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); $subdomain = '/(?:http[s]*\:\/\/)*(.*?)\.(?=[^\/]*\..{2,5})/i'; // subdomain if (preg_match($subdomain, $candidate, $matches)) { @@ -44,15 +77,22 @@ public function cleanupId(string $candidate):?string { } /** - * Returns the profile-picture url + * Returns all possible profile ids for contact * - * @param {string} profileId the profile-id + * @param {array} contact information * - * @return string|null + * @return array of string profile ids */ - public function getImageUrl(string $profileId):?string { - $recipe = 'https://api.tumblr.com/v2/blog/{socialId}/avatar/512'; - $connector = str_replace("{socialId}", $profileId, $recipe); - return $connector; + protected function getProfileIds($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profileIds = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profileIds[] = $this->cleanupId($profile['value']); + } + } + } + return $profileIds; } } diff --git a/lib/Service/Social/TwitterProvider.php b/lib/Service/Social/TwitterProvider.php index 953eb1a09..18cd0d467 100644 --- a/lib/Service/Social/TwitterProvider.php +++ b/lib/Service/Social/TwitterProvider.php @@ -26,14 +26,47 @@ use OCP\Http\Client\IClientService; class TwitterProvider implements ISocialProvider { - /** @var IClientService */ private $httpClient; + /** @var string */ + public $name = "twitter"; + public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); } - + + /** + * Returns if this provider supports this contact + * + * @param {array} contact info + * + * @return bool + */ + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfileIds($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns the profile-picture url + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + foreach ($profileIds as $profileId) { + $recipe = 'https://mobile.twitter.com/{socialId}'; + $connector = str_replace("{socialId}", $profileId, $recipe); + $connector = $this->getFromHtml($connector, '_normal'); + $urls[] = $connector; + } + return $urls; + } + /** * Returns the profile-id * @@ -41,7 +74,7 @@ public function __construct(IClientService $httpClient) { * * @return string */ - public function cleanupId(string $candidate):string { + protected function cleanupId(string $candidate):string { $candidate = basename($candidate); if ($candidate[0] === '@') { $candidate = substr($candidate, 1); @@ -50,19 +83,25 @@ public function cleanupId(string $candidate):string { } /** - * Returns the profile-picture url + * Returns all possible profile ids for contact * - * @param {string} profileId the profile-id + * @param {array} contact information * - * @return string|null + * @return array of string profile ids */ - public function getImageUrl(string $profileId):?string { - $recipe = 'https://mobile.twitter.com/{socialId}'; - $connector = str_replace("{socialId}", $profileId, $recipe); - $connector = $this->getFromHtml($connector, '_normal'); - return $connector; + protected function getProfileIds($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profileIds = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profileIds[] = $this->cleanupId($profile['value']); + } + } + } + return $profileIds; } - + /** * extracts desired value from an html page * @@ -88,7 +127,7 @@ protected function getFromHtml(string $url, string $desired) : ?string { } } return null; - } catch (Exception $e) { + } catch (\Exception $e) { return null; } } diff --git a/lib/Service/Social/XingProvider.php b/lib/Service/Social/XingProvider.php index 739d1fd2f..a752d3c60 100644 --- a/lib/Service/Social/XingProvider.php +++ b/lib/Service/Social/XingProvider.php @@ -30,41 +30,54 @@ class XingProvider implements ISocialProvider { /** @var IClientService */ private $httpClient; - /** @var boolean */ - private $looping; + /** @var string */ + public $name = "xing"; public function __construct(IClientService $httpClient) { $this->httpClient = $httpClient->NewClient(); - $this->looping = false; } - + /** - * Returns the profile-id + * Returns if this provider supports this contact * - * @param {string} the value from the contact's x-socialprofile + * @param {array} contact info * - * @return string + * @return bool */ - public function cleanupId(string $candidate):string { - $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); - try { - if (strpos($candidate, 'http') !== 0) { - $candidate = 'https://www.xing.com/profile/' . $candidate; + public function supportsContact(array $contact):bool { + $socialprofiles = $this->getProfileIds($contact); + return isset($socialprofiles) && count($socialprofiles) > 0; + } + + /** + * Returns all possible profile-picture urls + * + * @param {array} contact information + * + * @return array + */ + public function getImageUrls(array $contact):array { + $profileIds = $this->getProfileIds($contact); + $urls = []; + + foreach ($profileIds as $profileId) { + $url = $this->getImageUrl($profileId); + if (isset($url)) { + $urls[] = $url; } - } catch (Exception $e) { - $candidate = null; } - return $candidate; + + return $urls; } /** * Returns the profile-picture url * - * @param {string} profileId the profile-id + * @param {string} profile url * * @return string|null */ - public function getImageUrl(string $profileUrl):?string { + protected function getImageUrl(string $profileUrl):?string { try { $result = $this->httpClient->get($profileUrl); $htmlResult = $result->getBody(); @@ -75,8 +88,50 @@ public function getImageUrl(string $profileUrl):?string { } // keyword not found, maybe page changed? return null; - } catch (Exception $e) { + } catch (\Exception $e) { return null; } } + + /** + * Returns the profile-id + * + * @param {string} the value from the contact's x-socialprofile + * + * @return string + */ + protected function cleanupId(string $candidate):?string { + $candidate = preg_replace('/^' . preg_quote('x-apple:', '/') . '/', '', $candidate); + try { + if (strpos($candidate, 'http') !== 0) { + $candidate = 'https://www.xing.com/profile/' . $candidate; + } + } catch (\Exception $e) { + $candidate = null; + } + return $candidate; + } + + /** + * Returns all possible profile ids for contact + * + * @param {array} contact information + * + * @return string of first profile url else null + */ + protected function getProfileIds($contact):array { + $socialprofiles = $contact['X-SOCIALPROFILE']; + $profileIds = []; + if (isset($socialprofiles)) { + foreach ($socialprofiles as $profile) { + if (strtolower($profile['type']) == $this->name) { + $profileId = $this->cleanupId($profile['value']); + if (isset($profileId)) { + $profileIds[] = $profileId; + } + } + } + } + return $profileIds; + } } diff --git a/lib/Service/SocialApiService.php b/lib/Service/SocialApiService.php index 03484c99e..446a9ef69 100644 --- a/lib/Service/SocialApiService.php +++ b/lib/Service/SocialApiService.php @@ -50,9 +50,9 @@ class SocialApiService { private $config; /** @var IClientService */ private $clientService; - /** @var IL10N */ + /** @var IL10N */ private $l10n; - /** @var IURLGenerator */ + /** @var IURLGenerator */ private $urlGen; /** @var CardDavBackend */ private $davBackend; @@ -84,7 +84,7 @@ public function __construct( /** * returns an array of supported social networks * - * @returns {array} array of the supported social networks + * @return {array} array of the supported social networks */ public function getSupportedNetworks() : array { $syncAllowedByAdmin = $this->config->getAppValue($this->appName, 'allowSocialSync', 'yes'); @@ -167,8 +167,11 @@ protected function registerAddressbooks($userId, IManager $manager) { * * @returns {JSONResponse} an empty JSONResponse with respective http status code */ - public function updateContact(string $addressbookId, string $contactId, string $network) : JSONResponse { - $url = null; + public function updateContact(string $addressbookId, string $contactId, ?string $network) : JSONResponse { + $socialdata = null; + $imageType = null; + $urls = []; + $allConnectors = $this->socialProvider->getSocialConnectors(); try { // get corresponding addressbook @@ -179,20 +182,42 @@ public function updateContact(string $addressbookId, string $contactId, string $ // search contact in that addressbook, get social data $contact = $addressBook->search($contactId, ['UID'], ['types' => true])[0]; - if (!isset($contact['X-SOCIALPROFILE'])) { + + if (!isset($contact)) { + return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); + } + + if ($network) { + $allConnectors = [$this->socialProvider->getSocialConnector($network)]; + } + + $connectors = array_filter($allConnectors, function ($connector) use ($contact) { + return $connector->supportsContact($contact); + }); + + if (count($connectors) == 0) { return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); } - $socialprofiles = $contact['X-SOCIALPROFILE']; - // retrieve data - $url = $this->socialProvider->getSocialConnector($socialprofiles, $network); - if (empty($url)) { + foreach ($connectors as $connector) { + $urls = array_merge($connector->getImageUrls($contact), $urls); + } + + if (count($urls) == 0) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } - $httpResult = $this->clientService->NewClient()->get($url); - $socialdata = $httpResult->getBody(); - $imageType = $httpResult->getHeader('content-type'); + foreach ($urls as $url) { + try { + $httpResult = $this->clientService->NewClient()->get($url); + $socialdata = $httpResult->getBody(); + $imageType = $httpResult->getHeader('content-type'); + if (isset($socialdata) && isset($imageType)) { + break; + } + } catch (\Exception $e) { + } + } if (!$socialdata || $imageType === null) { return new JSONResponse([], Http::STATUS_NOT_FOUND); diff --git a/src/components/ContactDetails/ContactDetailsAvatar.vue b/src/components/ContactDetails/ContactDetailsAvatar.vue index b13e94111..92b1befb7 100644 --- a/src/components/ContactDetails/ContactDetailsAvatar.vue +++ b/src/components/ContactDetails/ContactDetailsAvatar.vue @@ -196,11 +196,15 @@ export default { return false }, supportedSocial() { + const emails = this.contact.vCard.getAllProperties('email') // get social networks set for the current contact const available = this.contact.vCard.getAllProperties('x-socialprofile') .map(a => a.jCal[1].type.toString().toLowerCase()) // get list of social networks that allow for avatar download const supported = supportedNetworks.map(v => v.toLowerCase()) + if (emails.length) { + available.push('gravatar') + } // return supported social networks which are set return supported.filter(i => available.includes(i)) .map(j => this.capitalize(j)) diff --git a/tests/unit/Service/Social/DiasporaProviderTest.php b/tests/unit/Service/Social/DiasporaProviderTest.php new file mode 100644 index 000000000..e4b066c1c --- /dev/null +++ b/tests/unit/Service/Social/DiasporaProviderTest.php @@ -0,0 +1,182 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class DiasporaProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new DiasporaProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "diaspora"], + ["value" => "two", "type" => "diaspora"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with diaspora fields' => [$contactWithSocial, true], + 'contact without diaspora fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one@two", "type" => "diaspora"], + ["value" => "two@three", "type" => "diaspora"] + ] + ]; + $contactWithSocialUrls = [ + "https://two/public/one.atom", + "https://three/public/two.atom" + ]; + $contactWithSocialHtml = array_map(function ($url) { + return "".$url."-small-avatar.jpg"; + }, $contactWithSocialUrls); + $contactWithSocialImg = array_map(function ($url) { + return $url."-large-avatar.jpg"; + }, $contactWithSocialUrls); + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + $contactWithoutSocialHtml = []; + $contactWithoutSocialImg = []; + + return [ + 'contact with diaspora fields' => [ + $contactWithSocial, + $contactWithSocialUrls, + $contactWithSocialHtml, + $contactWithSocialImg + ], + 'contact without diaspora fields' => [ + $contactWithoutSocial, + $contactWithoutSocialUrls, + $contactWithoutSocialHtml, + $contactWithoutSocialImg + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $urls, $htmls, $imgs) { + if (count($urls)) { + $this->response + ->method('getBody') + ->willReturnOnConsecutiveCalls(...$htmls); + + $urlArgs = array_map(function ($url) { + return [$url]; + }, $urls); + + $this->client + ->expects($this->exactly(count($urls))) + ->method('get') + ->withConsecutive(...$urlArgs) + ->willReturn($this->response); + } + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($imgs, $result); + } + + public function testGetImageUrlLoop() { + $contact = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one@two", "type" => "diaspora"], + ] + ]; + $url1 = "https://two/public/one.atom"; + $url2 = "https://four/public/three.atom"; + $html1 = ''; + $html2 = "".$url2."-small-avatar.jpg"; + $img = $url2."-large-avatar.jpg"; + + $this->response + ->method('getBody') + ->willReturnOnConsecutiveCalls($html1, $html2); + + $this->client + ->expects($this->exactly(2)) + ->method('get') + ->withConsecutive([$url1], [$url2]) + ->willReturn($this->response); + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals([$img], $result); + } +} diff --git a/tests/unit/Service/Social/FacebookProviderTest.php b/tests/unit/Service/Social/FacebookProviderTest.php new file mode 100644 index 000000000..b2e3d8e0b --- /dev/null +++ b/tests/unit/Service/Social/FacebookProviderTest.php @@ -0,0 +1,156 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class FacebookProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new FacebookProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "123124123", "type" => "facebook"], + ["value" => "23426523423", "type" => "facebook"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with facebook fields' => [$contactWithSocial, true], + 'contact without facebook fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "123456", "type" => "facebook"], + ["value" => "7891011", "type" => "facebook"] + ] + ]; + $contactWithSocialUrls = [ + "https://graph.facebook.com/123456/picture?width=720", + "https://graph.facebook.com/7891011/picture?width=720", + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + + return [ + 'contact with facebook fields' => [ + $contactWithSocial, + $contactWithSocialUrls + ], + 'contact without facebook fields' => [ + $contactWithoutSocial, + $contactWithoutSocialUrls + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $urls) { + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($urls, $result); + } + + public function testGetImageUrlLookup() { + $contact = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "facebook"], + ] + ]; + $url1 = "https://facebook.com/username1"; + $url2 = "https://graph.facebook.com/1234567/picture?width=720"; + $html1 = '"entity_id":"1234567"'; + + $this->response + ->method('getBody') + ->willReturn($html1); + + $this->response + ->method('getStatusCode') + ->willReturn(200); + + $this->client + ->expects($this->once()) + ->method('get') + ->with($url1) + ->willReturn($this->response); + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals([$url2], $result); + } +} diff --git a/tests/unit/Service/Social/GravatarProviderTest.php b/tests/unit/Service/Social/GravatarProviderTest.php new file mode 100644 index 000000000..8de4f5656 --- /dev/null +++ b/tests/unit/Service/Social/GravatarProviderTest.php @@ -0,0 +1,92 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use ChristophWurst\Nextcloud\Testing\TestCase; + +class GravatarProviderTest extends TestCase { + private $provider; + + protected function setUp(): void { + parent::setUp(); + + $this->provider = new GravatarProvider( + ); + } + + public function dataProviderSupportsContact() { + $contactWithEmail = [ + 'EMAIL' => [["value" => "one"], ["value" => "two"]] + ]; + + $contactWithoutEmail = [ + 'PHONE' => [["value" => "one"], ["value" => "two"]] + ]; + + return [ + 'contact with email' => [$contactWithEmail, true], + 'contact without email' => [$contactWithoutEmail, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithEmail = [ + 'EMAIL' => [["value" => "one"], ["value" => "two"]] + ]; + + $contactWithoutEmail = [ + 'PHONE' => [["value" => "one"], ["value" => "two"]] + ]; + + $urls = []; + + foreach ($contactWithEmail['EMAIL'] as $email) { + $hash = md5(strtolower(trim($email['value']))); + $recipe = 'https://www.gravatar.com/avatar/{hash}?s=720&d=404'; + $urls[] = str_replace("{hash}", $hash, $recipe); + } + + return [ + 'contact with email' => [$contactWithEmail, $urls], + 'contact without email' => [$contactWithoutEmail, []] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $expected) { + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($expected, $result); + } +} diff --git a/tests/unit/Service/Social/InstagramProviderTest.php b/tests/unit/Service/Social/InstagramProviderTest.php new file mode 100644 index 000000000..6fa0e8668 --- /dev/null +++ b/tests/unit/Service/Social/InstagramProviderTest.php @@ -0,0 +1,158 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class InstagramProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new InstagramProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "instagram"], + ["value" => "username2", "type" => "instagram"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with instagram fields' => [$contactWithSocial, true], + 'contact without instagram fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "instagram"], + ["value" => "username2", "type" => "instagram"] + ] + ]; + $contactWithSocialUrls = [ + "https://www.instagram.com/username1/?__a=1", + "https://www.instagram.com/username2/?__a=1", + ]; + $contactWithSocialJson = [ + json_encode( + ["graphql" => ["user" => ["profile_pic_url_hd" => "username1.jpg"]]] + ), + json_encode( + ["graphql" => ["user" => ["profile_pic_url_hd" => "username2.jpg"]]] + ) + ]; + $contactWithSocialImgs = [ + "username1.jpg", + "username2.jpg" + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + $contactWithoutSocialJson = []; + $contactWithoutSocialImgs = []; + + return [ + 'contact with instagram fields' => [ + $contactWithSocial, + $contactWithSocialJson, + $contactWithSocialUrls, + $contactWithSocialImgs + ], + 'contact without instagram fields' => [ + $contactWithoutSocial, + $contactWithoutSocialJson, + $contactWithoutSocialUrls, + $contactWithoutSocialImgs + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $json, $urls, $imgs) { + if (count($urls)) { + $this->response->method("getBody")->willReturnOnConsecutiveCalls(...$json); + $this->client + ->expects($this->exactly(count($urls))) + ->method("get") + ->withConsecutive(...array_map(function ($a) { + return [$a]; + }, $urls)) + ->willReturn($this->response); + } + + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($imgs, $result); + } +} diff --git a/tests/unit/Service/Social/MastodonProviderTest.php b/tests/unit/Service/Social/MastodonProviderTest.php new file mode 100644 index 000000000..9611bf6b3 --- /dev/null +++ b/tests/unit/Service/Social/MastodonProviderTest.php @@ -0,0 +1,154 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class MastodonProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new MastodonProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "user1@cloud1", "type" => "mastodon"], + ["value" => "user2@cloud2", "type" => "mastodon"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with mastodon fields' => [$contactWithSocial, true], + 'contact without mastodon fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "user1@cloud1", "type" => "mastodon"], + ["value" => "user2@cloud2", "type" => "mastodon"] + ] + ]; + $contactWithSocialUrls = [ + "https://cloud1/@user1", + "https://cloud2/@user2", + ]; + $contactWithSocialHtml = [ + '', + '' + ]; + $contactWithSocialImgs = [ + "user1.jpg", + "user2.jpg" + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + $contactWithoutSocialHtml = []; + $contactWithoutSocialImgs = []; + + return [ + 'contact with mastodon fields' => [ + $contactWithSocial, + $contactWithSocialHtml, + $contactWithSocialUrls, + $contactWithSocialImgs + ], + 'contact without mastodon fields' => [ + $contactWithoutSocial, + $contactWithoutSocialHtml, + $contactWithoutSocialUrls, + $contactWithoutSocialImgs + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $htmls, $urls, $imgs) { + if (count($urls)) { + $this->response->method("getBody")->willReturnOnConsecutiveCalls(...$htmls); + $this->client + ->expects($this->exactly(count($urls))) + ->method("get") + ->withConsecutive(...array_map(function ($a) { + return [$a]; + }, $urls)) + ->willReturn($this->response); + } + + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($imgs, $result); + } +} diff --git a/tests/unit/Service/Social/TumblrProviderTest.php b/tests/unit/Service/Social/TumblrProviderTest.php new file mode 100644 index 000000000..79e1c931f --- /dev/null +++ b/tests/unit/Service/Social/TumblrProviderTest.php @@ -0,0 +1,100 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use ChristophWurst\Nextcloud\Testing\TestCase; + +class TumblrProviderTest extends TestCase { + private $provider; + + protected function setUp(): void { + parent::setUp(); + + $this->provider = new TumblrProvider( + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "tumblr"], + ["value" => "username2", "type" => "tumblr"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with email' => [$contactWithSocial, true], + 'contact without email' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "tumblr"], + ["value" => "username2", "type" => "tumblr"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + foreach ($contactWithSocial['X-SOCIALPROFILE'] as $profile) { + $urls[] = "https://api.tumblr.com/v2/blog/".$profile['value']."/avatar/512"; + } + + return [ + 'contact with email' => [$contactWithSocial, $urls], + 'contact without email' => [$contactWithoutSocial, []] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $expected) { + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($expected, $result); + } +} diff --git a/tests/unit/Service/Social/TwitterProviderTest.php b/tests/unit/Service/Social/TwitterProviderTest.php new file mode 100644 index 000000000..55515b0ff --- /dev/null +++ b/tests/unit/Service/Social/TwitterProviderTest.php @@ -0,0 +1,154 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class TwitterProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new TwitterProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "twitter"], + ["value" => "username2", "type" => "twitter"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with twitter fields' => [$contactWithSocial, true], + 'contact without twitter fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "https://twitter.com/username1", "type" => "twitter"], + ["value" => "https://twitter.com/@username2", "type" => "twitter"] + ] + ]; + $contactWithSocialUrls = [ + "https://mobile.twitter.com/username1", + "https://mobile.twitter.com/username2", + ]; + $contactWithSocialHtml = [ + '', + '', + ]; + $contactWithSocialImgs = [ + "username1_400x400.jpg", + "username2_400x400.jpg" + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + $contactWithoutSocialHtml = []; + $contactWithoutSocialImgs = []; + + return [ + 'contact with twitter fields' => [ + $contactWithSocial, + $contactWithSocialHtml, + $contactWithSocialUrls, + $contactWithSocialImgs + ], + 'contact without twitter fields' => [ + $contactWithoutSocial, + $contactWithoutSocialHtml, + $contactWithoutSocialUrls, + $contactWithoutSocialImgs + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $htmls, $urls, $imgs) { + if (count($urls)) { + $this->response->method("getBody")->willReturnOnConsecutiveCalls(...$htmls); + $this->client + ->expects($this->exactly(count($urls))) + ->method("get") + ->withConsecutive(...array_map(function ($a) { + return [$a]; + }, $urls)) + ->willReturn($this->response); + } + + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($imgs, $result); + } +} diff --git a/tests/unit/Service/Social/XingProviderTest.php b/tests/unit/Service/Social/XingProviderTest.php new file mode 100644 index 000000000..cb417346d --- /dev/null +++ b/tests/unit/Service/Social/XingProviderTest.php @@ -0,0 +1,156 @@ + + * + * @author Matthias Heinisch + * + * @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\Contacts\Service\Social; + +use OCP\Http\Client\IClient; +use OCP\Http\Client\IResponse; +use OCP\Http\Client\IClientService; +use ChristophWurst\Nextcloud\Testing\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class XingProviderTest extends TestCase { + private $provider; + + /** @var IClientService|MockObject */ + private $clientService; + + /** @var IClient|MockObject */ + private $client; + + /** @var IResponse|MockObject */ + private $response; + + protected function setUp(): void { + parent::setUp(); + $this->clientService = $this->createMock(IClientService::class); + $this->response = $this->createMock(IResponse::class); + $this->client = $this->createMock(IClient::class); + + $this->clientService + ->method('NewClient') + ->willReturn($this->client); + + $this->provider = new XingProvider( + $this->clientService + ); + } + + public function dataProviderSupportsContact() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "xing"], + ["value" => "username2", "type" => "xing"] + ] + ]; + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + + return [ + 'contact with xing fields' => [$contactWithSocial, true], + 'contact without xing fields' => [$contactWithoutSocial, false] + ]; + } + + /** + * @dataProvider dataProviderSupportsContact + */ + public function testSupportsContact($contact, $expected) { + $result = $this->provider->supportsContact($contact); + $this->assertEquals($expected, $result); + } + + public function dataProviderGetImageUrls() { + $contactWithSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "username1", "type" => "xing"], + ["value" => "username2", "type" => "xing"] + ] + ]; + $contactWithSocialUrls = [ + "https://www.xing.com/profile/username1", + "https://www.xing.com/profile/username2" + ]; + $contactWithSocialHtml = array_map(function ($profile) { + return ''; + }, $contactWithSocial['X-SOCIALPROFILE']); + $contactWithSocialImg = array_map(function ($profile) { + return 'https://profile-images-abc'.$profile['value'].".jpg"; + }, $contactWithSocial['X-SOCIALPROFILE']); + + $contactWithoutSocial = [ + 'X-SOCIALPROFILE' => [ + ["value" => "one", "type" => "social2"], + ["value" => "two", "type" => "social1"] + ] + ]; + $contactWithoutSocialUrls = []; + $contactWithoutSocialHtml = []; + $contactWithoutSocialImg = []; + + return [ + 'contact with xing fields' => [ + $contactWithSocial, + $contactWithSocialUrls, + $contactWithSocialHtml, + $contactWithSocialImg + ], + 'contact without xing fields' => [ + $contactWithoutSocial, + $contactWithoutSocialUrls, + $contactWithoutSocialHtml, + $contactWithoutSocialImg + ] + ]; + } + + /** + * @dataProvider dataProviderGetImageUrls + */ + public function testGetImageUrls($contact, $urls, $htmls, $imgs) { + if (count($urls)) { + $this->response + ->method('getBody') + ->willReturnOnConsecutiveCalls(...$htmls); + + $urlArgs = array_map(function ($url) { + return [$url]; + }, $urls); + + $this->client + ->expects($this->exactly(count($urls))) + ->method('get') + ->withConsecutive(...$urlArgs) + ->willReturn($this->response); + } + + $result = $this->provider->getImageUrls($contact); + $this->assertEquals($imgs, $result); + } +} diff --git a/tests/unit/Service/SocialApiServiceTest.php b/tests/unit/Service/SocialApiServiceTest.php index 327bf1610..e7f4c8e3f 100644 --- a/tests/unit/Service/SocialApiServiceTest.php +++ b/tests/unit/Service/SocialApiServiceTest.php @@ -25,6 +25,7 @@ namespace OCA\Contacts\Service; use OCA\Contacts\Service\Social\CompositeSocialProvider; +use OCA\Contacts\Service\Social\ISocialProvider; use OCP\AppFramework\Http; use OCP\Http\Client\IClient; @@ -45,7 +46,7 @@ class SocialApiServiceTest extends TestCase { private $service; - /** @var CompositeSocialProvider */ + /** @var CompositeSocialProvider|MockObject */ private $socialProvider; /** @var IManager|MockObject */ private $manager; @@ -53,22 +54,56 @@ class SocialApiServiceTest extends TestCase { private $config; /** @var IClientService|MockObject */ private $clientService; - /** @var IL10N|MockObject */ + /** @var IL10N|MockObject */ private $l10n; - /** @var IURLGenerator|MockObject */ + /** @var IURLGenerator|MockObject */ private $urlGen; /** @var CardDavBackend|MockObject */ private $davBackend; /** @var ITimeFactory|MockObject */ private $timeFactory; - public function socialProfileProvider() { + public function allSocialProfileProviders() { + $body = "the body"; + $imageType = "jpg"; + $contact = [ + 'URI' => '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', + 'VERSION' => '4.0' + ]; + $connector = $this->createMock(ISocialProvider::class); + $connector->method('supportsContact')->willReturn(true); + $connector->method('getImageUrls')->willReturn(["url1"]); + + $connectorNoSupport = $this->createMock(ISocialProvider::class); + $connectorNoSupport->method('supportsContact')->willReturn(false); + + $connectorNoUrl = $this->createMock(ISocialProvider::class); + $connectorNoUrl->method('supportsContact')->willReturn(true); + $connectorNoUrl->method('getImageUrls')->willReturn([]); + + $addressbookEmpty = $this->createMock(IAddressBook::class); + $addressbookEmpty + ->method('getUri') + ->willReturn('contacts'); + $addressbookEmpty + ->method('search') + ->willReturn(null); + + $addressbook = $this->createMock(IAddressBook::class); + $addressbook + ->method('getUri') + ->willReturn('contacts'); + $addressbook + ->method('search') + ->willReturn([$contact]); + return [ - 'no social profiles set' => [null, 'someConnector', 'someResult', Http::STATUS_PRECONDITION_FAILED], - 'valid social profile' => [[['type' => 'someNetwork', 'value' => 'someId']], 'someConnector', 'someResult', Http::STATUS_OK], - 'bad formatted profile id' => [[['type' => 'someNetwork', 'value' => 'someId']], null, 'someResult', Http::STATUS_BAD_REQUEST], - 'not existing profile id' => [[['type' => 'someNetwork', 'value' => 'someId']], 'someConnector', '', Http::STATUS_NOT_FOUND], - 'unchanged data' => [[['type' => 'someNetwork', 'value' => 'someId']], 'someConnector', 'thePhoto', Http::STATUS_NOT_MODIFIED], + 'no address book found' => [null, [], "", "", Http::STATUS_BAD_REQUEST], + 'no contact found' => [[$addressbookEmpty], [], "", "", Http::STATUS_PRECONDITION_FAILED], + 'no supporting contacts found' => [[$addressbook], [$connectorNoSupport], "", "", Http::STATUS_PRECONDITION_FAILED], + 'no url found' => [[$addressbook], [$connectorNoUrl], "", "", Http::STATUS_BAD_REQUEST], + 'no image found' => [[$addressbook], [$connector], "", "", Http::STATUS_NOT_FOUND], + 'image found' => [[$addressbook], [$connector], $body, $imageType, Http::STATUS_OK] ]; } @@ -114,15 +149,54 @@ public function testDeactivatedSocial() { } /** - * @dataProvider socialProfileProvider + * @dataProvider allSocialProfileProviders */ - public function testUpdateContact($social, $connector, $httpResult, $expected) { + public function testUpdateContactWithoutNetwork($addressbooks, $providers, $body, $imageType, $status) { + $this->manager + ->method('getUserAddressBooks') + ->willReturn($addressbooks); + + $this->socialProvider + ->method('getSocialConnectors') + ->willReturn($providers); + + $response = $this->createMock(IResponse::class); + $response + ->method('getBody') + ->willReturn($body); + $response + ->method('getHeader') + ->willReturn($imageType); + $client = $this->createMock(IClient::class); + $client + ->method('get') + ->willReturn($response); + $this->clientService + ->method('NewClient') + ->willReturn($client); + + $result = $this->service + ->updateContact( + 'contacts', + '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', + null); + $this->assertEquals($status, $result->getStatus()); + } + + public function testUpdateContactWithNetwork() { + $network = "mastodon"; + $body = "the body"; + $imageType = "jpg"; + $addressBookId = "contacts"; + $contactId = "3225c0d5-1bd2-43e5-a08c-4e65eaa406b0"; $contact = [ - 'URI' => '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', - 'VERSION' => '4.0', - 'PHOTO' => "data:" . $httpResult . ";base64," . base64_encode('thePhoto'), - 'X-SOCIALPROFILE' => $social, + 'URI' => $contactId, + 'VERSION' => '4.0' ]; + $provider = $this->createMock(ISocialProvider::class); + $provider->method('supportsContact')->willReturn(true); + $provider->method('getImageUrls')->willReturn(["url1"]); + $addressbook = $this->createMock(IAddressBook::class); $addressbook ->method('getUri') @@ -135,17 +209,21 @@ public function testUpdateContact($social, $connector, $httpResult, $expected) { ->method('getUserAddressBooks') ->willReturn([$addressbook]); + $this->socialProvider + ->method('getSocialConnectors') + ->willReturn([$provider]); + $this->socialProvider ->method('getSocialConnector') - ->willReturn($connector); + ->willReturn($provider); $response = $this->createMock(IResponse::class); $response ->method('getBody') - ->willReturn($httpResult); + ->willReturn($body); $response ->method('getHeader') - ->willReturn($httpResult); + ->willReturn($imageType); $client = $this->createMock(IClient::class); $client ->method('get') @@ -154,9 +232,24 @@ public function testUpdateContact($social, $connector, $httpResult, $expected) { ->method('NewClient') ->willReturn($client); - $result = $this->service->updateContact('contacts', '3225c0d5-1bd2-43e5-a08c-4e65eaa406b0', 'theSocialNetwork'); + $changes = [ + 'URI' => $contact['URI'], + 'VERSION' => $contact['VERSION'], + 'PHOTO' => "data:".$imageType.";base64," . base64_encode($body) + ]; + + $this->socialProvider + ->expects($this->once())->method("getSocialConnector")->with($network); + $provider->expects($this->once())->method("supportsContact")->with($contact); + $addressbook->expects($this->once())->method("createOrUpdate")->with($changes, $addressBookId); - $this->assertEquals($expected, $result->getStatus()); + $result = $this->service + ->updateContact( + $addressBookId, + $contactId, + $network); + + $this->assertEquals(Http::STATUS_OK, $result->getStatus()); } protected function setupAddressbooks() { @@ -219,15 +312,33 @@ protected function setupAddressbooks() { ->method('getUserAddressBooks') ->willReturn([$addressbook1, $addressbook2]); - $socialConnectorMap = [ - [$validContact1['X-SOCIALPROFILE'], 'any', 'validConnector'], - [$validContact2['X-SOCIALPROFILE'], 'any', 'validConnector'], - [$invalidContact['X-SOCIALPROFILE'], 'any', 'invalidConnector'], - [$emptyContact['X-SOCIALPROFILE'], 'any', 'emptyConnector'], + $providerSupportsMap = [ + [$validContact1, true], + [$emptyContact, false], + [$invalidContact, false], + [$validContact2, true] ]; + + $providerUrlMap = [ + [$validContact1, ["url1"]], + [$emptyContact, []], + [$invalidContact, []], + [$validContact2, ["url1"]] + ]; + + $provider = $this->createMock(ISocialProvider::class); + $provider->method('getImageUrls') + ->will($this->returnValueMap($providerUrlMap)); + $provider->method('supportsContact') + ->will($this->returnValueMap($providerSupportsMap)); + + $this->socialProvider + ->method('getSocialConnectors') + ->willReturn([$provider]); + $this->socialProvider ->method('getSocialConnector') - ->will($this->returnValueMap($socialConnectorMap)); + ->willReturn($provider); $validResponse = $this->createMock(IResponse::class); $validResponse @@ -236,29 +347,16 @@ protected function setupAddressbooks() { $validResponse ->method('getHeader') ->willReturn('someHeader'); - $invalidResponse = $this->createMock(IResponse::class); - $invalidResponse - ->method('getBody') - ->willReturn(''); - $invalidResponse - ->method('getHeader') - ->willReturn(''); - $clientResponseMap = [ - ['validConnector', [], $validResponse], - ['invalidConnector', [], $invalidResponse], - ['emptyConnector', [], $invalidResponse], - ]; $client = $this->createMock(IClient::class); $client ->method('get') - ->will($this->returnValueMap($clientResponseMap)); + ->willReturn($validResponse); $this->clientService ->method('NewClient') ->willReturn($client); } - /** * @dataProvider updateAddressbookProvider */ @@ -287,9 +385,9 @@ public function testUpdateAddressbooks($syncAllowedByAdmin, $bgSyncEnabledByUser $this->assertArrayHasKey('checked', $report[0]); $this->assertContains('Valid Contact Two', $report[0]['checked']); $this->assertArrayHasKey('failed', $report[0]); - $this->assertArrayHasKey('404', $report[0]['failed']); - $this->assertContains('Invalid Contact', $report[0]['failed']['404']); - $this->assertNotContains('Empty Contact', $report[0]['failed']['404']); + $this->assertArrayHasKey('412', $report[0]['failed']); + $this->assertContains('Invalid Contact', $report[0]['failed']['412']); + $this->assertContains('Empty Contact', $report[0]['failed']['412']); } }