Completed
Push — master ( ba9b17...94004c )
by Lukas
05:19 queued 04:59
created

SyncService::updateUser()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 4
nop 1
dl 0
loc 23
rs 8.7972
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\ILogger;
30
use OCP\IUser;
31
use OCP\IUserManager;
32
use Sabre\DAV\Client;
33
use Sabre\DAV\Xml\Response\MultiStatus;
34
use Sabre\DAV\Xml\Service;
35
use Sabre\HTTP\ClientHttpException;
36
use Sabre\VObject\Reader;
37
38
class SyncService {
39
40
	/** @var CardDavBackend */
41
	private $backend;
42
43
	/** @var IUserManager */
44
	private $userManager;
45
46
	/** @var ILogger */
47
	private $logger;
48
49
	/** @var array */
50
	private $localSystemAddressBook;
51
52
	/** @var AccountManager */
53
	private $accountManager;
54
55
	/**
56
	 * SyncService constructor.
57
	 *
58
	 * @param CardDavBackend $backend
59
	 * @param IUserManager $userManager
60
	 * @param ILogger $logger
61
	 * @param AccountManager $accountManager
62
	 */
63
	public function __construct(CardDavBackend $backend, IUserManager $userManager, ILogger $logger, AccountManager $accountManager) {
64
		$this->backend = $backend;
65
		$this->userManager = $userManager;
66
		$this->logger = $logger;
67
		$this->accountManager = $accountManager;
68
	}
69
70
	/**
71
	 * @param string $url
72
	 * @param string $userName
73
	 * @param string $sharedSecret
74
	 * @param string $syncToken
75
	 * @param int $targetBookId
76
	 * @param string $targetPrincipal
77
	 * @param array $targetProperties
78
	 * @return string
79
	 * @throws \Exception
80
	 */
81
	public function syncRemoteAddressBook($url, $userName, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) {
82
		// 1. create addressbook
83
		$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookId, $targetProperties);
84
		$addressBookId = $book['id'];
85
86
		// 2. query changes
87
		try {
88
			$response = $this->requestSyncReport($url, $userName, $sharedSecret, $syncToken);
89
		} 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...
90
			if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
91
				// remote server revoked access to the address book, remove it
92
				$this->backend->deleteAddressBook($addressBookId);
93
				$this->logger->info('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
94
				throw $ex;
95
			}
96
		}
97
98
		// 3. apply changes
99
		// TODO: use multi-get for download
100
		foreach ($response['response'] as $resource => $status) {
101
			$cardUri = basename($resource);
102
			if (isset($status[200])) {
103
				$vCard = $this->download($url, $sharedSecret, $resource);
104
				$existingCard = $this->backend->getCard($addressBookId, $cardUri);
105
				if ($existingCard === false) {
106
					$this->backend->createCard($addressBookId, $cardUri, $vCard['body']);
107
				} else {
108
					$this->backend->updateCard($addressBookId, $cardUri, $vCard['body']);
109
				}
110
			} else {
111
				$this->backend->deleteCard($addressBookId, $cardUri);
112
			}
113
		}
114
115
		return $response['token'];
116
	}
117
118
	/**
119
	 * @param string $principal
120
	 * @param string $id
121
	 * @param array $properties
122
	 * @return array|null
123
	 * @throws \Sabre\DAV\Exception\BadRequest
124
	 */
125
	public function ensureSystemAddressBookExists($principal, $id, $properties) {
126
		$book = $this->backend->getAddressBooksByUri($principal, $id);
127
		if (!is_null($book)) {
128
			return $book;
129
		}
130
		$this->backend->createAddressBook($principal, $id, $properties);
131
132
		return $this->backend->getAddressBooksByUri($principal, $id);
133
	}
134
135
	/**
136
	 * @param string $url
137
	 * @param string $userName
138
	 * @param string $sharedSecret
139
	 * @param string $syncToken
140
	 * @return array
141
	 */
142
	protected function requestSyncReport($url, $userName, $sharedSecret, $syncToken) {
143
		$settings = [
144
			'baseUri' => $url . '/',
145
			'userName' => $userName,
146
			'password' => $sharedSecret,
147
		];
148
		$client = new Client($settings);
149
		$client->setThrowExceptions(true);
150
151
		$addressBookUrl = "remote.php/dav/addressbooks/system/system/system";
152
		$body = $this->buildSyncCollectionRequestBody($syncToken);
153
154
		$response = $client->request('REPORT', $addressBookUrl, $body, [
155
			'Content-Type' => 'application/xml'
156
		]);
157
158
		$result = $this->parseMultiStatus($response['body']);
159
160
		return $result;
161
	}
