431 lines
13 KiB
PHP
431 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* @copyright Copyright (c) 2020 Matthias Heinisch <nextcloud@matthiasheinisch.de>
|
|
*
|
|
* @author Matthias Heinisch <nextcloud@matthiasheinisch.de>
|
|
*
|
|
* @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 <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
namespace OCA\Contacts\Service;
|
|
|
|
use OCA\Contacts\AppInfo\Application;
|
|
use OCA\Contacts\Service\Social\CompositeSocialProvider;
|
|
|
|
use OCA\DAV\CardDAV\CardDavBackend;
|
|
use OCA\DAV\CardDAV\ContactsManager;
|
|
|
|
use OCP\AppFramework\Http;
|
|
use OCP\AppFramework\Http\JSONResponse;
|
|
use OCP\AppFramework\Utility\ITimeFactory;
|
|
use OCP\Contacts\IManager;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\IAddressBook;
|
|
use OCP\IConfig;
|
|
use OCP\IL10N;
|
|
use OCP\IURLGenerator;
|
|
use OCP\Util;
|
|
|
|
class SocialApiService {
|
|
private $appName;
|
|
/** @var CompositeSocialProvider */
|
|
private $socialProvider;
|
|
/** @var IManager */
|
|
private $manager;
|
|
/** @var IConfig */
|
|
private $config;
|
|
/** @var IClientService */
|
|
private $clientService;
|
|
/** @var IL10N */
|
|
private $l10n;
|
|
/** @var IURLGenerator */
|
|
private $urlGen;
|
|
/** @var CardDavBackend */
|
|
private $davBackend;
|
|
/** @var ITimeFactory */
|
|
private $timeFactory;
|
|
|
|
|
|
public function __construct(
|
|
CompositeSocialProvider $socialProvider,
|
|
IManager $manager,
|
|
IConfig $config,
|
|
IClientService $clientService,
|
|
IL10N $l10n,
|
|
IURLGenerator $urlGen,
|
|
CardDavBackend $davBackend,
|
|
ITimeFactory $timeFactory) {
|
|
$this->appName = Application::APP_ID;
|
|
$this->socialProvider = $socialProvider;
|
|
$this->manager = $manager;
|
|
$this->config = $config;
|
|
$this->clientService = $clientService;
|
|
$this->l10n = $l10n;
|
|
$this->urlGen = $urlGen;
|
|
$this->davBackend = $davBackend;
|
|
$this->timeFactory = $timeFactory;
|
|
}
|
|
|
|
|
|
/**
|
|
* returns an array of supported social networks
|
|
*
|
|
* @return {array} array of the supported social networks
|
|
*/
|
|
public function getSupportedNetworks() : array {
|
|
$syncAllowedByAdmin = $this->config->getAppValue($this->appName, 'allowSocialSync', 'yes');
|
|
if ($syncAllowedByAdmin !== 'yes') {
|
|
return [];
|
|
}
|
|
return $this->socialProvider->getSupportedNetworks();
|
|
}
|
|
|
|
|
|
/**
|
|
* Adds/updates photo for contact
|
|
*
|
|
* @param {pointer} contact reference to the contact to update
|
|
* @param {string} imageType the image type of the photo
|
|
* @param {string} photo the photo as base64 string
|
|
*/
|
|
protected function addPhoto(array &$contact, string $imageType, string $photo) {
|
|
$version = $contact['VERSION'];
|
|
|
|
if (!empty($contact['PHOTO'])) {
|
|
// overwriting without notice!
|
|
}
|
|
|
|
if ($version >= 4.0) {
|
|
// overwrite photo
|
|
$contact['PHOTO'] = "data:" . $imageType . ";base64," . $photo;
|
|
} elseif ($version >= 3.0) {
|
|
// add new photo
|
|
$imageType = str_replace('image/', '', $imageType);
|
|
$contact['PHOTO;ENCODING=b;TYPE=' . $imageType . ';VALUE=BINARY'] = $photo;
|
|
|
|
// remove previous photo (necessary as new attribute is not equal to 'PHOTO')
|
|
unset($contact['PHOTO']);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the addressbook of an addressbookId
|
|
*
|
|
* @param {String} addressbookId the identifier of the addressbook
|
|
* @param {IManager} manager optional a ContactManager to use
|
|
*
|
|
* @returns {IAddressBook} the corresponding addressbook or null
|
|
*/
|
|
protected function getAddressBook(string $addressbookId, IManager $manager = null) : ?IAddressBook {
|
|
$addressBook = null;
|
|
if ($manager === null) {
|
|
$manager = $this->manager;
|
|
}
|
|
$addressBooks = $manager->getUserAddressBooks();
|
|
foreach ($addressBooks as $ab) {
|
|
if ($ab->getUri() === $addressbookId) {
|
|
$addressBook = $ab;
|
|
}
|
|
}
|
|
return $addressBook;
|
|
}
|
|
|
|
|
|
/**
|
|
* Retrieves and initiates all addressbooks from a user
|
|
*
|
|
* @param {string} userId the user to query
|
|
* @param {IManager} the contact manager to load
|
|
*/
|
|
protected function registerAddressbooks($userId, IManager $manager) {
|
|
$coma = new ContactsManager($this->davBackend, $this->l10n);
|
|
$coma->setupContactsProvider($manager, $userId, $this->urlGen);
|
|
$this->manager = $manager;
|
|
}
|
|
|
|
/**
|
|
* Retrieves social profile data for a contact and updates the entry
|
|
*
|
|
* @param {String} addressbookId the addressbook identifier
|
|
* @param {String} contactId the contact identifier
|
|
* @param {String} network the social network to use (if unkown: take first match)
|
|
*
|
|
* @returns {JSONResponse} an empty JSONResponse with respective http status code
|
|
*/
|
|
public function updateContact(string $addressbookId, string $contactId, ?string $network) : JSONResponse {
|
|
$socialdata = null;
|
|
$imageType = null;
|
|
$urls = [];
|
|
$allConnectors = $this->socialProvider->getSocialConnectors();
|
|
|
|
try {
|
|
// get corresponding addressbook
|
|
$addressBook = $this->getAddressBook(urldecode($addressbookId));
|
|
if (is_null($addressBook)) {
|
|
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
// search contact in that addressbook, get social data
|
|
$contact = $addressBook->search($contactId, ['UID'], ['types' => true])[0];
|
|
|
|
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);
|
|
}
|
|
|
|
foreach ($connectors as $connector) {
|
|
$urls = array_merge($connector->getImageUrls($contact), $urls);
|
|
}
|
|
|
|
if (count($urls) == 0) {
|
|
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// update contact
|
|
$changes = [];
|
|
$changes['URI'] = $contact['URI'];
|
|
$changes['VERSION'] = $contact['VERSION'];
|
|
$this->addPhoto($changes, $imageType, base64_encode($socialdata));
|
|
|
|
if (isset($contact['PHOTO']) && $changes['PHOTO'] === $contact['PHOTO']) {
|
|
return new JSONResponse([], Http::STATUS_NOT_MODIFIED);
|
|
}
|
|
|
|
$addressBook->createOrUpdate($changes, $addressbookId);
|
|
} catch (\Exception $e) {
|
|
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
|
|
}
|
|
return new JSONResponse([], Http::STATUS_OK);
|
|
}
|
|
|
|
/**
|
|
* checks an addressbook is existing
|
|
*
|
|
* @param {string} searchBookId the UID of the addressbook to verify
|
|
* @param {string} userId the user that should have access
|
|
*
|
|
* @returns {bool} true if the addressbook exists
|
|
*/
|
|
public function existsAddressBook(string $searchBookId, string $userId): bool {
|
|
$manager = $this->manager;
|
|
$coma = new ContactsManager($this->davBackend, $this->l10n);
|
|
$coma->setupContactsProvider($manager, $userId, $this->urlGen);
|
|
$addressBooks = $manager->getUserAddressBooks();
|
|
return $this->getAddressBook($searchBookId, $manager) !== null;
|
|
}
|
|
|
|
/**
|
|
* checks a contact exists in an addressbook
|
|
*
|
|
* @param string searchContactId the UID of the contact to verify
|
|
* @param string searchBookId the UID of the addressbook to look in
|
|
* @param string userId the user that should have access
|
|
*
|
|
* @returns bool true if the contact exists
|
|
*/
|
|
public function existsContact(string $searchContactId, string $searchBookId, string $userId): bool {
|
|
// load address books for the user
|
|
$manager = $this->manager;
|
|
$coma = new ContactsManager($this->davBackend, $this->l10n);
|
|
$coma->setupContactsProvider($manager, $userId, $this->urlGen);
|
|
$addressBook = $this->getAddressBook($searchBookId, $manager);
|
|
if ($addressBook == null) {
|
|
return false;
|
|
}
|
|
|
|
$check = $addressBook->search($searchContactId, ['UID'], ['types' => true]);
|
|
return !empty($check);
|
|
}
|
|
|
|
/**
|
|
* Stores the result of social avatar updates for each contact
|
|
* (used during batch updates in updateAddressbooks)
|
|
*
|
|
* @param {array} report where the results are added
|
|
* @param {String} entry the element to add
|
|
* @param {string} status the (http) status code
|
|
*
|
|
* @returns {array} the report including the new entry
|
|
*/
|
|
protected function registerUpdateResult(array $report, string $entry, string $status) : array {
|
|
// initialize report on first call
|
|
if (empty($report)) {
|
|
$report = [
|
|
'updated' => [],
|
|
'checked' => [],
|
|
'failed' => [],
|
|
];
|
|
}
|
|
// add entry to respective sub-array
|
|
switch ($status) {
|
|
case Http::STATUS_OK:
|
|
array_push($report['updated'], $entry);
|
|
break;
|
|
case Http::STATUS_NOT_MODIFIED:
|
|
array_push($report['checked'], $entry);
|
|
break;
|
|
default:
|
|
if (!isset($report['failed'][$status])) {
|
|
$report['failed'][$status] = [];
|
|
}
|
|
array_push($report['failed'][$status], $entry);
|
|
}
|
|
return $report;
|
|
}
|
|
|
|
/**
|
|
* sorts an array of address books
|
|
*
|
|
* @param {IAddressBook} a
|
|
* @param {IAddressBook} b
|
|
*
|
|
* @returns {bool} comparison by URI
|
|
*/
|
|
protected function sortAddressBooks(IAddressBook $a, IAddressBook $b) {
|
|
return strcmp($a->getURI(), $b->getURI());
|
|
}
|
|
|
|
/**
|
|
* sorts an array of contacts
|
|
*
|
|
* @param {array} a
|
|
* @param {array} b
|
|
*
|
|
* @returns {bool} comparison by UID
|
|
*/
|
|
protected function sortContacts(array $a, array $b) {
|
|
return strcmp($a['UID'], $b['UID']);
|
|
}
|
|
|
|
/**
|
|
* Updates social profile data for all contacts of an addressbook
|
|
*
|
|
* @param {String} network the social network to use (fallback: take first match)
|
|
* @param {String} userId the address book owner
|
|
*
|
|
* @returns {JSONResponse} JSONResponse with the list of changed and failed contacts
|
|
*/
|
|
public function updateAddressbooks(string $network, string $userId, string $offsetBook = null, string $offsetContact = null) : JSONResponse {
|
|
|
|
// double check!
|
|
$syncAllowedByAdmin = $this->config->getAppValue($this->appName, 'allowSocialSync', 'yes');
|
|
$bgSyncEnabledByUser = $this->config->getUserValue($userId, $this->appName, 'enableSocialSync', 'no');
|
|
if (($syncAllowedByAdmin !== 'yes') || ($bgSyncEnabledByUser !== 'yes')) {
|
|
return new JSONResponse([], Http::STATUS_FORBIDDEN);
|
|
}
|
|
|
|
$delay = 1;
|
|
$response = [];
|
|
$startTime = $this->timeFactory->getTime();
|
|
|
|
// get corresponding addressbook
|
|
$this->registerAddressbooks($userId, $this->manager);
|
|
$addressBooks = $this->manager->getUserAddressBooks();
|
|
usort($addressBooks, [$this, 'sortAddressBooks']); // make sure the order stays the same in consecutive calls
|
|
|
|
foreach ($addressBooks as $addressBook) {
|
|
if ((is_null($addressBook) ||
|
|
(Util::getVersion()[0] >= 20) &&
|
|
//TODO: remove version check ^ when dependency for contacts is min NCv20 (see info.xml)
|
|
($addressBook->isShared() || $addressBook->isSystemAddressBook()))) {
|
|
// TODO: filter out deactivated books, see https://github.com/nextcloud/server/issues/17537
|
|
continue;
|
|
}
|
|
|
|
// in case this is a follow-up, jump to the last stopped address book
|
|
if (!is_null($offsetBook)) {
|
|
if ($addressBook->getURI() !== $offsetBook) {
|
|
continue;
|
|
}
|
|
$offsetBook = null;
|
|
}
|
|
|
|
// get contacts in that addressbook
|
|
//TODO: activate this optimization when nextcloud/server#22085 is merged
|
|
/*
|
|
if (Util::getVersion()[0] < 21) {
|
|
//TODO: remove this branch when dependency for contacts is min NCv21 (see info.xml)
|
|
$contacts = $addressBook->search('', ['UID'], ['types' => true]);
|
|
} else {
|
|
$contacts = $addressBook->search('', ['X-SOCIALPROFILE'], ['types' => true]);
|
|
}
|
|
*/
|
|
$contacts = $addressBook->search('', ['UID'], ['types' => true]);
|
|
usort($contacts, [$this, 'sortContacts']); // make sure the order stays the same in consecutive calls
|
|
|
|
// update one contact after another
|
|
foreach ($contacts as $contact) {
|
|
// in case this is a follow-up, jump to the last stopped contact
|
|
if (!is_null($offsetContact)) {
|
|
if ($contact['UID'] !== $offsetContact) {
|
|
continue;
|
|
}
|
|
$offsetContact = null;
|
|
}
|
|
|
|
try {
|
|
$r = $this->updateContact($addressBook->getURI(), $contact['UID'], $network);
|
|
$response = $this->registerUpdateResult($response, $contact['FN'], $r->getStatus());
|
|
} catch (\Exception $e) {
|
|
$response = $this->registerUpdateResult($response, $contact['FN'], '-1');
|
|
}
|
|
|
|
// stop after 15sec (to be continued with next chunk)
|
|
if (($this->timeFactory->getTime() - $startTime) > 15) {
|
|
$response['stoppedAt'] = [
|
|
'addressBook' => $addressBook->getURI(),
|
|
'contact' => $contact['UID'],
|
|
];
|
|
return new JSONResponse([$response], Http::STATUS_PARTIAL_CONTENT);
|
|
}
|
|
|
|
// delay to prevent rate limiting issues
|
|
sleep($delay);
|
|
}
|
|
}
|
|
return new JSONResponse([$response], Http::STATUS_OK);
|
|
}
|
|
}
|