Completed
Push — master ( b79211...94ab2e )
by Morris
27:13 queued 14:34
created

SyncService::getCertPath()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 3
nop 0
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Björn Schießle <[email protected]>
6
 * @author Roeland Jago Douma <[email protected]>
7
 * @author Thomas Müller <[email protected]>
8
 *
9
 * @license AGPL-3.0
10
 *
11
 * This code is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU Affero General Public License, version 3,
13
 * as published by the Free Software Foundation.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License, version 3,
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
22
 *
23
 */
24
25
namespace OCA\DAV\CardDAV;
26
27
use OC\Accounts\AccountManager;
28
use OCP\AppFramework\Http;
29
use OCP\ICertificateManager;
30
use OCP\ILogger;
31
use OCP\IUser;
32
use OCP\IUserManager;
33
use Sabre\DAV\Client;
34
use Sabre\DAV\Xml\Response\MultiStatus;
35
use Sabre\DAV\Xml\Service;
36
use Sabre\HTTP\ClientHttpException;
37
use Sabre\VObject\Reader;
38
39
class SyncService {
40
41
	/** @var CardDavBackend */
42
	private $backend;
43
44
	/** @var IUserManager */
45
	private $userManager;
46
47
	/** @var ILogger */
48
	private $logger;
49
50
	/** @var array */
51
	private $localSystemAddressBook;
52
53
	/** @var AccountManager */
54
	private $accountManager;
55
56
	/** @var string */
57
	protected $certPath;
58
59
	/**
60
	 * SyncService constructor.
61
	 *
62
	 * @param CardDavBackend $backend
63
	 * @param IUserManager $userManager
64
	 * @param ILogger $logger
65
	 * @param AccountManager $accountManager
66
	 */
67
	public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) {
68
		$this->backend = $backend;
69
		$this->userManager = $userManager;
70
		$this->logger = $logger;
71
		$this->accountManager = $accountManager;
72
		$this->certPath = '';
73
	}
74
75
	/**
76
	 * @param string $url
77
	 * @param string $userName
78
	 * @param string $sharedSecret
79
	 * @param string $syncToken
80
	 * @param int $targetBookId
81
	 * @param string $targetPrincipal
82
	 * @param array $targetProperties
83
	 * @return string
84
	 * @throws \Exception
85
	 */
86
	public function syncRemoteAddressBook($url, $userName, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
87
		// 1. create addressbook
88
		$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties);
89
		$addressBookId = $book['id'];
90
91
		// 2. query changes
92
		try {
93
			$response = $this->requestSyncReport($url, $userName, $sharedSecret, $syncToken);
94
		} catch (ClientHttpException $ex) {
0 ignored issues
show
Bug introduced by
The class Sabre\HTTP\ClientHttpException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
95
			if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
96
				// remote server revoked access to the address book, remove it
97
				$this->backend->deleteAddressBook($addressBookId);
98
				$this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
99
				throw $ex;
100
			}
101
		}
102
103
		// 3. apply changes
104
		// TODO: use multi-get for download
105
		foreach ($response['response'] as $resource => $status) {
106
			$cardUri = basename($resource);
107
			if (isset($status[200])) {
108
				$vCard = $this->download($url, $sharedSecret, $resource);
109
				$existingCard = $this->backend->getCard($addressBookId, $cardUri);
110
				if ($existingCard === false) {
111
					$this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
112
				} else {
113
					$this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
114
				}
115
			} else {
116
				$this->backend->deleteCard($addressBookId, $cardUri);
117
			}
118
		}
119
120
		return $response['token'];
121
	}
122
123
	/**
124
	 * @param string $principal
125
	 * @param string $id
126
	 * @param array $properties
127
	 * @return array|null
128
	 * @throws \Sabre\DAV\Exception\BadRequest
129
	 */
130
	public function ensureSystemAddressBookExists($principal, $id, $properties) {
131
		$book = $this->backend->getAddressBooksByUri($principal, $id);
132
		if (!is_null($book)) {
133
			return $book;
134
		}
135
		$this->backend->createAddressBook($principal, $id, $properties);
136
137
		return $this->backend->getAddressBooksByUri($principal, $id);
138
	}
139
140
	/**
141
	 * Check if there is a valid certPath we should use
142
	 *
143
	 * @return string
144
	 */
145
	protected function getCertPath() {
146
147
		// we already have a valid certPath
148
		if ($this->certPath !== '') {
149
			return $this->certPath;
150
		}
151
152
		/** @var ICertificateManager $certManager */
153
		$certManager = \OC::$server->getCertificateManager(null);
154
		$certPath = $certManager->getAbsoluteBundlePath();
155
		if (file_exists($certPath)) {
156
			$this->certPath = $certPath;
157
		}
158
159
		return $this->certPath;
160
	}
161
162
	/**
163
	 * @param string $url
164
	 * @param string $userName
165
	 * @param string $sharedSecret
166
	 * @return Client
167
	 */
168
	protected function getClient($url, $userName, $sharedSecret) {
169
		$settings = [
170
			'baseUri' => $url . '/',
171
			'userName' => $userName,
172
			'password' => $sharedSecret,
173
		];
174
		$client = new Client($settings);
175
		$certPath = $this->getCertPath();
176
		$client->setThrowExceptions(true);
177
178
		if ($certPath !== '' && strpos($url, 'http://') !== 0) {
179
			$client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
180
		}
181
182
		return $client;
183
	}