162
163
	/**
164
	 * @param string $url
165
	 * @param string $sharedSecret
166
	 * @param string $resourcePath
167
	 * @return array
168
	 */
169
	protected function download($url, $sharedSecret, $resourcePath) {
170
		$settings = [
171
			'baseUri' => $url,
172
			'userName' => 'system',
173
			'password' => $sharedSecret,
174
		];
175
		$client = new Client($settings);
176
		$client->setThrowExceptions(true);
177
178
		$response = $client->request('GET', $resourcePath);
179
		return $response;
180
	}
181
182
	/**
183
	 * @param string|null $syncToken
184
	 * @return string
185
	 */
186
	private function buildSyncCollectionRequestBody($syncToken) {
187
188
		$dom = new \DOMDocument('1.0', 'UTF-8');
189
		$dom->formatOutput = true;
190
		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
191
		$sync = $dom->createElement('d:sync-token', $syncToken);
192
		$prop = $dom->createElement('d:prop');
193
		$cont = $dom->createElement('d:getcontenttype');
194
		$etag = $dom->createElement('d:getetag');
195
196
		$prop->appendChild($cont);
197
		$prop->appendChild($etag);
198
		$root->appendChild($sync);
199
		$root->appendChild($prop);
200
		$dom->appendChild($root);
201
		$body = $dom->saveXML();
202
203
		return $body;
204
	}
205
206
	/**
207
	 * @param string $body
208
	 * @return array
209
	 * @throws \Sabre\Xml\ParseException
210
	 */
211
	private function parseMultiStatus($body) {
212
		$xml = new Service();
213
214
		/** @var MultiStatus $multiStatus */
215
		$multiStatus = $xml->expect('{DAV:}multistatus', $body);
216
217
		$result = [];
218
		foreach ($multiStatus->getResponses() as $response) {
219
			$result[$response->getHref()] = $response->getResponseProperties();
220
		}
221
222
		return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
223
	}
224
225
	/**
226
	 * @param IUser $user
227
	 */
228
	public function updateUser($user) {
229
		$systemAddressBook = $this->getLocalSystemAddressBook();
230
		$addressBookId = $systemAddressBook['id'];
231
		$converter = new Converter($this->accountManager);
232
		$name = $user->getBackendClassName();
233
		$userId = $user->getUID();
234
235
		$cardId = "$name:$userId.vcf";
236
		$card = $this->backend->getCard($addressBookId, $cardId);
237
		if ($card === false) {
238
			$vCard = $converter->createCardFromUser($user);
239
			if ($vCard !== null) {
240
				$this->backend->createCard($addressBookId, $cardId, $vCard->serialize());
241
			}
242
		} else {
243
			$vCard = $converter->createCardFromUser($user);
244
			if (is_null($vCard)) {
245
				$this->backend->deleteCard($addressBookId, $cardId);
246
			} else {
247
				$this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
248
			}
249
		}
250
	}
251
252
	/**
253
	 * @param IUser|string $userOrCardId
254
	 */
255
	public function deleteUser($userOrCardId) {
256
		$systemAddressBook = $this->getLocalSystemAddressBook();
257
		if ($userOrCardId instanceof IUser){
258
			$name = $userOrCardId->getBackendClassName();
259
			$userId = $userOrCardId->getUID();
260
261
			$userOrCardId = "$name:$userId.vcf";
262
		}
263
		$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
264
	}
265
266
	/**
267
	 * @return array|null
268
	 */
269
	public function getLocalSystemAddressBook() {
270
		if (is_null($this->localSystemAddressBook)) {
271
			$systemPrincipal = "principals/system/system";
272
			$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...
273
				'{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
274
			]);
275
		}
276
277
		return $this->localSystemAddressBook;
278
	}
279
280
	public function syncInstance(\Closure $progressCallback = null) {
281
		$systemAddressBook = $this->getLocalSystemAddressBook();
282
		$this->userManager->callForAllUsers(function($user) use ($systemAddressBook, $progressCallback) {
283
			$this->updateUser($user);
284
			if (!is_null($progressCallback)) {
285
				$progressCallback();
286
			}
287
		});
288
289
		// remove no longer existing
290
		$allCards = $this->backend->getCards($systemAddressBook['id']);
291
		foreach($allCards as $card) {
292
			$vCard = Reader::read($card['carddata']);
293
			$uid = $vCard->UID->getValue();
294
			// load backend and see if user exists
295
			if (!$this->userManager->userExists($uid)) {
296
				$this->deleteUser($card['uri']);
297
			}
298
		}
299
	}
300
301
302
}
303