Passed
Push — master ( 465e91...f452e2 )
by Roeland
12:18 queued 10s
created

AbstractPrincipalBackend::setGroupMemberSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright 2019, Georg Ehrke <[email protected]>
4
 *
5
 * @author Georg Ehrke <[email protected]>
6
 *
7
 * @license GNU AGPL version 3 or any later version
8
 *
9
 * This program is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU Affero General Public License as
11
 * published by the Free Software Foundation, either version 3 of the
12
 * License, or (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
 *
22
 */
23
namespace OCA\DAV\CalDAV\ResourceBooking;
24
25
use OCA\DAV\CalDAV\Proxy\ProxyMapper;
26
use OCA\DAV\Traits\PrincipalProxyTrait;
27
use OCP\IDBConnection;
28
use OCP\IGroupManager;
29
use OCP\ILogger;
30
use OCP\IUserSession;
31
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
32
use Sabre\DAV\Exception;
33
use \Sabre\DAV\PropPatch;
34
35
abstract class AbstractPrincipalBackend implements BackendInterface {
36
37
	/** @var IDBConnection */
38
	private $db;
39
40
	/** @var IUserSession */
41
	private $userSession;
42
43
	/** @var IGroupManager */
44
	private $groupManager;
45
46
	/** @var ILogger */
47
	private $logger;
48
49
	/** @var ProxyMapper */
50
	private $proxyMapper;
51
52
	/** @var string */
53
	private $principalPrefix;
54
55
	/** @var string */
56
	private $dbTableName;
57
58
	/** @var string */
59
	private $dbMetaDataTableName;
60
61
	/** @var string */
62
	private $dbForeignKeyName;
63
64
	/** @var string */
65
	private $cuType;
66
67
	/**
68
	 * @param IDBConnection $dbConnection
69
	 * @param IUserSession $userSession
70
	 * @param IGroupManager $groupManager
71
	 * @param ILogger $logger
72
	 * @param string $principalPrefix
73
	 * @param string $dbPrefix
74
	 * @param string $cuType
75
	 */
76
	public function __construct(IDBConnection $dbConnection,
77
								IUserSession $userSession,
78
								IGroupManager $groupManager,
79
								ILogger $logger,
80
								ProxyMapper $proxyMapper,
81
								string $principalPrefix,
82
								string $dbPrefix,
83
								string $cuType) {
84
		$this->db = $dbConnection;
85
		$this->userSession = $userSession;
86
		$this->groupManager = $groupManager;
87
		$this->logger = $logger;
88
		$this->proxyMapper = $proxyMapper;
89
		$this->principalPrefix = $principalPrefix;
90
		$this->dbTableName = 'calendar_' . $dbPrefix . 's';
91
		$this->dbMetaDataTableName = $this->dbTableName . '_md';
92
		$this->dbForeignKeyName = $dbPrefix . '_id';
93
		$this->cuType = $cuType;
94
	}
95
96
	use PrincipalProxyTrait;
97
98
	/**
99
	 * Returns a list of principals based on a prefix.
100
	 *
101
	 * This prefix will often contain something like 'principals'. You are only
102
	 * expected to return principals that are in this base path.
103
	 *
104
	 * You are expected to return at least a 'uri' for every user, you can
105
	 * return any additional properties if you wish so. Common properties are:
106
	 *   {DAV:}displayname
107
	 *
108
	 * @param string $prefixPath
109
	 * @return string[]
110
	 */
111
	public function getPrincipalsByPrefix($prefixPath) {
112
		$principals = [];
113
114
		if ($prefixPath === $this->principalPrefix) {
115
			$query = $this->db->getQueryBuilder();
116
			$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
117
				->from($this->dbTableName);
118
			$stmt = $query->execute();
119
120
			$metaDataQuery = $this->db->getQueryBuilder();
121
			$metaDataQuery->select([$this->dbForeignKeyName, 'key', 'value'])
122
				->from($this->dbMetaDataTableName);
123
			$metaDataStmt = $metaDataQuery->execute();
124
			$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
125
126
			$metaDataById = [];
127
			foreach($metaDataRows as $metaDataRow) {
128
				if (!isset($metaDataById[$metaDataRow[$this->dbForeignKeyName]])) {
129
					$metaDataById[$metaDataRow[$this->dbForeignKeyName]] = [];
130
				}
131
132
				$metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] =
133
					$metaDataRow['value'];
134
			}
135
136
			while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
137
				$id = $row['id'];
138
139
				if (isset($metaDataById[$id])) {
140
					$principals[] = $this->rowToPrincipal($row, $metaDataById[$id]);
141
				} else {
142
					$principals[] = $this->rowToPrincipal($row);
143
				}
144
145
			}
146
147
			$stmt->closeCursor();
148
		}
149
150
		return $principals;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $principals returns an array which contains values of type array which are incompatible with the documented value type string.
Loading history...
151
	}
