Completed
Push — master ( a27a8d...b12464 )
by Lukas
45:55 queued 29:49
created

CalDavBackend::deleteCalendar()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Stefan Weil <[email protected]>
7
 * @author Thomas Citharel <[email protected]>
8
 * @author Thomas Müller <[email protected]>
9
 *
10
 * @license AGPL-3.0
11
 *
12
 * This code is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License, version 3,
14
 * as published by the Free Software Foundation.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License, version 3,
22
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
23
 *
24
 */
25
26
namespace OCA\DAV\CalDAV;
27
28
use OCA\DAV\DAV\Sharing\IShareable;
29
use OCP\DB\QueryBuilder\IQueryBuilder;
30
use OCA\DAV\Connector\Sabre\Principal;
31
use OCA\DAV\DAV\Sharing\Backend;
32
use OCP\IDBConnection;
33
use OCP\IUser;
34
use OCP\IUserManager;
35
use Sabre\CalDAV\Backend\AbstractBackend;
36
use Sabre\CalDAV\Backend\SchedulingSupport;
37
use Sabre\CalDAV\Backend\SubscriptionSupport;
38
use Sabre\CalDAV\Backend\SyncSupport;
39
use Sabre\CalDAV\Plugin;
40
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
41
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
42
use Sabre\DAV;
43
use Sabre\DAV\Exception\Forbidden;
44
use Sabre\DAV\PropPatch;
45
use Sabre\HTTP\URLUtil;
46
use Sabre\VObject\DateTimeParser;
47
use Sabre\VObject\Reader;
48
use Sabre\VObject\Recur\EventIterator;
49
50
/**
51
 * Class CalDavBackend
52
 *
53
 * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
54
 *
55
 * @package OCA\DAV\CalDAV
56
 */
57
class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
58
59
	/**
60
	 * We need to specify a max date, because we need to stop *somewhere*
61
	 *
62
	 * On 32 bit system the maximum for a signed integer is 2147483647, so
63
	 * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
64
	 * in 2038-01-19 to avoid problems when the date is converted
65
	 * to a unix timestamp.
66
	 */
67
	const MAX_DATE = '2038-01-01';
68
69
	const CLASSIFICATION_PUBLIC = 0;
70
	const CLASSIFICATION_PRIVATE = 1;
71
	const CLASSIFICATION_CONFIDENTIAL = 2;
72
73
	/**
74
	 * List of CalDAV properties, and how they map to database field names
75
	 * Add your own properties by simply adding on to this array.
76
	 *
77
	 * Note that only string-based properties are supported here.
78
	 *
79
	 * @var array
80
	 */
81
	public $propertyMap = [
82
		'{DAV:}displayname'                          => 'displayname',
83
		'{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
84
		'{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
85
		'{http://apple.com/ns/ical/}calendar-order'  => 'calendarorder',
86
		'{http://apple.com/ns/ical/}calendar-color'  => 'calendarcolor',
87
	];
88
89
	/**
90
	 * List of subscription properties, and how they map to database field names.
91
	 *
92
	 * @var array
93
	 */
94
	public $subscriptionPropertyMap = [
95
		'{DAV:}displayname'                                           => 'displayname',
96
		'{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
97
		'{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
98
		'{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
99
		'{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
100
		'{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
101
		'{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
102
	];
103
104
	/**
105
	 * @var string[] Map of uid => display name
106
	 */
107
	protected $userDisplayNames;
108
109
	/** @var IDBConnection */
110
	private $db;
111
112
	/** @var Backend */
113
	private $sharingBackend;
114
115
	/** @var Principal */
116
	private $principalBackend;
117
118
	/** @var IUserManager */
119
	private $userManager;
120
121
	/**
122
	 * CalDavBackend constructor.
123
	 *
124
	 * @param IDBConnection $db
125
	 * @param Principal $principalBackend
126
	 * @param IUserManager $userManager
127
	 */
128 View Code Duplication
	public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager) {
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...
129
		$this->db = $db;
130
		$this->principalBackend = $principalBackend;
131
		$this->userManager = $userManager;
132
		$this->sharingBackend = new Backend($this->db, $principalBackend, 'calendar');
133
	}
134
135
	/**
136
	 * Returns a list of calendars for a principal.
137
	 *
138
	 * Every project is an array with the following keys:
139
	 *  * id, a unique id that will be used by other functions to modify the
140
	 *    calendar. This can be the same as the uri or a database key.
141
	 *  * uri, which the basename of the uri with which the calendar is
142
	 *    accessed.
143
	 *  * principaluri. The owner of the calendar. Almost always the same as
144
	 *    principalUri passed to this method.
145
	 *
146
	 * Furthermore it can contain webdav properties in clark notation. A very
147
	 * common one is '{DAV:}displayname'.
148
	 *
149
	 * Many clients also require:
150
	 * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
151
	 * For this property, you can just return an instance of
152
	 * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
153
	 *
154
	 * If you return {http://sabredav.org/ns}read-only and set the value to 1,
155
	 * ACL will automatically be put in read-only mode.
156
	 *
157
	 * @param string $principalUri
158
	 * @return array
159
	 */
160
	function getCalendarsForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
161
		$principalUriOriginal = $principalUri;
162
		$principalUri = $this->convertPrincipal($principalUri, true);
163
		$fields = array_values($this->propertyMap);
164
		$fields[] = 'id';
165
		$fields[] = 'uri';
166
		$fields[] = 'synctoken';
167
		$fields[] = 'components';
168
		$fields[] = 'principaluri';
169
		$fields[] = 'transparent';
170
171
		// Making fields a comma-delimited list
172
		$query = $this->db->getQueryBuilder();
173
		$query->select($fields)->from('calendars')
174
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
175
				->orderBy('calendarorder', 'ASC');
176
		$stmt = $query->execute();
177
178
		$calendars = [];
179
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
180
181
			$components = [];
182
			if ($row['components']) {
183
				$components = explode(',',$row['components']);
184
			}
185
186
			$calendar = [
187
				'id' => $row['id'],
188
				'uri' => $row['uri'],
189
				'principaluri' => $this->convertPrincipal($row['principaluri'], false),
190
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
191
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
192
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
193
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
194
			];
