Completed
Push — master ( e1740c...d98dea )
by Morris
14:21
created

BirthdayService::getAllAffectedPrincipals()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 3
nop 1
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2016, Georg Ehrke
5
 *
6
 * @author Achim Königs <[email protected]>
7
 * @author Georg Ehrke <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 * @author Thomas Müller <[email protected]>
12
 *
13
 * @license AGPL-3.0
14
 *
15
 * This code is free software: you can redistribute it and/or modify
16
 * it under the terms of the GNU Affero General Public License, version 3,
17
 * as published by the Free Software Foundation.
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, version 3,
25
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
26
 *
27
 */
28
29
namespace OCA\DAV\CalDAV;
30
31
use Exception;
32
use OCA\DAV\CardDAV\CardDavBackend;
33
use OCA\DAV\DAV\GroupPrincipalBackend;
34
use OCP\IConfig;
35
use Sabre\VObject\Component\VCalendar;
36
use Sabre\VObject\Component\VCard;
37
use Sabre\VObject\DateTimeParser;
38
use Sabre\VObject\Document;
39
use Sabre\VObject\InvalidDataException;
40
use Sabre\VObject\Property\VCard\DateAndOrTime;
41
use Sabre\VObject\Reader;
42
43
class BirthdayService {
44
45
	const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
46
47
	/** @var GroupPrincipalBackend */
48
	private $principalBackend;
49
50
	/** @var CalDavBackend  */
51
	private $calDavBackEnd;
52
53
	/** @var CardDavBackend  */
54
	private $cardDavBackEnd;
55
56
	/** @var IConfig */
57
	private $config;
58
59
	/**
60
	 * BirthdayService constructor.
61
	 *
62
	 * @param CalDavBackend $calDavBackEnd
63
	 * @param CardDavBackend $cardDavBackEnd
64
	 * @param GroupPrincipalBackend $principalBackend
65
	 * @param IConfig $config;
0 ignored issues
show
Documentation introduced by
There is no parameter named $config;. Did you maybe mean $config?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
66
	 */
67
	public function __construct(CalDavBackend $calDavBackEnd, CardDavBackend $cardDavBackEnd, GroupPrincipalBackend $principalBackend, IConfig $config) {
68
		$this->calDavBackEnd = $calDavBackEnd;
69
		$this->cardDavBackEnd = $cardDavBackEnd;
70
		$this->principalBackend = $principalBackend;
71
		$this->config = $config;
72
	}
73
74
	/**
75
	 * @param int $addressBookId
76
	 * @param string $cardUri
77
	 * @param string $cardData
78
	 */
79
	public function onCardChanged($addressBookId, $cardUri, $cardData) {
80
		if (!$this->isGloballyEnabled()) {
81
			return;
82
		}
83
84
		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
85
		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
86
		$targetPrincipals[] = $book['principaluri'];
87
		$datesToSync = [
88
			['postfix' => '', 'field' => 'BDAY', 'symbol' => '*'],
89
			['postfix' => '-death', 'field' => 'DEATHDATE', 'symbol' => "†"],
90
			['postfix' => '-anniversary', 'field' => 'ANNIVERSARY', 'symbol' => "⚭"],
91
		];
92
		foreach ($targetPrincipals as $principalUri) {
93
			if (!$this->isUserEnabled($principalUri)) {
94
				continue;
95
			}
96
97
			$calendar = $this->ensureCalendarExists($principalUri);
98
			foreach ($datesToSync as $type) {
99
				$this->updateCalendar($cardUri, $cardData, $book, $calendar['id'], $type);
100
			}
101
		}
102
	}
103
104
	/**
105
	 * @param int $addressBookId
106
	 * @param string $cardUri
107
	 */
108
	public function onCardDeleted($addressBookId, $cardUri) {
109
		if (!$this->isGloballyEnabled()) {
110
			return;
111
		}
112
113
		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
114
		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
115
		$targetPrincipals[] = $book['principaluri'];
116
		foreach ($targetPrincipals as $principalUri) {
117
			if (!$this->isUserEnabled($principalUri)) {
118
				continue;
119
			}
120
121
			$calendar = $this->ensureCalendarExists($principalUri);
122
			foreach (['', '-death', '-anniversary'] as $tag) {
123
				$objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
124
				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri);
125
			}
126
		}
127
	}
128
129
	/**
130
	 * @param string $principal
131
	 * @return array|null
132
	 * @throws \Sabre\DAV\Exception\BadRequest
133
	 */
134
	public function ensureCalendarExists($principal) {
135
		$book = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
136
		if (!is_null($book)) {
137
			return $book;
138
		}
139
		$this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
140
			'{DAV:}displayname' => 'Contact birthdays',
141
			'{http://apple.com/ns/ical/}calendar-color' => '#FFFFCA',
142
			'components'   => 'VEVENT',
143
		]);
144
145
		return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
146
	}
147
148
	/**
149
	 * @param string $cardData
150
	 * @param string $dateField
151
	 * @param string $summarySymbol
152
	 * @return null|VCalendar
153
	 */
154
	public function buildDateFromContact($cardData, $dateField, $summarySymbol) {
155
		if (empty($cardData)) {
156
			return null;
157
		}
158
		try {
159
			$doc = Reader::read($cardData);
160
			// We're always converting to vCard 4.0 so we can rely on the
161
			// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
162
			if (!$doc instanceof VCard) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Component\VCard does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
163
				return null;
164
			}
165
			$doc = $doc->convert(Document::VCARD40);
166
		} catch (Exception $e) {
167
			return null;
168
		}
169
170
		if (!isset($doc->{$dateField})) {
171
			return null;
172
		}
173
		if (!isset($doc->FN)) {
174
			return null;
175
		}
176
		$birthday = $doc->{$dateField};
