Passed
Push — master ( 555550...c77cc8 )
by Blizzz
13:10 queued 12s
created

Manager   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 131
c 1
b 0
f 0
dl 0
loc 337
rs 8.96
wmc 43

14 Methods

Rating   Name   Duplication   Size   Complexity  
A searchForPrincipal() 0 23 3
A unregisterCalendar() 0 2 1
A __construct() 0 8 1
A search() 0 12 3
A getCalendarsForPrincipal() 0 20 3
A getCalendars() 0 4 1
B handleIMipReply() 0 63 10
A register() 0 2 1
A clear() 0 3 1
A isEnabled() 0 2 2
A loadCalendars() 0 5 2
A registerCalendar() 0 2 1
A newQuery() 0 2 1
C handleIMipCancel() 0 73 13

How to fix   Complexity   

Complex Class

Complex classes like Manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Manager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright 2017, Georg Ehrke <[email protected]>
7
 *
8
 * @author Christoph Wurst <[email protected]>
9
 * @author Georg Ehrke <[email protected]>
10
 *
11
 * @license GNU AGPL version 3 or any later version
12
 *
13
 * This program is free software: you can redistribute it and/or modify
14
 * it under the terms of the GNU Affero General Public License as
15
 * published by the Free Software Foundation, either version 3 of the
16
 * License, or (at your option) any later version.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License
24
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
25
 *
26
 */
