Passed
Push — master ( 33c34d...f0c85a )
by Morris
09:38
created

Principal::getPrincipalPrefix()   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 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright Copyright (c) 2018, Georg Ehrke
5
 *
6
 * @author Bart Visscher <[email protected]>
7
 * @author Jakob Sack <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Roeland Jago Douma <[email protected]>
12
 * @author Thomas Müller <[email protected]>
13
 * @author Thomas Tanghus <[email protected]>
14
 * @author Vincent Petry <[email protected]>
15
 * @author Georg Ehrke <[email protected]>
16
 * @author Vinicius Cubas Brand <[email protected]>
17
 * @author Daniel Tygel <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OCA\DAV\Connector\Sabre;
36
37
use OCA\Circles\Exceptions\CircleDoesNotExistException;
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Exceptions\CircleDoesNotExistException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
38
use OCP\App\IAppManager;
39
use OCP\AppFramework\QueryException;
40
use OCP\IConfig;
41
use OCP\IGroup;
42
use OCP\IGroupManager;
43
use OCP\IUser;
44
use OCP\IUserManager;
45
use OCP\IUserSession;
46
use OCP\Share\IManager as IShareManager;
47
use Sabre\DAV\Exception;
48
use \Sabre\DAV\PropPatch;
49
use Sabre\DAVACL\PrincipalBackend\BackendInterface;
50
51
class Principal implements BackendInterface {
52
53
	/** @var IUserManager */
54
	private $userManager;
55
56
	/** @var IGroupManager */
57
	private $groupManager;
58
59
	/** @var IShareManager */
60
	private $shareManager;
61
62
	/** @var IUserSession */
63
	private $userSession;
64
65
	/** @var IConfig */
66
	private $config;
67
68
	/** @var IAppManager */
69
	private $appManager;
70
71
	/** @var string */
72
	private $principalPrefix;
73
74
	/** @var bool */
75
	private $hasGroups;
76
77
	/** @var bool */
78
	private $hasCircles;
79
80
	/**
81
	 * @param IUserManager $userManager
82
	 * @param IGroupManager $groupManager
83
	 * @param IShareManager $shareManager
84
	 * @param IUserSession $userSession
85
	 * @param IConfig $config
86
	 * @param string $principalPrefix
87
	 */
88
	public function __construct(IUserManager $userManager,
89
								IGroupManager $groupManager,
90
								IShareManager $shareManager,
91
								IUserSession $userSession,
92
								IConfig $config,
93
								IAppManager $appManager,
94
								$principalPrefix = 'principals/users/') {
95
		$this->userManager = $userManager;
96
		$this->groupManager = $groupManager;
97
		$this->shareManager = $shareManager;
98
		$this->userSession = $userSession;
99
		$this->config = $config;
100
		$this->appManager = $appManager;
101
		$this->principalPrefix = trim($principalPrefix, '/');
102
		$this->hasGroups = $this->hasCircles = ($principalPrefix === 'principals/users/');
103
	}
104
105
	/**
106
	 * Returns a list of principals based on a prefix.
107
	 *
108
	 * This prefix will often contain something like 'principals'. You are only
109
	 * expected to return principals that are in this base path.
110
	 *
111
	 * You are expected to return at least a 'uri' for every user, you can
112
	 * return any additional properties if you wish so. Common properties are:
113
	 *   {DAV:}displayname
114
	 *
115
	 * @param string $prefixPath
116
	 * @return string[]
117
	 */
118
	public function getPrincipalsByPrefix($prefixPath) {
119
		$principals = [];
120
121
		if ($prefixPath === $this->principalPrefix) {
122
			foreach($this->userManager->search('') as $user) {
123
				$principals[] = $this->userToPrincipal($user);
124
			}
125
		}
126
127
		return $principals;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $principals returns an array which contains values of type array<string,string> which are incompatible with the documented value type string.
Loading history...
128
	}
129
130
	/**
131
	 * Returns a specific principal, specified by it's path.
132
	 * The returned structure should be the exact same as from
133
	 * getPrincipalsByPrefix.
134
	 *
135
	 * @param string $path
136
	 * @return array
137
	 */
138
	public function getPrincipalByPath($path) {
139
		list($prefix, $name) = \Sabre\Uri\split($path);
140
141
		if ($prefix === $this->principalPrefix) {
142
			$user = $this->userManager->get($name);
143
144
			if ($user !== null) {
145
				return $this->userToPrincipal($user);
146
			}
147
		} else if ($prefix === 'principals/circles') {
148
			return $this->circleToPrincipal($name);
149
		}
150
		return null;
151
	}
152
153
	/**
154
	 * Returns the list of members for a group-principal
155
	 *
156
	 * @param string $principal
157
	 * @return string[]
158
	 * @throws Exception
159
	 */
160
	public function getGroupMemberSet($principal) {
161
		// TODO: for now the group principal has only one member, the user itself
162
		$principal = $this->getPrincipalByPath($principal);
163
		if (!$principal) {
164
			throw new Exception('Principal not found');
165
		}
166
167
		return [$principal['uri']];
168
	}
169
170
	/**
171
	 * Returns the list of groups a principal is a member of
172
	 *
173
	 * @param string $principal
174
	 * @param bool $needGroups
175
	 * @return array
176
	 * @throws Exception
177
	 */
178
	public function getGroupMembership($principal, $needGroups = false) {
179
		list($prefix, $name) = \Sabre\Uri\split($principal);
180
181
		if ($prefix === $this->principalPrefix) {
182
			$user = $this->userManager->get($name);
183
			if (!$user) {
184
				throw new Exception('Principal not found');
185
			}
186
187
			if ($this->hasGroups || $needGroups) {
188
				$groups = $this->groupManager->getUserGroups($user);
189
				$groups = array_map(function($group) {
190
					/** @var IGroup $group */
191
					return 'principals/groups/' . urlencode($group->getGID());
192
				}, $groups);
193
194
				return $groups;
195
			}
196
		}
197
		return [];
198
	}
199
200
	/**
201
	 * Updates the list of group members for a group principal.
202
	 *
203
	 * The principals should be passed as a list of uri's.
204
	 *
205
	 * @param string $principal
206
	 * @param string[] $members
207
	 * @throws Exception
208
	 */
209
	public function setGroupMemberSet($principal, array $members) {
210
		throw new Exception('Setting members of the group is not supported yet');
211
	}
212
213
	/**
214
	 * @param string $path
215
	 * @param PropPatch $propPatch
216
	 * @return int
217
	 */
218
	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...
219
		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...
220
	}