177
		if (!(string)$birthday) {
178
			return null;
179
		}
180
		// Skip if the BDAY property is not of the right type.
181
		if (!$birthday instanceof DateAndOrTime) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\Property\VCard\DateAndOrTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
182
			return null;
183
		}
184
185
		// Skip if we can't parse the BDAY value.
186
		try {
187
			$dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
188
		} catch (InvalidDataException $e) {
0 ignored issues
show
Bug introduced by
The class Sabre\VObject\InvalidDataException 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...
189
			return null;
190
		}
191
192
		$unknownYear = false;
193
		if (!$dateParts['year']) {
194
			$birthday = '1900-' . $dateParts['month'] . '-' . $dateParts['date'];
195
196
			$unknownYear = true;
197
		}
198
199
		try {
200
			$date = new \DateTime($birthday);
201
		} catch (Exception $e) {
202
			return null;
203
		}
204
		if ($unknownYear) {
205
			$summary = $doc->FN->getValue() . ' ' . $summarySymbol;
206
		} else {
207
			$year = (int)$date->format('Y');
208
			$summary = $doc->FN->getValue() . " ($summarySymbol$year)";
209
		}
210
		$vCal = new VCalendar();
211
		$vCal->VERSION = '2.0';
212
		$vEvent = $vCal->createComponent('VEVENT');
213
		$vEvent->add('DTSTART');
214
		$vEvent->DTSTART->setDateTime(
215
			$date
216
		);
217
		$vEvent->DTSTART['VALUE'] = 'DATE';
218
		$vEvent->add('DTEND');
219
		$date->add(new \DateInterval('P1D'));
220
		$vEvent->DTEND->setDateTime(
221
			$date
222
		);
223
		$vEvent->DTEND['VALUE'] = 'DATE';
224
		$vEvent->{'UID'} = $doc->UID;
225
		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
226
		$vEvent->{'SUMMARY'} = $summary;
227
		$vEvent->{'TRANSP'} = 'TRANSPARENT';
228
		$alarm = $vCal->createComponent('VALARM');
229
		$alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
230
		$alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
231
		$alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
232
		$vEvent->add($alarm);
233
		$vCal->add($vEvent);
234
		return $vCal;
235
	}
236
237
	/**
238
	 * @param string $user
239
	 */
240
	public function syncUser($user) {
241
		$principal = 'principals/users/'.$user;
242
		$this->ensureCalendarExists($principal);
243
		$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
244
		foreach($books as $book) {
245
			$cards = $this->cardDavBackEnd->getCards($book['id']);
246
			foreach($cards as $card) {
247
				$this->onCardChanged($book['id'], $card['uri'], $card['carddata']);
248
			}
249
		}
250
	}
251
252
	/**
253
	 * @param string $existingCalendarData
254
	 * @param VCalendar $newCalendarData
255
	 * @return bool
256
	 */
257
	public function birthdayEvenChanged($existingCalendarData, $newCalendarData) {
258
		try {
259
			$existingBirthday = Reader::read($existingCalendarData);
260
		} catch (Exception $ex) {
261
			return true;
262
		}
263
		if ($newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
264
			$newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
265
		) {
266
			return true;
267
		}
268
		return false;
269
	}
270
271
	/**
272
	 * @param integer $addressBookId
273
	 * @return mixed
274
	 */
275
	protected function getAllAffectedPrincipals($addressBookId) {
276
		$targetPrincipals = [];
277
		$shares = $this->cardDavBackEnd->getShares($addressBookId);
278
		foreach ($shares as $share) {
279
			if ($share['{http://owncloud.org/ns}group-share']) {
280
				$users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']);
281
				foreach ($users as $user) {
282
					$targetPrincipals[] = $user['uri'];
283
				}
284
			} else {
285
				$targetPrincipals[] = $share['{http://owncloud.org/ns}principal'];
286
			}
287
		}
288
		return array_values(array_unique($targetPrincipals, SORT_STRING));
289
	}
290
291
	/**
292
	 * @param string $cardUri
293
	 * @param string  $cardData
294
	 * @param array $book
295
	 * @param int $calendarId
296
	 * @param string[] $type
297
	 */
298
	private function updateCalendar($cardUri, $cardData, $book, $calendarId, $type) {
299
		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
300
		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['symbol']);
301
		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
302
		if (is_null($calendarData)) {
303
			if (!is_null($existing)) {
304
				$this->calDavBackEnd->deleteCalendarObject($calendarId, $objectUri);
305
			}
306
		} else {
307
			if (is_null($existing)) {
308
				$this->calDavBackEnd->createCalendarObject($calendarId, $objectUri, $calendarData->serialize());
309
			} else {
310
				if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) {
311
					$this->calDavBackEnd->updateCalendarObject($calendarId, $objectUri, $calendarData->serialize());
312
				}
313
			}
314
		}
315
	}
316
317
	/**
318
	 * checks if the admin opted-out of birthday calendars
319
	 *
320
	 * @return bool
321
	 */
322
	private function isGloballyEnabled() {
323
		$isGloballyEnabled = $this->config->getAppValue('dav', 'generateBirthdayCalendar', 'yes');
324
		return $isGloballyEnabled === 'yes';
325
	}
326
327
	/**
328
	 * checks if the user opted-out of birthday calendars
329
	 *
330
	 * @param $userPrincipal
331
	 * @return bool
332
	 */
333
	private function isUserEnabled($userPrincipal) {
334
		if (strpos($userPrincipal, 'principals/users/') === 0) {
335
			$userId = substr($userPrincipal, 17);
336
			$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
337
			return $isEnabled === 'yes';
338
		}
339
340
		// not sure how we got here, just be on the safe side and return true
341
		return true;
342
	}
343
344
}
345