Completed
Push — master ( e22914...1dbd22 )
by
unknown
20:24 queued 15s
created
apps/dav/lib/CardDAV/SyncService.php 1 patch
Indentation   +274 added lines, -274 removed lines patch added patch discarded remove patch
@@ -26,107 +26,107 @@  discard block
 block discarded – undo
26 26
 
27 27
 class SyncService {
28 28
 
29
-	use TTransactional;
30
-	private ?array $localSystemAddressBook = null;
31
-	protected string $certPath;
32
-
33
-	public function __construct(
34
-		private CardDavBackend $backend,
35
-		private IUserManager $userManager,
36
-		private IDBConnection $dbConnection,
37
-		private LoggerInterface $logger,
38
-		private Converter $converter,
39
-		private IClientService $clientService,
40
-		private IConfig $config,
41
-	) {
42
-		$this->certPath = '';
43
-	}
44
-
45
-	/**
46
-	 * @throws \Exception
47
-	 */
48
-	public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
49
-		// 1. create addressbook
50
-		$book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
51
-		$addressBookId = $book['id'];
52
-
53
-		// 2. query changes
54
-		try {
55
-			$response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
56
-		} catch (ClientExceptionInterface $ex) {
57
-			if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
58
-				// remote server revoked access to the address book, remove it
59
-				$this->backend->deleteAddressBook($addressBookId);
60
-				$this->logger->error('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
61
-				throw $ex;
62
-			}
63
-			$this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
64
-			throw $ex;
65
-		}
66
-
67
-		// 3. apply changes
68
-		// TODO: use multi-get for download
69
-		foreach ($response['response'] as $resource => $status) {
70
-			$cardUri = basename($resource);
71
-			if (isset($status[200])) {
72
-				$vCard = $this->download($url, $userName, $sharedSecret, $resource);
73
-				$this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
74
-					$existingCard = $this->backend->getCard($addressBookId, $cardUri);
75
-					if ($existingCard === false) {
76
-						$this->backend->createCard($addressBookId, $cardUri, $vCard);
77
-					} else {
78
-						$this->backend->updateCard($addressBookId, $cardUri, $vCard);
79
-					}
80
-				}, $this->dbConnection);
81
-			} else {
82
-				$this->backend->deleteCard($addressBookId, $cardUri);
83
-			}
84
-		}
85
-
86
-		return $response['token'];
87
-	}
88
-
89
-	/**
90
-	 * @throws \Sabre\DAV\Exception\BadRequest
91
-	 */
92
-	public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
93
-		try {
94
-			return $this->atomic(function () use ($principal, $uri, $properties) {
95
-				$book = $this->backend->getAddressBooksByUri($principal, $uri);
96
-				if (!is_null($book)) {
97
-					return $book;
98
-				}
99
-				$this->backend->createAddressBook($principal, $uri, $properties);
100
-
101
-				return $this->backend->getAddressBooksByUri($principal, $uri);
102
-			}, $this->dbConnection);
103
-		} catch (Exception $e) {
104
-			// READ COMMITTED doesn't prevent a nonrepeatable read above, so
105
-			// two processes might create an address book here. Ignore our
106
-			// failure and continue loading the entry written by the other process
107
-			if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
108
-				throw $e;
109
-			}
110
-
111
-			// If this fails we might have hit a replication node that does not
112
-			// have the row written in the other process.
113
-			// TODO: find an elegant way to handle this
114
-			$ab = $this->backend->getAddressBooksByUri($principal, $uri);
115
-			if ($ab === null) {
116
-				throw new Exception('Could not create system address book', $e->getCode(), $e);
117
-			}
118
-			return $ab;
119
-		}
120
-	}
121
-
122
-	public function ensureLocalSystemAddressBookExists(): ?array {
123
-		return $this->ensureSystemAddressBookExists('principals/system/system', 'system', [
124
-			'{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
125
-		]);
126
-	}
127
-
128
-	private function prepareUri(string $host, string $path): string {
129
-		/*
29
+    use TTransactional;
30
+    private ?array $localSystemAddressBook = null;
31
+    protected string $certPath;
32
+
33
+    public function __construct(
34
+        private CardDavBackend $backend,
35
+        private IUserManager $userManager,
36
+        private IDBConnection $dbConnection,
37
+        private LoggerInterface $logger,
38
+        private Converter $converter,
39
+        private IClientService $clientService,
40
+        private IConfig $config,
41
+    ) {
42
+        $this->certPath = '';
43
+    }
44
+
45
+    /**
46
+     * @throws \Exception
47
+     */
48
+    public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string {
49
+        // 1. create addressbook
50
+        $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties);
51
+        $addressBookId = $book['id'];
52
+
53
+        // 2. query changes
54
+        try {
55
+            $response = $this->requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken);
56
+        } catch (ClientExceptionInterface $ex) {
57
+            if ($ex->getCode() === Http::STATUS_UNAUTHORIZED) {
58
+                // remote server revoked access to the address book, remove it
59
+                $this->backend->deleteAddressBook($addressBookId);
60
+                $this->logger->error('Authorization failed, remove address book: ' . $url, ['app' => 'dav']);
61
+                throw $ex;
62
+            }
63
+            $this->logger->error('Client exception:', ['app' => 'dav', 'exception' => $ex]);
64
+            throw $ex;
65
+        }
66
+
67
+        // 3. apply changes
68
+        // TODO: use multi-get for download
69
+        foreach ($response['response'] as $resource => $status) {
70
+            $cardUri = basename($resource);
71
+            if (isset($status[200])) {
72
+                $vCard = $this->download($url, $userName, $sharedSecret, $resource);
73
+                $this->atomic(function () use ($addressBookId, $cardUri, $vCard): void {
74
+                    $existingCard = $this->backend->getCard($addressBookId, $cardUri);
75
+                    if ($existingCard === false) {
76
+                        $this->backend->createCard($addressBookId, $cardUri, $vCard);
77
+                    } else {
78
+                        $this->backend->updateCard($addressBookId, $cardUri, $vCard);
79
+                    }
80
+                }, $this->dbConnection);
81
+            } else {
82
+                $this->backend->deleteCard($addressBookId, $cardUri);
83
+            }
84
+        }
85
+
86
+        return $response['token'];
87
+    }
88
+
89
+    /**
90
+     * @throws \Sabre\DAV\Exception\BadRequest
91
+     */
92
+    public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array {
93
+        try {
94
+            return $this->atomic(function () use ($principal, $uri, $properties) {
95
+                $book = $this->backend->getAddressBooksByUri($principal, $uri);
96
+                if (!is_null($book)) {
97
+                    return $book;
98
+                }
99
+                $this->backend->createAddressBook($principal, $uri, $properties);
100
+
101
+                return $this->backend->getAddressBooksByUri($principal, $uri);
102
+            }, $this->dbConnection);
103
+        } catch (Exception $e) {
104
+            // READ COMMITTED doesn't prevent a nonrepeatable read above, so
105
+            // two processes might create an address book here. Ignore our
106
+            // failure and continue loading the entry written by the other process
107
+            if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
108
+                throw $e;
109
+            }
110
+
111
+            // If this fails we might have hit a replication node that does not
112
+            // have the row written in the other process.
113
+            // TODO: find an elegant way to handle this
114
+            $ab = $this->backend->getAddressBooksByUri($principal, $uri);
115
+            if ($ab === null) {
116
+                throw new Exception('Could not create system address book', $e->getCode(), $e);
117
+            }
118
+            return $ab;
119
+        }
120
+    }
121
+
122
+    public function ensureLocalSystemAddressBookExists(): ?array {
123
+        return $this->ensureSystemAddressBookExists('principals/system/system', 'system', [
124
+            '{' . Plugin::NS_CARDDAV . '}addressbook-description' => 'System addressbook which holds all users of this instance'
125
+        ]);
126
+    }
127
+
128
+    private function prepareUri(string $host, string $path): string {
129
+        /*
130 130
 		 * The trailing slash is important for merging the uris together.
131 131
 		 *
132 132
 		 * $host is stored in oc_trusted_servers.url and usually without a trailing slash.
@@ -147,177 +147,177 @@  discard block
 block discarded – undo
147 147
 		 * The response from the remote usually contains the webroot already and must be normalized to:
148 148
 		 * https://server.internal/cloud/remote.php/dav/addressbooks/system/system/system/Database:alice.vcf
149 149
 		 */
150
-		$host = rtrim($host, '/') . '/';
151
-
152
-		$uri = \GuzzleHttp\Psr7\UriResolver::resolve(
153
-			\GuzzleHttp\Psr7\Utils::uriFor($host),
154
-			\GuzzleHttp\Psr7\Utils::uriFor($path)
155
-		);
156
-
157
-		return (string)$uri;
158
-	}
159
-
160
-	/**
161
-	 * @throws ClientExceptionInterface
162
-	 */
163
-	protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
164
-		$client = $this->clientService->newClient();
165
-		$uri = $this->prepareUri($url, $addressBookUrl);
166
-
167
-		$options = [
168
-			'auth' => [$userName, $sharedSecret],
169
-			'body' => $this->buildSyncCollectionRequestBody($syncToken),
170
-			'headers' => ['Content-Type' => 'application/xml'],
171
-			'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT),
172
-			'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
173
-		];
174
-
175
-		$response = $client->request(
176
-			'REPORT',
177
-			$uri,
178
-			$options
179
-		);
180
-
181
-		$body = $response->getBody();
182
-		assert(is_string($body));
183
-
184
-		return $this->parseMultiStatus($body);
185
-	}
186
-
187
-	protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
188
-		$client = $this->clientService->newClient();
189
-		$uri = $this->prepareUri($url, $resourcePath);
190
-
191
-		$options = [
192
-			'auth' => [$userName, $sharedSecret],
193
-			'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
194
-		];
195
-
196
-		$response = $client->get(
197
-			$uri,
198
-			$options
199
-		);
200
-
201
-		return (string)$response->getBody();
202
-	}
203
-
204
-	private function buildSyncCollectionRequestBody(?string $syncToken): string {
205
-		$dom = new \DOMDocument('1.0', 'UTF-8');
206
-		$dom->formatOutput = true;
207
-		$root = $dom->createElementNS('DAV:', 'd:sync-collection');
208
-		$sync = $dom->createElement('d:sync-token', $syncToken ?? '');
209
-		$prop = $dom->createElement('d:prop');
210
-		$cont = $dom->createElement('d:getcontenttype');
211
-		$etag = $dom->createElement('d:getetag');
212
-
213
-		$prop->appendChild($cont);
214
-		$prop->appendChild($etag);
215
-		$root->appendChild($sync);
216
-		$root->appendChild($prop);
217
-		$dom->appendChild($root);
218
-		return $dom->saveXML();
219
-	}
220
-
221
-	/**
222
-	 * @param string $body
223
-	 * @return array
224
-	 * @throws \Sabre\Xml\ParseException
225
-	 */
226
-	private function parseMultiStatus($body) {
227
-		$xml = new Service();
228
-
229
-		/** @var MultiStatus $multiStatus */
230
-		$multiStatus = $xml->expect('{DAV:}multistatus', $body);
231
-
232
-		$result = [];
233
-		foreach ($multiStatus->getResponses() as $response) {
234
-			$result[$response->getHref()] = $response->getResponseProperties();
235
-		}
236
-
237
-		return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
238
-	}
239
-
240
-	/**
241
-	 * @param IUser $user
242
-	 */
243
-	public function updateUser(IUser $user): void {
244
-		$systemAddressBook = $this->getLocalSystemAddressBook();
245
-		$addressBookId = $systemAddressBook['id'];
246
-
247
-		$cardId = self::getCardUri($user);
248
-		if ($user->isEnabled()) {
249
-			$this->atomic(function () use ($addressBookId, $cardId, $user): void {
250
-				$card = $this->backend->getCard($addressBookId, $cardId);
251
-				if ($card === false) {
252
-					$vCard = $this->converter->createCardFromUser($user);
253
-					if ($vCard !== null) {
254
-						$this->backend->createCard($addressBookId, $cardId, $vCard->serialize(), false);
255
-					}
256
-				} else {
257
-					$vCard = $this->converter->createCardFromUser($user);
258
-					if (is_null($vCard)) {
259
-						$this->backend->deleteCard($addressBookId, $cardId);
260
-					} else {
261
-						$this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
262
-					}
263
-				}
264
-			}, $this->dbConnection);
265
-		} else {
266
-			$this->backend->deleteCard($addressBookId, $cardId);
267
-		}
268
-	}
269
-
270
-	/**
271
-	 * @param IUser|string $userOrCardId
272
-	 */
273
-	public function deleteUser($userOrCardId) {
274
-		$systemAddressBook = $this->getLocalSystemAddressBook();
275
-		if ($userOrCardId instanceof IUser) {
276
-			$userOrCardId = self::getCardUri($userOrCardId);
277
-		}
278
-		$this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
279
-	}
280
-
281
-	/**
282
-	 * @return array|null
283
-	 */
284
-	public function getLocalSystemAddressBook() {
285
-		if (is_null($this->localSystemAddressBook)) {
286
-			$this->localSystemAddressBook = $this->ensureLocalSystemAddressBookExists();
287
-		}
288
-
289
-		return $this->localSystemAddressBook;
290
-	}
291
-
292
-	/**
293
-	 * @return void
294
-	 */
295
-	public function syncInstance(?\Closure $progressCallback = null) {
296
-		$systemAddressBook = $this->getLocalSystemAddressBook();
297
-		$this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback): void {
298
-			$this->updateUser($user);
299
-			if (!is_null($progressCallback)) {
300
-				$progressCallback();
301
-			}
302
-		});
303
-
304
-		// remove no longer existing
305
-		$allCards = $this->backend->getCards($systemAddressBook['id']);
306
-		foreach ($allCards as $card) {
307
-			$vCard = Reader::read($card['carddata']);
308
-			$uid = $vCard->UID->getValue();
309
-			// load backend and see if user exists
310
-			if (!$this->userManager->userExists($uid)) {
311
-				$this->deleteUser($card['uri']);
312
-			}
313
-		}
314
-	}
315
-
316
-	/**
317
-	 * @param IUser $user
318
-	 * @return string
319
-	 */
320
-	public static function getCardUri(IUser $user): string {
321
-		return $user->getBackendClassName() . ':' . $user->getUID() . '.vcf';
322
-	}
150
+        $host = rtrim($host, '/') . '/';
151
+
152
+        $uri = \GuzzleHttp\Psr7\UriResolver::resolve(
153
+            \GuzzleHttp\Psr7\Utils::uriFor($host),
154
+            \GuzzleHttp\Psr7\Utils::uriFor($path)
155
+        );
156
+
157
+        return (string)$uri;
158
+    }
159
+
160
+    /**
161
+     * @throws ClientExceptionInterface
162
+     */
163
+    protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array {
164
+        $client = $this->clientService->newClient();
165
+        $uri = $this->prepareUri($url, $addressBookUrl);
166
+
167
+        $options = [
168
+            'auth' => [$userName, $sharedSecret],
169
+            'body' => $this->buildSyncCollectionRequestBody($syncToken),
170
+            'headers' => ['Content-Type' => 'application/xml'],
171
+            'timeout' => $this->config->getSystemValueInt('carddav_sync_request_timeout', IClient::DEFAULT_REQUEST_TIMEOUT),
172
+            'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
173
+        ];
174
+
175
+        $response = $client->request(
176
+            'REPORT',
177
+            $uri,
178
+            $options
179
+        );
180
+
181
+        $body = $response->getBody();
182
+        assert(is_string($body));
183
+
184
+        return $this->parseMultiStatus($body);
185
+    }
186
+
187
+    protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): string {
188
+        $client = $this->clientService->newClient();
189
+        $uri = $this->prepareUri($url, $resourcePath);
190
+
191
+        $options = [
192
+            'auth' => [$userName, $sharedSecret],
193
+            'verify' => !$this->config->getSystemValue('sharing.federation.allowSelfSignedCertificates', false),
194
+        ];
195
+
196
+        $response = $client->get(
197
+            $uri,
198
+            $options
199
+        );
200
+
201
+        return (string)$response->getBody();
202
+    }
203
+
204
+    private function buildSyncCollectionRequestBody(?string $syncToken): string {
205
+        $dom = new \DOMDocument('1.0', 'UTF-8');
206
+        $dom->formatOutput = true;
207
+        $root = $dom->createElementNS('DAV:', 'd:sync-collection');
208
+        $sync = $dom->createElement('d:sync-token', $syncToken ?? '');
209
+        $prop = $dom->createElement('d:prop');
210
+        $cont = $dom->createElement('d:getcontenttype');
211
+        $etag = $dom->createElement('d:getetag');
212
+
213
+        $prop->appendChild($cont);
214
+        $prop->appendChild($etag);
215
+        $root->appendChild($sync);
216
+        $root->appendChild($prop);
217
+        $dom->appendChild($root);
218
+        return $dom->saveXML();
219
+    }
220
+
221
+    /**
222
+     * @param string $body
223
+     * @return array
224
+     * @throws \Sabre\Xml\ParseException
225
+     */
226
+    private function parseMultiStatus($body) {
227
+        $xml = new Service();
228
+
229
+        /** @var MultiStatus $multiStatus */
230
+        $multiStatus = $xml->expect('{DAV:}multistatus', $body);
231
+
232
+        $result = [];
233
+        foreach ($multiStatus->getResponses() as $response) {
234
+            $result[$response->getHref()] = $response->getResponseProperties();
235
+        }
236
+
237
+        return ['response' => $result, 'token' => $multiStatus->getSyncToken()];
238
+    }
239
+
240
+    /**
241
+     * @param IUser $user
242
+     */
243
+    public function updateUser(IUser $user): void {
244
+        $systemAddressBook = $this->getLocalSystemAddressBook();
245
+        $addressBookId = $systemAddressBook['id'];
246
+
247
+        $cardId = self::getCardUri($user);
248
+        if ($user->isEnabled()) {
249
+            $this->atomic(function () use ($addressBookId, $cardId, $user): void {
250
+                $card = $this->backend->getCard($addressBookId, $cardId);
251
+                if ($card === false) {
252
+                    $vCard = $this->converter->createCardFromUser($user);
253
+                    if ($vCard !== null) {
254
+                        $this->backend->createCard($addressBookId, $cardId, $vCard->serialize(), false);
255
+                    }
256
+                } else {
257
+                    $vCard = $this->converter->createCardFromUser($user);
258
+                    if (is_null($vCard)) {
259
+                        $this->backend->deleteCard($addressBookId, $cardId);
260
+                    } else {
261
+                        $this->backend->updateCard($addressBookId, $cardId, $vCard->serialize());
262
+                    }
263
+                }
264
+            }, $this->dbConnection);
265
+        } else {
266
+            $this->backend->deleteCard($addressBookId, $cardId);
267
+        }
268
+    }
269
+
270
+    /**
271
+     * @param IUser|string $userOrCardId
272
+     */
273
+    public function deleteUser($userOrCardId) {
274
+        $systemAddressBook = $this->getLocalSystemAddressBook();
275
+        if ($userOrCardId instanceof IUser) {
276
+            $userOrCardId = self::getCardUri($userOrCardId);
277
+        }
278
+        $this->backend->deleteCard($systemAddressBook['id'], $userOrCardId);
279
+    }
280
+
281
+    /**
282
+     * @return array|null
283
+     */
284
+    public function getLocalSystemAddressBook() {
285
+        if (is_null($this->localSystemAddressBook)) {
286
+            $this->localSystemAddressBook = $this->ensureLocalSystemAddressBookExists();
287
+        }
288
+
289
+        return $this->localSystemAddressBook;
290
+    }
291
+
292
+    /**
293
+     * @return void
294
+     */
295
+    public function syncInstance(?\Closure $progressCallback = null) {
296
+        $systemAddressBook = $this->getLocalSystemAddressBook();
297
+        $this->userManager->callForAllUsers(function ($user) use ($systemAddressBook, $progressCallback): void {
298
+            $this->updateUser($user);
299
+            if (!is_null($progressCallback)) {
300
+                $progressCallback();
301
+            }
302
+        });
303
+
304
+        // remove no longer existing
305
+        $allCards = $this->backend->getCards($systemAddressBook['id']);
306
+        foreach ($allCards as $card) {
307
+            $vCard = Reader::read($card['carddata']);
308
+            $uid = $vCard->UID->getValue();
309
+            // load backend and see if user exists
310
+            if (!$this->userManager->userExists($uid)) {
311
+                $this->deleteUser($card['uri']);
312
+            }
313
+        }
314
+    }
315
+
316
+    /**
317
+     * @param IUser $user
318
+     * @return string
319
+     */
320
+    public static function getCardUri(IUser $user): string {
321
+        return $user->getBackendClassName() . ':' . $user->getUID() . '.vcf';
322
+    }
323 323
 }
Please login to merge, or discard this patch.