184
185
	/**
186
	 * @param string $url
187
	 * @param string $userName
188
	 * @param string $sharedSecret
189
	 * @param string $syncToken
190
	 * @return array
191
	 */
192
	protected function requestSyncReport($url, $userName, $sharedSecret, $syncToken) {
193
		$client = $this->getClient($url, $userName, $sharedSecret);
194
195
		$addressBookUrl = "remote.php/dav/addressbooks/system/system/system";
196
		$body = $this->buildSyncCollectionRequestBody($syncToken);
197
198
		$response = $client->request('REPORT', $addressBookUrl, $body, [
199
			'Content-Type' => 'application/xml'
200
		]);
201
202
		return $this->parseMultiStatus($response['body']);
203
	}
204
205
	/**
206
	 * @param string $url
207
	 * @param string $sharedSecret
208
	 * @param string $resourcePath
209
	 * @return array
210
	 */
211
	protected function download($url, $sharedSecret, $resourcePath) {
212
		$client = $this->getClient($url, 'system', $sharedSecret);
213
		return $client->request('GET', $resourcePath);
214
	}
215
216
	/**
217
	 * @param string|null $syncToken
218
	 * @return string
219
	 */
220
	private function buildSyncCollectionRequestBody($syncToken) {
221
222
		$dom = new \DOMDocument('1.0', 'UTF-8');
223
		$dom->formatOutput = true;
224
		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
225
		$sync = $dom->createElement('d:sync-token', $syncToken);
226
		$prop = $dom->createElement('d:prop');
227
		$cont = $dom->createElement('d:getcontenttype');
228
		$etag = $dom->createElement('d:getetag');
229
230
		$prop->appendChild($cont);
231
		$prop->appendChild($etag);
232
		$root->appendChild($sync);
233
		$root->appendChild($prop);
234
		$dom->appendChild($root);
235
		$body = $dom->saveXML();
236
237
		return $body;
238
	}
239
240
	/**
241
	 * @param string $body
242
	 * @return array
243
	 * @throws \Sabre\Xml\ParseException
244
	 */
245
	private function parseMultiStatus($body) {
246
		$xml = new Service();
247
248
		/** @var MultiStatus $multiStatus */
249
		$multiStatus = $xml->expect('{DAV:}multistatus', $body);
250
251
		$result = [];
252
		foreach ($multiStatus->getResponses() as $response) {
253
			$result[$response->getHref()] = $response->getResponseProperties();
254
		}
255
256
		return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
257
	}
258
259
	/**
260
	 * @param IUser $user
261
	 */
262
	public function updateUser($user) {
263
		$systemAddressBook = $this->getLocalSystemAddressBook();
264
		$addressBookId = $systemAddressBook['id'];
265
		$converter = new Converter($this->accountManager);
266
		$name = $user->getBackendClassName();
267
		$userId = $user->getUID();
268
269
		$cardId = "$name:$userId.vcf";
270
		$card = $this->backend->getCard($addressBookId, $cardId);
271
		if ($card === false) {
272
			$vCard = $converter->createCardFromUser($user);
273
			if ($vCard !== null) {
274
				$this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
275
			}
276
		} else {
277
			$vCard = $converter->createCardFromUser($user);
278
			if (is_null($vCard)) {
279
				$this->backend->deleteCard($addressBookId, $cardId);
280
			} else {
281
				$this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
282
			}
283
		}
284
	}
285
286
	/**
287
	 * @param IUser|string $userOrCardId
288
	 */
289
	public function deleteUser($userOrCardId) {
290
		$systemAddressBook = $this->getLocalSystemAddressBook();
291
		if ($userOrCardId instanceof IUser){
292
			$name = $userOrCardId->getBackendClassName();
293
			$userId = $userOrCardId->getUID();
294
295
			$userOrCardId = "$name:$userId.vcf";
296
		}
297
		$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
298
	}
299
300
	/**
301
	 * @return array|null
302
	 */
303
	public function getLocalSystemAddressBook() {
304
		if (is_null($this->localSystemAddressBook)) {
305
			$systemPrincipal = "principals/system/system";
306
			$this->localSystemAddressBook = $this->ensureSystemAddressBookExists($systemPrincipal, 'system', [
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->ensureSystemAddre...ers of this instance')) can be null. However, the property $localSystemAddressBook is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
307
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
308
			]);
309
		}
310
311
		return $this->localSystemAddressBook;
312
	}
313
314
	public function syncInstance(\Closure $progressCallback = null) {
315
		$systemAddressBook = $this->getLocalSystemAddressBook();
316
		$this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) {
317
			$this->updateUser($user);
318
			if (!is_null($progressCallback)) {
319
				$progressCallback();
320
			}
321
		});
322
323
		// remove no longer existing
324
		$allCards = $this->backend->getCards($systemAddressBook['id']);
325
		foreach($allCards as $card) {
326
			$vCard = Reader::read($card['carddata']);
327
			$uid = $vCard->UID->getValue();
328
			// load backend and see if user exists
329
			if (!$this->userManager->userExists($uid)) {
330
				$this->deleteUser($card['uri']);
331
			}
332
		}
333
	}
334
335
336
}
337