Passed
Push — master ( b85c2d...d5c2e7 )
by Christoph
15:18 queued 13s
created

SyncService::getCardUri()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bjoern Schiessle <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Thomas Citharel <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Anna Larch <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
namespace OCA\DAV\CardDAV;
31
32
use OC\Accounts\AccountManager;
33
use OCP\AppFramework\Http;
34
use OCP\IUser;
35
use OCP\IUserManager;
36
use Psr\Log\LoggerInterface;
37
use Sabre\DAV\Client;
38
use Sabre\DAV\Xml\Response\MultiStatus;
39
use Sabre\DAV\Xml\Service;
40
use Sabre\HTTP\ClientHttpException;
41
use Sabre\VObject\Reader;
42
43
class SyncService {
44
	private CardDavBackend $backend;
45
	private IUserManager $userManager;
46
	private LoggerInterface $logger;
47
	private ?array $localSystemAddressBook = null;
48
	private Converter $converter;
49
	protected string $certPath;
50
51
	public function __construct(CardDavBackend $backend,
52
								IUserManager $userManager,
53
								LoggerInterface $logger,
54
								Converter $converter) {
55
		$this->backend = $backend;
56
		$this->userManager = $userManager;
57
		$this->logger = $logger;
58
		$this->converter = $converter;
59
		$this->certPath = '';
60
	}
61
62
	/**
63
	 * @throws \Exception
64
	 */
65
	public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
66
		// 1. create addressbook
67
		$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $book is correct as $this->ensureSystemAddre...ash, $targetProperties) targeting OCA\DAV\CardDAV\SyncServ...stemAddressBookExists() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
68
		$addressBookId = $book['id'];
69
70
		// 2. query changes
71
		try {
72
			$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
73
		} catch (ClientHttpException $ex) {
74
			if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
75
				// remote server revoked access to the address book, remove it
76
				$this->backend->deleteAddressBook($addressBookId);
77
				$this->logger->error('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
78
				throw $ex;
79
			}
80
			$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
81
			throw $ex;
82
		}
83
84
		// 3. apply changes
85
		// TODO: use multi-get for download
86
		foreach ($response['response'] as $resource => $status) {
87
			$cardUri = basename($resource);
88
			if (isset($status[200])) {
89
				$vCard = $this->download($url, $userName, $sharedSecret, $resource);
90
				$existingCard = $this->backend->getCard($addressBookId, $cardUri);
91
				if ($existingCard === false) {
92
					$this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
93
				} else {
94
					$this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
95
				}
96
			} else {
97
				$this->backend->deleteCard($addressBookId, $cardUri);
98
			}
99
		}
100
101
		return $response['token'];
102
	}
103
104
	/**
105
	 * @throws \Sabre\DAV\Exception\BadRequest
106
	 */
107
	public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
108
		$book = $this->backend->getAddressBooksByUri($principal, $uri);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $book is correct as $this->backend->getAddre...ByUri($principal, $uri) targeting OCA\DAV\CardDAV\CardDavB...:getAddressBooksByUri() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
109
		if (!is_null($book)) {
0 ignored issues
show
introduced by
The condition is_null($book) is always true.
Loading history...
110
			return $book;
111
		}
112
		// FIXME This might break in clustered DB setup
113
		$this->backend->createAddressBook($principal, $uri, $properties);
114
115
		return $this->backend->getAddressBooksByUri($principal, $uri);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->backend->getAddre...ByUri($principal, $uri) targeting OCA\DAV\CardDAV\CardDavB...:getAddressBooksByUri() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
116
	}
117
118
	/**
119
	 * Check if there is a valid certPath we should use
120
	 */
121
	protected function getCertPath(): string {
122
123
		// we already have a valid certPath
124
		if ($this->certPath !== '') {
125
			return $this->certPath;
126
		}
127
128
		$certManager = \OC::$server->getCertificateManager();
129
		$certPath = $certManager->getAbsoluteBundlePath();
130
		if (file_exists($certPath)) {
131
			$this->certPath = $certPath;
132
		}
133
134
		return $this->certPath;
135
	}
136
137
	protected function getClient(string $url, string $userName, string $sharedSecret): Client {
138
		$settings = [
139
			'baseUri' => $url . '/',
140
			'userName' => $userName,
141
			'password' => $sharedSecret,
142
		];
143
		$client = new Client($settings);
144
		$certPath = $this->getCertPath();
145
		$client->setThrowExceptions(true);
146
147
		if ($certPath !== '' && strpos($url, 'http://') !== 0) {
148
			$client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
149
		}
150
151
		return $client;
152
	}
153
154
	protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
155
		$client = $this->getClient($url, $userName, $sharedSecret);
156
157
		$body = $this->buildSyncCollectionRequestBody($syncToken);