221
222
	/**
223
	 * Search user principals
224
	 *
225
	 * @param array $searchProperties
226
	 * @param string $test
227
	 * @return array
228
	 */
229
	protected function searchUserPrincipals(array $searchProperties, $test = 'allof') {
230
		$results = [];
231
232
		// If sharing is disabled, return the empty array
233
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
234
		if (!$shareAPIEnabled) {
235
			return [];
236
		}
237
238
		// If sharing is restricted to group members only,
239
		// return only members that have groups in common
240
		$restrictGroups = false;
241
		if ($this->shareManager->shareWithGroupMembersOnly()) {
242
			$user = $this->userSession->getUser();
243
			if (!$user) {
244
				return [];
245
			}
246
247
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
248
		}
249
250
		foreach ($searchProperties as $prop => $value) {
251
			switch ($prop) {
252
				case '{http://sabredav.org/ns}email-address':
253
					$users = $this->userManager->getByEmail($value);
254
255
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
256
						// is sharing restricted to groups only?
257
						if ($restrictGroups !== false) {
258
							$userGroups = $this->groupManager->getUserGroupIds($user);
259
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
260
								return $carry;
261
							}
262
						}
263
264
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
265
						return $carry;
266
					}, []);
267
					break;
268
269
				case '{DAV:}displayname':
270
					$users = $this->userManager->searchDisplayName($value);
271
272
					$results[] = array_reduce($users, function(array $carry, IUser $user) use ($restrictGroups) {
273
						// is sharing restricted to groups only?
274
						if ($restrictGroups !== false) {
275
							$userGroups = $this->groupManager->getUserGroupIds($user);
276
							if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
277
								return $carry;
278
							}
279
						}
280
281
						$carry[] = $this->principalPrefix . '/' . $user->getUID();
282
						return $carry;
283
					}, []);
284
					break;
285
286
				default:
287
					$results[] = [];
288
					break;
289
			}
290
		}
291
292
		// results is an array of arrays, so this is not the first search result
293
		// but the results of the first searchProperty
294
		if (count($results) === 1) {
295
			return $results[0];
296
		}
297
298
		switch ($test) {
299
			case 'anyof':
300
				return array_values(array_unique(array_merge(...$results)));
301
302
			case 'allof':
303
			default:
304
				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

304
				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...
305
		}
306
	}
307
308
	/**
309
	 * @param string $prefixPath
310
	 * @param array $searchProperties
311
	 * @param string $test
312
	 * @return array
313
	 */
314
	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...
315
		if (count($searchProperties) === 0) {
316
			return [];
317
		}
318
319
		switch ($prefixPath) {
320
			case 'principals/users':
321
				return $this->searchUserPrincipals($searchProperties, $test);
322
323
			default:
324
				return [];
325
		}
326
	}
327
328
	/**
329
	 * @param string $uri
330
	 * @param string $principalPrefix
331
	 * @return string
332
	 */
333
	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...