195
196
			foreach($this->propertyMap as $xmlName=>$dbName) {
197
				$calendar[$xmlName] = $row[$dbName];
198
			}
199
200
			if (!isset($calendars[$calendar['id']])) {
201
				$calendars[$calendar['id']] = $calendar;
202
			}
203
		}
204
205
		$stmt->closeCursor();
206
207
		// query for shared calendars
208
		$principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
209
		$principals[]= $principalUri;
210
211
		$fields = array_values($this->propertyMap);
212
		$fields[] = 'a.id';
213
		$fields[] = 'a.uri';
214
		$fields[] = 'a.synctoken';
215
		$fields[] = 'a.components';
216
		$fields[] = 'a.principaluri';
217
		$fields[] = 'a.transparent';
218
		$fields[] = 's.access';
219
		$query = $this->db->getQueryBuilder();
220
		$result = $query->select($fields)
221
			->from('dav_shares', 's')
222
			->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
223
			->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri')))
224
			->andWhere($query->expr()->eq('s.type', $query->createParameter('type')))
225
			->setParameter('type', 'calendar')
226
			->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
227
			->execute();
228
229
		while($row = $result->fetch()) {
230
			list(, $name) = URLUtil::splitPath($row['principaluri']);
231
			$uri = $row['uri'] . '_shared_by_' . $name;
232
			$row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')';
233
			$components = [];
234
			if ($row['components']) {
235
				$components = explode(',',$row['components']);
236
			}
237
			$calendar = [
238
				'id' => $row['id'],
239
				'uri' => $uri,
240
				'principaluri' => $principalUri,
241
				'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
242
				'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
243
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
244
				'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
245
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $row['principaluri'],
246
				'{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
247
			];
248
249
			foreach($this->propertyMap as $xmlName=>$dbName) {
250
				$calendar[$xmlName] = $row[$dbName];
251
			}
252
253
			if (!isset($calendars[$calendar['id']])) {
254
				$calendars[$calendar['id']] = $calendar;
255
			}
256
		}
257
		$result->closeCursor();
258
259
		return array_values($calendars);
260
	}
261
262 View Code Duplication
	private function getUserDisplayName($uid) {
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...
263
		if (!isset($this->userDisplayNames[$uid])) {
264
			$user = $this->userManager->get($uid);
265
266
			if ($user instanceof IUser) {
267
				$this->userDisplayNames[$uid] = $user->getDisplayName();
268
			} else {
269
				$this->userDisplayNames[$uid] = $uid;
270
			}
271
		}
272
273
		return $this->userDisplayNames[$uid];
274
	}
275
276
	/**
277
	 * @param string $principal
278
	 * @param string $uri
279
	 * @return array|null
280
	 */
281
	public function getCalendarByUri($principal, $uri) {
282
		$fields = array_values($this->propertyMap);
283
		$fields[] = 'id';
284
		$fields[] = 'uri';
285
		$fields[] = 'synctoken';
286
		$fields[] = 'components';
287
		$fields[] = 'principaluri';
288
		$fields[] = 'transparent';
289
290
		// Making fields a comma-delimited list
291
		$query = $this->db->getQueryBuilder();
292
		$query->select($fields)->from('calendars')
293
			->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
294
			->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
295
			->setMaxResults(1);
296
		$stmt = $query->execute();
297
298
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
299
		$stmt->closeCursor();
300
		if ($row === false) {
301
			return null;
302
		}
303
304
		$components = [];
305
		if ($row['components']) {
306
			$components = explode(',',$row['components']);
307
		}
308
309
		$calendar = [
310
			'id' => $row['id'],
311
			'uri' => $row['uri'],
312
			'principaluri' => $row['principaluri'],
313
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
314
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
315
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
316
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
317
		];
318
319
		foreach($this->propertyMap as $xmlName=>$dbName) {
320
			$calendar[$xmlName] = $row[$dbName];
321
		}
322
323
		return $calendar;
324
	}
325
326
	public function getCalendarById($calendarId) {
327
		$fields = array_values($this->propertyMap);
328
		$fields[] = 'id';
329
		$fields[] = 'uri';
330
		$fields[] = 'synctoken';
331
		$fields[] = 'components';
332
		$fields[] = 'principaluri';
333
		$fields[] = 'transparent';
334
335
		// Making fields a comma-delimited list
336
		$query = $this->db->getQueryBuilder();
337
		$query->select($fields)->from('calendars')
338
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
339
			->setMaxResults(1);
340
		$stmt = $query->execute();
341
342
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
343
		$stmt->closeCursor();
344
		if ($row === false) {
345
			return null;
346
		}
347
348
		$components = [];
349
		if ($row['components']) {
350
			$components = explode(',',$row['components']);
351
		}
352
353
		$calendar = [
354
			'id' => $row['id'],
355
			'uri' => $row['uri'],
356
			'principaluri' => $row['principaluri'],
357
			'{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'),
358
			'{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0',
359
			'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
360
			'{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
361
		];
362
363
		foreach($this->propertyMap as $xmlName=>$dbName) {
364
			$calendar[$xmlName] = $row[$dbName];
365
		}
366
367
		return $calendar;
368
	}
369
370
	/**
371
	 * Creates a new calendar for a principal.
372
	 *
373
	 * If the creation was a success, an id must be returned that can be used to reference
374
	 * this calendar in other methods, such as updateCalendar.
375
	 *
376
	 * @param string $principalUri
377
	 * @param string $calendarUri
378
	 * @param array $properties
379
	 * @return int
380
	 */
381
	function createCalendar($principalUri, $calendarUri, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
382
		$values = [
383
			'principaluri' => $principalUri,
384
			'uri'          => $calendarUri,
385
			'synctoken'    => 1,
386
			'transparent'  => 0,
387
			'components'   => 'VEVENT,VTODO',
388
			'displayname'  => $calendarUri
389
		];
390
391
		// Default value
392
		$sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
393
		if (isset($properties[$sccs])) {
394
			if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
0 ignored issues
show
Bug introduced by
The class Sabre\CalDAV\Xml\Propert...tedCalendarComponentSet 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...
395
				throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
396
			}
397
			$values['components'] = implode(',',$properties[$sccs]->getValue());
398
		}
