Completed
Push — master ( 48b946...ed9b2d )
by Thomas
08:04
created

BirthdayService   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 249
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 249
rs 8.6
c 0
b 0
f 0
wmc 37
lcom 1
cbo 6

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A onCardChanged() 0 17 3
A onCardDeleted() 0 12 3
A ensureCalendarExists() 0 13 2
C buildDateFromContact() 0 82 12
A syncUser() 0 11 3
A birthdayEvenChanged() 0 13 4
A getAllAffectedPrincipals() 0 15 4
B updateCalendar() 0 18 5
1
<?php
2
/**
3
 * @author Achim Königs <[email protected]>
4
 * @author Thomas Müller <[email protected]>
5
 *
6
 * @copyright Copyright (c) 2016, ownCloud GmbH.
7
 * @license AGPL-3.0
8
 *
9
 * This code is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License, version 3,
11
 * as published by the Free Software Foundation.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License, version 3,
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
20
 *
21
 */
22
23
namespace OCA\DAV\CalDAV;
24
25
use Exception;
26
use OCA\DAV\CardDAV\CardDavBackend;
27
use OCA\DAV\DAV\GroupPrincipalBackend;
28
use Sabre\VObject\Component\VCalendar;
29
use Sabre\VObject\Component\VCard;
30
use Sabre\VObject\DateTimeParser;
31
use Sabre\VObject\Document;
32
use Sabre\VObject\InvalidDataException;
33
use Sabre\VObject\Property\VCard\DateAndOrTime;
34
use Sabre\VObject\Reader;
35
36
class BirthdayService {
37
38
	const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
39
40
	/** @var GroupPrincipalBackend */
41
	private $principalBackend;
42
43
	/**
44
	 * BirthdayService constructor.
45
	 *
46
	 * @param CalDavBackend $calDavBackEnd
47
	 * @param CardDavBackend $cardDavBackEnd
48
	 * @param GroupPrincipalBackend $principalBackend
49
	 */
50
	public function __construct(CalDavBackend $calDavBackEnd, CardDavBackend $cardDavBackEnd, GroupPrincipalBackend $principalBackend) {
51
		$this->calDavBackEnd = $calDavBackEnd;
0 ignored issues
show
Bug introduced by
The property calDavBackEnd does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
52
		$this->cardDavBackEnd = $cardDavBackEnd;
0 ignored issues
show
Bug introduced by
The property cardDavBackEnd does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53
		$this->principalBackend = $principalBackend;
54
	}
55
56
	/**
57
	 * @param int $addressBookId
58
	 * @param string $cardUri
59
	 * @param string $cardData
60
	 */
61
	public function onCardChanged($addressBookId, $cardUri, $cardData) {
62
		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
63
		
64
		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
65
		$targetPrincipals[] = $book['principaluri'];
66
		$datesToSync = [
67
			['postfix' => '', 'field' => 'BDAY', 'symbol' => '*'],
68
			['postfix' => '-death', 'field' => 'DEATHDATE', 'symbol' => "†"],
69
			['postfix' => '-anniversary', 'field' => 'ANNIVERSARY', 'symbol' => "⚭"],
70
		];
71
		foreach ($targetPrincipals as $principalUri) {
72
			$calendar = $this->ensureCalendarExists($principalUri);
73
			foreach ($datesToSync as $type) {
74
				$this->updateCalendar($cardUri, $cardData, $book, $calendar['id'], $type);
0 ignored issues
show
Documentation introduced by
$type is of type array<string,string,{"po...ng","symbol":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
75
			}
76
		}
77
	}
78
79
	/**
80
	 * @param int $addressBookId
81
	 * @param string $cardUri
82
	 */
83
	public function onCardDeleted($addressBookId, $cardUri) {
84
		$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
85
		$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
86
		$targetPrincipals[] = $book['principaluri'];
87
		foreach ($targetPrincipals as $principalUri) {
88
			$calendar = $this->ensureCalendarExists($principalUri);
89
			foreach (['', '-death', '-anniversary'] as $tag) {
90
				$objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
91
				$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri);
92
			}
93
		}
94
	}
95
96
	/**
97
	 * @param string $principal
98
	 * @return array|null
99
	 * @throws \Sabre\DAV\Exception\BadRequest
100
	 */
101
	public function ensureCalendarExists($principal) {
102
		$book = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
103
		if (!is_null($book)) {
104
			return $book;
105
		}
106
		$this->calDavBackEnd->createCalendar($principal, self::BIRTHDAY_CALENDAR_URI, [
107
			'{DAV:}displayname' => 'Contact birthdays',
108
			'{http://apple.com/ns/ical/}calendar-color' => '#FFFFCA',
109
			'components'   => 'VEVENT',
110
		]);
111
112
		return $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
113
	}
114
115
	/**
116
	 * @param string $cardData
117
	 * @param string $dateField
118
	 * @param string $summarySymbol
119
	 * @return null|VCalendar
120
	 */
121
	public function buildDateFromContact($cardData, $dateField, $summarySymbol) {
122
		if (empty($cardData)) {
123
			return null;
124
		}
125
		try {
126
			$doc = Reader::read($cardData);
127
			// We're always converting to vCard 4.0 so we can rely on the
128
			// VCardConverter handling the X-APPLE-OMIT-YEAR property for us.
129
			if (!$doc instanceof VCard) {
130
				return null;
131
			}
132
			$doc = $doc->convert(Document::VCARD40);
133
		} catch (Exception $e) {
134
			return null;
135
		}
136
137
		if (!isset($doc->{$dateField})) {
138
			return null;
139
		}
140
		if (!isset($doc->FN)) {
141
			return null;
142
		}
143
		$birthday = $doc->{$dateField};
144
		if (!(string)$birthday) {
145
			return null;
146
		}
147
		// Skip if the BDAY property is not of the right type.
148
		if (!$birthday instanceof DateAndOrTime) {
149
			return null;
150
		}
151
152
		// Skip if we can't parse the BDAY value.
153
		try {
154
			$dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue());
155
		} catch (InvalidDataException $e) {
156
			return null;
157
		}
158
159
		$unknownYear = false;
160
		if (!$dateParts['year']) {
161
			$birthday = '1900-' . $dateParts['month'] . '-' . $dateParts['date'];
162
163
			$unknownYear = true;
164
		}
165
166
		try {
167
			$date = new \DateTime($birthday);
168
		} catch (Exception $e) {
169
			return null;
170
		}
171
		if ($unknownYear) {
172
			$summary = $doc->FN->getValue() . ' ' . $summarySymbol;
173
		} else {
174
			$year = (int)$date->format('Y');
175
			$summary = $doc->FN->getValue() . " ($summarySymbol$year)";
176
		}
177
		$vCal = new VCalendar();
178
		$vCal->VERSION = '2.0';
179
		$vEvent = $vCal->createComponent('VEVENT');
180
		$vEvent->add('DTSTART');
181
		$vEvent->DTSTART->setDateTime(
182
			$date
183
		);
184
		$vEvent->DTSTART['VALUE'] = 'DATE';
185
		$vEvent->add('DTEND');
186
		$date->add(new \DateInterval('P1D'));
187
		$vEvent->DTEND->setDateTime(
188
			$date
189
		);
190
		$vEvent->DTEND['VALUE'] = 'DATE';
191
		$vEvent->{'UID'} = $doc->UID;
192
		$vEvent->{'RRULE'} = 'FREQ=YEARLY';
193
		$vEvent->{'SUMMARY'} = $summary;
194
		$vEvent->{'TRANSP'} = 'TRANSPARENT';
195
		$alarm = $vCal->createComponent('VALARM');
196
		$alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
197
		$alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
198
		$alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
199
		$vEvent->add($alarm);
200
		$vCal->add($vEvent);
201
		return $vCal;
202
	}
