Completed
Pull Request — master (#347)
by Maxence
01:49
created

DavService::manageNewCircles()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 25
rs 9.2088
c 0
b 0
f 0
cc 5
nc 9
nop 3
1
<?php
2
3
4
/**
5
 * Circles - Bring cloud-users closer together.
6
 *
7
 * This file is licensed under the Affero General Public License version 3 or
8
 * later. See the COPYING file.
9
 *
10
 * @author Maxence Lange <[email protected]>
11
 * @copyright 2017
12
 * @license GNU AGPL version 3 or any later version
13
 *
14
 * This program is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License as
16
 * published by the Free Software Foundation, either version 3 of the
17
 * License, or (at your option) any later version.
18
 *
19
 * This program is distributed in the hope that it will be useful,
20
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 * GNU Affero General Public License for more details.
23
 *
24
 * You should have received a copy of the GNU Affero General Public License
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
26
 *
27
 */
28
29
30
namespace OCA\Circles\Service;
31
32
33
use Exception;
34
use OCA\Circles\Db\CirclesRequest;
35
use OCA\Circles\Db\MembersRequest;
36
use OCA\Circles\Exceptions\CircleAlreadyExistsException;
37
use OCA\Circles\Exceptions\CircleDoesNotExistException;
38
use OCA\Circles\Exceptions\MemberAlreadyExistsException;
39
use OCA\Circles\Exceptions\MemberDoesNotExistException;
40
use OCA\Circles\Exceptions\NotLocalMemberException;
41
use OCA\Circles\Model\Circle;
42
use OCA\Circles\Model\DavCard;
43
use OCA\Circles\Model\Member;
44
use OCA\DAV\CardDAV\CardDavBackend;
45
use OCP\App\ManagerEvent;
46
use OCP\Federation\ICloudIdManager;
47
use OCP\IUserManager;
48
use Symfony\Component\EventDispatcher\GenericEvent;
49
50
51
/**
52
 * Class DavService
53
 *
54
 * @package OCA\Circles\Service
55
 */
56
class DavService {
57
58
59
	/** @var string */
60
	private $userId;
61
62
	/** @var IUserManager */
63
	private $userManager;
64
65
	/** @var CardDavBackend */
66
	private $cardDavBackend;
67
68
	/** @var ICloudIdManager */
69
	private $cloudManager;
70
71
	/** @var CirclesRequest */
72
	private $circlesRequest;
73
74
	/** @var MembersRequest */
75
	private $membersRequest;
76
77
	/** @var ConfigService */
78
	private $configService;
79
80
	/** @var MiscService */
81
	private $miscService;
82
83
84
	/** @var array */
85
	private $migratedBooks = [];
86
87
88
	/**
89
	 * TimezoneService constructor.
90
	 *
91
	 * @param string $userId
92
	 * @param IUserManager $userManager
93
	 * @param CardDavBackend $cardDavBackend
94
	 * @param ICloudIdManager $cloudManager
95
	 * @param CirclesRequest $circlesRequest
96
	 * @param MembersRequest $membersRequest
97
	 * @param ConfigService $configService
98
	 * @param MiscService $miscService
99
	 */
100 View Code Duplication
	public function __construct(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
101
		$userId, IUserManager $userManager, CardDavBackend $cardDavBackend, ICloudIdManager $cloudManager,
102
		CirclesRequest $circlesRequest, MembersRequest $membersRequest, ConfigService $configService,
103
		MiscService $miscService
104
	) {
105
		$this->userId = $userId;
106
		$this->userManager = $userManager;
107
		$this->cardDavBackend = $cardDavBackend;
108
		$this->cloudManager = $cloudManager;
109
		$this->circlesRequest = $circlesRequest;
110
		$this->membersRequest = $membersRequest;
111
		$this->configService = $configService;
112
		$this->miscService = $miscService;
113
	}
114
115
116
	/**
117
	 * @param ManagerEvent $event
118
	 */
119
	public function onAppEnabled(ManagerEvent $event) {
120
		if ($event->getAppID() !== 'circles') {
121
			return;
122
		}
123
124
		try {
125
			$this->migration();
126
		} catch (Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
127
		}
128
	}
129
130
131
	/**
132
	 * @param GenericEvent $event
133
	 */
134
	public function onCreateCard(GenericEvent $event) {
135
		$davCard = $this->generateDavCard($event);
136
		$this->manageDavCard($davCard);
137
	}
138
139
140
	/**
141
	 * @param GenericEvent $event
142
	 */
143
	public function onUpdateCard(GenericEvent $event) {
144
		$davCard = $this->generateDavCard($event);
145
		$this->manageDavCard($davCard);
146
	}
147
148
149
	/**
150
	 * @param GenericEvent $event
151
	 */
152
	public function onDeleteCard(GenericEvent $event) {
153
		$addressBookId = $event->getArgument('addressBookId');
0 ignored issues
show
Unused Code introduced by
$addressBookId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
154
		$cardUri = $event->getArgument('cardUri');
0 ignored issues
show
Unused Code introduced by
$cardUri is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
155
	}
156
157
158
	/**
159
	 * @param GenericEvent $event
160
	 *
161
	 * @return DavCard
162
	 */
163
	private function generateDavCard(GenericEvent $event): DavCard {
164
		$addressBookId = $event->getArgument('addressBookId');
165
		$cardUri = $event->getArgument('cardUri');
166
		$cardData = $event->getArgument('cardData');
167
168
		$davCard = new DavCard();
169
		$davCard->setOwner($this->getOwnerFromAddressBook($addressBookId));
170
		$davCard->importFromDav($cardData);
171
		$davCard->setAddressBookId($addressBookId);
172
		$davCard->setCardUri($cardUri);
173
174
		return $davCard;
175
	}
176
177
178
	/**
179
	 * @param DavCard $davCard
180
	 */
181
	private function manageDavCard(DavCard $davCard) {
182
		$this->miscService->log('Updating Card: ' . json_encode($davCard));
183
		$this->manageCircles($davCard);
184
		$this->manageContact($davCard);
185
	}
186
187
188
	/**
189
	 * @param DavCard $davCard
190
	 */
191
	private function manageContact(DavCard $davCard) {
192
		$this->manageDeprecatedMembers($davCard);
193
194
		switch ($this->getMemberType($davCard)) {
195
			case DavCard::TYPE_CONTACT:
196
				$this->manageRemoteContact($davCard);
197
				break;
198
199
			case DavCard::TYPE_LOCAL:
200
				$this->manageLocalContact($davCard);
201
				break;
202
203
//			case DavCard::TYPE_FEDERATED:
204
//				$this->manageFederatedContact($davCard);
205
//				break;
206
		}
207
	}
208
209
210
	/**
211
	 * @param DavCard $davCard
212
	 */
213
	private function manageDeprecatedMembers(DavCard $davCard) {
214
		$circles = array_map(
215
			function(Circle $circle) {
216
				return $circle->getUniqueId();
217
			}, $davCard->getCircles()
218
		);
219
220
		$members = $this->membersRequest->getMembersByContactId($davCard->getContactId());
221
		foreach ($members as $member) {
222
			if (!in_array($member->getCircleId(), $circles)) {
223
				$this->membersRequest->removeMember($member);
224
			}
225
		}
226
	}
227
228
229
	/**
230
	 * @param DavCard $davCard
231
	 */
232
	private function manageLocalContact(DavCard $davCard) {
233
		foreach ($davCard->getCircles() as $circle) {
234
			$this->manageMember($circle, $davCard, Member::TYPE_USER);
235
		}
236
	}
237
238
239
	/**
240
	 * @param DavCard $davCard
241
	 */
242
	private function manageRemoteContact(DavCard $davCard) {
243
		foreach ($davCard->getCircles() as $circle) {
244
			$this->manageMember($circle, $davCard, Member::TYPE_CONTACT);
245
		}
246
	}
247
248
249
	/**
250
	 * @param Circle $circle
251
	 * @param DavCard $davCard
252
	 * @param int $type
253
	 */
254
	private function manageMember(Circle $circle, DavCard $davCard, int $type) {
255
		try {
256
			$member =
257
				$this->membersRequest->getContactMember($circle->getUniqueId(), $davCard->getContactId());
258
259
			if ($member->getType() !== $type) {
260
				$this->membersRequest->removeMember($member);
261
				throw new MemberDoesNotExistException();
262
			}
263
		} catch (MemberDoesNotExistException $e) {
264
			$member = new Member();
265
			$member->setLevel(Member::LEVEL_MEMBER);
266
			$member->setStatus(Member::STATUS_MEMBER);
267
			$member->setContactId($davCard->getContactId());
268
			$member->setType($type);
269
			$member->setCircleId($circle->getUniqueId());
270
			$member->setUserId($davCard->getUserId());
271
272
			try {
273
				$this->membersRequest->createMember($member);
274
			} catch (MemberAlreadyExistsException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
275
			}
276
		}
277
	}
278
279
280
	/**
281
	 * @param DavCard $davCard
282
	 *
283
	 * @return int
284
	 */
285
	private function getMemberType(DavCard $davCard): int {
286
		foreach ($davCard->getEmails() as $email) {
287
			try {
288
				$davCard->setUserId($this->isLocalMember($email));
289
290
				return DavCard::TYPE_LOCAL;
291
			} catch (NotLocalMemberException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
292
			}
293
		}
294
295
		$davCard->setUserId($davCard->getOwner() . ':' . $davCard->getContactId());
296
297
		return DavCard::TYPE_CONTACT;
298
	}
299
300
301
	/**
302
	 * @param string $email
303
	 *
304
	 * @return string
305
	 * @throws NotLocalMemberException
306
	 */
307
	private function isLocalMember(string $email): string {
308
		if (strpos($email, '@') === false) {
309
			throw new NotLocalMemberException();
310
		}
311
312
		list ($username, $domain) = explode('@', $email);
313
		if (in_array($domain, $this->configService->getAvailableHosts())) {
314
			$user = $this->userManager->get($username);
315
			if ($user !== null) {
316
				return $user->getUID();
317
			}
318
		}
319
320
		throw new NotLocalMemberException();
321
	}
322
323
324
	/**
325
	 * @param DavCard $davCard
326
	 */
327
	private function manageCircles(DavCard $davCard) {
328
		$fromCard = $davCard->getGroups();
329
		$current = array_map(
330
			function(Circle $circle) {
331
				return $circle->getContactGroupName();
332
			}, $this->getCirclesFromBook($davCard->getAddressBookId())
333
		);
334
335
		$this->manageNewCircles($davCard, $fromCard, $current);
336
		$this->manageDeprecatedCircles($fromCard, $current);
0 ignored issues
show
Unused Code introduced by
The call to the method OCA\Circles\Service\DavS...nageDeprecatedCircles() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
337
338
		$this->assignCirclesToCard($davCard);
339
	}
340
341
342
	/**
343
	 * @param DavCard $davCard
344
	 * @param array $fromCard
345
	 * @param array $current
346
	 */
347
	private function manageNewCircles(DavCard $davCard, array $fromCard, array $current) {
348
		foreach ($fromCard as $group) {
349
			if (in_array($group, $current)) {
350
				continue;
351
			}
352
353
			$circle = new Circle(Circle::CIRCLES_PUBLIC, $group . ' - ' . $this->uuid(5));
0 ignored issues
show
Deprecated Code introduced by
The method OCA\Circles\Service\DavService::uuid() has been deprecated.

This method has been deprecated.

Loading history...
354
			$circle->setContactAddressBook($davCard->getAddressBookId());
355
			$circle->setContactGroupName($group);
356
357
			try {
358
				$this->circlesRequest->createCircle($circle, $davCard->getOwner());
359
				$member = new Member($davCard->getOwner(), Member::TYPE_USER, $circle->getUniqueId());
360
				$member->setLevel(Member::LEVEL_OWNER);
361
				$member->setStatus(Member::STATUS_MEMBER);
362
363
				try {
364
					$this->membersRequest->createMember($member);
365
				} catch (MemberAlreadyExistsException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
366
				}
367
			} catch (CircleAlreadyExistsException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
368
			}
369
370
		}
371
	}
372
373
374
	/**
375
	 * // TODO: Get all group from an addressbook
376
	 * // TODO: remove deprecated circles
377
	 *
378
	 * @param array $fromCard
379
	 * @param array $current
380
	 */
381
	private function manageDeprecatedCircles(array $fromCard, array $current) {
0 ignored issues
show
Unused Code introduced by
The parameter $fromCard is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $current is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
382
	}
383
384
385
	/**
386
	 * @param DavCard $davCard
387
	 */
388
	private function assignCirclesToCard(DavCard $davCard) {
389
		foreach ($davCard->getGroups() as $group) {
390
			try {
391
				$davCard->addCircle(
392
					$this->circlesRequest->getFromContactGroup($davCard->getAddressBookId(), $group)
393
				);
394
			} catch (CircleDoesNotExistException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
395
			}
396
		}
397
	}
398
399
400
	/**
401
	 * @param int $addressBookId
402
	 *
403
	 * @return Circle[]
404
	 */
405
	private function getCirclesFromBook(int $addressBookId): array {
406
		return $this->circlesRequest->getFromBook($addressBookId);
407
	}
408
409
410
	/**
411
	 * @param int $length
412
	 *
413
	 * @return string
414
	 * @deprecated
415
	 */
416
	protected function uuid(int $length = 0): string {
417
		$uuid = sprintf(
418
			'%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff),
419
			mt_rand(0, 0xffff), mt_rand(0, 0xfff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
420
			mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
421
		);
422
423
		if ($length > 0) {
424
			if ($length <= 16) {
425
				$uuid = str_replace('-', '', $uuid);
426
			}
427
428
			$uuid = substr($uuid, 0, $length);
429
		}
430
431
		return $uuid;
432
	}
433
434
435
	/**
436
	 * @param int $bookId
437
	 *
438
	 * @return string
439
	 */
440
	private function getOwnerFromAddressBook(int $bookId): string {
441
		$data = $this->cardDavBackend->getAddressBookById($bookId);
442
443
		// let's assume the format is principals/users/OWNER
444
		$owner = substr($data['principaluri'], 17);
445
446
		return $owner;
447
	}
448
449
450
	/**
451
	 *
452
	 * @throws Exception
453
	 */
454
	public function migration() {
455
		if ($this->configService->getAppValue(ConfigService::CIRCLES_CONTACT_BACKEND) !== '1') {
456
			throw new Exception('Circles needs to be set as Contacts App Backend first');
457
		}
458
459
460
		$users = $this->userManager->search('');
461
		foreach ($users as $user) {
462
			$books = $this->cardDavBackend->getAddressBooksForUser('principals/users/' . $user->getUID());
463
			foreach ($books as $book) {
464
				$this->migrateBook($book['id']);
465
			}
466
		}
467
	}
468
469
470
	/**
471
	 * @param int $bookId
472
	 */
473
	private function migrateBook(int $bookId) {
474
		if (in_array($bookId, $this->migratedBooks)) {
475
			return;
476
		}
477
478
		$owner = $this->getOwnerFromAddressBook($bookId);
479
480
		foreach ($this->cardDavBackend->getCards($bookId) as $card) {
481
			$davCard = new DavCard();
482
			$davCard->setOwner($owner);
483
			$davCard->importFromDav($card['carddata']);
484
			$davCard->setAddressBookId($bookId);
485
			$davCard->setCardUri($card['uri']);
486
487
			$this->manageDavCard($davCard);
488
		}
489
490
		$this->migratedBooks[] = $bookId;
491
	}
492
493
494
}
495
496
497