399
		$transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
400
		if (isset($properties[$transp])) {
401
			$values['transparent'] = $properties[$transp]->getValue()==='transparent';
402
		}
403
404
		foreach($this->propertyMap as $xmlName=>$dbName) {
405
			if (isset($properties[$xmlName])) {
406
				$values[$dbName] = $properties[$xmlName];
407
			}
408
		}
409
410
		$query = $this->db->getQueryBuilder();
411
		$query->insert('calendars');
412
		foreach($values as $column => $value) {
413
			$query->setValue($column, $query->createNamedParameter($value));
414
		}
415
		$query->execute();
416
		return $query->getLastInsertId();
417
	}
418
419
	/**
420
	 * Updates properties for a calendar.
421
	 *
422
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
423
	 * To do the actual updates, you must tell this object which properties
424
	 * you're going to process with the handle() method.
425
	 *
426
	 * Calling the handle method is like telling the PropPatch object "I
427
	 * promise I can handle updating this property".
428
	 *
429
	 * Read the PropPatch documentation for more info and examples.
430
	 *
431
	 * @param PropPatch $propPatch
432
	 * @return void
433
	 */
434
	function updateCalendar($calendarId, PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
435
		$supportedProperties = array_keys($this->propertyMap);
436
		$supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
437
438
		$propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
439
			$newValues = [];
440
			foreach ($mutations as $propertyName => $propertyValue) {
441
442
				switch ($propertyName) {
443
					case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' :
444
						$fieldName = 'transparent';
445
						$newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
446
						break;
447
					default :
448
						$fieldName = $this->propertyMap[$propertyName];
449
						$newValues[$fieldName] = $propertyValue;
450
						break;
451
				}
452
453
			}
454
			$query = $this->db->getQueryBuilder();
455
			$query->update('calendars');
456
			foreach ($newValues as $fieldName => $value) {
457
				$query->set($fieldName, $query->createNamedParameter($value));
458
			}
459
			$query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
460
			$query->execute();
461
462
			$this->addChange($calendarId, "", 2);
463
464
			return true;
465
		});
466
	}
467
468
	/**
469
	 * Delete a calendar and all it's objects
470
	 *
471
	 * @param mixed $calendarId
472
	 * @return void
473
	 */
474
	function deleteCalendar($calendarId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
475
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?');
476
		$stmt->execute([$calendarId]);
477
478
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?');
479
		$stmt->execute([$calendarId]);
480
481
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ?');
482
		$stmt->execute([$calendarId]);
483
484
		$this->sharingBackend->deleteAllShares($calendarId);
485
	}
486
487
	/**
488
	 * Returns all calendar objects within a calendar.
489
	 *
490
	 * Every item contains an array with the following keys:
491
	 *   * calendardata - The iCalendar-compatible calendar data
492
	 *   * uri - a unique key which will be used to construct the uri. This can
493
	 *     be any arbitrary string, but making sure it ends with '.ics' is a
494
	 *     good idea. This is only the basename, or filename, not the full
495
	 *     path.
496
	 *   * lastmodified - a timestamp of the last modification time
497
	 *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
498
	 *   '"abcdef"')
499
	 *   * size - The size of the calendar objects, in bytes.
500
	 *   * component - optional, a string containing the type of object, such
501
	 *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
502
	 *     the Content-Type header.
503
	 *
504
	 * Note that the etag is optional, but it's highly encouraged to return for
505
	 * speed reasons.
506
	 *
507
	 * The calendardata is also optional. If it's not returned
508
	 * 'getCalendarObject' will be called later, which *is* expected to return
509
	 * calendardata.
510
	 *
511
	 * If neither etag or size are specified, the calendardata will be
512
	 * used/fetched to determine these numbers. If both are specified the
513
	 * amount of times this is needed is reduced by a great degree.
514
	 *
515
	 * @param mixed $calendarId
516
	 * @return array
517
	 */
518
	function getCalendarObjects($calendarId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
519
		$query = $this->db->getQueryBuilder();
520
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
521
			->from('calendarobjects')
522
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
523
		$stmt = $query->execute();
524
525
		$result = [];
526
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
527
			$result[] = [
528
					'id'           => $row['id'],
529
					'uri'          => $row['uri'],
530
					'lastmodified' => $row['lastmodified'],
531
					'etag'         => '"' . $row['etag'] . '"',
532
					'calendarid'   => $row['calendarid'],
533
					'size'         => (int)$row['size'],
534
					'component'    => strtolower($row['componenttype']),
535
					'classification'=> (int)$row['classification']
536
			];
537
		}
538
539
		return $result;
540
	}
541
542
	/**
543
	 * Returns information from a single calendar object, based on it's object
544
	 * uri.
545
	 *
546
	 * The object uri is only the basename, or filename and not a full path.
547
	 *
548
	 * The returned array must have the same keys as getCalendarObjects. The
549
	 * 'calendardata' object is required here though, while it's not required
550
	 * for getCalendarObjects.
551
	 *
552
	 * This method must return null if the object did not exist.
553
	 *
554
	 * @param mixed $calendarId
555
	 * @param string $objectUri
556
	 * @return array|null
557
	 */
558
	function getCalendarObject($calendarId, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
559
560
		$query = $this->db->getQueryBuilder();
561
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
562
				->from('calendarobjects')
563
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
564
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)));
565
		$stmt = $query->execute();
566
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
567
568
		if(!$row) return null;
569
570
		return [
571
				'id'            => $row['id'],
572
				'uri'           => $row['uri'],
573
				'lastmodified'  => $row['lastmodified'],
574
				'etag'          => '"' . $row['etag'] . '"',
575
				'calendarid'    => $row['calendarid'],
576
				'size'          => (int)$row['size'],
577
				'calendardata'  => $this->readBlob($row['calendardata']),
578
				'component'     => strtolower($row['componenttype']),
579
				'classification'=> (int)$row['classification']
580
		];
581
	}