158
159
		$response = $client->request('REPORT', $addressBookUrl, $body, [
160
			'Content-Type' => 'application/xml'
161
		]);
162
163
		return $this->parseMultiStatus($response['body']);
164
	}
165
166
	protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): array {
167
		$client = $this->getClient($url, $userName, $sharedSecret);
168
		return $client->request('GET', $resourcePath);
169
	}
170
171
	private function buildSyncCollectionRequestBody(?string $syncToken): string {
172
		$dom = new \DOMDocument('1.0', 'UTF-8');
173
		$dom->formatOutput = true;
174
		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
175
		$sync = $dom->createElement('d:sync-token', $syncToken);
176
		$prop = $dom->createElement('d:prop');
177
		$cont = $dom->createElement('d:getcontenttype');
178
		$etag = $dom->createElement('d:getetag');
179
180
		$prop->appendChild($cont);
181
		$prop->appendChild($etag);
182
		$root->appendChild($sync);
183
		$root->appendChild($prop);
184
		$dom->appendChild($root);
185
		return $dom->saveXML();
186
	}
187
188
	/**
189
	 * @param string $body
190
	 * @return array
191
	 * @throws \Sabre\Xml\ParseException
192
	 */
193
	private function parseMultiStatus($body) {
194
		$xml = new Service();
195
196
		/** @var MultiStatus $multiStatus */
197
		$multiStatus = $xml->expect('{DAV:}multistatus', $body);
198
199
		$result = [];
200
		foreach ($multiStatus->getResponses() as $response) {
201
			$result[$response->getHref()] = $response->getResponseProperties();
202
		}
203
204
		return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
205
	}
206
207
	/**
208
	 * @param IUser $user
209
	 */
210
	public function updateUser(IUser $user) {
211
		$systemAddressBook = $this->getLocalSystemAddressBook();
212
		$addressBookId = $systemAddressBook['id'];
213
214
		$cardId = self::getCardUri($user);
215
		if ($user->isEnabled()) {
216
			$card = $this->backend->getCard($addressBookId, $cardId);
217
			if ($card === false) {
0 ignored issues
show
introduced by
The condition $card === false is always false.
Loading history...
218
				$vCard = $this->converter->createCardFromUser($user);
219
				if ($vCard !== null) {
220
					$this->backend->createCard($addressBookId, $cardId, $vCard->serialize(), false);
221
				}
222
			} else {
223
				$vCard = $this->converter->createCardFromUser($user);
224
				if (is_null($vCard)) {
225
					$this->backend->deleteCard($addressBookId, $cardId);
226
				} else {
227
					$this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
228
				}
229
			}
230
		} else {
231
			$this->backend->deleteCard($addressBookId, $cardId);
232
		}
233
	}
234
235
	/**
236
	 * @param IUser|string $userOrCardId
237
	 */
238
	public function deleteUser($userOrCardId) {
239
		$systemAddressBook = $this->getLocalSystemAddressBook();
240
		if ($userOrCardId instanceof IUser) {
241
			$userOrCardId = self::getCardUri($userOrCardId);
242
		}
243
		$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
244
	}
245
246
	/**
247
	 * @return array|null
248
	 */
249
	public function getLocalSystemAddressBook() {
250
		if (is_null($this->localSystemAddressBook)) {
251
			$systemPrincipal = "principals/system/system";
252
			$this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->localSystemAddressBook is correct as $this->ensureSystemAddre...ers of this instance')) targeting OCA\DAV\CardDAV\SyncServ...stemAddressBookExists() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
253
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
254
			]);
255
		}
256
257
		return $this->localSystemAddressBook;
258
	}
259
260
	public function syncInstance(\Closure $progressCallback = null) {
261
		$systemAddressBook = $this->getLocalSystemAddressBook();
262
		$this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback) {
0 ignored issues
show
Unused Code introduced by
The import $systemAddressBook is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
263
			$this->updateUser($user);
264
			if (!is_null($progressCallback)) {
265
				$progressCallback();
266
			}
267
		});
268
269
		// remove no longer existing
270
		$allCards = $this->backend->getCards($systemAddressBook['id']);
271
		foreach ($allCards as $card) {
272
			$vCard = Reader::read($card['carddata']);
273
			$uid = $vCard->UID->getValue();
0 ignored issues
show
Bug introduced by
The method getValue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

273
			/** @scrutinizer ignore-call */ 
274
   $uid = $vCard->UID->getValue();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
274
			// load backend and see if user exists
275
			if (!$this->userManager->userExists($uid)) {
276
				$this->deleteUser($card['uri']);
277
			}
278
		}
279
	}
280
281
	/**
282
	 * @param IUser $user
283
	 * @return string
284
	 */
285
	public static function getCardUri(IUser $user): string {
286
		return $user->getBackendClassName() . ':' . $user->getUID() . '.vcf';
287
	}
288
}
289