152
153
	/**
154
	 * Returns a specific principal, specified by it's path.
155
	 * The returned structure should be the exact same as from
156
	 * getPrincipalsByPrefix.
157
	 *
158
	 * @param string $path
159
	 * @return array
160
	 */
161
	public function getPrincipalByPath($path) {
162
		if (strpos($path, $this->principalPrefix) !== 0) {
163
			return null;
164
		}
165
		list(, $name) = \Sabre\Uri\split($path);
166
167
		list($backendId, $resourceId) = explode('-',  $name, 2);
168
169
		$query = $this->db->getQueryBuilder();
170
		$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
171
			->from($this->dbTableName)
172
			->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
173
			->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
174
		$stmt = $query->execute();
175
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
176
177
		if(!$row) {
178
			return null;
179
		}
180
181
		$metaDataQuery = $this->db->getQueryBuilder();
182
		$metaDataQuery->select(['key', 'value'])
183
			->from($this->dbMetaDataTableName)
184
			->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
185
		$metaDataStmt = $metaDataQuery->execute();
186
		$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
187
		$metadata = [];
188
189
		foreach($metaDataRows as $metaDataRow) {
190
			$metadata[$metaDataRow['key']] = $metaDataRow['value'];
191
		}
192
193
		return $this->rowToPrincipal($row, $metadata);
194
	}
195
196
	/**
197
	 * @param int $id
198
	 * @return array|null
199
	 */
200
	public function getPrincipalById($id):?array {
201
		$query = $this->db->getQueryBuilder();
202
		$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname'])
203
			->from($this->dbTableName)
204
			->where($query->expr()->eq('id', $query->createNamedParameter($id)));
205
		$stmt = $query->execute();
206
		$row = $stmt->fetch(\PDO::FETCH_ASSOC);
207
208
		if(!$row) {
209
			return null;
210
		}
211
212
		$metaDataQuery = $this->db->getQueryBuilder();
213
		$metaDataQuery->select(['key', 'value'])
214
			->from($this->dbMetaDataTableName)
215
			->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id'])));
216
		$metaDataStmt = $metaDataQuery->execute();
217
		$metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC);
218
		$metadata = [];
219
220
		foreach($metaDataRows as $metaDataRow) {
221
			$metadata[$metaDataRow['key']] = $metaDataRow['value'];
222
		}
223
224
		return $this->rowToPrincipal($row, $metadata);
225
	}
226
227
	/**
228
	 * @param string $path
229
	 * @param PropPatch $propPatch
230
	 * @return int
231
	 */
232
	function updatePrincipal($path, 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...
233
		return 0;
0 ignored issues
show
Bug Best Practice introduced by
The expression return 0 returns the type integer which is incompatible with the return type mandated by Sabre\DAVACL\PrincipalBa...face::updatePrincipal() of void.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
234
	}
235
236
	/**
237
	 * @param string $prefixPath
238
	 * @param array $searchProperties
239
	 * @param string $test
240
	 * @return array
241
	 */
242
	function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') {
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...
243
		$results = [];
244
		if (\count($searchProperties) === 0) {
245
			return [];
246
		}
247
		if ($prefixPath !== $this->principalPrefix) {
248
			return [];
249
		}
250
251
		$user = $this->userSession->getUser();
252
		if (!$user) {
253
			return [];
254
		}
255
		$usersGroups = $this->groupManager->getUserGroupIds($user);
256
257
		foreach ($searchProperties as $prop => $value) {
258
			switch ($prop) {
259
				case '{http://sabredav.org/ns}email-address':
260
					$query = $this->db->getQueryBuilder();
261
					$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
262
						->from($this->dbTableName)
263
						->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
264
265
					$stmt = $query->execute();
266
					$principals = [];
267
					while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
268
						if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
269
							continue;
270
						}
271
						$principals[] = $this->rowToPrincipal($row)['uri'];
272
					}
273
					$results[] = $principals;
274
275
					$stmt->closeCursor();
276
					break;
277
278
				case '{DAV:}displayname':
279
					$query = $this->db->getQueryBuilder();
280
					$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
281
						->from($this->dbTableName)
282
						->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
283
284
					$stmt = $query->execute();
285
					$principals = [];
286
					while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
287
						if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
288
							continue;
289
						}
290
						$principals[] = $this->rowToPrincipal($row)['uri'];
291
					}
292
					$results[] = $principals;
293
294
					$stmt->closeCursor();
295
					break;
296
297
				case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set':
298
					// If you add support for more search properties that qualify as a user-address,
299
					// please also add them to the array below
300
					$results[] = $this->searchPrincipals($this->principalPrefix, [
301
						'{http://sabredav.org/ns}email-address' => $value,
302
					], 'anyof');
303
					break;
304
305
				default:
306
					$rowsByMetadata = $this->searchPrincipalsByMetadataKey($prop, $value);
307
					$filteredRows = array_filter($rowsByMetadata, function($row) use ($usersGroups) {
308
						return $this->isAllowedToAccessResource($row, $usersGroups);
309
					});