203
204
	/**
205
	 * @param string $user
206
	 */
207
	public function syncUser($user) {
208
		$principal = 'principals/users/'.$user;
209
		$this->ensureCalendarExists($principal);
210
		$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
211
		foreach($books as $book) {
212
			$cards = $this->cardDavBackEnd->getCards($book['id']);
213
			foreach($cards as $card) {
214
				$this->onCardChanged($book['id'], $card['uri'], $card['carddata']);
215
			}
216
		}
217
	}
218
219
	/**
220
	 * @param string $existingCalendarData
221
	 * @param VCalendar $newCalendarData
222
	 * @return bool
223
	 */
224
	public function birthdayEvenChanged($existingCalendarData, $newCalendarData) {
225
		try {
226
			$existingBirthday = Reader::read($existingCalendarData);
227
		} catch (Exception $ex) {
228
			return true;
229
		}
230
		if ($newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
231
			$newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
232
		) {
233
			return true;
234
		}
235
		return false;
236
	}
237
238
	/**
239
	 * @param integer $addressBookId
240
	 * @return mixed
241
	 */
242
	protected function getAllAffectedPrincipals($addressBookId) {
243
		$targetPrincipals = [];
244
		$shares = $this->cardDavBackEnd->getShares($addressBookId);
245
		foreach ($shares as $share) {
246
			if ($share['{http://owncloud.org/ns}group-share']) {
247
				$users = $this->principalBackend->getGroupMemberSet($share['{http://owncloud.org/ns}principal']);
248
				foreach ($users as $user) {
249
					$targetPrincipals[] = $user['uri'];
250
				}
251
			} else {
252
				$targetPrincipals[] = $share['{http://owncloud.org/ns}principal'];
253
			}
254
		}
255
		return array_values(array_unique($targetPrincipals, SORT_STRING));
256
	}
257
258
	/**
259
	 * @param string $cardUri
260
	 * @param string  $cardData
261
	 * @param array $book
262
	 * @param int $calendarId
263
	 * @param string $type
264
	 */
265
	private function updateCalendar($cardUri, $cardData, $book, $calendarId, $type) {
266
		$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
267
		$calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['symbol']);
268
		$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
269
		if (is_null($calendarData)) {
270
			if (!is_null($existing)) {
271
				$this->calDavBackEnd->deleteCalendarObject($calendarId, $objectUri);
272
			}
273
		} else {
274
			if (is_null($existing)) {
275
				$this->calDavBackEnd->createCalendarObject($calendarId, $objectUri, $calendarData->serialize());
276
			} else {
277
				if ($this->birthdayEvenChanged($existing['calendardata'], $calendarData)) {
278
					$this->calDavBackEnd->updateCalendarObject($calendarId, $objectUri, $calendarData->serialize());
279
				}
280
			}
281
		}
282
	}
283
284
}
285