582
583
	/**
584
	 * Returns a list of calendar objects.
585
	 *
586
	 * This method should work identical to getCalendarObject, but instead
587
	 * return all the calendar objects in the list as an array.
588
	 *
589
	 * If the backend supports this, it may allow for some speed-ups.
590
	 *
591
	 * @param mixed $calendarId
592
	 * @param string[] $uris
593
	 * @return array
594
	 */
595
	function getMultipleCalendarObjects($calendarId, array $uris) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
596
		$query = $this->db->getQueryBuilder();
597
		$query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
598
				->from('calendarobjects')
599
				->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
600
				->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
601
				->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
602
603
		$stmt = $query->execute();
604
605
		$result = [];
606
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
607
608
			$result[] = [
609
					'id'           => $row['id'],
610
					'uri'          => $row['uri'],
611
					'lastmodified' => $row['lastmodified'],
612
					'etag'         => '"' . $row['etag'] . '"',
613
					'calendarid'   => $row['calendarid'],
614
					'size'         => (int)$row['size'],
615
					'calendardata' => $this->readBlob($row['calendardata']),
616
					'component'    => strtolower($row['componenttype']),
617
					'classification' => (int)$row['classification']
618
			];
619
620
		}
621
		return $result;
622
	}
623
624
	/**
625
	 * Creates a new calendar object.
626
	 *
627
	 * The object uri is only the basename, or filename and not a full path.
628
	 *
629
	 * It is possible return an etag from this function, which will be used in
630
	 * the response to this PUT request. Note that the ETag must be surrounded
631
	 * by double-quotes.
632
	 *
633
	 * However, you should only really return this ETag if you don't mangle the
634
	 * calendar-data. If the result of a subsequent GET to this object is not
635
	 * the exact same as this request body, you should omit the ETag.
636
	 *
637
	 * @param mixed $calendarId
638
	 * @param string $objectUri
639
	 * @param string $calendarData
640
	 * @return string
641
	 */
642
	function createCalendarObject($calendarId, $objectUri, $calendarData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
643
		$extraData = $this->getDenormalizedData($calendarData);
644
645
		$query = $this->db->getQueryBuilder();
646
		$query->insert('calendarobjects')
647
			->values([
648
				'calendarid' => $query->createNamedParameter($calendarId),
649
				'uri' => $query->createNamedParameter($objectUri),
650
				'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
651
				'lastmodified' => $query->createNamedParameter(time()),
652
				'etag' => $query->createNamedParameter($extraData['etag']),
653
				'size' => $query->createNamedParameter($extraData['size']),
654
				'componenttype' => $query->createNamedParameter($extraData['componentType']),
655
				'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
656
				'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
657
				'classification' => $query->createNamedParameter($extraData['classification']),
658
				'uid' => $query->createNamedParameter($extraData['uid']),
659
			])
660
			->execute();
661
662
		$this->addChange($calendarId, $objectUri, 1);
663
664
		return '"' . $extraData['etag'] . '"';
665
	}
666
667
	/**
668
	 * Updates an existing calendarobject, based on it's uri.
669
	 *
670
	 * The object uri is only the basename, or filename and not a full path.
671
	 *
672
	 * It is possible return an etag from this function, which will be used in
673
	 * the response to this PUT request. Note that the ETag must be surrounded
674
	 * by double-quotes.
675
	 *
676
	 * However, you should only really return this ETag if you don't mangle the
677
	 * calendar-data. If the result of a subsequent GET to this object is not
678
	 * the exact same as this request body, you should omit the ETag.
679
	 *
680
	 * @param mixed $calendarId
681
	 * @param string $objectUri
682
	 * @param string $calendarData
683
	 * @return string
684
	 */
685
	function updateCalendarObject($calendarId, $objectUri, $calendarData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
686
		$extraData = $this->getDenormalizedData($calendarData);
687
688
		$query = $this->db->getQueryBuilder();
689
		$query->update('calendarobjects')
690
				->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
691
				->set('lastmodified', $query->createNamedParameter(time()))
692
				->set('etag', $query->createNamedParameter($extraData['etag']))
693
				->set('size', $query->createNamedParameter($extraData['size']))
694
				->set('componenttype', $query->createNamedParameter($extraData['componentType']))
695
				->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
696
				->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
697
				->set('classification', $query->createNamedParameter($extraData['classification']))
698
				->set('uid', $query->createNamedParameter($extraData['uid']))
699
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
700
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
701
			->execute();
702
703
		$this->addChange($calendarId, $objectUri, 2);
704
705
		return '"' . $extraData['etag'] . '"';
706
	}
707
708
	/**
709
	 * @param int $calendarObjectId
710
	 * @param int $classification
711
	 */
712
	public function setClassification($calendarObjectId, $classification) {
713
		if (!in_array($classification, [
714
			self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
715
		])) {
716
			throw new \InvalidArgumentException();
717
		}
718
		$query = $this->db->getQueryBuilder();
719
		$query->update('calendarobjects')
720
			->set('classification', $query->createNamedParameter($classification))
721
			->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
722
			->execute();
723
	}
724
725
	/**
726
	 * Deletes an existing calendar object.
727
	 *
728
	 * The object uri is only the basename, or filename and not a full path.
729
	 *
730
	 * @param mixed $calendarId
731
	 * @param string $objectUri
732
	 * @return void
733
	 */
734
	function deleteCalendarObject($calendarId, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
735
		$stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ?');
736
		$stmt->execute([$calendarId, $objectUri]);
737
738
		$this->addChange($calendarId, $objectUri, 3);
739
	}