310
311
					$results[] = array_map(function($row) {
312
						return $row['uri'];
313
					}, $filteredRows);
314
315
					break;
316
			}
317
		}
318
319
		// results is an array of arrays, so this is not the first search result
320
		// but the results of the first searchProperty
321
		if (count($results) === 1) {
322
			return $results[0];
323
		}
324
325
		switch ($test) {
326
			case 'anyof':
327
				return array_values(array_unique(array_merge(...$results)));
328
329
			case 'allof':
330
			default:
331
				return array_values(array_intersect(...$results));
0 ignored issues
show
Bug introduced by
The call to array_intersect() has too few arguments starting with array2. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

331
				return array_values(/** @scrutinizer ignore-call */ array_intersect(...$results));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
332
		}
333
	}
334
335
	/**
336
	 * Searches principals based on their metadata keys.
337
	 * This allows to search for all principals with a specific key.
338
	 * e.g.:
339
	 * '{http://nextcloud.com/ns}room-building-address' => 'ABC Street 123, ...'
340
	 *
341
	 * @param $key
342
	 * @param $value
343
	 * @return array
344
	 */
345
	private function searchPrincipalsByMetadataKey($key, $value):array {
346
		$query = $this->db->getQueryBuilder();
347
		$query->select([$this->dbForeignKeyName])
348
			->from($this->dbMetaDataTableName)
349
			->where($query->expr()->eq('key', $query->createNamedParameter($key)))
350
			->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%')));
351
		$stmt = $query->execute();
352
353
		$rows = [];
354
		while($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
355
			$id = $row[$this->dbForeignKeyName];
356
357
			$principalRow = $this->getPrincipalById($id);
358
			if (!$principalRow) {
359
				continue;
360
			}
361
362
			$rows[] = $principalRow;
363
		}
364
365
		return $rows;
366
	}
367
368
	/**
369
	 * @param string $uri
370
	 * @param string $principalPrefix
371
	 * @return null|string
372
	 */
373
	function findByUri($uri, $principalPrefix) {
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...
374
		$user = $this->userSession->getUser();
375
		if (!$user) {
376
			return null;
377
		}
378
		$usersGroups = $this->groupManager->getUserGroupIds($user);
379
380
		if (strpos($uri, 'mailto:') === 0) {
381
			$email = substr($uri, 7);
382
			$query = $this->db->getQueryBuilder();
383
			$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
384
				->from($this->dbTableName)
385
				->where($query->expr()->eq('email', $query->createNamedParameter($email)));
386
387
			$stmt = $query->execute();
388
			$row = $stmt->fetch(\PDO::FETCH_ASSOC);
389
390
			if(!$row) {
391
				return null;
392
			}
393
			if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
394
				return null;
395
			}
396
397
			return $this->rowToPrincipal($row)['uri'];
398
		}
399
400
		if (strpos($uri, 'principal:') === 0) {
401
			$path = substr($uri, 10);
402
			if (strpos($path, $this->principalPrefix) !== 0) {
403
				return null;
404
			}
405
406
			list(, $name) = \Sabre\Uri\split($path);
407
			list($backendId, $resourceId) = explode('-',  $name, 2);
408
409
			$query = $this->db->getQueryBuilder();
410
			$query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions'])
411
				->from($this->dbTableName)
412
				->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId)))
413
				->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId)));
414
			$stmt = $query->execute();
415
			$row = $stmt->fetch(\PDO::FETCH_ASSOC);
416
417
			if(!$row) {
418
				return null;
419
			}
420
			if (!$this->isAllowedToAccessResource($row, $usersGroups)) {
421
				return null;
422
			}
423
424
			return $this->rowToPrincipal($row)['uri'];
425
		}
426
427
		return null;
428
	}
429
430
	/**
431
	 * convert database row to principal
432
	 *
433
	 * @param String[] $row
434
	 * @param String[] $metadata
435
	 * @return Array
436
	 */
437
	private function rowToPrincipal(array $row, array $metadata=[]):array {
438
		return array_merge([
439
			'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'],
440
			'{DAV:}displayname' => $row['displayname'],
441
			'{http://sabredav.org/ns}email-address' => $row['email'],
442
			'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType,
443
		], $metadata);
444
	}
445
446
	/**
447
	 * @param $row
448
	 * @param $userGroups
449
	 * @return bool
450
	 */
451
	private function isAllowedToAccessResource(array $row, array $userGroups):bool {
452
		if (!isset($row['group_restrictions']) ||
453
			$row['group_restrictions'] === null ||
454
			$row['group_restrictions'] === '') {
455
			return true;
456
		}
457
458
		// group restrictions contains something, but not parsable, deny access and log warning
459
		$json = json_decode($row['group_restrictions']);
460
		if (!\is_array($json)) {
461
			$this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource');
462
			return false;
463
		}
464
465
		// empty array => no group restrictions
466
		if (empty($json)) {
467
			return true;
468
		}
469
470
		return !empty(array_intersect($json, $userGroups));
471
	}
472
}
473