From 9c4460976dfa21201a2fc84e621403a6f3fa438c Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Sun, 22 Mar 2020 14:40:43 +0100 Subject: [PATCH 01/99] first steps --- app/Console/Commands/DavClientsUpdate.php | 33 ++ app/Console/Kernel.php | 2 + app/Services/VCard/Client.php | 366 ++++++++++++++++++++++ app/Services/VCard/ClientVCard.php | 188 +++++++++++ 4 files changed, 589 insertions(+) create mode 100644 app/Console/Commands/DavClientsUpdate.php create mode 100644 app/Services/VCard/Client.php create mode 100644 app/Services/VCard/ClientVCard.php diff --git a/app/Console/Commands/DavClientsUpdate.php b/app/Console/Commands/DavClientsUpdate.php new file mode 100644 index 00000000000..ada91134ff9 --- /dev/null +++ b/app/Console/Commands/DavClientsUpdate.php @@ -0,0 +1,33 @@ +execute([]); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index fda422dc369..c45a647b8ba 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -18,6 +18,7 @@ use App\Console\Commands\SendStayInTouch; use App\Console\Commands\SetupProduction; use App\Console\Commands\UpdateGravatars; +use App\Console\Commands\DavClientsUpdate; use App\Console\Commands\PingVersionServer; use App\Console\Commands\SetPremiumAccount; use Illuminate\Console\Scheduling\Schedule; @@ -38,6 +39,7 @@ class Kernel extends ConsoleKernel protected $commands = [ CalculateStatistics::class, Clean::class, + DavClientsUpdate::class, Deactivate2FA::class, ExportAll::class, GetVersion::class, diff --git a/app/Services/VCard/Client.php b/app/Services/VCard/Client.php new file mode 100644 index 00000000000..68ee31bafc8 --- /dev/null +++ b/app/Services/VCard/Client.php @@ -0,0 +1,366 @@ +client = is_null($client) ? new GuzzleClient([ + 'base_uri' => $settings['base_uri'], + 'auth' => [ + $settings['username'], + $settings['password'], + ], + ]) : $client; + + $this->xml = new Service(); + } + + /** + * Follow rfc6764 to get carddav service url + * + * @see https://tools.ietf.org/html/rfc6764 + */ + public function getServiceUrl() + { + $baseUri = $this->client->getConfig('base_uri')->withPath('/.well-known/carddav'); + + $response = $this->client->get($baseUri, ['allow_redirects' => false]); + + $code = $response->getStatusCode(); // 200 + + if (($code === 301 || $code === 302) && $response->hasHeader('Location')) { + return $response->getHeader('Location')[0]; + } + } + + public function setBaseUri($uri) + { + $this->client = new GuzzleClient( + Arr::except($this->client->getConfig(), ['base_uri']) + + + ['base_uri' => $uri] + ); + } + + + /** + * Does a PROPFIND request. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * The returned array will contain a list of filenames as keys, and + * properties as values. + * + * The properties array will contain the list of properties. Only properties + * that are actually returned from the server (without error) will be + * returned, anything else is discarded. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * @param string $url + * @param array $properties + * @param int $depth + * + * @return array + */ + public function propFind(string $url, array $properties, int $depth = 0) : array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->createElementNS('DAV:', 'd:propfind'); + $prop = $dom->createElement('d:prop'); + + foreach ($properties as $property) { + list( + $namespace, + $elementName + ) = \Sabre\Xml\Service::parseClarkNotation($property); + + if ('DAV:' === $namespace) { + $element = $dom->createElement('d:'.$elementName); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + + $dom->appendChild($root)->appendChild($prop); + $body = $dom->saveXML(); + + $request = new Request('PROPFIND', $url, [ + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + ], $body); + + $response = $this->client->send($request); + + /* + if ($response->getStatusCode() >= 400) { + throw new ClientException($response); + } + */ + + $result = $this->parseMultiStatus((string) $response->getBody()); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + + public function getProperty(string $property, string $url = '') + { + $propfind = $this->propfind($url, [ + $property, + ]); + + if (!isset($propfind[$property])) { + return null; + } + + return $propfind[$property][0]['value']; + } + + /** + * Updates a list of properties on the server. + * + * The list of properties must have clark-notation properties for the keys, + * and the actual (string) value for the value. If the value is null, an + * attempt is made to delete the property. + * + * @param string $url + * @param array $properties + * + * @return bool + */ + public function propPatch(string $url, array $properties) : bool + { + $propPatch = new PropPatch(); + $propPatch->properties = $properties; + $xml = $this->xml->write( + '{DAV:}propertyupdate', + $propPatch + ); + + //$url = $this->getAbsoluteUrl($url); + $request = new Request('PROPPATCH', $url, [ + 'Content-Type' => 'application/xml', + ], $xml); + $response = $this->client->send($request); + + /* + if ($response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + */ + + if (207 === $response->getStatusCode()) { + // If it's a 207, the request could still have failed, but the + // information is hidden in the response body. + $result = $this->parseMultiStatus($response->getBody()); + + $errorProperties = []; + foreach ($result as $href => $statusList) { + foreach ($statusList as $status => $properties) { + if ($status >= 400) { + foreach ($properties as $propName => $propValue) { + $errorProperties[] = $propName.' ('.$status.')'; + } + } + } + } + if ($errorProperties) { + throw new ClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties), $request); + } + } + + return true; + } + + /** + * Performs an HTTP options request. + * + * This method returns all the features from the 'DAV:' header as an array. + * If there was no DAV header, or no contents this method will return an + * empty array. + * + * @return array + */ + public function options() : array + { + $request = new Request('OPTIONS', ''/*, $this->getAbsoluteUrl('')*/); + $response = $this->client->send($request); + + $dav = $response->getHeader('Dav'); + if (!$dav) { + return []; + } + + foreach ($dav as &$v) { + $v = trim($v); + } + + return $dav; + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * If the specified url is relative, it will be expanded based on the base + * url. + * + * The returned array contains 3 keys: + * * body - the response body + * * httpCode - a HTTP code (200, 404, etc) + * * headers - a list of response http headers. The header names have + * been lowercased. + * + * For large uploads, it's highly recommended to specify body as a stream + * resource. You can easily do this by simply passing the result of + * fopen(..., 'r'). + * + * This method will throw an exception if an HTTP error was received. Any + * HTTP status code above 399 is considered an error. + * + * Note that it is no longer recommended to use this method, use the send() + * method instead. + * + * @param string $method + * @param string $url + * @param string|resource|null $body + * @param array $headers + * + * @throws ClientException, in case a curl error occurred + * + * @return array + */ + public function request(string $method, string $url = '', ?string $body = null, array $headers = []) : array + { + //$url = $this->getAbsoluteUrl($url); + + $response = $this->client->send(new Request($method, $url, $headers, $body)); + + return [ + 'body' => $response->getBody(), + 'statusCode' => $response->getStatusCode(), + 'headers' => array_change_key_case($response->getHeaders()), + ]; + } + + /** + * Parses a WebDAV multistatus response body. + * + * This method returns an array with the following structure + * + * [ + * 'url/to/resource' => [ + * '200' => [ + * '{DAV:}property1' => 'value1', + * '{DAV:}property2' => 'value2', + * ], + * '404' => [ + * '{DAV:}property1' => null, + * '{DAV:}property2' => null, + * ], + * ], + * 'url/to/resource2' => [ + * .. etc .. + * ] + * ] + * + * + * @param string $body xml body + * + * @return array + */ + public function parseMultiStatus(string $body) : array + { + $multistatus = $this->xml->expect('{DAV:}multistatus', $body); + + $result = []; + + foreach ($multistatus->getResponses() as $response) { + $result[$response->getHref()] = $response->getResponseProperties(); + } + + return $result; + } +} diff --git a/app/Services/VCard/ClientVCard.php b/app/Services/VCard/ClientVCard.php new file mode 100644 index 00000000000..40436ff29ac --- /dev/null +++ b/app/Services/VCard/ClientVCard.php @@ -0,0 +1,188 @@ +getAddressBook($data, $httpClient); + + $client = $this->getClient($httpClient); + + } + + private function getAddressBook(array $data, GuzzleClient $httpClient = null) + { + $client = $this->getClient($httpClient); + + try { + + $baseUri = $client->getServiceUrl(); + $client->setBaseUri($baseUri); + + $this->checkOptions($client); + + + // /dav/principals/admin@admin.com/ + $principal = $this->getCurrentUserPrincipal($client); + + $addressbook = $this->getAddressBookUrl($client, $principal); + + } catch (ClientException $e) { + $r = $e->getResponse(); + $s = (string) $r->getBody(); + } catch (\Exception $e) { + } + + } + + private function checkOptions(Client $client) + { + $options = $client->options(); + $options = explode(', ', $options[0]); + + // https://tools.ietf.org/html/rfc2518#section-15 + if (!in_array('1', $options) || !in_array('3', $options) || !in_array('addressbook', $options)) { + throw new \Exception('server is not compliant with rfc2518 section 15.1, or rfc6352 section 6.1'); + } + } + + /** + * @see https://tools.ietf.org/html/rfc5397#section-3 + */ + private function getCurrentUserPrincipal(Client $client) : string + { + $prop = $client->getProperty('{DAV:}current-user-principal'); + + if (is_null($prop)) { + throw new \Exception('Server does not support rfc 5397 section 3 (DAV:current-user-principal)'); + } + + return $prop; + } + + /** + * @see https://tools.ietf.org/html/rfc6352#section-7.1.1 + */ + private function getAddressBookHome(Client $client, string $principal) : string + { + $prop = $client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-home-set', $principal); + + if (is_null($prop)) { + throw new \Exception('Server does not support rfc 6352 section 7.1.1 (CARD:addressbook-home-set)'); + } + + return $prop; + } + + private function getAddressBookUrl(Client $client, string $principal) : string + { + $home = $this->getAddressBookHome($client, $principal); + + $books = $client->propfind($home, [], 1); + + foreach ($books as $book => $properties) { + if ($book == $home) { + continue; + } + + if ($resources = Arr::get($properties, '{DAV:}resourcetype', null)) { + if ($resources->is('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook')) { + return $book; + } + } + } + } + + private function getClient(GuzzleClient $client = null) : Client + { + $settings = [ + 'base_uri' => 'http://monica.test/dav/', + 'username' => 'admin@admin.com', + 'password' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZjUzYmRkYzJhNzQ0ZTgxZDRiYmUwMTQ2ODY1YTA3ZTIyMWM0ZTQ5YzkyNzJkN2FhZmE1ODk5ZDRmM2NkZGRlMWQyMWQ5MmM5M2E4YTNkMGMiLCJpYXQiOjE1ODQ4MDkxNDQsIm5iZiI6MTU4NDgwOTE0NCwiZXhwIjoxNjE2MzQ1MTQ0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.MUtcgy3PWk9McA59zx4SBJxKAkdSiGv1a9ZtVKwlgtk09bJEx1lJgymGSfDlrNqKvD2LqhTu0y95jLZNoj4-uM6DBZm3RMo18mw2xCEywB4st1hZpMYSYoOmtrOcsZweoP5r31zf_jzMX3mLde6MAeEkJcotGfO9z57M74FquLKixZRLvVruES2DcZoL1hwCKoxvv11BGRE78RQsWiipv0cfgmcSNEQVR820BWkM0X_4WwpufJdzZ5p1EpTy5AP2XXlx6amGXqxgMUIY7C-KyF1uw1Rmr6B-bTcMLJHZBH6TzU0yFoaJnhZZ9tJFyf7E70BL8SaO9_P6nA7ACjDREjAJBD9dZYrP46G-mqJXjWyVOcDVJZNW7dhF5vnEp7gghIVWhAm4lLy5nPI_CNpB0mqPrdkj57Avoi3MAEwf4ADy9CZp1EoLZIvNjBuMpgwwONTF5oP18NMaHJcsbFkmviY7eW-DIuIcNtCuoAM7Q4ulhuVX4tVry5NLsiab0_W8_l63C_n1-ICpv2t04jSh9H3SwgIXAZXhe-0vMt0gTIc3c_1HZ4eRd1kOuUs-708Esiq7J_Nt98PJZB8AP6qbeuScI0Cxnm7IulJ1WaI7mLjA7JPDvISeL2rrYjwqmguDbA8nQ7UjEq1dLN-PaAaL08p_iKU3ssPV2YziNSi_Alc', + ]; + + return new Client($settings, $client); + } + + public function propFind(GuzzleClient $client, $url, array $properties, $depth = 0) + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->createElementNS('DAV:', 'd:propfind'); + $prop = $dom->createElement('d:prop'); + + foreach ($properties as $property) { + list( + $namespace, + $elementName + ) = \Sabre\Xml\Service::parseClarkNotation($property); + + if ('DAV:' === $namespace) { + $element = $dom->createElement('d:'.$elementName); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + + $dom->appendChild($root)->appendChild($prop); + $body = $dom->saveXML(); + + $url = $this->getAbsoluteUrl($url); + + $request = new HTTP\Request('PROPFIND', $url, [ + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + ], $body); + + $response = $this->send($request); + + if ((int) $response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + $result = $this->parseMultiStatus($response->getBodyAsString()); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + +} From 27226494679409d640700e511997e38dfa64c860 Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Mon, 23 Mar 2020 12:46:04 +0100 Subject: [PATCH 02/99] update --- app/Services/VCard/Client.php | 265 ++++++++++++++++++++--------- app/Services/VCard/ClientVCard.php | 89 +++++++++- 2 files changed, 273 insertions(+), 81 deletions(-) diff --git a/app/Services/VCard/Client.php b/app/Services/VCard/Client.php index 68ee31bafc8..29a6c02a490 100644 --- a/app/Services/VCard/Client.php +++ b/app/Services/VCard/Client.php @@ -4,31 +4,20 @@ use Sabre\DAV\Xml\Service; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use GuzzleHttp\Psr7\Request; +use Illuminate\Support\Collection; use Sabre\DAV\Xml\Request\PropPatch; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\ClientException; +use Sabre\CardDAV\Plugin as CardDAVPlugin; -/** - * SabreDAV DAV client. - * - * This client wraps around Curl to provide a convenient API to a WebDAV - * server. - * - * NOTE: This class is experimental, it's api will likely change in the future. - * - * @copyright Copyright (C) fruux GmbH (https://fruux.com/) - * @author Evert Pot (http://evertpot.com/) - * @license http://sabre.io/license/ Modified BSD License - */ class Client { /** * The xml service. * - * Uset this service to configure the property and namespace maps. - * - * @var mixed + * @var Service */ public $xml; @@ -39,24 +28,7 @@ class Client /** - * Constructor. - * - * Settings are provided through the 'settings' argument. The following - * settings are supported: - * - * * baseUri - * * userName (optional) - * * password (optional) - * * proxy (optional) - * * authType (optional) - * * encoding (optional) - * - * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST - * and self::AUTH_NTLM. If you know which authentication method will be - * used, it's recommended to set it, as it will save a great deal of - * requests to 'discover' this information. - * - * Encoding is a bitmap with one of the ENCODING constants. + * Create a new client. * * @param array $settings * @param GuzzleClient $client @@ -67,8 +39,6 @@ public function __construct(array $settings, GuzzleClient $client = null) throw new \InvalidArgumentException('A baseUri must be provided'); } - //parent::__construct(); - $this->client = is_null($client) ? new GuzzleClient([ 'base_uri' => $settings['base_uri'], 'auth' => [ @@ -87,15 +57,53 @@ public function __construct(array $settings, GuzzleClient $client = null) */ public function getServiceUrl() { - $baseUri = $this->client->getConfig('base_uri')->withPath('/.well-known/carddav'); - - $response = $this->client->get($baseUri, ['allow_redirects' => false]); + // Get well-known register (section 9.1) + $wkUri = $this->getBaseUri('/.well-known/carddav'); - $code = $response->getStatusCode(); // 200 + $response = $this->client->get($wkUri, ['allow_redirects' => false]); + $code = $response->getStatusCode(); if (($code === 301 || $code === 302) && $response->hasHeader('Location')) { return $response->getHeader('Location')[0]; } + + // Get service name register (section 9.2) + $target = $this->getServiceUrlSrv('_carddavs._tcp', true); + if (is_null($target)) { + $target = $this->getServiceUrlSrv('_carddav._tcp', false); + } + + return $target; + } + + /** + * Service Discovery via SRV Records + * + * @see https://tools.ietf.org/html/rfc6352#section-11 + */ + private function getServiceUrlSrv(string $name, bool $https): ?string + { + $host = parse_url($this->getBaseUri(), PHP_URL_HOST); + $entry = dns_get_record($name.'.'.$host, DNS_SRV); + + if ($entry && count($entry) > 0) { + $target = isset($entry[0]['target']) ? $entry[0]['target'] : null; + $port = isset($entry[0]['port']) ? $entry[0]['port'] : null; + if ($target) { + if ($port === 443 && $https) { + $port = null; + } else if ($port === 80 && !$https) { + $port = null; + } + return ($https ? 'https' : 'http') . '://' . $target . (is_null($port) ? '' : ':'.$port); + } + } + } + + public function getBaseUri(?string $path = null) + { + $baseUri = $this->client->getConfig('base_uri'); + return is_null($path) ? $baseUri : $baseUri->withPath($path); } public function setBaseUri($uri) @@ -134,44 +142,28 @@ public function propFind(string $url, array $properties, int $depth = 0) : array { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; - $root = $dom->createElementNS('DAV:', 'd:propfind'); - $prop = $dom->createElement('d:prop'); - - foreach ($properties as $property) { - list( - $namespace, - $elementName - ) = \Sabre\Xml\Service::parseClarkNotation($property); + $root = $dom->appendChild($dom->createElementNS('DAV:', 'd:propfind')); + $prop = $root->appendChild($dom->createElement('d:prop')); - if ('DAV:' === $namespace) { - $element = $dom->createElement('d:'.$elementName); - } else { - $element = $dom->createElementNS($namespace, 'x:'.$elementName); - } + $namespaces = [ + 'DAV:' => 'd' + ]; - $prop->appendChild($element); - } + $this->fetchProperties($dom, $prop, $properties, $namespaces); - $dom->appendChild($root)->appendChild($prop); $body = $dom->saveXML(); $request = new Request('PROPFIND', $url, [ 'Depth' => $depth, - 'Content-Type' => 'application/xml', + 'Content-Type' => 'application/xml; charset=utf-8', ], $body); $response = $this->client->send($request); - /* - if ($response->getStatusCode() >= 400) { - throw new ClientException($response); - } - */ - $result = $this->parseMultiStatus((string) $response->getBody()); // If depth was 0, we only return the top item - if (0 === $depth) { + if ($depth === 0) { reset($result); $result = current($result); @@ -186,6 +178,96 @@ public function propFind(string $url, array $properties, int $depth = 0) : array return $newResult; } + /** + * @see https://tools.ietf.org/html/rfc6578 + */ + public function syncCollection(string $url, string $syncToken, array $properties) : array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->appendChild($dom->createElementNS('DAV:', 'd:sync-collection')); + + $root->appendChild($dom->createElement('d:sync-token', $syncToken)); + $root->appendChild($dom->createElement('d:sync-level', '1')); + + $prop = $root->appendChild($dom->createElement('d:prop')); + + $namespaces = [ + 'DAV:' => 'd' + ]; + + $this->fetchProperties($dom, $prop, $properties, $namespaces); + + $body = $dom->saveXML(); + + $request = new Request('REPORT', $url, [ + 'Depth' => '0', + 'Content-Type' => 'application/xml; charset=utf-8', + ], $body); + + $response = $this->client->send($request); + + $result = $this->parseMultiStatus((string) $response->getBody()); + + return $result; + } + + /** + * + */ + public function addressbookMultiget(string $url, array $properties, Collection $contacts) : array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->appendChild($dom->createElementNS(CardDAVPlugin::NS_CARDDAV, 'card:addressbook-multiget')); + $dom->createAttributeNS('DAV:', 'd:e'); + + $prop = $root->appendChild($dom->createElement('d:prop')); + + $namespaces = [ + 'DAV:' => 'd', + CardDAVPlugin::NS_CARDDAV => 'card', + ]; + + $this->fetchProperties($dom, $prop, $properties, $namespaces); + + foreach ($contacts as $contact) { + $root->appendChild($dom->createElement('d:href', $contact)); + } + + $body = $dom->saveXML(); + + $request = new Request('REPORT', $url, [ + 'Depth' => '1', + 'Content-Type' => 'application/xml; charset=utf-8', + ], $body); + + $response = $this->client->send($request); + + $result = $this->parseMultiStatus((string) $response->getBody()); + + return $result; + } + + private function fetchProperties($dom, $prop, array $properties, array $namespaces) + { + foreach ($properties as $property) { + list($namespace, $elementName) = Service::parseClarkNotation($property); + + $value = Arr::get($namespaces, $namespace, null); + if (!is_null($value)) { + $element = $dom->createElement("$value:$elementName"); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + } + + /** + * @return string|null|mixed + */ public function getProperty(string $property, string $url = '') { $propfind = $this->propfind($url, [ @@ -196,7 +278,36 @@ public function getProperty(string $property, string $url = '') return null; } - return $propfind[$property][0]['value']; + $prop = $propfind[$property]; + + if (is_string($prop)) { + return $prop; + } else if (is_array($prop)) { + $value = $prop[0]; + if (is_string($value)) { + return $value; + } else if (is_array($value)) { + return Arr::get($value, 'value', $value); + } + } + + return $prop; + } + + public function getSupportedReportSet() + { + $propName = '{DAV:}supported-report-set'; + $supportedReportSets = $this->propFind('', [$propName])[$propName]; + + return array_map(function ($supportedReportSet) { + foreach ($supportedReportSet['value'] as $kind) { + if ($kind['name'] == '{DAV:}report') { + foreach($kind['value'] as $type) { + return $type['name']; + } + } + } + }, $supportedReportSets); } /** @@ -220,25 +331,18 @@ public function propPatch(string $url, array $properties) : bool $propPatch ); - //$url = $this->getAbsoluteUrl($url); $request = new Request('PROPPATCH', $url, [ - 'Content-Type' => 'application/xml', + 'Content-Type' => 'application/xml; charset=utf-8', ], $xml); $response = $this->client->send($request); - /* - if ($response->getStatus() >= 400) { - throw new HTTP\ClientHttpException($response); - } - */ - - if (207 === $response->getStatusCode()) { + if ($response->getStatusCode() === 207) { // If it's a 207, the request could still have failed, but the // information is hidden in the response body. - $result = $this->parseMultiStatus($response->getBody()); + $result = $this->parseMultiStatus((string) $response->getBody()); $errorProperties = []; - foreach ($result as $href => $statusList) { + foreach ($result as $statusList) { foreach ($statusList as $status => $properties) { if ($status >= 400) { foreach ($properties as $propName => $propValue) { @@ -266,7 +370,7 @@ public function propPatch(string $url, array $properties) : bool */ public function options() : array { - $request = new Request('OPTIONS', ''/*, $this->getAbsoluteUrl('')*/); + $request = new Request('OPTIONS', ''); $response = $this->client->send($request); $dav = $response->getHeader('Dav'); @@ -314,12 +418,10 @@ public function options() : array */ public function request(string $method, string $url = '', ?string $body = null, array $headers = []) : array { - //$url = $this->getAbsoluteUrl($url); - $response = $this->client->send(new Request($method, $url, $headers, $body)); return [ - 'body' => $response->getBody(), + 'body' => (string) $response->getBody(), 'statusCode' => $response->getStatusCode(), 'headers' => array_change_key_case($response->getHeaders()), ]; @@ -361,6 +463,11 @@ public function parseMultiStatus(string $body) : array $result[$response->getHref()] = $response->getResponseProperties(); } + $synctoken = $multistatus->getSyncToken(); + if (! empty($synctoken)) { + $result['synctoken'] = $synctoken; + } + return $result; } } diff --git a/app/Services/VCard/ClientVCard.php b/app/Services/VCard/ClientVCard.php index 40436ff29ac..cfdc7739c5a 100644 --- a/app/Services/VCard/ClientVCard.php +++ b/app/Services/VCard/ClientVCard.php @@ -3,6 +3,7 @@ namespace App\Services\VCard; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use App\Services\BaseService; use Sabre\VObject\Component\VCard; use GuzzleHttp\Client as GuzzleClient; @@ -31,11 +32,93 @@ public function execute(array $data, GuzzleClient $httpClient = null) { $addressbook = $this->getAddressBook($data, $httpClient); - $client = $this->getClient($httpClient); + try { + + $client = $this->getClient($httpClient); + + $client->setBaseUri($addressbook); + + $displayname = $client->getProperty('{DAV:}displayname'); + + + $supportedReportSet = $client->getSupportedReportSet(); + + // INITIAL SYNC + if (in_array('{DAV:}sync-collection', $supportedReportSet)) { + + // get ctag + + //$syncToken = $client->getProperty('{DAV:}sync-token'); + + // initial sync + $collection = $client->syncCollection('', '', [ + '{DAV:}getcontenttype', + '{DAV:}getetag' + ]); + + } else { + + // synchronisation + + $collection = $client->propFind('', [ + '{DAV:}getcontenttype', + '{DAV:}getetag', + ], 1); + } + + + $refresh = collect(); + foreach ($collection as $href => $contact) { + if (isset($contact[200]) && Str::contains($contact[200]['{DAV:}getcontenttype'], 'text/vcard')) { + // test si le contact existe + // si non -> on l'ajoute + // si oui : test du etag + // si pas identique : on l'ajoute + + $refresh->push([ + 'href' => $href, + 'etag' => $contact[200]['{DAV:}getetag'], + ]); + } + } + + + if (in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet)) { + + $datas = $client->addressbookMultiget('', [ + '{DAV:}getetag', + '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + ], $refresh->map(function ($contact) { return $contact['href']; })); + + foreach ($datas as $href => $contact) { + if (isset($contact[200])) { + $etag = $contact[200]['{DAV:}getetag']; + $vcard = $contact[200]['{'.CardDAVPlugin::NS_CARDDAV.'}address-data']; + } + } + + } else { + + foreach ($refresh as $contact) { + $c = $client->request('GET', $contact['href']); + if ($c['statusCode'] === 200) { + $etag = $contact['etag']; + $vcard = $c['body']; + } + } + } + + } catch (ClientException $e) { + $r = $e->getResponse(); + $s = (string) $r->getBody(); + } catch (\Exception $e) { + dump($e->getMessage()); + } } - private function getAddressBook(array $data, GuzzleClient $httpClient = null) + + private function getAddressBook(array $data, GuzzleClient $httpClient = null) : string { $client = $this->getClient($httpClient); @@ -52,6 +135,8 @@ private function getAddressBook(array $data, GuzzleClient $httpClient = null) $addressbook = $this->getAddressBookUrl($client, $principal); + return $client->getBaseUri($addressbook); + } catch (ClientException $e) { $r = $e->getResponse(); $s = (string) $r->getBody(); From be78bc5b6b6010f3aa0bad6a61eae22a8e7f3a0d Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Tue, 24 Mar 2020 14:02:40 +0100 Subject: [PATCH 03/99] update --- app/Console/Commands/DavClientsUpdate.php | 4 +- .../DAV/Backend/CalDAV/CalDAVBackend.php | 11 +- .../DAV/Backend/CalDAV/CalDAVBirthdays.php | 18 +- .../DAV/Backend/CalDAV/CalDAVTasks.php | 23 +- .../DAV/Backend/CardDAV/CardDAVBackend.php | 32 +-- .../DAV/Backend/SyncDAVBackend.php | 32 ++- app/Providers/DAVServiceProvider.php | 7 +- app/Services/DavClient/AddAddressBook.php | 141 +++++++++++ .../{VCard => DavClient}/ClientVCard.php | 3 +- .../{VCard => DavClient/Dav}/Client.php | 5 +- .../DavClient/SynchronizeAddressBook.php | 226 ++++++++++++++++++ 11 files changed, 450 insertions(+), 52 deletions(-) create mode 100644 app/Services/DavClient/AddAddressBook.php rename app/Services/{VCard => DavClient}/ClientVCard.php (99%) rename app/Services/{VCard => DavClient/Dav}/Client.php (99%) create mode 100644 app/Services/DavClient/SynchronizeAddressBook.php diff --git a/app/Console/Commands/DavClientsUpdate.php b/app/Console/Commands/DavClientsUpdate.php index ada91134ff9..4f84b885be9 100644 --- a/app/Console/Commands/DavClientsUpdate.php +++ b/app/Console/Commands/DavClientsUpdate.php @@ -3,7 +3,7 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use App\Services\VCard\ClientVCard; +use App\Services\DavClient\SynchronizeAddressBook; class DavClientsUpdate extends Command { @@ -28,6 +28,6 @@ class DavClientsUpdate extends Command */ public function handle() { - app(ClientVCard::class)->execute([]); + app(SynchronizeAddressBook::class)->execute([]); } } diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php index 0a0eb558b4e..60e5826a841 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php @@ -3,12 +3,17 @@ namespace App\Http\Controllers\DAV\Backend\CalDAV; use Sabre\DAV; -use App\Models\User\SyncToken; use Sabre\CalDAV\Backend\SyncSupport; use Sabre\CalDAV\Backend\AbstractBackend; class CalDAVBackend extends AbstractBackend implements SyncSupport { + public function __construct($account, $user) + { + $this->account = $account; + $this->user = $user; + } + /** * Set the Calendar backends. * @@ -17,8 +22,8 @@ class CalDAVBackend extends AbstractBackend implements SyncSupport private function getBackends(): array { return [ - new CalDAVBirthdays(), - new CalDAVTasks(), + new CalDAVBirthdays($this->account, $this->user), + new CalDAVTasks($this->account, $this->user), ]; } diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php index 11065be099d..ecd71ec3b8e 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php @@ -2,10 +2,8 @@ namespace App\Http\Controllers\DAV\Backend\CalDAV; -use App\Models\Contact\Contact; use Illuminate\Support\Facades\Log; use App\Models\Instance\SpecialDate; -use Illuminate\Support\Facades\Auth; use Sabre\DAV\Server as SabreServer; use Sabre\CalDAV\Plugin as CalDAVPlugin; use App\Services\VCalendar\ExportVCalendar; @@ -14,6 +12,12 @@ class CalDAVBirthdays extends AbstractCalDAVBackend { + public function __construct($account, $user) + { + $this->account = $account; + $this->user = $user; + } + /** * Returns the uri for this backend. * @@ -30,8 +34,8 @@ public function getDescription() + [ '{DAV:}displayname' => trans('app.dav_birthdays'), '{'.SabreServer::NS_SABREDAV.'}read-only' => true, - '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_birthdays_description', ['name' => Auth::user()->name]), - '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => Auth::user()->timezone, + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_birthdays_description', ['name' => $this->user->name]), + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone, '{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']), '{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), ]; @@ -59,7 +63,7 @@ public function prepareData($date) try { $vcal = app(ExportVCalendar::class) ->execute([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'special_date_id' => $date->id, ]); @@ -100,7 +104,7 @@ private function hasBirthday($contact) public function getObjectUuid($uuid) { return SpecialDate::where([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'uuid' => $uuid, ])->first(); } @@ -112,7 +116,7 @@ public function getObjectUuid($uuid) */ public function getObjects() { - $contacts = Auth::user()->account + $contacts = $this->account ->contacts() ->real() ->active() diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php index 70860317141..65d22f97e7d 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php @@ -6,7 +6,6 @@ use App\Models\Contact\Task; use App\Services\Task\DestroyTask; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Auth; use App\Services\VCalendar\ExportTask; use App\Services\VCalendar\ImportTask; use Sabre\CalDAV\Plugin as CalDAVPlugin; @@ -15,6 +14,12 @@ class CalDAVTasks extends AbstractCalDAVBackend { + public function __construct($account, $user) + { + $this->account = $account; + $this->user = $user; + } + /** * Returns the uri for this backend. * @@ -30,8 +35,8 @@ public function getDescription() return parent::getDescription() + [ '{DAV:}displayname' => trans('app.dav_tasks'), - '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_tasks_description', ['name' => Auth::user()->name]), - '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => Auth::user()->timezone, + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_tasks_description', ['name' => $this->user->name]), + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone, '{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']), '{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), ]; @@ -44,7 +49,7 @@ public function getDescription() */ public function getObjects() { - return Auth::user()->account + return $this->account ->tasks() ->get(); } @@ -58,7 +63,7 @@ public function getObjects() public function getObjectUuid($uuid) { return Task::where([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'uuid' => $uuid, ])->first(); } @@ -85,7 +90,7 @@ public function prepareData($task) try { $vcal = app(ExportTask::class) ->execute([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'task_id' => $task->id, ]); @@ -135,13 +140,13 @@ public function updateOrCreateCalendarObject($objectUri, $calendarData) try { $result = app(ImportTask::class) ->execute([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'task_id' => $task_id, 'entry' => $calendarData, ]); if (! Arr::has($result, 'error')) { - $task = Task::where('account_id', Auth::user()->account_id) + $task = Task::where('account_id', $this->account->id) ->find($result['task_id']); $calendar = $this->prepareData($task); @@ -169,7 +174,7 @@ public function deleteCalendarObject($objectUri) try { app(DestroyTask::class) ->execute([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'task_id' => $task->id, ]); } catch (\Exception $e) { diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php index 36f1f4c1ea6..16778ed977f 100644 --- a/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php @@ -4,13 +4,11 @@ use Sabre\DAV; use Illuminate\Support\Arr; -use App\Models\User\SyncToken; +use App\Models\Account\Account; use App\Models\Contact\Contact; -use Sabre\VObject\Component\VCard; use App\Services\VCard\ExportVCard; use App\Services\VCard\ImportVCard; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Auth; use Sabre\DAV\Server as SabreServer; use Sabre\CardDAV\Backend\SyncSupport; use Sabre\CalDAV\Plugin as CalDAVPlugin; @@ -26,6 +24,12 @@ class CardDAVBackend extends AbstractBackend implements SyncSupport, IDAVBackend { use SyncDAVBackend; + public function __construct($account, $user) + { + $this->account = $account; + $this->user = $user; + } + /** * Returns the uri for this backend. * @@ -62,7 +66,7 @@ public function getAddressBooksForUser($principalUri) 'uri' => $this->backendUri(), 'principaluri' => PrincipalBackend::getPrincipalUser(), '{DAV:}displayname' => trans('app.dav_contacts'), - '{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => trans('app.dav_contacts_description', ['name' => Auth::user()->name]), + '{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => trans('app.dav_contacts_description', ['name' => $this->user->name]), ]; if ($token) { $des += [ @@ -72,10 +76,10 @@ public function getAddressBooksForUser($principalUri) ]; } - $me = auth()->user()->me; + $me = $this->user->me; if ($me) { $des += [ - '{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card' => '/'.config('laravelsabre.path').'/addressbooks/'.Auth::user()->email.'/contacts/'.$this->encodeUri($me), + '{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card' => '/'.config('laravelsabre.path').'/addressbooks/'.$this->user->email.'/contacts/'.$this->encodeUri($me), ]; } @@ -166,7 +170,7 @@ private function prepareCard($contact) try { $vcard = app(ExportVCard::class) ->execute([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'contact_id' => $contact->id, ]); @@ -194,7 +198,7 @@ private function prepareCard($contact) public function getObjectUuid($uuid) { return Contact::where([ - 'account_id' => Auth::user()->account_id, + 'account_id' => $this->account->id, 'uuid' => $uuid, ])->first(); } @@ -206,7 +210,7 @@ public function getObjectUuid($uuid) */ public function getObjects() { - return Auth::user()->account + return Account::find($this->account->id) ->contacts() ->real() ->active() @@ -333,15 +337,15 @@ public function updateCard($addressBookId, $cardUri, $cardData) try { $result = app(ImportVCard::class) ->execute([ - 'account_id' => Auth::user()->account_id, - 'user_id' => Auth::user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, 'contact_id' => $contact_id, 'entry' => $cardData, 'behaviour' => ImportVCard::BEHAVIOUR_REPLACE, ]); if (! Arr::has($result, 'error')) { - $contact = Contact::where('account_id', Auth::user()->account_id) + $contact = Contact::where('account_id', $this->account->id) ->find($result['contact_id']); $card = $this->prepareCard($contact); @@ -388,8 +392,8 @@ public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch) $data = [ 'contact_id' => $contact->id, - 'account_id' => auth()->user()->account_id, - 'user_id' => auth()->user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, ]; app(SetMeContact::class)->execute($data); diff --git a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php index 72f0724efd0..d7f5ca0f162 100644 --- a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php @@ -4,11 +4,19 @@ use Illuminate\Support\Str; use App\Models\User\SyncToken; -use App\Models\Contact\Contact; -use Illuminate\Support\Facades\Auth; trait SyncDAVBackend { + /** + * @var \App\Models\Account\Account + */ + protected $account; + + /** + * @var \App\Models\User\User + */ + protected $user; + /** * This method returns a sync-token for this collection. * @@ -20,8 +28,8 @@ trait SyncDAVBackend protected function getCurrentSyncToken() { $tokens = SyncToken::where([ - 'account_id' => Auth::user()->account_id, - 'user_id' => Auth::user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, 'name' => $this->backendUri(), ]) ->orderBy('created_at') @@ -48,8 +56,8 @@ protected function getCurrentSyncToken() protected function getSyncToken($syncToken) { return SyncToken::where([ - 'account_id' => Auth::user()->account_id, - 'user_id' => Auth::user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, 'name' => $this->backendUri(), ]) ->find($syncToken); @@ -66,8 +74,8 @@ private function createSyncToken() if ($max) { return SyncToken::create([ - 'account_id' => Auth::user()->account_id, - 'user_id' => Auth::user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, 'name' => $this->backendUri(), 'timestamp' => $max, ]); @@ -82,8 +90,8 @@ private function createSyncToken() private function createSyncTokenNow() { return SyncToken::create([ - 'account_id' => Auth::user()->account_id, - 'user_id' => Auth::user()->id, + 'account_id' => $this->account->id, + 'user_id' => $this->user->id, 'name' => $this->backendUri(), 'timestamp' => now(), ]); @@ -183,8 +191,10 @@ public function getChanges($syncToken) $obj->created_at >= $timestamp; }); + $currentSyncToken = $this->getCurrentSyncToken(); + return [ - 'syncToken' => $token->id, + 'syncToken' => $currentSyncToken->id, 'added' => $added->map(function ($obj) { return $this->encodeUri($obj); })->toArray(), diff --git a/app/Providers/DAVServiceProvider.php b/app/Providers/DAVServiceProvider.php index 8f00c978788..1eb5bda5c54 100644 --- a/app/Providers/DAVServiceProvider.php +++ b/app/Providers/DAVServiceProvider.php @@ -8,6 +8,7 @@ use Sabre\CalDAV\ICSExportPlugin; use Sabre\CardDAV\VCFExportPlugin; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Auth; use Sabre\DAVACL\Plugin as AclPlugin; use Sabre\DAVACL\PrincipalCollection; use Illuminate\Support\ServiceProvider; @@ -58,10 +59,12 @@ public function boot() */ private function nodes(): array { + $user = Auth::user(); + // Initiate custom backends for link between Sabre and Monica $principalBackend = new PrincipalBackend(); // User rights - $carddavBackend = new CardDAVBackend(); // Contacts - $caldavBackend = new CalDAVBackend(); // Calendar + $carddavBackend = new CardDAVBackend($user->account, $user); // Contacts + $caldavBackend = new CalDAVBackend($user->account, $user); // Calendar return [ new PrincipalCollection($principalBackend), diff --git a/app/Services/DavClient/AddAddressBook.php b/app/Services/DavClient/AddAddressBook.php new file mode 100644 index 00000000000..5663f4927ad --- /dev/null +++ b/app/Services/DavClient/AddAddressBook.php @@ -0,0 +1,141 @@ + 'http://monica.test/dav/', + 'username' => 'admin@admin.com', + 'password' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZjUzYmRkYzJhNzQ0ZTgxZDRiYmUwMTQ2ODY1YTA3ZTIyMWM0ZTQ5YzkyNzJkN2FhZmE1ODk5ZDRmM2NkZGRlMWQyMWQ5MmM5M2E4YTNkMGMiLCJpYXQiOjE1ODQ4MDkxNDQsIm5iZiI6MTU4NDgwOTE0NCwiZXhwIjoxNjE2MzQ1MTQ0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.MUtcgy3PWk9McA59zx4SBJxKAkdSiGv1a9ZtVKwlgtk09bJEx1lJgymGSfDlrNqKvD2LqhTu0y95jLZNoj4-uM6DBZm3RMo18mw2xCEywB4st1hZpMYSYoOmtrOcsZweoP5r31zf_jzMX3mLde6MAeEkJcotGfO9z57M74FquLKixZRLvVruES2DcZoL1hwCKoxvv11BGRE78RQsWiipv0cfgmcSNEQVR820BWkM0X_4WwpufJdzZ5p1EpTy5AP2XXlx6amGXqxgMUIY7C-KyF1uw1Rmr6B-bTcMLJHZBH6TzU0yFoaJnhZZ9tJFyf7E70BL8SaO9_P6nA7ACjDREjAJBD9dZYrP46G-mqJXjWyVOcDVJZNW7dhF5vnEp7gghIVWhAm4lLy5nPI_CNpB0mqPrdkj57Avoi3MAEwf4ADy9CZp1EoLZIvNjBuMpgwwONTF5oP18NMaHJcsbFkmviY7eW-DIuIcNtCuoAM7Q4ulhuVX4tVry5NLsiab0_W8_l63C_n1-ICpv2t04jSh9H3SwgIXAZXhe-0vMt0gTIc3c_1HZ4eRd1kOuUs-708Esiq7J_Nt98PJZB8AP6qbeuScI0Cxnm7IulJ1WaI7mLjA7JPDvISeL2rrYjwqmguDbA8nQ7UjEq1dLN-PaAaL08p_iKU3ssPV2YziNSi_Alc', + ]; + + $client = $this->getClient($data, $httpClient); + + $addressbook = $this->getAddressBook($data, $client); + + // save addressbook url + } + + + private function getAddressBook(array $data, Client $client) : string + { + try { + + $baseUri = $client->getServiceUrl(); + $client->setBaseUri($baseUri); + + $this->checkOptions($client); + + + // /dav/principals/admin@admin.com/ + $principal = $this->getCurrentUserPrincipal($client); + + $addressbook = $this->getAddressBookUrl($client, $principal); + + return $client->getBaseUri($addressbook); + + } catch (ClientException $e) { + $r = $e->getResponse(); + $s = (string) $r->getBody(); + } catch (\Exception $e) { + } + + } + + private function checkOptions(Client $client) + { + $options = $client->options(); + $options = explode(', ', $options[0]); + + // https://tools.ietf.org/html/rfc2518#section-15 + if (!in_array('1', $options) || !in_array('3', $options) || !in_array('addressbook', $options)) { + throw new \Exception('server is not compliant with rfc2518 section 15.1, or rfc6352 section 6.1'); + } + } + + /** + * @see https://tools.ietf.org/html/rfc5397#section-3 + */ + private function getCurrentUserPrincipal(Client $client) : string + { + $prop = $client->getProperty('{DAV:}current-user-principal'); + + if (is_null($prop)) { + throw new \Exception('Server does not support rfc 5397 section 3 (DAV:current-user-principal)'); + } + + return $prop; + } + + /** + * @see https://tools.ietf.org/html/rfc6352#section-7.1.1 + */ + private function getAddressBookHome(Client $client, string $principal) : string + { + $prop = $client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-home-set', $principal); + + if (is_null($prop)) { + throw new \Exception('Server does not support rfc 6352 section 7.1.1 (CARD:addressbook-home-set)'); + } + + return $prop; + } + + private function getAddressBookUrl(Client $client, string $principal) : string + { + $home = $this->getAddressBookHome($client, $principal); + + $books = $client->propfind($home, [], 1); + + foreach ($books as $book => $properties) { + if ($book == $home) { + continue; + } + + if ($resources = Arr::get($properties, '{DAV:}resourcetype', null)) { + if ($resources->is('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook')) { + return $book; + } + } + } + } + + private function getClient(array $data, GuzzleClient $client = null) : Client + { + $settings = Arr::only($data, [ + 'base_uri', + 'username', + 'password', + ]); + + return new Client($settings, $client); + } +} diff --git a/app/Services/VCard/ClientVCard.php b/app/Services/DavClient/ClientVCard.php similarity index 99% rename from app/Services/VCard/ClientVCard.php rename to app/Services/DavClient/ClientVCard.php index cfdc7739c5a..4d2c90f6253 100644 --- a/app/Services/VCard/ClientVCard.php +++ b/app/Services/DavClient/ClientVCard.php @@ -1,11 +1,12 @@ 'required', + 'addressbookId' => 'required', + 'username' => 'required', + 'password' => 'required', + 'localSyncToken' => 'nullable', + 'syncToken' => 'nullable', + ]; + } + + /** + * + * @param array $data + * @return VCard + */ + public function execute(array $data, GuzzleClient $httpClient = null) + { + $data = [ + 'addressbook' => 'http://monica.test/dav/addressbooks/admin@admin.com/contacts/', + 'addressbookId' => 0, + 'username' => 'admin@admin.com', + 'password' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZjUzYmRkYzJhNzQ0ZTgxZDRiYmUwMTQ2ODY1YTA3ZTIyMWM0ZTQ5YzkyNzJkN2FhZmE1ODk5ZDRmM2NkZGRlMWQyMWQ5MmM5M2E4YTNkMGMiLCJpYXQiOjE1ODQ4MDkxNDQsIm5iZiI6MTU4NDgwOTE0NCwiZXhwIjoxNjE2MzQ1MTQ0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.MUtcgy3PWk9McA59zx4SBJxKAkdSiGv1a9ZtVKwlgtk09bJEx1lJgymGSfDlrNqKvD2LqhTu0y95jLZNoj4-uM6DBZm3RMo18mw2xCEywB4st1hZpMYSYoOmtrOcsZweoP5r31zf_jzMX3mLde6MAeEkJcotGfO9z57M74FquLKixZRLvVruES2DcZoL1hwCKoxvv11BGRE78RQsWiipv0cfgmcSNEQVR820BWkM0X_4WwpufJdzZ5p1EpTy5AP2XXlx6amGXqxgMUIY7C-KyF1uw1Rmr6B-bTcMLJHZBH6TzU0yFoaJnhZZ9tJFyf7E70BL8SaO9_P6nA7ACjDREjAJBD9dZYrP46G-mqJXjWyVOcDVJZNW7dhF5vnEp7gghIVWhAm4lLy5nPI_CNpB0mqPrdkj57Avoi3MAEwf4ADy9CZp1EoLZIvNjBuMpgwwONTF5oP18NMaHJcsbFkmviY7eW-DIuIcNtCuoAM7Q4ulhuVX4tVry5NLsiab0_W8_l63C_n1-ICpv2t04jSh9H3SwgIXAZXhe-0vMt0gTIc3c_1HZ4eRd1kOuUs-708Esiq7J_Nt98PJZB8AP6qbeuScI0Cxnm7IulJ1WaI7mLjA7JPDvISeL2rrYjwqmguDbA8nQ7UjEq1dLN-PaAaL08p_iKU3ssPV2YziNSi_Alc', + 'localSyncToken' => '22', + 'syncToken' => 'http://sabre.io/ns/sync/15', + ]; + + $user = User::find(1); + $backend = new CardDAVBackend($user->account, $user); + + try { + $client = $this->getClient($data, $httpClient); + + $localChanges = $backend->getChangesForAddressBook($data['addressbookId'], $data['localSyncToken'], 1); + + $supportedReportSet = $client->getSupportedReportSet(); + + $refresh = $this->distantChanges($data, $client, $supportedReportSet, $backend); + + $this->refreshContacts($data, $client, $supportedReportSet, $backend, $refresh); + + $this->pushContacts($data, $client, $supportedReportSet, $backend, $refresh, $localChanges); + + + } catch (ClientException $e) { + $r = $e->getResponse(); + $s = (string) $r->getBody(); + } catch (\Exception $e) { + dump($e->getMessage()); + } + } + + private function distantChanges(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend) + { + $distantChanges = $this->getContactsList($data, $client, $supportedReportSet) ?? []; + + $refresh = collect(); + foreach ($distantChanges as $href => $contact) { + if (isset($contact[200]) && Str::contains($contact[200]['{DAV:}getcontenttype'], 'text/vcard')) { + + $localContact = $backend->getCard($data['addressbookId'], $href); + $etag = $contact[200]['{DAV:}getetag']; + + if ($localContact !== false && $localContact['etag'] == $etag) { + // contact already exist, and the etag is the same + continue; + } + + $refresh->push([ + 'href' => $href, + 'etag' => $contact[200]['{DAV:}getetag'], + ]); + } + } + + return $refresh; + } + + private function refreshContacts(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend, Collection $refresh) + { + $refreshContacts = collect(); + if (in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet)) { + + $hrefs = $refresh->map(function ($contact) { return $contact['href']; }); + $datas = $client->addressbookMultiget('', [ + '{DAV:}getetag', + '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + ], $hrefs); + + foreach ($datas as $href => $contact) { + if (isset($contact[200])) { + $refreshContacts->push([ + 'href' => $href, + 'etag' => $contact[200]['{DAV:}getetag'], + 'vcard' => $contact[200]['{'.CardDAVPlugin::NS_CARDDAV.'}address-data'], + ]); + } + } + + } else { + + foreach ($refresh as $contact) { + $c = $client->request('GET', $contact['href']); + if ($c['statusCode'] === 200) { + $refreshContacts->push([ + 'href' => $contact['href'], + 'etag' => $contact['etag'], + 'vcard' => $c['body'], + ]); + } + } + } + + foreach ($refreshContacts as $contact) + { + $newtag = $backend->updateCard($data['addressbookId'], $contact['href'], $contact['vcard']); + + if ($newtag != $contact['etag']) { + Log::warning(__CLASS__.' refreshContacts: wrong etag update'); + } + } + } + + private function pushContacts(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend, Collection $refresh, array $localChanges) + { + $refreshIds = $refresh->map(function ($contact) use ($backend) { + $c = $backend->getObject($contact['href']); + if ($c) { + return $c->uuid; + } + })->toArray(); + + // All added contact must be pushed + $localChangesAdd = collect($localChanges['added']); + // We don't push contact that have just been updated + $localChangesUpdate = collect($localChanges['modified'])->reject(function ($value) use ($refreshIds) { + $uuid = pathinfo(urldecode($value), PATHINFO_FILENAME); + return in_array($uuid, $refreshIds); + }); + + foreach ($localChangesAdd->union($localChangesUpdate) as $uri) + { + $contact = $backend->getCard($data['addressbookId'], $uri); + $client->request('PUT', $uri, $contact['carddata'], [ + 'If-Match' => '*' + ]); + } + + } + + private function getContactsList(array $data, Client $client, $supportedReportSet): array + { + // With sync-collection + if (in_array('{DAV:}sync-collection', $supportedReportSet)) { + + // get the current distant syncToken + $syncToken = $client->getProperty('{DAV:}sync-token'); + + if ($syncToken == $data['syncToken']) { + // no change at all + + } + + // get sync + $collection = $client->syncCollection('', $data['syncToken'], [ + '{DAV:}getcontenttype', + '{DAV:}getetag', + ]); + + $newSyncToken = $collection['synctoken']; + + if ($newSyncToken == $data['syncToken']) { + // no change + } + + // save the new syncToken as current one + + } else { + + // synchronisation + $collection = $client->propFind('', [ + '{DAV:}getcontenttype', + '{DAV:}getetag', + ], 1); + } + + return $collection; + } + + + + private function getClient(array $data, GuzzleClient $client = null) : Client + { + $settings = [ + 'base_uri' => $data['addressbook'], + 'username' => $data['username'], + 'password' => $data['password'], + ]; + + return new Client($settings, $client); + } +} From 4fca040bca455c5acdc07a48e4822d949701ef58 Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Tue, 24 Mar 2020 16:44:13 +0100 Subject: [PATCH 04/99] update --- .../DAV/Backend/SyncDAVBackend.php | 17 +++++++++++++--- .../DavClient/SynchronizeAddressBook.php | 20 +++++++------------ 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php index d7f5ca0f162..588b9d8fd26 100644 --- a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php @@ -205,7 +205,7 @@ public function getChanges($syncToken) ]; } - protected function encodeUri($obj) + protected function encodeUri($obj) : string { if (empty($obj->uuid)) { // refresh model from database @@ -222,11 +222,22 @@ protected function encodeUri($obj) return urlencode($obj->uuid.$this->getExtension()); } - private function decodeUri($uri) + private function decodeUri($uri) : string { return pathinfo(urldecode($uri), PATHINFO_FILENAME); } + /** + * Returns the contact uuid for the specific uri. + * + * @param string $uri + * @return string + */ + public function getUuid($uri) : string + { + return $this->decodeUri($uri); + } + /** * Returns the contact for the specific uri. * @@ -236,7 +247,7 @@ private function decodeUri($uri) public function getObject($uri) { try { - return $this->getObjectUuid($this->decodeUri($uri)); + return $this->getObjectUuid($this->getUuid($uri)); } catch (\Exception $e) { // Object not found } diff --git a/app/Services/DavClient/SynchronizeAddressBook.php b/app/Services/DavClient/SynchronizeAddressBook.php index fb773afbf91..65a8da6e867 100644 --- a/app/Services/DavClient/SynchronizeAddressBook.php +++ b/app/Services/DavClient/SynchronizeAddressBook.php @@ -65,7 +65,6 @@ public function execute(array $data, GuzzleClient $httpClient = null) $this->pushContacts($data, $client, $supportedReportSet, $backend, $refresh, $localChanges); - } catch (ClientException $e) { $r = $e->getResponse(); $s = (string) $r->getBody(); @@ -85,7 +84,7 @@ private function distantChanges(array $data, Client $client, array $supportedRep $localContact = $backend->getCard($data['addressbookId'], $href); $etag = $contact[200]['{DAV:}getetag']; - if ($localContact !== false && $localContact['etag'] == $etag) { + if ($localContact !== false && $localContact['etag'] == $etag) { // contact already exist, and the etag is the same continue; } @@ -105,7 +104,7 @@ private function refreshContacts(array $data, Client $client, array $supportedRe $refreshContacts = collect(); if (in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet)) { - $hrefs = $refresh->map(function ($contact) { return $contact['href']; }); + $hrefs = $refresh->pluck('href'); $datas = $client->addressbookMultiget('', [ '{DAV:}getetag', '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', @@ -147,19 +146,15 @@ private function refreshContacts(array $data, Client $client, array $supportedRe private function pushContacts(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend, Collection $refresh, array $localChanges) { - $refreshIds = $refresh->map(function ($contact) use ($backend) { - $c = $backend->getObject($contact['href']); - if ($c) { - return $c->uuid; - } - })->toArray(); + $refreshIds = $refresh->pluck('href'); // All added contact must be pushed $localChangesAdd = collect($localChanges['added']); + // We don't push contact that have just been updated - $localChangesUpdate = collect($localChanges['modified'])->reject(function ($value) use ($refreshIds) { - $uuid = pathinfo(urldecode($value), PATHINFO_FILENAME); - return in_array($uuid, $refreshIds); + $localChangesUpdate = collect($localChanges['modified'])->reject(function ($uri) use ($refreshIds, $backend) { + $uuid = $backend->getUuid($uri); + return $refreshIds->contains($uuid); }); foreach ($localChangesAdd->union($localChangesUpdate) as $uri) @@ -169,7 +164,6 @@ private function pushContacts(array $data, Client $client, array $supportedRepor 'If-Match' => '*' ]); } - } private function getContactsList(array $data, Client $client, $supportedReportSet): array From 8cb2aea13a7ac90963ec35d1e96d271a118e4190 Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Tue, 24 Mar 2020 21:01:23 +0100 Subject: [PATCH 05/99] update --- app/Services/DavClient/Dav/Client.php | 49 ++++++++++--- .../DavClient/SynchronizeAddressBook.php | 72 ++++++++++++++----- 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/app/Services/DavClient/Dav/Client.php b/app/Services/DavClient/Dav/Client.php index 3b074ebd392..59dd51ca36d 100644 --- a/app/Services/DavClient/Dav/Client.php +++ b/app/Services/DavClient/Dav/Client.php @@ -59,7 +59,9 @@ public function getServiceUrl() // Get well-known register (section 9.1) $wkUri = $this->getBaseUri('/.well-known/carddav'); - $response = $this->client->get($wkUri, ['allow_redirects' => false]); + $response = $this->client->get($wkUri, [ + 'allow_redirects' => false + ]); $code = $response->getStatusCode(); if (($code === 301 || $code === 302) && $response->hasHeader('Location')) { @@ -180,7 +182,7 @@ public function propFind(string $url, array $properties, int $depth = 0) : array /** * @see https://tools.ietf.org/html/rfc6578 */ - public function syncCollection(string $url, string $syncToken, array $properties) : array + public function syncCollection(string $url, array $properties, string $syncToken) : array { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; @@ -214,7 +216,7 @@ public function syncCollection(string $url, string $syncToken, array $properties /** * @see https://tools.ietf.org/html/rfc6352#section-8.7 */ - public function addressbookMultiget(string $url, array $properties, Collection $contacts) : array + public function addressbookMultiget(string $url, array $properties, \ArrayAccess $contacts) : array { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; @@ -248,19 +250,48 @@ public function addressbookMultiget(string $url, array $properties, Collection $ return $result; } + /** + * Add properties to the prop object. + * + * Properties must follow: + * [ + * // Simple value + * '{namespace}value', + * + * // More complex value element + * [ + * 'name' => '{namespace}value', + * 'value' => 'content element', + * 'attributes' => ['name' => 'value', ...], + * ] + * ] + */ private function fetchProperties($dom, $prop, array $properties, array $namespaces) { foreach ($properties as $property) { + if (is_array($property)) { + $propertyExt = $property; + $property = $propertyExt['name']; + } list($namespace, $elementName) = Service::parseClarkNotation($property); $value = Arr::get($namespaces, $namespace, null); if (!is_null($value)) { - $element = $dom->createElement("$value:$elementName"); + $element = $prop->appendChild($dom->createElement("$value:$elementName")); } else { - $element = $dom->createElementNS($namespace, 'x:'.$elementName); + $element = $prop->appendChild($dom->createElementNS($namespace, 'x:'.$elementName)); } - $prop->appendChild($element); + if (isset($propertyExt)) { + if (isset($propertyExt['value']) && !is_null($propertyExt['value'])) { + $element->nodeValue = $propertyExt['value']; + } + if (isset($propertyExt['attributes'])) { + foreach ($propertyExt['attributes'] as $name => $property) { + $element->appendChild($dom->createAttribute($name))->value = $property; + } + } + } } } @@ -285,8 +316,10 @@ public function getProperty(string $property, string $url = '') $value = $prop[0]; if (is_string($value)) { return $value; - } else if (is_array($value)) { - return Arr::get($value, 'value', $value); + //} else if (is_array($value)) { + // return Arr::get($value, 'value', $value); + } else { + return $prop; } } diff --git a/app/Services/DavClient/SynchronizeAddressBook.php b/app/Services/DavClient/SynchronizeAddressBook.php index 65a8da6e867..2155b3dc4ff 100644 --- a/app/Services/DavClient/SynchronizeAddressBook.php +++ b/app/Services/DavClient/SynchronizeAddressBook.php @@ -3,6 +3,7 @@ namespace App\Services\DavClient; use App\Models\User\User; +use Illuminate\Support\Arr; use Illuminate\Support\Str; use App\Services\BaseService; use Illuminate\Support\Collection; @@ -24,12 +25,14 @@ class SynchronizeAddressBook extends BaseService public function rules() { return [ - 'addressbook' => 'required', - 'addressbookId' => 'required', - 'username' => 'required', - 'password' => 'required', - 'localSyncToken' => 'nullable', - 'syncToken' => 'nullable', + 'account_id' => 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'addressbook' => 'required|string|url', + 'addressbookId' => 'required|integer', + 'username' => 'required|string', + 'password' => 'required|string', + 'localSyncToken' => 'nullable|integer|exists:syncToken,id', + 'syncToken' => 'nullable|string', ]; } @@ -41,6 +44,8 @@ public function rules() public function execute(array $data, GuzzleClient $httpClient = null) { $data = [ + 'account_id' => 1, + 'user_id' => 1, 'addressbook' => 'http://monica.test/dav/addressbooks/admin@admin.com/contacts/', 'addressbookId' => 0, 'username' => 'admin@admin.com', @@ -49,7 +54,13 @@ public function execute(array $data, GuzzleClient $httpClient = null) 'syncToken' => 'http://sabre.io/ns/sync/15', ]; - $user = User::find(1); + /* TESTS */ + + $this->validate($data); + + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + $backend = new CardDAVBackend($user->account, $user); try { @@ -86,7 +97,7 @@ private function distantChanges(array $data, Client $client, array $supportedRep if ($localContact !== false && $localContact['etag'] == $etag) { // contact already exist, and the etag is the same - continue; + //continue; } $refresh->push([ @@ -104,10 +115,34 @@ private function refreshContacts(array $data, Client $client, array $supportedRe $refreshContacts = collect(); if (in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet)) { + // get the supported card format + $addressData = collect($client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}supported-address-data')); + $datas = $addressData->firstWhere('attributes.version', '4.0'); + if (!$datas) { + $datas = $addressData->firstWhere('attributes.version', '3.0'); + } + + if (!$datas) { + // It should not happen ! + $datas = [ + 'attributes' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ]; + } + $hrefs = $refresh->pluck('href'); $datas = $client->addressbookMultiget('', [ '{DAV:}getetag', - '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + [ + 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + 'value' => null, + 'attributes' => [ + 'content-type' => $datas['attributes']['content-type'], + 'version' => $datas['attributes']['version'], + ], + ] ], $hrefs); foreach ($datas as $href => $contact) { @@ -171,27 +206,26 @@ private function getContactsList(array $data, Client $client, $supportedReportSe // With sync-collection if (in_array('{DAV:}sync-collection', $supportedReportSet)) { + $syncToken = Arr::get($data, 'syncToken', ''); + // get the current distant syncToken - $syncToken = $client->getProperty('{DAV:}sync-token'); + $distantSyncToken = $client->getProperty('{DAV:}sync-token'); - if ($syncToken == $data['syncToken']) { + if ($syncToken == $distantSyncToken) { // no change at all - + return []; } // get sync - $collection = $client->syncCollection('', $data['syncToken'], [ + $collection = $client->syncCollection('', [ '{DAV:}getcontenttype', '{DAV:}getetag', - ]); + ], $syncToken); + // save the new syncToken as current one $newSyncToken = $collection['synctoken']; - if ($newSyncToken == $data['syncToken']) { - // no change - } - - // save the new syncToken as current one + // TODO } else { From 96b56aadf1e65aa1571dd959ae0ae4699485512f Mon Sep 17 00:00:00 2001 From: Alexis Saettler Date: Thu, 26 Mar 2020 09:30:27 +0100 Subject: [PATCH 06/99] update --- app/Console/Commands/DavClientsUpdate.php | 18 +- .../Controllers/Api/ApiContactController.php | 1 + app/Http/Controllers/Api/ApiTagController.php | 1 + app/Http/Controllers/ContactsController.php | 6 +- .../Backend/CalDAV/AbstractCalDAVBackend.php | 2 +- .../DAV/Backend/CalDAV/CalDAVBackend.php | 4 +- .../DAV/Backend/CalDAV/CalDAVBirthdays.php | 6 +- .../DAV/Backend/CalDAV/CalDAVTasks.php | 5 +- .../DAV/Backend/CardDAV/AddressBook.php | 2 +- .../DAV/Backend/CardDAV/CardDAVBackend.php | 81 +++++--- .../Controllers/DAV/Backend/IDAVBackend.php | 5 +- .../DAV/Backend/SyncDAVBackend.php | 59 +++--- app/Http/Controllers/DashboardController.php | 5 +- .../Settings/SubscriptionsController.php | 2 +- app/Jobs/SynchronizeAddressBooks.php | 52 +++++ app/Models/Account/AddressBook.php | 121 +++++++++++ app/Models/Contact/Contact.php | 21 ++ app/Services/DavClient/AddAddressBook.php | 118 ++++++++++- app/Services/DavClient/Dav/Client.php | 9 +- .../DavClient/SynchronizeAddressBook.php | 188 +++++++++--------- app/Services/VCard/ImportVCard.php | 47 ++++- .../2020_03_25_055551_add_address_book.php | 45 +++++ ..._25_082324_add_contact_address_book_id.php | 33 +++ ...20_03_25_201407_add_contact_vcard_data.php | 32 +++ .../views/people/introductions/edit.blade.php | 2 +- 25 files changed, 678 insertions(+), 187 deletions(-) create mode 100644 app/Jobs/SynchronizeAddressBooks.php create mode 100644 app/Models/Account/AddressBook.php create mode 100644 database/migrations/2020_03_25_055551_add_address_book.php create mode 100644 database/migrations/2020_03_25_082324_add_contact_address_book_id.php create mode 100644 database/migrations/2020_03_25_201407_add_contact_vcard_data.php diff --git a/app/Console/Commands/DavClientsUpdate.php b/app/Console/Commands/DavClientsUpdate.php index 4f84b885be9..9a3398dc4a9 100644 --- a/app/Console/Commands/DavClientsUpdate.php +++ b/app/Console/Commands/DavClientsUpdate.php @@ -3,7 +3,9 @@ namespace App\Console\Commands; use Illuminate\Console\Command; -use App\Services\DavClient\SynchronizeAddressBook; +use App\Models\Account\AddressBook; +use App\Jobs\SynchronizeAddressBooks; +use App\Services\DavClient\AddAddressBook; class DavClientsUpdate extends Command { @@ -12,14 +14,14 @@ class DavClientsUpdate extends Command * * @var string */ - protected $signature = 'monica:dav'; + protected $signature = 'monica:davclients'; /** * The console command description. * * @var string */ - protected $description = ''; + protected $description = 'Update all dav subscriptions'; /** * Execute the console command. @@ -28,6 +30,14 @@ class DavClientsUpdate extends Command */ public function handle() { - app(SynchronizeAddressBook::class)->execute([]); + //app(AddAddressBook::class)->execute([]); + //return; + + $addressBooks = AddressBook::all(); + + foreach ($addressBooks as $addressBook) + { + SynchronizeAddressBooks::dispatch($addressBook); + } } } diff --git a/app/Http/Controllers/Api/ApiContactController.php b/app/Http/Controllers/Api/ApiContactController.php index 916fc571cf4..2267ee8fac0 100644 --- a/app/Http/Controllers/Api/ApiContactController.php +++ b/app/Http/Controllers/Api/ApiContactController.php @@ -71,6 +71,7 @@ public function index(Request $request) try { $contacts = auth()->user()->account->contacts() ->real() + ->addressBook() ->active() ->orderBy($this->sort, $this->sortDirection) ->paginate($this->getLimitPerPage()); diff --git a/app/Http/Controllers/Api/ApiTagController.php b/app/Http/Controllers/Api/ApiTagController.php index 85137d00ace..2aa9a067c64 100644 --- a/app/Http/Controllers/Api/ApiTagController.php +++ b/app/Http/Controllers/Api/ApiTagController.php @@ -146,6 +146,7 @@ public function contacts(Request $request, int $tagId) try { $contacts = auth()->user()->account->contacts() ->real() + ->addressBook() ->active() ->whereHas('tags', function (Builder $query) use ($tagId) { $query->where('id', $tagId); diff --git a/app/Http/Controllers/ContactsController.php b/app/Http/Controllers/ContactsController.php index 7a2e08ec281..7f418d3a5f9 100644 --- a/app/Http/Controllers/ContactsController.php +++ b/app/Http/Controllers/ContactsController.php @@ -79,7 +79,7 @@ private function contacts(Request $request, bool $active) ]); } - $contacts = $user->account->contacts()->real(); + $contacts = $user->account->contacts()->real()->addressBook(); if ($active) { $nbArchived = $contacts->count(); $contacts = $contacts->active(); @@ -280,7 +280,7 @@ public function show(Contact $contact) $reminders = $reminders->sortBy('next_expected_date'); // list of active features - $modules = $contact->account->modules()->active()->get(); + $modules = $contact->account->modules()->active()->addressBook()->get(); // add `---` at the top of the dropdowns $days = DateHelper::getListOfDays(); @@ -672,7 +672,7 @@ public function list(Request $request) $url = ''; $count = 1; - $contacts = $user->account->contacts()->real(); + $contacts = $user->account->contacts()->real()->addressBook(); // filter out archived contacts if necessary if ($request->input('show_archived') != 'true') { diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php index 70bf2d72c06..08ac874fbee 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php @@ -15,7 +15,7 @@ abstract class AbstractCalDAVBackend implements ICalDAVBackend, IDAVBackend public function getDescription() { - $token = $this->getCurrentSyncToken(); + $token = $this->getCurrentSyncToken(null); $des = [ 'id' => $this->backendUri(), diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php index 60e5826a841..27e9b5c5570 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php @@ -131,7 +131,7 @@ public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limi { $backend = $this->getBackend($calendarId); if ($backend) { - return $backend->getChanges($syncToken); + return $backend->getChanges($calendarId, $syncToken); } } @@ -170,7 +170,7 @@ public function getCalendarObjects($calendarId) { $backend = $this->getBackend($calendarId); if ($backend) { - $objs = $backend->getObjects(); + $objs = $backend->getObjects($calendarId); return $objs ->map(function ($date) use ($backend) { diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php index ecd71ec3b8e..2a94d29bef7 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php @@ -98,10 +98,11 @@ private function hasBirthday($contact) /** * Returns the date for the specific uuid. * + * @param mixed|null $addressBookId * @param string $uuid * @return mixed */ - public function getObjectUuid($uuid) + public function getObjectUuid($addressBookId, $uuid) { return SpecialDate::where([ 'account_id' => $this->account->id, @@ -114,11 +115,12 @@ public function getObjectUuid($uuid) * * @return \Illuminate\Support\Collection */ - public function getObjects() + public function getObjects($addressBookId) { $contacts = $this->account ->contacts() ->real() + ->addressBook() ->active() ->get(); diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php index 65d22f97e7d..67cf03f9692 100644 --- a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php @@ -47,7 +47,7 @@ public function getDescription() * * @return \Illuminate\Support\Collection */ - public function getObjects() + public function getObjects($addressBookId) { return $this->account ->tasks() @@ -57,10 +57,11 @@ public function getObjects() /** * Returns the contact for the specific uuid. * + * @param mixed|null $addressBookId * @param string $uuid * @return mixed */ - public function getObjectUuid($uuid) + public function getObjectUuid($addressBookId, $uuid) { return Task::where([ 'account_id' => $this->account->id, diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/AddressBook.php b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBook.php index bd151cb4722..fd71991fe09 100644 --- a/app/Http/Controllers/DAV/Backend/CardDAV/AddressBook.php +++ b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBook.php @@ -69,7 +69,7 @@ public function getChildACL() public function getLastModified() { if ($this->carddavBackend instanceof CardDAVBackend) { - $date = $this->carddavBackend->getLastModified(); + $date = $this->carddavBackend->getLastModified(null); if (! is_null($date)) { return $date->timestamp; } diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php index 16778ed977f..4b28bf50d54 100644 --- a/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php @@ -19,6 +19,7 @@ use App\Http\Controllers\DAV\Backend\IDAVBackend; use App\Http\Controllers\DAV\Backend\SyncDAVBackend; use App\Http\Controllers\DAV\DAVACL\PrincipalBackend; +use App\Models\Account\AddressBook; class CardDAVBackend extends AbstractBackend implements SyncSupport, IDAVBackend { @@ -59,11 +60,32 @@ public function backendUri() */ public function getAddressBooksForUser($principalUri) { - $token = $this->getCurrentSyncToken(); + $result = []; + $result[] = $this->getDefaultAddressBook($principalUri); + + $addressbooks = AddressBook::where('account_id', $this->account->id) + ->get(); + + foreach ($addressbooks as $addressbook) { + $result[] = [ + 'id' => $addressbook->addressBookId, + 'uri' => $addressbook->addressBookId, + 'principaluri' => PrincipalBackend::getPrincipalUser(), + '{DAV:}displayname' => $addressbook->name, + ]; + } + + return $result; + } + + private function getDefaultAddressBook($principalUri) + { + $id = $this->backendUri(); + $token = $this->getCurrentSyncToken(null); $des = [ - 'id' => $this->backendUri(), - 'uri' => $this->backendUri(), + 'id' => $id, + 'uri' => $id, 'principaluri' => PrincipalBackend::getPrincipalUser(), '{DAV:}displayname' => trans('app.dav_contacts'), '{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => trans('app.dav_contacts_description', ['name' => $this->user->name]), @@ -83,9 +105,7 @@ public function getAddressBooksForUser($principalUri) ]; } - return [ - $des, - ]; + return $des; } /** @@ -156,7 +176,7 @@ public function getExtension() */ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) { - return $this->getChanges($syncToken); + return $this->getChanges($addressBookId, $syncToken); } /** @@ -168,13 +188,16 @@ public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, private function prepareCard($contact) { try { - $vcard = app(ExportVCard::class) - ->execute([ - 'account_id' => $this->account->id, - 'contact_id' => $contact->id, - ]); - - $carddata = $vcard->serialize(); + $carddata = $contact->vcard; + if (empty($carddat)) { + $vcard = app(ExportVCard::class) + ->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $contact->id, + ]); + + $carddata = $vcard->serialize(); + } return [ 'id' => $contact->hashID(), @@ -192,14 +215,24 @@ private function prepareCard($contact) /** * Returns the contact for the specific uuid. * + * @param mixed|null $addressBookId * @param string $uuid * @return Contact */ - public function getObjectUuid($uuid) + public function getObjectUuid($addressBookId, $uuid) { + $addressBook = null; + if ($addressBookId) { + $addressBook = AddressBook::where([ + 'account_id' => $this->account->id, + 'addressBookId' => $addressBookId == $this->backendUri() ? null : $addressBookId, + ])->first(); + } + return Contact::where([ 'account_id' => $this->account->id, 'uuid' => $uuid, + 'addressbook_id' => $addressBook ? $addressBook->id : null, ])->first(); } @@ -208,11 +241,12 @@ public function getObjectUuid($uuid) * * @return \Illuminate\Support\Collection */ - public function getObjects() + public function getObjects($addressBookId) { return Account::find($this->account->id) ->contacts() ->real() + ->addressBook($this->account->id, $addressBookId) ->active() ->get(); } @@ -233,12 +267,12 @@ public function getObjects() * calculating them. If they are specified, you can also ommit carddata. * This may speed up certain requests, especially with large cards. * - * @param mixed $addressbookId + * @param mixed $addressBookId * @return array */ - public function getCards($addressbookId) + public function getCards($addressBookId) { - $contacts = $this->getObjects(); + $contacts = $this->getObjects($addressBookId); return $contacts->map(function ($contact) { return $this->prepareCard($contact); @@ -259,7 +293,7 @@ public function getCards($addressbookId) */ public function getCard($addressBookId, $cardUri) { - $contact = $this->getObject($cardUri); + $contact = $this->getObject($addressBookId, $cardUri); if ($contact) { return $this->prepareCard($contact); @@ -327,7 +361,7 @@ public function updateCard($addressBookId, $cardUri, $cardData) { $contact_id = null; if ($cardUri) { - $contact = $this->getObject($cardUri); + $contact = $this->getObject($addressBookId, $cardUri); if ($contact) { $contact_id = $contact->id; @@ -342,6 +376,7 @@ public function updateCard($addressBookId, $cardUri, $cardData) 'contact_id' => $contact_id, 'entry' => $cardData, 'behaviour' => ImportVCard::BEHAVIOUR_REPLACE, + 'addressBookId' => $addressBookId == $this->backendUri() ? null : $addressBookId, ]); if (! Arr::has($result, 'error')) { @@ -387,8 +422,8 @@ public function deleteCard($addressBookId, $cardUri) */ public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch) { - $propPatch->handle('{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card', function ($props) { - $contact = $this->getObject($props->getHref()); + $propPatch->handle('{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card', function ($props) use ($addressBookId) { + $contact = $this->getObject($addressBookId, $props->getHref()); $data = [ 'contact_id' => $contact->id, diff --git a/app/Http/Controllers/DAV/Backend/IDAVBackend.php b/app/Http/Controllers/DAV/Backend/IDAVBackend.php index 1828ea5c30c..9cafb13f2a3 100644 --- a/app/Http/Controllers/DAV/Backend/IDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/IDAVBackend.php @@ -14,17 +14,18 @@ public function backendUri(); /** * Returns the object for the specific uuid. * + * @param mixed|null $addressBookId * @param string $uuid * @return mixed */ - public function getObjectUuid($uuid); + public function getObjectUuid($addressBookId, $uuid); /** * Returns the collection of objects. * * @return \Illuminate\Support\Collection */ - public function getObjects(); + public function getObjects($addressBookId); /** * Returns the extension for this backend. diff --git a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php index 588b9d8fd26..3d9cd3896e4 100644 --- a/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php +++ b/app/Http/Controllers/DAV/Backend/SyncDAVBackend.php @@ -23,26 +23,31 @@ trait SyncDAVBackend * If null is returned from this function, the plugin assumes there's no * sync information available. * + * @param mixed|null $addressBookId * @return SyncToken|null */ - protected function getCurrentSyncToken() + protected function getCurrentSyncToken($addressBookId) { $tokens = SyncToken::where([ 'account_id' => $this->account->id, 'user_id' => $this->user->id, - 'name' => $this->backendUri(), + 'name' => $addressBookId ?? $this->backendUri(), ]) ->orderBy('created_at') ->get(); if ($tokens->count() <= 0) { - $token = $this->createSyncToken(); + $token = $this->createSyncToken($addressBookId); } else { $token = $tokens->last(); + /* + } else if ($refresh) { + $token = $tokens->last(); - if ($token->timestamp < $this->getLastModified()) { - $token = $this->createSyncToken(); + if ($token->timestamp < $this->getLastModified($addressBookId)) { + $token = $this->createSyncToken($addressBookId); } + */ } return $token; @@ -51,14 +56,15 @@ protected function getCurrentSyncToken() /** * Get SyncToken by token id. * + * @param mixed|null $addressBookId * @return SyncToken|null */ - protected function getSyncToken($syncToken) + protected function getSyncToken($addressBookId, $syncToken) { return SyncToken::where([ 'account_id' => $this->account->id, 'user_id' => $this->user->id, - 'name' => $this->backendUri(), + 'name' => $addressBookId ?? $this->backendUri(), ]) ->find($syncToken); } @@ -66,17 +72,18 @@ protected function getSyncToken($syncToken) /** * Create a token. * + * @param mixed|null $addressBookId * @return SyncToken|null */ - private function createSyncToken() + private function createSyncToken($addressBookId) { - $max = $this->getLastModified(); + $max = $this->getLastModified($addressBookId); if ($max) { return SyncToken::create([ 'account_id' => $this->account->id, 'user_id' => $this->user->id, - 'name' => $this->backendUri(), + 'name' => $addressBookId ?? $this->backendUri(), 'timestamp' => $max, ]); } @@ -85,14 +92,15 @@ private function createSyncToken() /** * Create a token with now timestamp. * + * @param mixed|null $addressBookId * @return SyncToken */ - private function createSyncTokenNow() + private function createSyncTokenNow($addressBookId) { return SyncToken::create([ 'account_id' => $this->account->id, 'user_id' => $this->user->id, - 'name' => $this->backendUri(), + 'name' => $addressBookId ?? $this->backendUri(), 'timestamp' => now(), ]); } @@ -100,11 +108,12 @@ private function createSyncTokenNow() /** * Returns the last modification date. * + * @param mixed|null $addressBookId * @return \Carbon\Carbon|null */ - public function getLastModified() + public function getLastModified($addressBookId) { - return $this->getObjects() + return $this->getObjects($addressBookId) ->max('updated_at'); } @@ -158,15 +167,16 @@ public function getLastModified() * * The limit is 'suggestive'. You are free to ignore it. * + * @param string $addressBookId * @param string $syncToken * @return array|null */ - public function getChanges($syncToken) + public function getChanges($addressBookId, $syncToken) { $token = null; $timestamp = null; if (! empty($syncToken)) { - $token = $this->getSyncToken($syncToken); + $token = $this->getSyncToken($addressBookId, $syncToken); if (is_null($token)) { // syncToken is not recognized @@ -175,11 +185,11 @@ public function getChanges($syncToken) $timestamp = $token->timestamp; } else { - $token = $this->createSyncTokenNow(); + $token = $this->createSyncTokenNow($addressBookId); $timestamp = null; } - $objs = $this->getObjects(); + $objs = $this->getObjects($addressBookId); $modified = $objs->filter(function ($obj) use ($timestamp) { return ! is_null($timestamp) && @@ -191,7 +201,7 @@ public function getChanges($syncToken) $obj->created_at >= $timestamp; }); - $currentSyncToken = $this->getCurrentSyncToken(); + $currentSyncToken = $this->getCurrentSyncToken($addressBookId); return [ 'syncToken' => $currentSyncToken->id, @@ -241,13 +251,14 @@ public function getUuid($uri) : string /** * Returns the contact for the specific uri. * + * @param mixed|null $addressBookId * @param string $uri * @return mixed */ - public function getObject($uri) + public function getObject($addressBookId, $uri) { try { - return $this->getObjectUuid($this->getUuid($uri)); + return $this->getObjectUuid($addressBookId, $this->getUuid($uri)); } catch (\Exception $e) { // Object not found } @@ -256,17 +267,19 @@ public function getObject($uri) /** * Returns the object for the specific uuid. * + * @param mixed|null $addressBookId * @param string $uuid * @return mixed */ - abstract public function getObjectUuid($uuid); + abstract public function getObjectUuid($addressBookId, $uuid); /** * Returns the collection of objects. * + * @param mixed|null $addressBookId * @return \Illuminate\Support\Collection */ - abstract public function getObjects(); + abstract public function getObjects($addressBookId); abstract public function getExtension(); } diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php index 4d91539d8b8..0a76beb3143 100644 --- a/app/Http/Controllers/DashboardController.php +++ b/app/Http/Controllers/DashboardController.php @@ -28,7 +28,7 @@ public function index() )->with('debts.contact') ->first(); - if ($account->contacts()->real()->active()->count() === 0) { + if ($account->contacts()->real()->addressBook()->active()->count() === 0) { return view('dashboard.blank'); } @@ -36,6 +36,7 @@ public function index() $lastUpdatedContactsCollection = collect([]); $lastUpdatedContacts = $account->contacts() ->real() + ->addressBook() ->active() ->alive() ->latest('last_consulted_at') @@ -77,7 +78,7 @@ public function index() $data = [ 'lastUpdatedContacts' => $lastUpdatedContactsCollection, - 'number_of_contacts' => $account->contacts()->real()->active()->count(), + 'number_of_contacts' => $account->contacts()->real()->addressBook()->active()->count(), 'number_of_reminders' => $account->reminders_count, 'number_of_notes' => $account->notes_count, 'number_of_activities' => $account->activities_count, diff --git a/app/Http/Controllers/Settings/SubscriptionsController.php b/app/Http/Controllers/Settings/SubscriptionsController.php index f9411fa40d2..4fbf4a441e7 100644 --- a/app/Http/Controllers/Settings/SubscriptionsController.php +++ b/app/Http/Controllers/Settings/SubscriptionsController.php @@ -178,7 +178,7 @@ public function downgrade() } return view('settings.subscriptions.downgrade-checklist') - ->with('numberOfActiveContacts', $account->contacts()->active()->count()) + ->with('numberOfActiveContacts', $account->contacts()->addressBook()->active()->count()) ->with('numberOfPendingInvitations', $account->invitations()->count()) ->with('numberOfUsers', $account->users()->count()) ->with('accountHasLimitations', AccountHelper::hasLimitations($account)) diff --git a/app/Jobs/SynchronizeAddressBooks.php b/app/Jobs/SynchronizeAddressBooks.php new file mode 100644 index 00000000000..578c1bdad43 --- /dev/null +++ b/app/Jobs/SynchronizeAddressBooks.php @@ -0,0 +1,52 @@ +addressBook = $addressBook; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + app(SynchronizeAddressBook::class)->execute([ + 'account_id' => $this->addressBook->account_id, + 'user_id' => $this->addressBook->user_id, + 'addressbook_id' => $this->addressBook->id, + /* + 'addressbook' => $this->addressBook->uri, + 'addressBookId' => $this->addressBook->addressBookId, + 'username' => $this->addressBook->username, + 'password' => $this->addressBook->password, + 'localSyncToken' => null, // TODO + 'syncToken' => $this->addressBook->syncToken, + 'capabilities' => $this->addressBook->capabilities, + */ + ]); + } +} diff --git a/app/Models/Account/AddressBook.php b/app/Models/Account/AddressBook.php new file mode 100644 index 00000000000..bf841058032 --- /dev/null +++ b/app/Models/Account/AddressBook.php @@ -0,0 +1,121 @@ +belongsTo(Account::class); + } + + /** + * Get the user record associated with the address book. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get all contacts for this address book. + * + * @return HasMany + */ + public function contacts() + { + return $this->hasMany(Contact::class); + } + + /** + * Get capabilities. + * + * @param string $value + * @return array + */ + public function getCapabilitiesAttribute($value) + { + return json_decode($value, true); + } + + /** + * Set capabilities. + * + * @param string $value + * @return void + */ + public function setCapabilitiesAttribute($value) + { + $this->attributes['capabilities'] = json_encode($value); + } + + /** + * Get password. + * + * @param string $value + * @return string + */ + public function getPasswordAttribute($value) + { + return Crypt::decryptString($value); + } + + /** + * Set password. + * + * @param string $value + * @return void + */ + public function setPasswordAttribute($value) + { + $this->attributes['password'] = Crypt::encryptString($value); + } +} diff --git a/app/Models/Contact/Contact.php b/app/Models/Contact/Contact.php index 2382189fa7c..eb042693958 100644 --- a/app/Models/Contact/Contact.php +++ b/app/Models/Contact/Contact.php @@ -35,6 +35,7 @@ use Illuminate\Contracts\Filesystem\FileNotFoundException; use App\Http\Resources\Contact\ContactShort as ContactShortResource; use App\Http\Resources\ContactField\ContactField as ContactFieldResource; +use App\Models\Account\AddressBook; /** * @property \App\Models\Instance\SpecialDate $birthdate @@ -549,6 +550,26 @@ public function scopeNotActive($query) return $query->where('is_active', 0); } + /** + * Scope a query to only include contacts from designated address book. + * + * @param Builder $query + * @param int|null $accountId + * @param mixed|null $addressBookId + * @return Builder + */ + public function scopeAddressBook($query, $accountId = null, $addressBookId = null) + { + $addressBook = null; + if ($accountId && $addressBookId) { + $addressBook = AddressBook::where([ + 'account_id' => $accountId, + 'addressBookId' => $addressBookId + ])->first(); + } + return $query->where('addressbook_id', $addressBook ? $addressBook->id : null); + } + /** * Mutator first_name. * Get the first name of the contact. diff --git a/app/Services/DavClient/AddAddressBook.php b/app/Services/DavClient/AddAddressBook.php index 5663f4927ad..86eef07852b 100644 --- a/app/Services/DavClient/AddAddressBook.php +++ b/app/Services/DavClient/AddAddressBook.php @@ -2,10 +2,11 @@ namespace App\Services\DavClient; +use App\Models\User\User; use Illuminate\Support\Arr; -use Illuminate\Support\Str; use App\Services\BaseService; use Sabre\VObject\Component\VCard; +use App\Models\Account\AddressBook; use App\Services\DavClient\Dav\Client; use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\ClientException; @@ -21,6 +22,10 @@ class AddAddressBook extends BaseService public function rules() { return [ + 'account_id' => 'required|integer|exists:accounts,id', + 'base_uri' => 'required|string|url', + 'username' => 'required|string', + 'password' => 'required|string', ]; } @@ -32,20 +37,63 @@ public function rules() public function execute(array $data, GuzzleClient $httpClient = null) { $data = [ + 'account_id' => 2, 'base_uri' => 'http://monica.test/dav/', 'username' => 'admin@admin.com', 'password' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZjUzYmRkYzJhNzQ0ZTgxZDRiYmUwMTQ2ODY1YTA3ZTIyMWM0ZTQ5YzkyNzJkN2FhZmE1ODk5ZDRmM2NkZGRlMWQyMWQ5MmM5M2E4YTNkMGMiLCJpYXQiOjE1ODQ4MDkxNDQsIm5iZiI6MTU4NDgwOTE0NCwiZXhwIjoxNjE2MzQ1MTQ0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.MUtcgy3PWk9McA59zx4SBJxKAkdSiGv1a9ZtVKwlgtk09bJEx1lJgymGSfDlrNqKvD2LqhTu0y95jLZNoj4-uM6DBZm3RMo18mw2xCEywB4st1hZpMYSYoOmtrOcsZweoP5r31zf_jzMX3mLde6MAeEkJcotGfO9z57M74FquLKixZRLvVruES2DcZoL1hwCKoxvv11BGRE78RQsWiipv0cfgmcSNEQVR820BWkM0X_4WwpufJdzZ5p1EpTy5AP2XXlx6amGXqxgMUIY7C-KyF1uw1Rmr6B-bTcMLJHZBH6TzU0yFoaJnhZZ9tJFyf7E70BL8SaO9_P6nA7ACjDREjAJBD9dZYrP46G-mqJXjWyVOcDVJZNW7dhF5vnEp7gghIVWhAm4lLy5nPI_CNpB0mqPrdkj57Avoi3MAEwf4ADy9CZp1EoLZIvNjBuMpgwwONTF5oP18NMaHJcsbFkmviY7eW-DIuIcNtCuoAM7Q4ulhuVX4tVry5NLsiab0_W8_l63C_n1-ICpv2t04jSh9H3SwgIXAZXhe-0vMt0gTIc3c_1HZ4eRd1kOuUs-708Esiq7J_Nt98PJZB8AP6qbeuScI0Cxnm7IulJ1WaI7mLjA7JPDvISeL2rrYjwqmguDbA8nQ7UjEq1dLN-PaAaL08p_iKU3ssPV2YziNSi_Alc', ]; + /* TESTS */ + + $this->validate($data); + + $addressbook = $this->getAddressBook($data, $httpClient); + + $lastAddressBook = AddressBook::where('account_id', $data['account_id']) + ->get() + ->last(); + + $lastId = 0; + if ($lastAddressBook) { + $lastId = intval(preg_replace('/\w+(\d+)/i', '$1', $lastAddressBook->identifier)); + } + $nextAddressBookId = 'contacts'.($lastId+1); + + $addressbook = AddressBook::create([ + 'account_id' => $data['account_id'], + 'user_id' => $data['user_id'], + 'addressBookId' => $nextAddressBookId, + 'username' => $data['username'], + ] + + $addressbook + ); + $addressbook->password = $data['password']; + $addressbook->save(); + + return $addressbook; + } + + public function getAddressBook(array $data, GuzzleClient $httpClient = null) + { $client = $this->getClient($data, $httpClient); - $addressbook = $this->getAddressBook($data, $client); + $uri = $this->getAddressBookBaseUri($data, $client); + + $client->setBaseUri($uri); - // save addressbook url + $capabilities = $this->getCapabilities($client); + + $name = $client->getProperty('{DAV:}displayname'); + + return [ + 'uri' => $uri, + 'capabilities' => $capabilities, + 'name' => $name, + ]; } - private function getAddressBook(array $data, Client $client) : string + private function getAddressBookBaseUri(array $data, Client $client) : string { try { @@ -67,15 +115,16 @@ private function getAddressBook(array $data, Client $client) : string $s = (string) $r->getBody(); } catch (\Exception $e) { } - } + /** + * @see https://tools.ietf.org/html/rfc2518#section-15 + */ private function checkOptions(Client $client) { $options = $client->options(); $options = explode(', ', $options[0]); - // https://tools.ietf.org/html/rfc2518#section-15 if (!in_array('1', $options) || !in_array('3', $options) || !in_array('addressbook', $options)) { throw new \Exception('server is not compliant with rfc2518 section 15.1, or rfc6352 section 6.1'); } @@ -88,11 +137,11 @@ private function getCurrentUserPrincipal(Client $client) : string { $prop = $client->getProperty('{DAV:}current-user-principal'); - if (is_null($prop)) { + if (is_null($prop) || count($prop) == 0) { throw new \Exception('Server does not support rfc 5397 section 3 (DAV:current-user-principal)'); } - return $prop; + return $prop[0]['value']; } /** @@ -102,14 +151,14 @@ private function getAddressBookHome(Client $client, string $principal) : string { $prop = $client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-home-set', $principal); - if (is_null($prop)) { + if (is_null($prop) || count($prop) == 0) { throw new \Exception('Server does not support rfc 6352 section 7.1.1 (CARD:addressbook-home-set)'); } - return $prop; + return $prop[0]['value']; } - private function getAddressBookUrl(Client $client, string $principal) : string + private function getAddressBookUrl(Client $client, string $principal) : ?string { $home = $this->getAddressBookHome($client, $principal); @@ -128,6 +177,53 @@ private function getAddressBookUrl(Client $client, string $principal) : string } } + private function getCapabilities(Client $client) + { + return $this->getSupportedReportSet($client) + + + $this->getSupportedAddressData($client); + } + + private function getSupportedReportSet(Client $client) + { + $supportedReportSet = $client->getSupportedReportSet(); + + $addressbookMultiget = in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet); + $syncCollection = in_array('{DAV:}sync-collection', $supportedReportSet); + + return [ + 'addressbookMultiget' => $addressbookMultiget, + 'syncCollection' => $syncCollection, + ]; + } + + private function getSupportedAddressData(Client $client) + { + // get the supported card format + $addressData = collect($client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}supported-address-data')); + $datas = $addressData->firstWhere('attributes.version', '4.0'); + if (!$datas) { + $datas = $addressData->firstWhere('attributes.version', '3.0'); + } + + if (!$datas) { + // It should not happen ! + $datas = [ + 'attributes' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ]; + } + + return [ + 'addressData' => [ + 'content-type' => $datas['attributes']['content-type'], + 'version' => $datas['attributes']['version'], + ], + ]; + } + private function getClient(array $data, GuzzleClient $client = null) : Client { $settings = Arr::only($data, [ diff --git a/app/Services/DavClient/Dav/Client.php b/app/Services/DavClient/Dav/Client.php index 59dd51ca36d..ca933509ef7 100644 --- a/app/Services/DavClient/Dav/Client.php +++ b/app/Services/DavClient/Dav/Client.php @@ -314,13 +314,7 @@ public function getProperty(string $property, string $url = '') return $prop; } else if (is_array($prop)) { $value = $prop[0]; - if (is_string($value)) { - return $value; - //} else if (is_array($value)) { - // return Arr::get($value, 'value', $value); - } else { - return $prop; - } + return is_string($value) ? $value : $prop; } return $prop; @@ -482,7 +476,6 @@ public function request(string $method, string $url = '', ?string $body = null, * * * @param string $body xml body - * * @return array */ public function parseMultiStatus(string $body) : array diff --git a/app/Services/DavClient/SynchronizeAddressBook.php b/app/Services/DavClient/SynchronizeAddressBook.php index 2155b3dc4ff..b1a28d1c3dc 100644 --- a/app/Services/DavClient/SynchronizeAddressBook.php +++ b/app/Services/DavClient/SynchronizeAddressBook.php @@ -14,6 +14,7 @@ use GuzzleHttp\Exception\ClientException; use Sabre\CardDAV\Plugin as CardDAVPlugin; use App\Http\Controllers\DAV\Backend\CardDAV\CardDAVBackend; +use App\Models\Account\AddressBook; class SynchronizeAddressBook extends BaseService { @@ -27,12 +28,16 @@ public function rules() return [ 'account_id' => 'required|integer|exists:accounts,id', 'user_id' => 'required|integer|exists:users,id', + 'addressbook_id' => 'required|integer|exists:addressbooks,id', + /* 'addressbook' => 'required|string|url', - 'addressbookId' => 'required|integer', + 'addressBookId' => 'required|string', 'username' => 'required|string', 'password' => 'required|string', + 'capabilities' => 'required|array', 'localSyncToken' => 'nullable|integer|exists:syncToken,id', 'syncToken' => 'nullable|string', + */ ]; } @@ -43,38 +48,26 @@ public function rules() */ public function execute(array $data, GuzzleClient $httpClient = null) { - $data = [ - 'account_id' => 1, - 'user_id' => 1, - 'addressbook' => 'http://monica.test/dav/addressbooks/admin@admin.com/contacts/', - 'addressbookId' => 0, - 'username' => 'admin@admin.com', - 'password' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZjUzYmRkYzJhNzQ0ZTgxZDRiYmUwMTQ2ODY1YTA3ZTIyMWM0ZTQ5YzkyNzJkN2FhZmE1ODk5ZDRmM2NkZGRlMWQyMWQ5MmM5M2E4YTNkMGMiLCJpYXQiOjE1ODQ4MDkxNDQsIm5iZiI6MTU4NDgwOTE0NCwiZXhwIjoxNjE2MzQ1MTQ0LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.MUtcgy3PWk9McA59zx4SBJxKAkdSiGv1a9ZtVKwlgtk09bJEx1lJgymGSfDlrNqKvD2LqhTu0y95jLZNoj4-uM6DBZm3RMo18mw2xCEywB4st1hZpMYSYoOmtrOcsZweoP5r31zf_jzMX3mLde6MAeEkJcotGfO9z57M74FquLKixZRLvVruES2DcZoL1hwCKoxvv11BGRE78RQsWiipv0cfgmcSNEQVR820BWkM0X_4WwpufJdzZ5p1EpTy5AP2XXlx6amGXqxgMUIY7C-KyF1uw1Rmr6B-bTcMLJHZBH6TzU0yFoaJnhZZ9tJFyf7E70BL8SaO9_P6nA7ACjDREjAJBD9dZYrP46G-mqJXjWyVOcDVJZNW7dhF5vnEp7gghIVWhAm4lLy5nPI_CNpB0mqPrdkj57Avoi3MAEwf4ADy9CZp1EoLZIvNjBuMpgwwONTF5oP18NMaHJcsbFkmviY7eW-DIuIcNtCuoAM7Q4ulhuVX4tVry5NLsiab0_W8_l63C_n1-ICpv2t04jSh9H3SwgIXAZXhe-0vMt0gTIc3c_1HZ4eRd1kOuUs-708Esiq7J_Nt98PJZB8AP6qbeuScI0Cxnm7IulJ1WaI7mLjA7JPDvISeL2rrYjwqmguDbA8nQ7UjEq1dLN-PaAaL08p_iKU3ssPV2YziNSi_Alc', - 'localSyncToken' => '22', - 'syncToken' => 'http://sabre.io/ns/sync/15', - ]; - - /* TESTS */ - $this->validate($data); $user = User::where('account_id', $data['account_id']) ->findOrFail($data['user_id']); + $addressbook = AddressBook::where('account_id', $data['account_id']) + ->findOrFail($data['addressbook_id']); + $backend = new CardDAVBackend($user->account, $user); try { - $client = $this->getClient($data, $httpClient); - - $localChanges = $backend->getChangesForAddressBook($data['addressbookId'], $data['localSyncToken'], 1); + $client = $this->getClient($addressbook, $httpClient); - $supportedReportSet = $client->getSupportedReportSet(); + $localChanges = $backend->getChangesForAddressBook($addressbook->addressBookId, $addressbook->localSyncToken, 1); - $refresh = $this->distantChanges($data, $client, $supportedReportSet, $backend); + $changes = $this->distantChanges($addressbook, $client, $backend); - $this->refreshContacts($data, $client, $supportedReportSet, $backend, $refresh); + $this->getContacts($addressbook, $client, $backend, $changes); - $this->pushContacts($data, $client, $supportedReportSet, $backend, $refresh, $localChanges); + $this->pushContacts($addressbook, $client, $backend, $changes, $localChanges); } catch (ClientException $e) { $r = $e->getResponse(); @@ -84,20 +77,20 @@ public function execute(array $data, GuzzleClient $httpClient = null) } } - private function distantChanges(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend) + private function distantChanges($addressbook, Client $client, CardDAVBackend $backend) { - $distantChanges = $this->getContactsList($data, $client, $supportedReportSet) ?? []; + $distantChanges = $this->getContactsList($addressbook, $client) ?? []; $refresh = collect(); foreach ($distantChanges as $href => $contact) { if (isset($contact[200]) && Str::contains($contact[200]['{DAV:}getcontenttype'], 'text/vcard')) { - $localContact = $backend->getCard($data['addressbookId'], $href); + $localContact = $backend->getCard($addressbook->addressBookId, $href); $etag = $contact[200]['{DAV:}getetag']; if ($localContact !== false && $localContact['etag'] == $etag) { // contact already exist, and the etag is the same - //continue; + continue; } $refresh->push([ @@ -110,68 +103,17 @@ private function distantChanges(array $data, Client $client, array $supportedRep return $refresh; } - private function refreshContacts(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend, Collection $refresh) + private function getContacts($addressbook, Client $client, CardDAVBackend $backend, Collection $refresh) { - $refreshContacts = collect(); - if (in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet)) { - - // get the supported card format - $addressData = collect($client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}supported-address-data')); - $datas = $addressData->firstWhere('attributes.version', '4.0'); - if (!$datas) { - $datas = $addressData->firstWhere('attributes.version', '3.0'); - } - - if (!$datas) { - // It should not happen ! - $datas = [ - 'attributes' => [ - 'content-type' => 'text/vcard', - 'version' => '4.0', - ], - ]; - } - - $hrefs = $refresh->pluck('href'); - $datas = $client->addressbookMultiget('', [ - '{DAV:}getetag', - [ - 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', - 'value' => null, - 'attributes' => [ - 'content-type' => $datas['attributes']['content-type'], - 'version' => $datas['attributes']['version'], - ], - ] - ], $hrefs); - - foreach ($datas as $href => $contact) { - if (isset($contact[200])) { - $refreshContacts->push([ - 'href' => $href, - 'etag' => $contact[200]['{DAV:}getetag'], - 'vcard' => $contact[200]['{'.CardDAVPlugin::NS_CARDDAV.'}address-data'], - ]); - } - } - + if (Arr::get($addressbook->capabilities, 'addressbookMultiget', false)) { + $refreshContacts = $this->refreshMultigetContacts($addressbook, $client, $refresh); } else { - - foreach ($refresh as $contact) { - $c = $client->request('GET', $contact['href']); - if ($c['statusCode'] === 200) { - $refreshContacts->push([ - 'href' => $contact['href'], - 'etag' => $contact['etag'], - 'vcard' => $c['body'], - ]); - } - } + $refreshContacts = $this->refreshSimpleGetContacts($client, $refresh); } foreach ($refreshContacts as $contact) { - $newtag = $backend->updateCard($data['addressbookId'], $contact['href'], $contact['vcard']); + $newtag = $backend->updateCard($addressbook->addressBookId, $contact['href'], $contact['vcard']); if ($newtag != $contact['etag']) { Log::warning(__CLASS__.' refreshContacts: wrong etag update'); @@ -179,7 +121,55 @@ private function refreshContacts(array $data, Client $client, array $supportedRe } } - private function pushContacts(array $data, Client $client, array $supportedReportSet, CardDAVBackend $backend, Collection $refresh, array $localChanges) + private function refreshMultigetContacts($addressbook, Client $client, Collection $refresh) + { + $addressDataAttributes = Arr::get($addressbook->capabilities, 'addressData', [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ]); + + $hrefs = $refresh->pluck('href'); + $datas = $client->addressbookMultiget('', [ + '{DAV:}getetag', + [ + 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + 'value' => null, + 'attributes' => $addressDataAttributes, + ] + ], $hrefs); + + return collect($datas) + ->filter(function ($contact) { + return isset($contact[200]); + }) + ->map(function($contact, $href) { + return [ + 'href' => $href, + 'etag' => $contact[200]['{DAV:}getetag'], + 'vcard' => $contact[200]['{'.CardDAVPlugin::NS_CARDDAV.'}address-data'], + ]; + }); + } + + private function refreshSimpleGetContacts(Client $client, Collection $refresh) + { + $refreshContacts = collect(); + + foreach ($refresh as $contact) { + $c = $client->request('GET', $contact['href']); + if ($c['statusCode'] === 200) { + $refreshContacts->push([ + 'href' => $contact['href'], + 'etag' => $contact['etag'], + 'vcard' => $c['body'], + ]); + } + } + + return $refreshContacts; + } + + private function pushContacts($addressbook, Client $client, CardDAVBackend $backend, Collection $refresh, array $localChanges) { $refreshIds = $refresh->pluck('href'); @@ -192,21 +182,26 @@ private function pushContacts(array $data, Client $client, array $supportedRepor return $refreshIds->contains($uuid); }); - foreach ($localChangesAdd->union($localChangesUpdate) as $uri) + foreach ($localChangesUpdate as $uri) { - $contact = $backend->getCard($data['addressbookId'], $uri); + $contact = $backend->getCard($addressbook->addressBookId, $uri); $client->request('PUT', $uri, $contact['carddata'], [ 'If-Match' => '*' ]); } + foreach ($localChangesAdd as $uri) + { + $contact = $backend->getCard($addressbook->addressBookId, $uri); + $client->request('PUT', $uri, $contact['carddata'], []); + } } - private function getContactsList(array $data, Client $client, $supportedReportSet): array + private function getContactsList($addressbook, Client $client): array { // With sync-collection - if (in_array('{DAV:}sync-collection', $supportedReportSet)) { + if (Arr::get($addressbook->capabilities, 'syncCollection', false)) { - $syncToken = Arr::get($data, 'syncToken', ''); + $syncToken = $addressbook->syncToken ?? ''; // get the current distant syncToken $distantSyncToken = $client->getProperty('{DAV:}sync-token'); @@ -223,9 +218,8 @@ private function getContactsList(array $data, Client $client, $supportedReportSe ], $syncToken); // save the new syncToken as current one - $newSyncToken = $collection['synctoken']; - - // TODO + $addressbook->syncToken = $collection['synctoken']; + $addressbook->save(); } else { @@ -241,14 +235,12 @@ private function getContactsList(array $data, Client $client, $supportedReportSe - private function getClient(array $data, GuzzleClient $client = null) : Client + private function getClient($addressbook, GuzzleClient $client = null) : Client { - $settings = [ - 'base_uri' => $data['addressbook'], - 'username' => $data['username'], - 'password' => $data['password'], - ]; - - return new Client($settings, $client); + return new Client([ + 'base_uri' => $addressbook->uri, + 'username' => $addressbook->username, + 'password' => $addressbook->password, + ], $client); } } diff --git a/app/Services/VCard/ImportVCard.php b/app/Services/VCard/ImportVCard.php index 7476cd4542b..9a7e8cf35c5 100644 --- a/app/Services/VCard/ImportVCard.php +++ b/app/Services/VCard/ImportVCard.php @@ -19,6 +19,7 @@ use App\Models\Contact\Contact; use Illuminate\Validation\Rule; use App\Helpers\CountriesHelper; +use App\Models\Account\AddressBook; use Sabre\VObject\ParseException; use Sabre\VObject\Component\VCard; use App\Models\Contact\ContactField; @@ -85,6 +86,11 @@ class ImportVCard extends BaseService */ protected $genders; + /** + * @var AddressBook + */ + protected $addressBook; + /** * Get the validation rules that apply to the service. * @@ -109,6 +115,7 @@ function ($attribute, $value, $fail) { 'required', Rule::in(self::$behaviourTypes), ], + 'addressBookId' => 'nullable|string|exists:addressbooks,addressBookId', ]; } @@ -130,6 +137,13 @@ public function execute(array $data): array ->findOrFail($contactId); } + if ($addressBookId = Arr::get($data, 'addressBookId')) { + AddressBook::where([ + 'account_id' => $data['account_id'], + 'addressBookId' => $addressBookId, + ])->firstOrFail(); + } + return $this->process($data); } @@ -139,6 +153,7 @@ private function clear() $this->genders = []; $this->accountId = 0; $this->userId = 0; + $this->addressBook = null; } /** @@ -155,6 +170,13 @@ private function process(array $data): array } $this->userId = $data['user_id']; + if ($addressBookId = Arr::get($data, 'addressBookId')) { + $this->addressBook = AddressBook::where([ + 'account_id' => $data['account_id'], + 'addressBookId' => $addressBookId, + ])->first(); + } + $entry = $this->getEntry($data); if (! $entry) { @@ -370,7 +392,10 @@ private function getExistingContact(VCard $entry, $contact_id = null) { $contact = null; if (! is_null($contact_id)) { - $contact = Contact::where('account_id', $this->accountId) + $contact = Contact::where([ + 'account_id' => $this->accountId, + 'addressbook_id' => $this->addressBook ? $this->addressBook->id : null, + ]) ->find($contact_id); } @@ -403,7 +428,12 @@ private function existingContactWithEmail(VCard $entry) 'contact_field_type_id' => $this->getContactFieldTypeId(ContactFieldType::EMAIL), ])->whereIn('data', iterator_to_array($entry->EMAIL))->first(); - if ($contactField) { + if ($contactField && + ( + $this->addressBook && $contactField->contact->addressbook_id == $this->addressBook->id + || !$this->addressBook + ) + ) { return $contactField->contact; } } @@ -425,6 +455,7 @@ private function existingContactWithName(VCard $entry) 'first_name' => $contact->first_name, 'middle_name' => $contact->middle_name, 'last_name' => $contact->last_name, + 'addressbook_id' => $this->addressBook ? $this->addressBook->id : null, ])->first(); } @@ -442,8 +473,13 @@ private function importEntry($contact, VCard $entry): Contact $contact->account_id = $this->accountId; $contact->gender_id = $this->getGender('O')->id; $contact->setAvatarColor(); - $contact->uuid = Str::uuid()->toString(); + $contact->addressbook_id = $this->addressBook ? $this->addressBook->id : null; $contact->save(); + + $this->importUid($contact, $entry); + if (empty($contact->uuid)) { + $contact->uuid = Str::uuid()->toString(); + } } $this->importNames($contact, $entry); @@ -458,6 +494,11 @@ private function importEntry($contact, VCard $entry): Contact $this->importSocialProfile($contact, $entry); $this->importCategories($contact, $entry); + // Save vcard content + if ($contact->addressbook_id) { + $contact->vcard = $entry->serialize(); + } + $contact->save(); return $contact; diff --git a/database/migrations/2020_03_25_055551_add_address_book.php b/database/migrations/2020_03_25_055551_add_address_book.php new file mode 100644 index 00000000000..47a57a3380a --- /dev/null +++ b/database/migrations/2020_03_25_055551_add_address_book.php @@ -0,0 +1,45 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + + $table->string('uri', 2096); + $table->string('name', 500)->nullable(); + $table->string('addressBookId', 100); + $table->string('capabilities', 1024); + $table->string('username', 1024); + $table->string('password', 2048); + $table->string('syncToken', 512)->nullable(); + $table->timestamps(); + + $table->index('addressBookId'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('addressbooks'); + } +} diff --git a/database/migrations/2020_03_25_082324_add_contact_address_book_id.php b/database/migrations/2020_03_25_082324_add_contact_address_book_id.php new file mode 100644 index 00000000000..45e247dd039 --- /dev/null +++ b/database/migrations/2020_03_25_082324_add_contact_address_book_id.php @@ -0,0 +1,33 @@ +unsignedBigInteger('addressbook_id')->after('account_id')->nullable(); + $table->foreign('addressbook_id')->references('id')->on('addressbooks')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('addressbook_id'); + }); + } +} diff --git a/database/migrations/2020_03_25_201407_add_contact_vcard_data.php b/database/migrations/2020_03_25_201407_add_contact_vcard_data.php new file mode 100644 index 00000000000..2e5bdf48e1f --- /dev/null +++ b/database/migrations/2020_03_25_201407_add_contact_vcard_data.php @@ -0,0 +1,32 @@ +string('vcard', 65536)->after('gravatar_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('vcard'); + }); + } +} diff --git a/resources/views/people/introductions/edit.blade.php b/resources/views/people/introductions/edit.blade.php index 2fdc99e8221..a53acc95897 100644 --- a/resources/views/people/introductions/edit.blade.php +++ b/resources/views/people/introductions/edit.blade.php @@ -49,7 +49,7 @@ - @foreach (auth()->user()->account->contacts()->real()->active()->get() as $metThroughContact) + @foreach (auth()->user()->account->contacts()->real()->addressBook()->active()->get() as $metThroughContact) @if ($metThroughContact->id != $contact->id)