740
741
	/**
742
	 * Performs a calendar-query on the contents of this calendar.
743
	 *
744
	 * The calendar-query is defined in RFC4791 : CalDAV. Using the
745
	 * calendar-query it is possible for a client to request a specific set of
746
	 * object, based on contents of iCalendar properties, date-ranges and
747
	 * iCalendar component types (VTODO, VEVENT).
748
	 *
749
	 * This method should just return a list of (relative) urls that match this
750
	 * query.
751
	 *
752
	 * The list of filters are specified as an array. The exact array is
753
	 * documented by Sabre\CalDAV\CalendarQueryParser.
754
	 *
755
	 * Note that it is extremely likely that getCalendarObject for every path
756
	 * returned from this method will be called almost immediately after. You
757
	 * may want to anticipate this to speed up these requests.
758
	 *
759
	 * This method provides a default implementation, which parses *all* the
760
	 * iCalendar objects in the specified calendar.
761
	 *
762
	 * This default may well be good enough for personal use, and calendars
763
	 * that aren't very large. But if you anticipate high usage, big calendars
764
	 * or high loads, you are strongly advised to optimize certain paths.
765
	 *
766
	 * The best way to do so is override this method and to optimize
767
	 * specifically for 'common filters'.
768
	 *
769
	 * Requests that are extremely common are:
770
	 *   * requests for just VEVENTS
771
	 *   * requests for just VTODO
772
	 *   * requests with a time-range-filter on either VEVENT or VTODO.
773
	 *
774
	 * ..and combinations of these requests. It may not be worth it to try to
775
	 * handle every possible situation and just rely on the (relatively
776
	 * easy to use) CalendarQueryValidator to handle the rest.
777
	 *
778
	 * Note that especially time-range-filters may be difficult to parse. A
779
	 * time-range filter specified on a VEVENT must for instance also handle
780
	 * recurrence rules correctly.
781
	 * A good example of how to interprete all these filters can also simply
782
	 * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
783
	 * as possible, so it gives you a good idea on what type of stuff you need
784
	 * to think of.
785
	 *
786
	 * @param mixed $calendarId
787
	 * @param array $filters
788
	 * @return array
789
	 */
790
	function calendarQuery($calendarId, array $filters) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
791
		$componentType = null;
792
		$requirePostFilter = true;
793
		$timeRange = null;
794
795
		// if no filters were specified, we don't need to filter after a query
796
		if (!$filters['prop-filters'] && !$filters['comp-filters']) {
797
			$requirePostFilter = false;
798
		}
799
800
		// Figuring out if there's a component filter
801
		if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
802
			$componentType = $filters['comp-filters'][0]['name'];
803
804
			// Checking if we need post-filters
805 View Code Duplication
			if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
806
				$requirePostFilter = false;
807
			}
808
			// There was a time-range filter
809
			if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
810
				$timeRange = $filters['comp-filters'][0]['time-range'];
811
812
				// If start time OR the end time is not specified, we can do a
813
				// 100% accurate mysql query.
814 View Code Duplication
				if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
815
					$requirePostFilter = false;
816
				}
817
			}
818
819
		}
820
		$columns = ['uri'];
821
		if ($requirePostFilter) {
822
			$columns = ['uri', 'calendardata'];
823
		}
824
		$query = $this->db->getQueryBuilder();
825
		$query->select($columns)
826
			->from('calendarobjects')
827
			->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
828
829
		if ($componentType) {
830
			$query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
831
		}
832
833
		if ($timeRange && $timeRange['start']) {
834
			$query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
835
		}
836
		if ($timeRange && $timeRange['end']) {
837
			$query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
838
		}
839
840
		$stmt = $query->execute();
841
842
		$result = [];
843
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
844
			if ($requirePostFilter) {
845
				if (!$this->validateFilterForObject($row, $filters)) {
846
					continue;
847
				}
848
			}
849
			$result[] = $row['uri'];
850
		}
851
852
		return $result;
853
	}
854
855
	/**
856
	 * Searches through all of a users calendars and calendar objects to find
857
	 * an object with a specific UID.
858
	 *
859
	 * This method should return the path to this object, relative to the
860
	 * calendar home, so this path usually only contains two parts:
861
	 *
862
	 * calendarpath/objectpath.ics
863
	 *
864
	 * If the uid is not found, return null.
865
	 *
866
	 * This method should only consider * objects that the principal owns, so
867
	 * any calendars owned by other principals that also appear in this
868
	 * collection should be ignored.
869
	 *
870
	 * @param string $principalUri
871
	 * @param string $uid
872
	 * @return string|null
873
	 */
874
	function getCalendarObjectByUID($principalUri, $uid) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
875
876
		$query = $this->db->getQueryBuilder();
877
		$query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
878
			->from('calendarobjects', 'co')
879
			->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
880
			->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
881
			->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)));
882
883
		$stmt = $query->execute();
884
885
		if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
886
			return $row['calendaruri'] . '/' . $row['objecturi'];
887
		}
888
889
		return null;
890
	}
891
892
	/**
893
	 * The getChanges method returns all the changes that have happened, since
894
	 * the specified syncToken in the specified calendar.
895
	 *
896
	 * This function should return an array, such as the following:
897
	 *
898
	 * [
899
	 *   'syncToken' => 'The current synctoken',
900
	 *   'added'   => [
901
	 *      'new.txt',
902
	 *   ],
903
	 *   'modified'   => [
904
	 *      'modified.txt',
905
	 *   ],
906
	 *   'deleted' => [
907
	 *      'foo.php.bak',
908
	 *      'old.txt'
909
	 *   ]
910
	 * );
911
	 *
912
	 * The returned syncToken property should reflect the *current* syncToken
913
	 * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
914
	 * property This is * needed here too, to ensure the operation is atomic.
915
	 *
916
	 * If the $syncToken argument is specified as null, this is an initial
917
	 * sync, and all members should be reported.
918
	 *
919
	 * The modified property is an array of nodenames that have changed since
920
	 * the last token.
921
	 *
922
	 * The deleted property is an array with nodenames, that have been deleted
923
	 * from collection.
924
	 *
925
	 * The $syncLevel argument is basically the 'depth' of the report. If it's
926
	 * 1, you only have to report changes that happened only directly in
927
	 * immediate descendants. If it's 2, it should also include changes from
928
	 * the nodes below the child collections. (grandchildren)
929
	 *
930
	 * The $limit argument allows a client to specify how many results should
931
	 * be returned at most. If the limit is not specified, it should be treated
932
	 * as infinite.
933
	 *
934
	 * If the limit (infinite or not) is higher than you're willing to return,
935
	 * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
936
	 *
937
	 * If the syncToken is expired (due to data cleanup) or unknown, you must
938
	 * return null.
939
	 *
940
	 * The limit is 'suggestive'. You are free to ignore it.
941
	 *
942
	 * @param string $calendarId
943
	 * @param string $syncToken
944
	 * @param int $syncLevel
945
	 * @param int $limit
946
	 * @return array
947
	 */