27
namespace OC\Calendar;
28
29
use OC\AppFramework\Bootstrap\Coordinator;
30
use OCP\AppFramework\Utility\ITimeFactory;
31
use OCP\Calendar\Exceptions\CalendarException;
32
use OCP\Calendar\ICalendar;
33
use OCP\Calendar\ICalendarProvider;
34
use OCP\Calendar\ICalendarQuery;
35
use OCP\Calendar\ICreateFromString;
36
use OCP\Calendar\IManager;
37
use Psr\Container\ContainerInterface;
38
use Psr\Log\LoggerInterface;
39
use Sabre\VObject\Component\VCalendar;
40
use Sabre\VObject\Component\VEvent;
41
use Sabre\VObject\Property\VCard\DateTime;
42
use Sabre\VObject\Reader;
43
use Throwable;
44
use function array_map;
45
use function array_merge;
46
47
class Manager implements IManager {
48
49
	/**
50
	 * @var ICalendar[] holds all registered calendars
51
	 */
52
	private $calendars = [];
53
54
	/**
55
	 * @var \Closure[] to call to load/register calendar providers
56
	 */
57
	private $calendarLoaders = [];
58
59
	/** @var Coordinator */
60
	private $coordinator;
61
62
	/** @var ContainerInterface */
63
	private $container;
64
65
	/** @var LoggerInterface */
66
	private $logger;
67
68
	private ITimeFactory $timeFactory;
69
70
71
	public function __construct(Coordinator $coordinator,
72
								ContainerInterface $container,
73
								LoggerInterface $logger,
74
								ITimeFactory $timeFactory) {
75
		$this->coordinator = $coordinator;
76
		$this->container = $container;
77
		$this->logger = $logger;
78
		$this->timeFactory = $timeFactory;
79
	}
80
81
	/**
82
	 * This function is used to search and find objects within the user's calendars.
83
	 * In case $pattern is empty all events/journals/todos will be returned.
84
	 *
85
	 * @param string $pattern which should match within the $searchProperties
86
	 * @param array $searchProperties defines the properties within the query pattern should match
87
	 * @param array $options - optional parameters:
88
	 * 	['timerange' => ['start' => new DateTime(...), 'end' => new DateTime(...)]]
89
	 * @param integer|null $limit - limit number of search results
90
	 * @param integer|null $offset - offset for paging of search results
91
	 * @return array an array of events/journals/todos which are arrays of arrays of key-value-pairs
92
	 * @since 13.0.0
93
	 */
94
	public function search($pattern, array $searchProperties = [], array $options = [], $limit = null, $offset = null) {
95
		$this->loadCalendars();
96
		$result = [];
97
		foreach ($this->calendars as $calendar) {
98
			$r = $calendar->search($pattern, $searchProperties, $options, $limit, $offset);
99
			foreach ($r as $o) {
100
				$o['calendar-key'] = $calendar->getKey();
101
				$result[] = $o;
102
			}
103
		}
104
105
		return $result;
106
	}
107
108
	/**
109
	 * Check if calendars are available
110
	 *
111
	 * @return bool true if enabled, false if not
112
	 * @since 13.0.0
113
	 */
114
	public function isEnabled() {
115
		return !empty($this->calendars) || !empty($this->calendarLoaders);
116
	}
117
118
	/**
119
	 * Registers a calendar
120
	 *
121
	 * @param ICalendar $calendar
122
	 * @return void
123
	 * @since 13.0.0
124
	 */
125
	public function registerCalendar(ICalendar $calendar) {
126
		$this->calendars[$calendar->getKey()] = $calendar;
127
	}
128
129
	/**
130
	 * Unregisters a calendar
131
	 *
132
	 * @param ICalendar $calendar
133
	 * @return void
134
	 * @since 13.0.0
135
	 */
136
	public function unregisterCalendar(ICalendar $calendar) {
137
		unset($this->calendars[$calendar->getKey()]);
138
	}
139
140
	/**
141
	 * In order to improve lazy loading a closure can be registered which will be called in case
142
	 * calendars are actually requested
143
	 *
144
	 * @param \Closure $callable
145
	 * @return void
146
	 * @since 13.0.0
147
	 */
148
	public function register(\Closure $callable) {
149
		$this->calendarLoaders[] = $callable;
150
	}
151
152
	/**
153
	 * @return ICalendar[]
154
	 * @since 13.0.0
155
	 */
156
	public function getCalendars() {
157
		$this->loadCalendars();
158
159
		return array_values($this->calendars);
160
	}
161
162
	/**
163
	 * removes all registered calendar instances
164
	 * @return void
165
	 * @since 13.0.0
166
	 */
167
	public function clear() {
168
		$this->calendars = [];
169
		$this->calendarLoaders = [];
170
	}
171
172
	/**
173
	 * loads all calendars
174
	 */
175
	private function loadCalendars() {
176
		foreach ($this->calendarLoaders as $callable) {
177
			$callable($this);
178
		}
179
		$this->calendarLoaders = [];
180
	}
181
182
	/**
183
	 * @param string $principalUri
184
	 * @param array $calendarUris
185
	 * @return ICreateFromString[]
186
	 */
187
	public function getCalendarsForPrincipal(string $principalUri, array $calendarUris = []): array {
188
		$context = $this->coordinator->getRegistrationContext();
189
		if ($context === null) {
190
			return [];
191
		}
192
193
		return array_merge(
194
			...array_map(function ($registration) use ($principalUri, $calendarUris) {
195
				try {
196
					/** @var ICalendarProvider $provider */
197
					$provider = $this->container->get($registration->getService());
198
				} catch (Throwable $e) {
199
					$this->logger->error('Could not load calendar provider ' . $registration->getService() . ': ' . $e->getMessage(), [
200
						'exception' => $e,
201
					]);
202
					return [];
203
				}
204
205
				return $provider->getCalendars($principalUri, $calendarUris);
206
			}, $context->getCalendarProviders())
207
		);
208
	}
209
210
	public function searchForPrincipal(ICalendarQuery $query): array {
211
		/** @var CalendarQuery $query */
212
		$calendars = $this->getCalendarsForPrincipal(
213
			$query->getPrincipalUri(),
214
			$query->getCalendarUris(),
215
		);
216
217
		$results = [];
218
		foreach ($calendars as $calendar) {
219
			$r = $calendar->search(
220
				$query->getSearchPattern() ?? '',
221
				$query->getSearchProperties(),
222
				$query->getOptions(),
223
				$query->getLimit(),
224
				$query->getOffset()
225
			);
226
227
			foreach ($r as $o) {
228
				$o['calendar-key'] = $calendar->getKey();
229
				$results[] = $o;
230
			}
231
		}
232
		return $results;
233
	}
234
235
	public function newQuery(string $principalUri): ICalendarQuery {
236
		return new CalendarQuery($principalUri);
237
	}
238
239
	/**
240
	 * @throws \OCP\DB\Exception
241
	 */
242
	public function handleIMipReply(string $principalUri, string $sender, string $recipient, string $calendarData): bool {
243
		/** @var VCalendar $vObject */
244
		$vObject = Reader::read($calendarData);
245
		/** @var VEvent $vEvent */
246
		$vEvent = $vObject->{'VEVENT'};
247
248
		// First, we check if the correct method is passed to us
249
		if (strcasecmp('REPLY', $vObject->{'METHOD'}->getValue()) !== 0) {
250
			$this->logger->warning('Wrong method provided for processing');
251
			return false;
252
		}
253
254
		// check if mail recipient and organizer are one and the same
255
		$organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
256
257
		if (strcasecmp($recipient, $organizer) !== 0) {
258
			$this->logger->warning('Recipient and ORGANIZER must be identical');
259
			return false;
260
		}
261
262
		//check if the event is in the future
263
		/** @var DateTime $eventTime */
264
		$eventTime = $vEvent->{'DTSTART'};
265
		if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
266
			$this->logger->warning('Only events in the future are processed');
267
			return false;
268
		}
269
270
		$calendars = $this->getCalendarsForPrincipal($principalUri);
271
		if (empty($calendars)) {
272
			$this->logger->warning('Could not find any calendars for principal ' . $principalUri);
273
			return false;
274
		}
275
276
		$found = null;
277
		// if the attendee has been found in at least one calendar event with the UID of the iMIP event
278
		// we process it.
279
		// Benefit: no attendee lost
280
		// Drawback: attendees that have been deleted will still be able to update their partstat
281
		foreach ($calendars as $calendar) {
282
			// We should not search in writable calendars
283
			if ($calendar instanceof ICreateFromString) {
284
				$o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
285
				if (!empty($o)) {
286
					$found = $calendar;
287
					$name = $o[0]['uri'];
288
					break;
289
				}
290
			}
291
		}
292
293
		if (empty($found)) {
294
			$this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
295
			return false;
296
		}
297
298
		try {
299
			$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $name does not seem to be defined for all execution paths leading up to this point.
Loading history...
300
		} catch (CalendarException $e) {
301
			$this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
302
			return false;
303
		}
304
		return true;
305
	}