334
		// If sharing is disabled, return the empty array
335
		$shareAPIEnabled = $this->shareManager->shareApiEnabled();
336
		if (!$shareAPIEnabled) {
337
			return null;
338
		}
339
340
		// If sharing is restricted to group members only,
341
		// return only members that have groups in common
342
		$restrictGroups = false;
343
		if ($this->shareManager->shareWithGroupMembersOnly()) {
344
			$user = $this->userSession->getUser();
345
			if (!$user) {
346
				return null;
347
			}
348
349
			$restrictGroups = $this->groupManager->getUserGroupIds($user);
350
		}
351
352
		if (strpos($uri, 'mailto:') === 0) {
353
			if ($principalPrefix === 'principals/users') {
354
				$users = $this->userManager->getByEmail(substr($uri, 7));
355
				if (count($users) !== 1) {
356
					return null;
357
				}
358
				$user = $users[0];
359
360
				if ($restrictGroups !== false) {
361
					$userGroups = $this->groupManager->getUserGroupIds($user);
362
					if (count(array_intersect($userGroups, $restrictGroups)) === 0) {
363
						return null;
364
					}
365
				}
366
367
				return $this->principalPrefix . '/' . $user->getUID();
368
			}
369
		}
370
		if (substr($uri, 0, 10) === 'principal:') {
371
			$principal = substr($uri, 10);
372
			$principal = $this->getPrincipalByPath($principal);
373
			if ($principal !== null) {
0 ignored issues
show
introduced by
The condition $principal !== null is always true.
Loading history...
374
				return $principal['uri'];
375
			}
376
		}
377
378
		return null;
379
	}
380
381
	/**
382
	 * @param IUser $user
383
	 * @return array
384
	 */
385
	protected function userToPrincipal($user) {
386
		$userId = $user->getUID();
387
		$displayName = $user->getDisplayName();
388
		$principal = [
389
				'uri' => $this->principalPrefix . '/' . $userId,
390
				'{DAV:}displayname' => is_null($displayName) ? $userId : $displayName,
0 ignored issues
show
introduced by
The condition is_null($displayName) is always false.
Loading history...
391
				'{urn:ietf:params:xml:ns:caldav}calendar-user-type' => 'INDIVIDUAL',
392
		];
393
394
		$email = $user->getEMailAddress();
395
		if (!empty($email)) {
396
			$principal['{http://sabredav.org/ns}email-address'] = $email;
397
		}
398
399
		return $principal;
400
	}
401
402
	public function getPrincipalPrefix() {
403
		return $this->principalPrefix;
404
	}
405
406
	/**
407
	 * @param string $circleUniqueId
408
	 * @return array|null
409
	 * @suppress PhanUndeclaredClassMethod
410
	 * @suppress PhanUndeclaredClassCatch
411
	 */
412
	protected function circleToPrincipal($circleUniqueId) {
413
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
414
			return null;
415
		}
416
417
		try {
418
			$circle = \OCA\Circles\Api\v1\Circles::detailsCircle($circleUniqueId, true);
0 ignored issues
show
Bug introduced by
The type OCA\Circles\Api\v1\Circles was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
419
		} catch(QueryException $ex) {
420
			return null;
421
		} catch(CircleDoesNotExistException $ex) {
422
			return null;
423
		}
424
425
		if (!$circle) {
426
			return null;
427
		}
428
429
		$principal = [
430
			'uri' => 'principals/circles/' . $circleUniqueId,
431
			'{DAV:}displayname' => $circle->getName(),
432
		];
433
434
		return $principal;
435
	}
436
437
	/**
438
	 * Returns the list of circles a principal is a member of
439
	 *
440
	 * @param string $principal
441
	 * @param bool $needGroups
442
	 * @return array
443
	 * @throws Exception
444
	 * @suppress PhanUndeclaredClassMethod
445
	 */
446
	public function getCircleMembership($principal):array {
447
		if (!$this->appManager->isEnabledForUser('circles') || !class_exists('\OCA\Circles\Api\v1\Circles')) {
448
			return [];
449
		}
450
451
		list($prefix, $name) = \Sabre\Uri\split($principal);
452
		if ($this->hasCircles && $prefix === $this->principalPrefix) {
453
			$user = $this->userManager->get($name);
454
			if (!$user) {
455
				throw new Exception('Principal not found');
456
			}
457
458
			$circles = \OCA\Circles\Api\v1\Circles::joinedCircles($name, true);
459
460
			$circles = array_map(function($circle) {
461
				/** @var \OCA\Circles\Model\Circle $group */
462
				return 'principals/circles/' . urlencode($circle->getUniqueId());
463
			}, $circles);
464
465
			return $circles;
466
467
		}
468
		return [];
469
	}
470
471
}
472