948 View Code Duplication
	function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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...
949
		// Current synctoken
950
		$stmt = $this->db->prepare('SELECT `synctoken` FROM `*PREFIX*calendars` WHERE `id` = ?');
951
		$stmt->execute([ $calendarId ]);
952
		$currentToken = $stmt->fetchColumn(0);
953
954
		if (is_null($currentToken)) {
955
			return null;
956
		}
957
958
		$result = [
959
			'syncToken' => $currentToken,
960
			'added'     => [],
961
			'modified'  => [],
962
			'deleted'   => [],
963
		];
964
965
		if ($syncToken) {
966
967
			$query = "SELECT `uri`, `operation` FROM `*PREFIX*calendarchanges` WHERE `synctoken` >= ? AND `synctoken` < ? AND `calendarid` = ? ORDER BY `synctoken`";
968
			if ($limit>0) {
969
				$query.= " `LIMIT` " . (int)$limit;
970
			}
971
972
			// Fetching all changes
973
			$stmt = $this->db->prepare($query);
974
			$stmt->execute([$syncToken, $currentToken, $calendarId]);
975
976
			$changes = [];
977
978
			// This loop ensures that any duplicates are overwritten, only the
979
			// last change on a node is relevant.
980
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
981
982
				$changes[$row['uri']] = $row['operation'];
983
984
			}
985
986
			foreach($changes as $uri => $operation) {
987
988
				switch($operation) {
989
					case 1 :
990
						$result['added'][] = $uri;
991
						break;
992
					case 2 :
993
						$result['modified'][] = $uri;
994
						break;
995
					case 3 :
996
						$result['deleted'][] = $uri;
997
						break;
998
				}
999
1000
			}
1001
		} else {
1002
			// No synctoken supplied, this is the initial sync.
1003
			$query = "SELECT `uri` FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ?";
1004
			$stmt = $this->db->prepare($query);
1005
			$stmt->execute([$calendarId]);
1006
1007
			$result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
1008
		}
1009
		return $result;
1010
1011
	}
1012
1013
	/**
1014
	 * Returns a list of subscriptions for a principal.
1015
	 *
1016
	 * Every subscription is an array with the following keys:
1017
	 *  * id, a unique id that will be used by other functions to modify the
1018
	 *    subscription. This can be the same as the uri or a database key.
1019
	 *  * uri. This is just the 'base uri' or 'filename' of the subscription.
1020
	 *  * principaluri. The owner of the subscription. Almost always the same as
1021
	 *    principalUri passed to this method.
1022
	 *
1023
	 * Furthermore, all the subscription info must be returned too:
1024
	 *
1025
	 * 1. {DAV:}displayname
1026
	 * 2. {http://apple.com/ns/ical/}refreshrate
1027
	 * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
1028
	 *    should not be stripped).
1029
	 * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
1030
	 *    should not be stripped).
1031
	 * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
1032
	 *    attachments should not be stripped).
1033
	 * 6. {http://calendarserver.org/ns/}source (Must be a
1034
	 *     Sabre\DAV\Property\Href).
1035
	 * 7. {http://apple.com/ns/ical/}calendar-color
1036
	 * 8. {http://apple.com/ns/ical/}calendar-order
1037
	 * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
1038
	 *    (should just be an instance of
1039
	 *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
1040
	 *    default components).
1041
	 *
1042
	 * @param string $principalUri
1043
	 * @return array
1044
	 */
1045
	function getSubscriptionsForUser($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1046
		$fields = array_values($this->subscriptionPropertyMap);
1047
		$fields[] = 'id';
1048
		$fields[] = 'uri';
1049
		$fields[] = 'source';
1050
		$fields[] = 'principaluri';
1051
		$fields[] = 'lastmodified';
1052
1053
		$query = $this->db->getQueryBuilder();
1054
		$query->select($fields)
1055
			->from('calendarsubscriptions')
1056
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1057
			->orderBy('calendarorder', 'asc');
1058
		$stmt =$query->execute();
1059
1060
		$subscriptions = [];
1061
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
1062
1063
			$subscription = [
1064
				'id'           => $row['id'],
1065
				'uri'          => $row['uri'],
1066
				'principaluri' => $row['principaluri'],
1067
				'source'       => $row['source'],
1068
				'lastmodified' => $row['lastmodified'],
1069
1070
				'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
1071
			];
1072
1073
			foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1074
				if (!is_null($row[$dbName])) {
1075
					$subscription[$xmlName] = $row[$dbName];
1076
				}
1077
			}
1078
1079
			$subscriptions[] = $subscription;
1080
1081
		}
1082
1083
		return $subscriptions;
1084
	}
1085
1086
	/**
1087
	 * Creates a new subscription for a principal.
1088
	 *
1089
	 * If the creation was a success, an id must be returned that can be used to reference
1090
	 * this subscription in other methods, such as updateSubscription.
1091
	 *
1092
	 * @param string $principalUri
1093
	 * @param string $uri
1094
	 * @param array $properties
1095
	 * @return mixed
1096
	 */
1097
	function createSubscription($principalUri, $uri, array $properties) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1098
1099
		if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
1100
			throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
1101
		}
1102
1103
		$values = [
1104
			'principaluri' => $principalUri,
1105
			'uri'          => $uri,
1106
			'source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
1107
			'lastmodified' => time(),
1108
		];
1109
1110
		$propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
1111
1112
		foreach($this->subscriptionPropertyMap as $xmlName=>$dbName) {
1113
			if (array_key_exists($xmlName, $properties)) {
1114
					$values[$dbName] = $properties[$xmlName];
1115
					if (in_array($dbName, $propertiesBoolean)) {
1116
						$values[$dbName] = true;
1117
				}
1118
			}
1119
		}
1120
1121
		$valuesToInsert = array();