306
307
	/**
308
	 * @since 25.0.0
309
	 * @throws \OCP\DB\Exception
310
	 */
311
	public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool {
312
		$vObject = Reader::read($calendarData);
313
		/** @var VEvent $vEvent */
314
		$vEvent = $vObject->{'VEVENT'};
315
316
		// First, we check if the correct method is passed to us
317
		if (strcasecmp('CANCEL', $vObject->{'METHOD'}->getValue()) !== 0) {
318
			$this->logger->warning('Wrong method provided for processing');
319
			return false;
320
		}
321
322
		$attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
323
		if (strcasecmp($recipient, $attendee) !== 0) {
324
			$this->logger->warning('Recipient must be an ATTENDEE of this event');
325
			return false;
326
		}
327
328
		// Thirdly, we need to compare the email address the CANCEL is coming from (in Mail)
329
		// or the Reply- To Address submitted with the CANCEL email
330
		// to the email address in the ORGANIZER.
331
		// We don't want to accept a CANCEL request from just anyone
332
		$organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
333
		$isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
334
		if ($isNotOrganizer) {
335
			$this->logger->warning('Sender must be the ORGANIZER of this event');
336
			return false;
337
		}
338
339
		//check if the event is in the future
340
		/** @var DateTime $eventTime */
341
		$eventTime = $vEvent->{'DTSTART'};
342
		if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
343
			$this->logger->warning('Only events in the future are processed');
344
			return false;
345
		}
346
347
		// Check if we have a calendar to work with
348
		$calendars = $this->getCalendarsForPrincipal($principalUri);
349
		if (empty($calendars)) {
350
			$this->logger->warning('Could not find any calendars for principal ' . $principalUri);
351
			return false;
352
		}
353
354
		$found = null;
355
		// if the attendee has been found in at least one calendar event with the UID of the iMIP event
356
		// we process it.
357
		// Benefit: no attendee lost
358
		// Drawback: attendees that have been deleted will still be able to update their partstat
359
		foreach ($calendars as $calendar) {
360
			// We should not search in writable calendars
361
			if ($calendar instanceof ICreateFromString) {
362
				$o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
363
				if (!empty($o)) {
364
					$found = $calendar;
365
					$name = $o[0]['uri'];
366
					break;
367
				}
368
			}
369
		}
370
371
		if (empty($found)) {
372
			$this->logger->info('Event not found in any calendar for principal ' . $principalUri . 'and UID' . $vEvent->{'UID'}->getValue());
373
			// this is a safe operation
374
			// we can ignore events that have been cancelled but were not in the calendar anyway
375
			return true;
376
		}
377
378
		try {
379
			$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $name does not seem to be defined for all execution paths leading up to this point.
Loading history...
380
			return true;
381
		} catch (CalendarException $e) {
382
			$this->logger->error('Could not update calendar for iMIP processing', ['exception' => $e]);
383
			return false;
384
		}
385
	}
386
}
387