1122
1123
		$query = $this->db->getQueryBuilder();
1124
1125
		foreach (array_keys($values) as $name) {
1126
			$valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
1127
		}
1128
1129
		$query->insert('calendarsubscriptions')
1130
			->values($valuesToInsert)
1131
			->execute();
1132
1133
		return $this->db->lastInsertId('*PREFIX*calendarsubscriptions');
1134
	}
1135
1136
	/**
1137
	 * Updates a subscription
1138
	 *
1139
	 * The list of mutations is stored in a Sabre\DAV\PropPatch object.
1140
	 * To do the actual updates, you must tell this object which properties
1141
	 * you're going to process with the handle() method.
1142
	 *
1143
	 * Calling the handle method is like telling the PropPatch object "I
1144
	 * promise I can handle updating this property".
1145
	 *
1146
	 * Read the PropPatch documentation for more info and examples.
1147
	 *
1148
	 * @param mixed $subscriptionId
1149
	 * @param PropPatch $propPatch
1150
	 * @return void
1151
	 */
1152
	function updateSubscription($subscriptionId, PropPatch $propPatch) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1153
		$supportedProperties = array_keys($this->subscriptionPropertyMap);
1154
		$supportedProperties[] = '{http://calendarserver.org/ns/}source';
1155
1156
		$propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
1157
1158
			$newValues = [];
1159
1160
			foreach($mutations as $propertyName=>$propertyValue) {
1161
				if ($propertyName === '{http://calendarserver.org/ns/}source') {
1162
					$newValues['source'] = $propertyValue->getHref();
1163
				} else {
1164
					$fieldName = $this->subscriptionPropertyMap[$propertyName];
1165
					$newValues[$fieldName] = $propertyValue;
1166
				}
1167
			}
1168
1169
			$query = $this->db->getQueryBuilder();
1170
			$query->update('calendarsubscriptions')
1171
				->set('lastmodified', $query->createNamedParameter(time()));
1172
			foreach($newValues as $fieldName=>$value) {
1173
				$query->set($fieldName, $query->createNamedParameter($value));
1174
			}
1175
			$query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1176
				->execute();
1177
1178
			return true;
1179
1180
		});
1181
	}
1182
1183
	/**
1184
	 * Deletes a subscription.
1185
	 *
1186
	 * @param mixed $subscriptionId
1187
	 * @return void
1188
	 */
1189
	function deleteSubscription($subscriptionId) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1190
		$query = $this->db->getQueryBuilder();
1191
		$query->delete('calendarsubscriptions')
1192
			->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
1193
			->execute();
1194
	}
1195
1196
	/**
1197
	 * Returns a single scheduling object for the inbox collection.
1198
	 *
1199
	 * The returned array should contain the following elements:
1200
	 *   * uri - A unique basename for the object. This will be used to
1201
	 *           construct a full uri.
1202
	 *   * calendardata - The iCalendar object
1203
	 *   * lastmodified - The last modification date. Can be an int for a unix
1204
	 *                    timestamp, or a PHP DateTime object.
1205
	 *   * etag - A unique token that must change if the object changed.
1206
	 *   * size - The size of the object, in bytes.
1207
	 *
1208
	 * @param string $principalUri
1209
	 * @param string $objectUri
1210
	 * @return array
1211
	 */
1212
	function getSchedulingObject($principalUri, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1213
		$query = $this->db->getQueryBuilder();
1214
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1215
			->from('schedulingobjects')
1216
			->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1217
			->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1218
			->execute();
1219
1220
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
1221
1222
		if(!$row) {
1223
			return null;
1224
		}
1225
1226
		return [
1227
				'uri'          => $row['uri'],
1228
				'calendardata' => $row['calendardata'],
1229
				'lastmodified' => $row['lastmodified'],
1230
				'etag'         => '"' . $row['etag'] . '"',
1231
				'size'         => (int)$row['size'],
1232
		];
1233
	}
1234
1235
	/**
1236
	 * Returns all scheduling objects for the inbox collection.
1237
	 *
1238
	 * These objects should be returned as an array. Every item in the array
1239
	 * should follow the same structure as returned from getSchedulingObject.
1240
	 *
1241
	 * The main difference is that 'calendardata' is optional.
1242
	 *
1243
	 * @param string $principalUri
1244
	 * @return array
1245
	 */
1246
	function getSchedulingObjects($principalUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1247
		$query = $this->db->getQueryBuilder();
1248
		$stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
1249
				->from('schedulingobjects')
1250
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1251
				->execute();
1252
1253
		$result = [];
1254
		foreach($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
1255
			$result[] = [
1256
					'calendardata' => $row['calendardata'],
1257
					'uri'          => $row['uri'],
1258
					'lastmodified' => $row['lastmodified'],
1259
					'etag'         => '"' . $row['etag'] . '"',
1260
					'size'         => (int)$row['size'],
1261
			];
1262
		}
1263
1264
		return $result;
1265
	}
1266
1267
	/**
1268
	 * Deletes a scheduling object from the inbox collection.
1269
	 *
1270
	 * @param string $principalUri
1271
	 * @param string $objectUri
1272
	 * @return void
1273
	 */
1274
	function deleteSchedulingObject($principalUri, $objectUri) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1275
		$query = $this->db->getQueryBuilder();
1276
		$query->delete('schedulingobjects')
1277
				->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
1278
				->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
1279
				->execute();
1280
	}
1281
1282
	/**
1283
	 * Creates a new scheduling object. This should land in a users' inbox.
1284
	 *
1285
	 * @param string $principalUri
1286
	 * @param string $objectUri
1287
	 * @param string $objectData
1288
	 * @return void
1289
	 */
1290
	function createSchedulingObject($principalUri, $objectUri, $objectData) {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
1291
		$query = $this->db->getQueryBuilder();
1292
		$query->insert('schedulingobjects')
1293
			->values([
1294
				'principaluri' => $query->createNamedParameter($principalUri),
1295
				'calendardata' => $query->createNamedParameter($objectData),
1296
				'uri' => $query->createNamedParameter($objectUri),
1297
				'lastmodified' => $query->createNamedParameter(time()),
1298
				'etag' => $query->createNamedParameter(md5($objectData)),
1299
				'size' => $query->createNamedParameter(strlen($objectData))
1300
			])
1301
			->execute();
1302
	}
1303
1304
	/**
1305
	 * Adds a change record to the calendarchanges table.
1306
	 *
1307
	 * @param mixed $calendarId
1308
	 * @param string $objectUri
1309
	 * @param int $operation 1 = add, 2 = modify, 3 = delete.
1310
	 * @return void
1311
	 */
1312 View Code Duplication
	protected function addChange($calendarId, $objectUri, $operation) {
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...
1313
1314
		$stmt = $this->db->prepare('INSERT INTO `*PREFIX*calendarchanges` (`uri`, `synctoken`, `calendarid`, `operation`) SELECT ?, `synctoken`, ?, ? FROM `*PREFIX*calendars` WHERE `id` = ?');
1315
		$stmt->execute([
1316
			$objectUri,
1317
			$calendarId,
1318
			$operation,
1319
			$calendarId
1320
		]);
1321
		$stmt = $this->db->prepare('UPDATE `*PREFIX*calendars` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?');
1322
		$stmt->execute([
1323
			$calendarId
1324
		]);
1325
1326
	}
1327
1328
	/**
1329
	 * Parses some information from calendar objects, used for optimized
1330
	 * calendar-queries.
1331
	 *
1332
	 * Returns an array with the following keys:
1333
	 *   * etag - An md5 checksum of the object without the quotes.
1334
	 *   * size - Size of the object in bytes
1335
	 *   * componentType - VEVENT, VTODO or VJOURNAL
1336
	 *   * firstOccurence
1337
	 *   * lastOccurence
1338
	 *   * uid - value of the UID property
1339
	 *
1340
	 * @param string $calendarData
1341
	 * @return array
1342
	 */
1343
	public function getDenormalizedData($calendarData) {
1344
1345
		$vObject = Reader::read($calendarData);
1346
		$componentType = null;
1347
		$component = null;
1348
		$firstOccurrence = null;
1349
		$lastOccurrence = null;
1350
		$uid = null;
1351
		$classification = self::CLASSIFICATION_PUBLIC;
1352
		foreach($vObject->getComponents() as $component) {
1353
			if ($component->name!=='VTIMEZONE') {
1354
				$componentType = $component->name;
1355
				$uid = (string)$component->UID;
1356
				break;
1357
			}
1358
		}
1359
		if (!$componentType) {
1360
			throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
1361
		}
1362
		if ($componentType === 'VEVENT' && $component->DTSTART) {
1363
			$firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
1364
			// Finding the last occurrence is a bit harder
1365
			if (!isset($component->RRULE)) {
1366
				if (isset($component->DTEND)) {
1367
					$lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
1368
				} elseif (isset($component->DURATION)) {
1369
					$endDate = clone $component->DTSTART->getDateTime();
1370
					$endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
1371
					$lastOccurrence = $endDate->getTimeStamp();
1372
				} elseif (!$component->DTSTART->hasTime()) {
1373
					$endDate = clone $component->DTSTART->getDateTime();
1374
					$endDate->modify('+1 day');
1375
					$lastOccurrence = $endDate->getTimeStamp();
1376
				} else {
1377
					$lastOccurrence = $firstOccurrence;
1378
				}
1379
			} else {
1380
				$it = new EventIterator($vObject, (string)$component->UID);
1381
				$maxDate = new \DateTime(self::MAX_DATE);
1382
				if ($it->isInfinite()) {
1383
					$lastOccurrence = $maxDate->getTimeStamp();
1384
				} else {
1385
					$end = $it->getDtEnd();
1386
					while($it->valid() && $end < $maxDate) {
1387
						$end = $it->getDtEnd();
1388
						$it->next();
1389
1390
					}
1391
					$lastOccurrence = $end->getTimeStamp();
1392
				}
1393
1394
			}
1395
		}
1396
1397
		if ($component->CLASS) {
1398
			$classification = CalDavBackend::CLASSIFICATION_PRIVATE;
1399
			switch ($component->CLASS->getValue()) {
1400
				case 'PUBLIC':
1401
					$classification = CalDavBackend::CLASSIFICATION_PUBLIC;
1402
					break;
1403
				case 'CONFIDENTIAL':
1404
					$classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
1405
					break;
1406
			}
1407
		}
1408
		return [
1409
			'etag' => md5($calendarData),
1410
			'size' => strlen($calendarData),
1411
			'componentType' => $componentType,
1412
			'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
1413
			'lastOccurence'  => $lastOccurrence,
1414
			'uid' => $uid,
1415
			'classification' => $classification
1416
		];
1417
1418
	}
1419
1420
	private function readBlob($cardData) {
1421
		if (is_resource($cardData)) {
1422
			return stream_get_contents($cardData);
1423
		}
1424
1425
		return $cardData;
1426
	}
1427
1428
	/**
1429
	 * @param IShareable $shareable
1430
	 * @param array $add
1431
	 * @param array $remove
1432
	 */
1433
	public function updateShares($shareable, $add, $remove) {
1434
		$this->sharingBackend->updateShares($shareable, $add, $remove);
1435
	}
1436
1437
	/**
1438
	 * @param int $resourceId
1439
	 * @return array
1440
	 */
1441
	public function getShares($resourceId) {
1442
		return $this->sharingBackend->getShares($resourceId);
1443
	}
1444
1445
	/**
1446
	 * @param int $resourceId
1447
	 * @param array $acl
1448
	 * @return array
1449
	 */
1450
	public function applyShareAcl($resourceId, $acl) {
1451
		return $this->sharingBackend->applyShareAcl($resourceId, $acl);
1452
	}
1453
1454 View Code Duplication
	private function convertPrincipal($principalUri, $toV2) {
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...
1455
		if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
1456
			list(, $name) = URLUtil::splitPath($principalUri);
1457
			if ($toV2 === true) {
1458
				return "principals/users/$name";
1459
			}
1460
			return "principals/$name";
1461
		}
1462
		return $principalUri;
1463
	}
1464
}
1465