Passed
Push — master ( 092a1f...510a29 )
by Joas
12:31 queued 11s
created

Group_LDAP::usersInGroup()   D

Complexity

Conditions 19
Paths 26

Size

Total Lines 99
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 65
dl 0
loc 99
rs 4.5166
c 1
b 0
f 0
cc 19
nc 26
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Alexander Bergolth <[email protected]>
6
 * @author Alex Weirig <[email protected]>
7
 * @author alexweirig <[email protected]>
8
 * @author Andreas Fischer <[email protected]>
9
 * @author Andreas Pflug <[email protected]>
10
 * @author Arthur Schiwon <[email protected]>
11
 * @author Bart Visscher <[email protected]>
12
 * @author Christopher Schäpers <[email protected]>
13
 * @author Frédéric Fortier <[email protected]>
14
 * @author Joas Schilling <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Morris Jobke <[email protected]>
17
 * @author Nicolas Grekas <[email protected]>
18
 * @author Robin McCorkell <[email protected]>
19
 * @author Roeland Jago Douma <[email protected]>
20
 * @author Roland Tapken <[email protected]>
21
 * @author Thomas Müller <[email protected]>
22
 * @author Victor Dubiniuk <[email protected]>
23
 * @author Vincent Petry <[email protected]>
24
 * @author Vinicius Cubas Brand <[email protected]>
25
 * @author Xuanwo <[email protected]>
26
 *
27
 * @license AGPL-3.0
28
 *
29
 * This code is free software: you can redistribute it and/or modify
30
 * it under the terms of the GNU Affero General Public License, version 3,
31
 * as published by the Free Software Foundation.
32
 *
33
 * This program is distributed in the hope that it will be useful,
34
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
36
 * GNU Affero General Public License for more details.
37
 *
38
 * You should have received a copy of the GNU Affero General Public License, version 3,
39
 * along with this program. If not, see <http://www.gnu.org/licenses/>
40
 *
41
 */
42
43
namespace OCA\User_LDAP;
44
45
use OC\Cache\CappedMemoryCache;
46
use OCP\Group\Backend\IGetDisplayNameBackend;
47
use OCP\GroupInterface;
48
use OCP\ILogger;
49
50
class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
51
	protected $enabled = false;
52
53
	/**
54
	 * @var string[] $cachedGroupMembers array of users with gid as key
55
	 */
56
	protected $cachedGroupMembers;
57
58
	/**
59
	 * @var string[] $cachedGroupsByMember array of groups with uid as key
60
	 */
61
	protected $cachedGroupsByMember;
62
63
	/**
64
	 * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
65
	 */
66
	protected $cachedNestedGroups;
67
68
	/** @var GroupPluginManager */
69
	protected $groupPluginManager;
70
71
	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
72
		parent::__construct($access);
73
		$filter = $this->access->connection->ldapGroupFilter;
74
		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
75
		if(!empty($filter) && !empty($gassoc)) {
76
			$this->enabled = true;
77
		}
78
79
		$this->cachedGroupMembers = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedGroupMembers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
80
		$this->cachedGroupsByMember = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedGroupsByMember.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
81
		$this->cachedNestedGroups = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type string[] of property $cachedNestedGroups.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
		$this->groupPluginManager = $groupPluginManager;
83
	}
84
85
	/**
86
	 * is user in group?
87
	 * @param string $uid uid of the user
88
	 * @param string $gid gid of the group
89
	 * @return bool
90
	 *
91
	 * Checks whether the user is member of a group or not.
92
	 */
93
	public function inGroup($uid, $gid) {
94
		if(!$this->enabled) {
95
			return false;
96
		}
97
		$cacheKey = 'inGroup'.$uid.':'.$gid;
98
		$inGroup = $this->access->connection->getFromCache($cacheKey);
99
		if(!is_null($inGroup)) {
100
			return (bool)$inGroup;
101
		}
102
103
		$userDN = $this->access->username2dn($uid);
104
105
		if(isset($this->cachedGroupMembers[$gid])) {
106
			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
0 ignored issues
show
Bug introduced by
$this->cachedGroupMembers[$gid] of type string is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

106
			$isInGroup = in_array($userDN, /** @scrutinizer ignore-type */ $this->cachedGroupMembers[$gid]);
Loading history...
107
			return $isInGroup;
108
		}
109
110
		$cacheKeyMembers = 'inGroup-members:'.$gid;
111
		$members = $this->access->connection->getFromCache($cacheKeyMembers);
112
		if(!is_null($members)) {
113
			$this->cachedGroupMembers[$gid] = $members;
114
			$isInGroup = in_array($userDN, $members, true);
115
			$this->access->connection->writeToCache($cacheKey, $isInGroup);
116
			return $isInGroup;
117
		}
118
119
		$groupDN = $this->access->groupname2dn($gid);
120
		// just in case
121
		if(!$groupDN || !$userDN) {
122
			$this->access->connection->writeToCache($cacheKey, false);
123
			return false;
124
		}
125
126
		//check primary group first
127
		if($gid === $this->getUserPrimaryGroup($userDN)) {
128
			$this->access->connection->writeToCache($cacheKey, true);
129
			return true;
130
		}
131
132
		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
133
		$members = $this->_groupMembers($groupDN);
134
		if(!is_array($members) || count($members) === 0) {
135
			$this->access->connection->writeToCache($cacheKey, false);
136
			return false;
137
		}
138
139
		//extra work if we don't get back user DNs
140
		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
141
			$dns = array();
142
			$filterParts = array();
143
			$bytes = 0;
144
			foreach($members as $mid) {
145
				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilter does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
146
				$filterParts[] = $filter;
147
				$bytes += strlen($filter);
148
				if($bytes >= 9000000) {
149
					// AD has a default input buffer of 10 MB, we do not want
150
					// to take even the chance to exceed it
151
					$filter = $this->access->combineFilterWithOr($filterParts);
152
					$bytes = 0;
153
					$filterParts = array();
154
					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
155
					$dns = array_merge($dns, $users);
156
				}
157
			}
158
			if(count($filterParts) > 0) {
159
				$filter = $this->access->combineFilterWithOr($filterParts);
160
				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
161
				$dns = array_merge($dns, $users);
162
			}
163
			$members = $dns;
164
		}
165
166
		$isInGroup = in_array($userDN, $members);
167
		$this->access->connection->writeToCache($cacheKey, $isInGroup);
168
		$this->access->connection->writeToCache($cacheKeyMembers, $members);
169
		$this->cachedGroupMembers[$gid] = $members;
170
171
		return $isInGroup;
172
	}
173
174
	/**
175
	 * @param string $dnGroup
176
	 * @return array
177
	 *
178
	 * For a group that has user membership defined by an LDAP search url attribute returns the users
179
	 * that match the search url otherwise returns an empty array.
180
	 */
181
	public function getDynamicGroupMembers($dnGroup) {
182
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapDynamicGroupMemberURL does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
183
184
		if (empty($dynamicGroupMemberURL)) {
185
			return array();
186
		}
187
188
		$dynamicMembers = array();
189
		$memberURLs = $this->access->readAttribute(
190
			$dnGroup,
191
			$dynamicGroupMemberURL,
192
			$this->access->connection->ldapGroupFilter
193
		);
194
		if ($memberURLs !== false) {
195
			// this group has the 'memberURL' attribute so this is a dynamic group
196
			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
197
			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
198
			$pos = strpos($memberURLs[0], '(');
199
			if ($pos !== false) {
200
				$memberUrlFilter = substr($memberURLs[0], $pos);
201
				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
202
				$dynamicMembers = array();
203
				foreach($foundMembers as $value) {
204
					$dynamicMembers[$value['dn'][0]] = 1;
205
				}
206
			} else {
207
				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

207
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
208
					'of group ' . $dnGroup, ILogger::DEBUG);
209
			}
210
		}
211
		return $dynamicMembers;
212
	}
213
214
	/**
215
	 * @param string $dnGroup
216
	 * @param array|null &$seen
217
	 * @return array|mixed|null
218
	 * @throws \OC\ServerNotAvailableException
219
	 */
220
	private function _groupMembers($dnGroup, &$seen = null) {
221
		if ($seen === null) {
222
			$seen = [];
223
		}
224
		$allMembers = [];
225
		if (array_key_exists($dnGroup, $seen)) {
226
			// avoid loops
227
			return [];
228
		}
229
		// used extensively in cron job, caching makes sense for nested groups
230
		$cacheKey = '_groupMembers'.$dnGroup;
231
		$groupMembers = $this->access->connection->getFromCache($cacheKey);
232
		if($groupMembers !== null) {
233
			return $groupMembers;
234
		}
235
		$seen[$dnGroup] = 1;
236
		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
237
		if (is_array($members)) {
238
			$fetcher = function($memberDN, &$seen) {
239
				return $this->_groupMembers($memberDN, $seen);
240
			};
241
			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
242
		}
243
244
		$allMembers += $this->getDynamicGroupMembers($dnGroup);
245
246
		$this->access->connection->writeToCache($cacheKey, $allMembers);
247
		return $allMembers;
248
	}
249
250
	/**
251
	 * @param string $DN
252
	 * @param array|null &$seen
253
	 * @return array
254
	 * @throws \OC\ServerNotAvailableException
255
	 */
256
	private function _getGroupDNsFromMemberOf($DN) {
257
		$groups = $this->access->readAttribute($DN, 'memberOf');
258
		if (!is_array($groups)) {
259
			return [];
260
		}
261
262
		$fetcher = function($groupDN) {
263
			if (isset($this->cachedNestedGroups[$groupDN])) {
264
				$nestedGroups = $this->cachedNestedGroups[$groupDN];
265
			} else {
266
				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
267
				if (!is_array($nestedGroups)) {
268
					$nestedGroups = [];
269
				}
270
				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
271
			}
272
			return $nestedGroups;
273
		};
274
275
		$groups = $this->walkNestedGroups($DN, $fetcher, $groups);
276
		return $this->access->groupsMatchFilter($groups);
277
	}
278
279
	/**
280
	 * @param string $dn
281
	 * @param \Closure $fetcher args: string $dn, array $seen, returns: string[] of dns
282
	 * @param array $list
283
	 * @return array
284
	 */
285
	private function walkNestedGroups(string $dn, \Closure $fetcher, array $list): array {
286
		$nesting = (int) $this->access->connection->ldapNestedGroups;
287
		// depending on the input, we either have a list of DNs or a list of LDAP records
288
		// also, the output expects either DNs or records. Testing the first element should suffice.
289
		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
290
291
		if ($nesting !== 1) {
292
			if($recordMode) {
293
				// the keys are numeric, but should hold the DN
294
				return array_reduce($list, function ($transformed, $record) use ($dn) {
295
					if($record['dn'][0] != $dn) {
296
						$transformed[$record['dn'][0]] = $record;
297
					}
298
					return $transformed;
299
				}, []);
300
			}
301
			return $list;
302
		}
303
304
		$seen = [];
305
		while ($record = array_pop($list)) {
306
			$recordDN = $recordMode ? $record['dn'][0] : $record;
307
			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
308
				// Prevent loops
309
				continue;
310
			}
311
			$fetched = $fetcher($record, $seen);
312
			$list = array_merge($list, $fetched);
313
			$seen[$recordDN] = $record;
314
		}
315
316
		return $recordMode ? $seen : array_keys($seen);
317
	}
318
319
	/**
320
	 * translates a gidNumber into an ownCloud internal name
321
	 * @param string $gid as given by gidNumber on POSIX LDAP
322
	 * @param string $dn a DN that belongs to the same domain as the group
323
	 * @return string|bool
324
	 */
325
	public function gidNumber2Name($gid, $dn) {
0 ignored issues
show
Unused Code introduced by
The parameter $dn is not used and could be removed. ( Ignorable by Annotation )

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

325
	public function gidNumber2Name($gid, /** @scrutinizer ignore-unused */ $dn) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
326
		$cacheKey = 'gidNumberToName' . $gid;
327
		$groupName = $this->access->connection->getFromCache($cacheKey);
328
		if(!is_null($groupName) && isset($groupName)) {
329
			return $groupName;
330
		}
331
332
		//we need to get the DN from LDAP
333
		$filter = $this->access->combineFilterWithAnd([
334
			$this->access->connection->ldapGroupFilter,
335
			'objectClass=posixGroup',
336
			$this->access->connection->ldapGidNumber . '=' . $gid
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGidNumber does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
337
		]);
338
		$result = $this->access->searchGroups($filter, array('dn'), 1);
339
		if(empty($result)) {
340
			return false;
341
		}
342
		$dn = $result[0]['dn'][0];
343
344
		//and now the group name
345
		//NOTE once we have separate ownCloud group IDs and group names we can
346
		//directly read the display name attribute instead of the DN
347
		$name = $this->access->dn2groupname($dn);
348
349
		$this->access->connection->writeToCache($cacheKey, $name);
350
351
		return $name;
352
	}
353
354
	/**
355
	 * returns the entry's gidNumber
356
	 * @param string $dn
357
	 * @param string $attribute
358
	 * @return string|bool
359
	 */
360
	private function getEntryGidNumber($dn, $attribute) {
361
		$value = $this->access->readAttribute($dn, $attribute);
362
		if(is_array($value) && !empty($value)) {
363
			return $value[0];
364
		}
365
		return false;
366
	}
367
368
	/**
369
	 * returns the group's primary ID
370
	 * @param string $dn
371
	 * @return string|bool
372
	 */
373
	public function getGroupGidNumber($dn) {
374
		return $this->getEntryGidNumber($dn, 'gidNumber');
375
	}
376
377
	/**
378
	 * returns the user's gidNumber
379
	 * @param string $dn
380
	 * @return string|bool
381
	 */
382
	public function getUserGidNumber($dn) {
383
		$gidNumber = false;
384
		if($this->access->connection->hasGidNumber) {
385
			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGidNumber does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
386
			if($gidNumber === false) {
387
				$this->access->connection->hasGidNumber = false;
388
			}
389
		}
390
		return $gidNumber;
391
	}
392
393
	/**
394
	 * returns a filter for a "users has specific gid" search or count operation
395
	 *
396
	 * @param string $groupDN
397
	 * @param string $search
398
	 * @return string
399
	 * @throws \Exception
400
	 */
401
	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
402
		$groupID = $this->getGroupGidNumber($groupDN);
403
		if($groupID === false) {
404
			throw new \Exception('Not a valid group');
405
		}
406
407
		$filterParts = [];
408
		$filterParts[] = $this->access->getFilterForUserCount();
409
		if ($search !== '') {
410
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
411
		}
412
		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
0 ignored issues
show
Bug introduced by
Are you sure $groupID of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

412
		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
413
414
		return $this->access->combineFilterWithAnd($filterParts);
415
	}
416
417
	/**
418
	 * returns a list of users that have the given group as gid number
419
	 *
420
	 * @param string $groupDN
421
	 * @param string $search
422
	 * @param int $limit
423
	 * @param int $offset
424
	 * @return string[]
425
	 */
426
	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
427
		try {
428
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
429
			$users = $this->access->fetchListOfUsers(
430
				$filter,
431
				[$this->access->connection->ldapUserDisplayName, 'dn'],
432
				$limit,
433
				$offset
434
			);
435
			return $this->access->nextcloudUserNames($users);
436
		} catch (\Exception $e) {
437
			return [];
438
		}
439
	}
440
441
	/**
442
	 * returns the number of users that have the given group as gid number
443
	 *
444
	 * @param string $groupDN
445
	 * @param string $search
446
	 * @param int $limit
447
	 * @param int $offset
448
	 * @return int
449
	 */
450
	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
451
		try {
452
			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
453
			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
454
			return (int)$users;
455
		} catch (\Exception $e) {
456
			return 0;
457
		}
458
	}
459
460
	/**
461
	 * gets the gidNumber of a user
462
	 * @param string $dn
463
	 * @return string
464
	 */
465
	public function getUserGroupByGid($dn) {
466
		$groupID = $this->getUserGidNumber($dn);
467
		if($groupID !== false) {
468
			$groupName = $this->gidNumber2Name($groupID, $dn);
469
			if($groupName !== false) {
470
				return $groupName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $groupName also could return the type true which is incompatible with the documented return type string.
Loading history...
471
			}
472
		}
473
474
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
475
	}
476
477
	/**
478
	 * translates a primary group ID into an Nextcloud internal name
479
	 * @param string $gid as given by primaryGroupID on AD
480
	 * @param string $dn a DN that belongs to the same domain as the group
481
	 * @return string|bool
482
	 */
483
	public function primaryGroupID2Name($gid, $dn) {
484
		$cacheKey = 'primaryGroupIDtoName';
485
		$groupNames = $this->access->connection->getFromCache($cacheKey);
486
		if(!is_null($groupNames) && isset($groupNames[$gid])) {
487
			return $groupNames[$gid];
488
		}
489
490
		$domainObjectSid = $this->access->getSID($dn);
491
		if($domainObjectSid === false) {
492
			return false;
493
		}
494
495
		//we need to get the DN from LDAP
496
		$filter = $this->access->combineFilterWithAnd(array(
497
			$this->access->connection->ldapGroupFilter,
498
			'objectsid=' . $domainObjectSid . '-' . $gid
0 ignored issues
show
Bug introduced by
Are you sure $domainObjectSid of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

498
			'objectsid=' . /** @scrutinizer ignore-type */ $domainObjectSid . '-' . $gid
Loading history...
499
		));
500
		$result = $this->access->searchGroups($filter, array('dn'), 1);
501
		if(empty($result)) {
502
			return false;
503
		}
504
		$dn = $result[0]['dn'][0];
505
506
		//and now the group name
507
		//NOTE once we have separate Nextcloud group IDs and group names we can
508
		//directly read the display name attribute instead of the DN
509
		$name = $this->access->dn2groupname($dn);
510
511
		$this->access->connection->writeToCache($cacheKey, $name);
512
513
		return $name;
514
	}
515
516
	/**
517
	 * returns the entry's primary group ID
518
	 * @param string $dn
519
	 * @param string $attribute
520
	 * @return string|bool
521
	 */
522
	private function getEntryGroupID($dn, $attribute) {
523
		$value = $this->access->readAttribute($dn, $attribute);
524
		if(is_array($value) && !empty($value)) {
525
			return $value[0];
526
		}
527
		return false;
528
	}
529
530
	/**
531
	 * returns the group's primary ID
532
	 * @param string $dn
533
	 * @return string|bool
534
	 */
535
	public function getGroupPrimaryGroupID($dn) {
536
		return $this->getEntryGroupID($dn, 'primaryGroupToken');
537
	}
538
539
	/**
540
	 * returns the user's primary group ID
541
	 * @param string $dn
542
	 * @return string|bool
543
	 */
544
	public function getUserPrimaryGroupIDs($dn) {
545
		$primaryGroupID = false;
546
		if($this->access->connection->hasPrimaryGroups) {
547
			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
548
			if($primaryGroupID === false) {
549
				$this->access->connection->hasPrimaryGroups = false;
550
			}
551
		}
552
		return $primaryGroupID;
553
	}
554
555
	/**
556
	 * returns a filter for a "users in primary group" search or count operation
557
	 *
558
	 * @param string $groupDN
559
	 * @param string $search
560
	 * @return string
561
	 * @throws \Exception
562
	 */
563
	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
564
		$groupID = $this->getGroupPrimaryGroupID($groupDN);
565
		if($groupID === false) {
566
			throw new \Exception('Not a valid group');
567
		}
568
569
		$filterParts = [];
570
		$filterParts[] = $this->access->getFilterForUserCount();
571
		if ($search !== '') {
572
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
573
		}
574
		$filterParts[] = 'primaryGroupID=' . $groupID;
0 ignored issues
show
Bug introduced by
Are you sure $groupID of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

574
		$filterParts[] = 'primaryGroupID=' . /** @scrutinizer ignore-type */ $groupID;
Loading history...
575
576
		return $this->access->combineFilterWithAnd($filterParts);
577
	}
578
579
	/**
580
	 * returns a list of users that have the given group as primary group
581
	 *
582
	 * @param string $groupDN
583
	 * @param string $search
584
	 * @param int $limit
585
	 * @param int $offset
586
	 * @return string[]
587
	 */
588
	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
589
		try {
590
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
591
			$users = $this->access->fetchListOfUsers(
592
				$filter,
593
				array($this->access->connection->ldapUserDisplayName, 'dn'),
594
				$limit,
595
				$offset
596
			);
597
			return $this->access->nextcloudUserNames($users);
598
		} catch (\Exception $e) {
599
			return array();
600
		}
601
	}
602
603
	/**
604
	 * returns the number of users that have the given group as primary group
605
	 *
606
	 * @param string $groupDN
607
	 * @param string $search
608
	 * @param int $limit
609
	 * @param int $offset
610
	 * @return int
611
	 */
612
	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
613
		try {
614
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
615
			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
616
			return (int)$users;
617
		} catch (\Exception $e) {
618
			return 0;
619
		}
620
	}
621
622
	/**
623
	 * gets the primary group of a user
624
	 * @param string $dn
625
	 * @return string
626
	 */
627
	public function getUserPrimaryGroup($dn) {
628
		$groupID = $this->getUserPrimaryGroupIDs($dn);
629
		if($groupID !== false) {
630
			$groupName = $this->primaryGroupID2Name($groupID, $dn);
631
			if($groupName !== false) {
632
				return $groupName;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $groupName also could return the type true which is incompatible with the documented return type string.
Loading history...
633
			}
634
		}
635
636
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
637
	}
638
639
	/**
640
	 * Get all groups a user belongs to
641
	 * @param string $uid Name of the user
642
	 * @return array with group names
643
	 *
644
	 * This function fetches all groups a user belongs to. It does not check
645
	 * if the user exists at all.
646
	 *
647
	 * This function includes groups based on dynamic group membership.
648
	 */
649
	public function getUserGroups($uid) {
650
		if(!$this->enabled) {
651
			return array();
652
		}
653
		$cacheKey = 'getUserGroups'.$uid;
654
		$userGroups = $this->access->connection->getFromCache($cacheKey);
655
		if(!is_null($userGroups)) {
656
			return $userGroups;
657
		}
658
		$userDN = $this->access->username2dn($uid);
659
		if(!$userDN) {
660
			$this->access->connection->writeToCache($cacheKey, array());
661
			return array();
662
		}
663
664
		$groups = [];
665
		$primaryGroup = $this->getUserPrimaryGroup($userDN);
666
		$gidGroupName = $this->getUserGroupByGid($userDN);
667
668
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapDynamicGroupMemberURL does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
669
670
		if (!empty($dynamicGroupMemberURL)) {
671
			// look through dynamic groups to add them to the result array if needed
672
			$groupsToMatch = $this->access->fetchListOfGroups(
673
				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
674
			foreach($groupsToMatch as $dynamicGroup) {
675
				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
676
					continue;
677
				}
678
				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
679
				if ($pos !== false) {
680
					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
681
					// apply filter via ldap search to see if this user is in this
682
					// dynamic group
683
					$userMatch = $this->access->readAttribute(
684
						$userDN,
685
						$this->access->connection->ldapUserDisplayName,
686
						$memberUrlFilter
687
					);
688
					if ($userMatch !== false) {
689
						// match found so this user is in this group
690
						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
691
						if(is_string($groupName)) {
692
							// be sure to never return false if the dn could not be
693
							// resolved to a name, for whatever reason.
694
							$groups[] = $groupName;
695
						}
696
					}
697
				} else {
698
					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

698
					/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
699
						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
700
				}
701
			}
702
		}
703
704
		// if possible, read out membership via memberOf. It's far faster than
705
		// performing a search, which still is a fallback later.
706
		// memberof doesn't support memberuid, so skip it here.
707
		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
0 ignored issues
show
Bug Best Practice introduced by
The property hasMemberOfFilterSupport does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
708
			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
0 ignored issues
show
Bug Best Practice introduced by
The property useMemberOfToDetectMembership does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
709
		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
710
		    ) {
711
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
712
			if (is_array($groupDNs)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
713
				foreach ($groupDNs as $dn) {
714
					$groupName = $this->access->dn2groupname($dn);
715
					if(is_string($groupName)) {
716
						// be sure to never return false if the dn could not be
717
						// resolved to a name, for whatever reason.
718
						$groups[] = $groupName;
719
					}
720
				}
721
			}
722
723
			if($primaryGroup !== false) {
0 ignored issues
show
introduced by
The condition $primaryGroup !== false is always true.
Loading history...
724
				$groups[] = $primaryGroup;
725
			}
726
			if($gidGroupName !== false) {
0 ignored issues
show
introduced by
The condition $gidGroupName !== false is always true.
Loading history...
727
				$groups[] = $gidGroupName;
728
			}
729
			$this->access->connection->writeToCache($cacheKey, $groups);
730
			return $groups;
731
		}
732
733
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
734
		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
735
			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
736
		) {
737
			$uid = $userDN;
738
		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
739
			$result = $this->access->readAttribute($userDN, 'uid');
740
			if ($result === false) {
741
				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

741
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
742
					$this->access->connection->ldapHost, ILogger::DEBUG);
743
				$uid = false;
744
			} else {
745
				$uid = $result[0];
746
			}
747
		} else {
748
			// just in case
749
			$uid = $userDN;
750
		}
751
752
		if($uid !== false) {
753
			if (isset($this->cachedGroupsByMember[$uid])) {
754
				$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
0 ignored issues
show
Bug introduced by
$this->cachedGroupsByMember[$uid] of type string is incompatible with the type array|null expected by parameter $array2 of array_merge(). ( Ignorable by Annotation )

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

754
				$groups = array_merge($groups, /** @scrutinizer ignore-type */ $this->cachedGroupsByMember[$uid]);
Loading history...
755
			} else {
756
				$groupsByMember = array_values($this->getGroupsByMember($uid));
757
				$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
758
				$this->cachedGroupsByMember[$uid] = $groupsByMember;
759
				$groups = array_merge($groups, $groupsByMember);
760
			}
761
		}
762
763
		if($primaryGroup !== false) {
0 ignored issues
show
introduced by
The condition $primaryGroup !== false is always true.
Loading history...
764
			$groups[] = $primaryGroup;
765
		}
766
		if($gidGroupName !== false) {
0 ignored issues
show
introduced by
The condition $gidGroupName !== false is always true.
Loading history...
767
			$groups[] = $gidGroupName;
768
		}
769
770
		$groups = array_unique($groups, SORT_LOCALE_STRING);
771
		$this->access->connection->writeToCache($cacheKey, $groups);
772
773
		return $groups;
774
	}
775
776
	/**
777
	 * @param string $dn
778
	 * @param array|null &$seen
779
	 * @return array
780
	 */
781
	private function getGroupsByMember($dn, &$seen = null) {
782
		if ($seen === null) {
783
			$seen = [];
784
		}
785
		if (array_key_exists($dn, $seen)) {
786
			// avoid loops
787
			return [];
788
		}
789
		$allGroups = [];
790
		$seen[$dn] = true;
791
		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
792
		$groups = $this->access->fetchListOfGroups($filter,
793
			[$this->access->connection->ldapGroupDisplayName, 'dn']);
794
		if (is_array($groups)) {
0 ignored issues
show
introduced by
The condition is_array($groups) is always true.
Loading history...
795
			$fetcher = function ($dn, &$seen) {
796
				if(is_array($dn) && isset($dn['dn'][0])) {
797
					$dn = $dn['dn'][0];
798
				}
799
				return $this->getGroupsByMember($dn, $seen);
800
			};
801
			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
802
		}
803
		$visibleGroups = $this->access->groupsMatchFilter(array_keys($allGroups));
804
		return array_intersect_key($allGroups, array_flip($visibleGroups));
805
	}
806
807
	/**
808
	 * get a list of all users in a group
809
	 *
810
	 * @param string $gid
811
	 * @param string $search
812
	 * @param int $limit
813
	 * @param int $offset
814
	 * @return array with user ids
815
	 * @throws \Exception
816
	 */
817
	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
818
		if(!$this->enabled) {
819
			return array();
820
		}
821
		if(!$this->groupExists($gid)) {
822
			return array();
823
		}
824
		$search = $this->access->escapeFilterPart($search, true);
825
		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
826
		// check for cache of the exact query
827
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
828
		if(!is_null($groupUsers)) {
829
			return $groupUsers;
830
		}
831
832
		// check for cache of the query without limit and offset
833
		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
834
		if(!is_null($groupUsers)) {
835
			$groupUsers = array_slice($groupUsers, $offset, $limit);
836
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
837
			return $groupUsers;
838
		}
839
840
		if($limit === -1) {
841
			$limit = null;
842
		}
843
		$groupDN = $this->access->groupname2dn($gid);
844
		if(!$groupDN) {
845
			// group couldn't be found, return empty resultset
846
			$this->access->connection->writeToCache($cacheKey, array());
847
			return array();
848
		}
849
850
		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
851
		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
852
		$members = $this->_groupMembers($groupDN);
853
		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
854
			//in case users could not be retrieved, return empty result set
855
			$this->access->connection->writeToCache($cacheKey, []);
856
			return [];
857
		}
858
859
		$groupUsers = array();
860
		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
861
		$attrs = $this->access->userManager->getAttributes(true);
862
		foreach($members as $member) {
863
			if($isMemberUid) {
864
				//we got uids, need to get their DNs to 'translate' them to user names
865
				$filter = $this->access->combineFilterWithAnd(array(
866
					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
867
					$this->access->combineFilterWithAnd([
868
						$this->access->getFilterPartForUserSearch($search),
869
						$this->access->connection->ldapUserFilter
870
					])
871
				));
872
				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
873
				if(count($ldap_users) < 1) {
874
					continue;
875
				}
876
				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
877
			} else {
878
				//we got DNs, check if we need to filter by search or we can give back all of them
879
				$uid = $this->access->dn2username($member);
880
				if(!$uid) {
881
					continue;
882
				}
883
884
				$cacheKey = 'userExistsOnLDAP' . $uid;
885
				$userExists = $this->access->connection->getFromCache($cacheKey);
886
				if($userExists === false) {
887
					continue;
888
				}
889
				if($userExists === null || $search !== '') {
890
					if (!$this->access->readAttribute($member,
891
						$this->access->connection->ldapUserDisplayName,
892
						$this->access->combineFilterWithAnd([
893
							$this->access->getFilterPartForUserSearch($search),
894
							$this->access->connection->ldapUserFilter
895
						])))
896
					{
897
						if($search === '') {
898
							$this->access->connection->writeToCache($cacheKey, false);
899
						}
900
						continue;
901
					}
902
					$this->access->connection->writeToCache($cacheKey, true);
903
				}
904
				$groupUsers[] = $uid;
905
			}
906
		}
907
908
		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
909
		natsort($groupUsers);
910
		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
911
		$groupUsers = array_slice($groupUsers, $offset, $limit);
912
913
		$this->access->connection->writeToCache($cacheKey, $groupUsers);
914
915
		return $groupUsers;
916
	}
917
918
	/**
919
	 * returns the number of users in a group, who match the search term
920
	 * @param string $gid the internal group name
921
	 * @param string $search optional, a search string
922
	 * @return int|bool
923
	 */
924
	public function countUsersInGroup($gid, $search = '') {
925
		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
926
			return $this->groupPluginManager->countUsersInGroup($gid, $search);
927
		}
928
929
		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
930
		if(!$this->enabled || !$this->groupExists($gid)) {
931
			return false;
932
		}
933
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
934
		if(!is_null($groupUsers)) {
935
			return $groupUsers;
936
		}
937
938
		$groupDN = $this->access->groupname2dn($gid);
939
		if(!$groupDN) {
940
			// group couldn't be found, return empty result set
941
			$this->access->connection->writeToCache($cacheKey, false);
942
			return false;
943
		}
944
945
		$members = $this->_groupMembers($groupDN);
946
		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
947
		if(!$members && $primaryUserCount === 0) {
948
			//in case users could not be retrieved, return empty result set
949
			$this->access->connection->writeToCache($cacheKey, false);
950
			return false;
951
		}
952
953
		if ($search === '') {
954
			$groupUsers = count($members) + $primaryUserCount;
955
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
956
			return $groupUsers;
957
		}
958
		$search = $this->access->escapeFilterPart($search, true);
959
		$isMemberUid =
960
			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
961
			=== 'memberuid');
962
963
		//we need to apply the search filter
964
		//alternatives that need to be checked:
965
		//a) get all users by search filter and array_intersect them
966
		//b) a, but only when less than 1k 10k ?k users like it is
967
		//c) put all DNs|uids in a LDAP filter, combine with the search string
968
		//   and let it count.
969
		//For now this is not important, because the only use of this method
970
		//does not supply a search string
971
		$groupUsers = array();
972
		foreach($members as $member) {
973
			if($isMemberUid) {
974
				//we got uids, need to get their DNs to 'translate' them to user names
975
				$filter = $this->access->combineFilterWithAnd(array(
976
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
977
					$this->access->getFilterPartForUserSearch($search)
978
				));
979
				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
980
				if(count($ldap_users) < 1) {
981
					continue;
982
				}
983
				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
984
			} else {
985
				//we need to apply the search filter now
986
				if(!$this->access->readAttribute($member,
987
					$this->access->connection->ldapUserDisplayName,
988
					$this->access->getFilterPartForUserSearch($search))) {
989
					continue;
990
				}
991
				// dn2username will also check if the users belong to the allowed base
992
				if($ocname = $this->access->dn2username($member)) {
993
					$groupUsers[] = $ocname;
994
				}
995
			}
996
		}
997
998
		//and get users that have the group as primary
999
		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
1000
1001
		return count($groupUsers) + $primaryUsers;
1002
	}
1003
1004
	/**
1005
	 * get a list of all groups
1006
	 *
1007
	 * @param string $search
1008
	 * @param $limit
1009
	 * @param int $offset
1010
	 * @return array with group names
1011
	 *
1012
	 * Returns a list with all groups (used by getGroups)
1013
	 */
1014
	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
1015
		if(!$this->enabled) {
1016
			return array();
1017
		}
1018
		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
1019
1020
		//Check cache before driving unnecessary searches
1021
		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

1021
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1022
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
1023
		if(!is_null($ldap_groups)) {
1024
			return $ldap_groups;
1025
		}
1026
1027
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1028
		// error. With a limit of 0, we get 0 results. So we pass null.
1029
		if($limit <= 0) {
1030
			$limit = null;
1031
		}
1032
		$filter = $this->access->combineFilterWithAnd(array(
1033
			$this->access->connection->ldapGroupFilter,
1034
			$this->access->getFilterPartForGroupSearch($search)
1035
		));
1036
		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

1036
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1037
		$ldap_groups = $this->access->fetchListOfGroups($filter,
1038
				array($this->access->connection->ldapGroupDisplayName, 'dn'),
1039
				$limit,
1040
				$offset);
1041
		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1042
1043
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1044
		return $ldap_groups;
1045
	}
1046
1047
	/**
1048
	 * get a list of all groups using a paged search
1049
	 *
1050
	 * @param string $search
1051
	 * @param int $limit
1052
	 * @param int $offset
1053
	 * @return array with group names
1054
	 *
1055
	 * Returns a list with all groups
1056
	 * Uses a paged search if available to override a
1057
	 * server side search limit.
1058
	 * (active directory has a limit of 1000 by default)
1059
	 */
1060
	public function getGroups($search = '', $limit = -1, $offset = 0) {
1061
		if(!$this->enabled) {
1062
			return array();
1063
		}
1064
		$search = $this->access->escapeFilterPart($search, true);
1065
		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1066
		if ($pagingSize <= 0) {
1067
			return $this->getGroupsChunk($search, $limit, $offset);
1068
		}
1069
		$maxGroups = 100000; // limit max results (just for safety reasons)
1070
		if ($limit > -1) {
1071
		   $overallLimit = min($limit + $offset, $maxGroups);
1072
		} else {
1073
		   $overallLimit = $maxGroups;
1074
		}
1075
		$chunkOffset = $offset;
1076
		$allGroups = array();
1077
		while ($chunkOffset < $overallLimit) {
1078
			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1079
			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1080
			$nread = count($ldapGroups);
1081
			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

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

1081
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
1082
			if ($nread) {
1083
				$allGroups = array_merge($allGroups, $ldapGroups);
1084
				$chunkOffset += $nread;
1085
			}
1086
			if ($nread < $chunkLimit) {
1087
				break;
1088
			}
1089
		}
1090
		return $allGroups;
1091
	}
1092
1093
	/**
1094
	 * @param string $group
1095
	 * @return bool
1096
	 */
1097
	public function groupMatchesFilter($group) {
1098
		return (strripos($group, $this->groupSearch) !== false);
0 ignored issues
show
Bug Best Practice introduced by
The property groupSearch does not exist on OCA\User_LDAP\Group_LDAP. Did you maybe forget to declare it?
Loading history...
1099
	}
1100
1101
	/**
1102
	 * check if a group exists
1103
	 * @param string $gid
1104
	 * @return bool
1105
	 */
1106
	public function groupExists($gid) {
1107
		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1108
		if(!is_null($groupExists)) {
1109
			return (bool)$groupExists;
1110
		}
1111
1112
		//getting dn, if false the group does not exist. If dn, it may be mapped
1113
		//only, requires more checking.
1114
		$dn = $this->access->groupname2dn($gid);
1115
		if(!$dn) {
1116
			$this->access->connection->writeToCache('groupExists'.$gid, false);
1117
			return false;
1118
		}
1119
1120
		//if group really still exists, we will be able to read its objectclass
1121
		if(!is_array($this->access->readAttribute($dn, ''))) {
1122
			$this->access->connection->writeToCache('groupExists'.$gid, false);
1123
			return false;
1124
		}
1125
1126
		$this->access->connection->writeToCache('groupExists'.$gid, true);
1127
		return true;
1128
	}
1129
1130
	/**
1131
	* Check if backend implements actions
1132
	* @param int $actions bitwise-or'ed actions
1133
	* @return boolean
1134
	*
1135
	* Returns the supported actions as int to be
1136
	* compared with GroupInterface::CREATE_GROUP etc.
1137
	*/
1138
	public function implementsActions($actions) {
1139
		return (bool)((GroupInterface::COUNT_USERS |
1140
				$this->groupPluginManager->getImplementedActions()) & $actions);
1141
	}
1142
1143
	/**
1144
	 * Return access for LDAP interaction.
1145
	 * @return Access instance of Access for LDAP interaction
1146
	 */
1147
	public function getLDAPAccess($gid) {
1148
		return $this->access;
1149
	}
1150
1151
	/**
1152
	 * create a group
1153
	 * @param string $gid
1154
	 * @return bool
1155
	 * @throws \Exception
1156
	 */
1157
	public function createGroup($gid) {
1158
		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1159
			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1160
				//updates group mapping
1161
				$uuid = $this->access->getUUID($dn, false);
1162
				if(is_string($uuid)) {
1163
					$this->access->mapAndAnnounceIfApplicable(
1164
						$this->access->getGroupMapper(),
1165
						$dn,
1166
						$gid,
1167
						$uuid,
1168
						false
1169
					);
1170
					$this->access->cacheGroupExists($gid);
1171
				}
1172
			}
1173
			return $dn != null;
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $dn of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
1174
		}
1175
		throw new \Exception('Could not create group in LDAP backend.');
1176
	}
1177
1178
	/**
1179
	 * delete a group
1180
	 * @param string $gid gid of the group to delete
1181
	 * @return bool
1182
	 * @throws \Exception
1183
	 */
1184
	public function deleteGroup($gid) {
1185
		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1186
			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1187
				#delete group in nextcloud internal db
1188
				$this->access->getGroupMapper()->unmap($gid);
1189
				$this->access->connection->writeToCache("groupExists".$gid, false);
1190
			}
1191
			return $ret;
1192
		}
1193
		throw new \Exception('Could not delete group in LDAP backend.');
1194
	}
1195
1196
	/**
1197
	 * Add a user to a group
1198
	 * @param string $uid Name of the user to add to group
1199
	 * @param string $gid Name of the group in which add the user
1200
	 * @return bool
1201
	 * @throws \Exception
1202
	 */
1203
	public function addToGroup($uid, $gid) {
1204
		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1205
			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1206
				$this->access->connection->clearCache();
1207
				unset($this->cachedGroupMembers[$gid]);
1208
			}
1209
			return $ret;
1210
		}
1211
		throw new \Exception('Could not add user to group in LDAP backend.');
1212
	}
1213
1214
	/**
1215
	 * Removes a user from a group
1216
	 * @param string $uid Name of the user to remove from group
1217
	 * @param string $gid Name of the group from which remove the user
1218
	 * @return bool
1219
	 * @throws \Exception
1220
	 */
1221
	public function removeFromGroup($uid, $gid) {
1222
		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1223
			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1224
				$this->access->connection->clearCache();
1225
				unset($this->cachedGroupMembers[$gid]);
1226
			}
1227
			return $ret;
1228
		}
1229
		throw new \Exception('Could not remove user from group in LDAP backend.');
1230
	}
1231
1232
	/**
1233
	 * Gets group details
1234
	 * @param string $gid Name of the group
1235
	 * @return array | false
1236
	 * @throws \Exception
1237
	 */
1238
	public function getGroupDetails($gid) {
1239
		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1240
			return $this->groupPluginManager->getGroupDetails($gid);
1241
		}
1242
		throw new \Exception('Could not get group details in LDAP backend.');
1243
	}
1244
1245
	/**
1246
	 * Return LDAP connection resource from a cloned connection.
1247
	 * The cloned connection needs to be closed manually.
1248
	 * of the current access.
1249
	 * @param string $gid
1250
	 * @return resource of the LDAP connection
1251
	 */
1252
	public function getNewLDAPConnection($gid) {
1253
		$connection = clone $this->access->getConnection();
1254
		return $connection->getConnectionResource();
1255
	}
1256
1257
	/**
1258
	 * @throws \OC\ServerNotAvailableException
1259
	 */
1260
	public function getDisplayName(string $gid): string {
1261
		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1262
			return $this->groupPluginManager->getDisplayName($gid);
1263
		}
1264
1265
		$cacheKey = 'group_getDisplayName' . $gid;
1266
		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1267
			return $displayName;
1268
		}
1269
1270
		$displayName = $this->access->readAttribute(
1271
			$this->access->groupname2dn($gid),
1272
			$this->access->connection->ldapGroupDisplayName);
1273
1274
		if ($displayName && (count($displayName) > 0)) {
1275
			$displayName = $displayName[0];
1276
			$this->access->connection->writeToCache($cacheKey, $displayName);
1277
			return $displayName;
1278
		}
1279
1280
		return '';
1281
	}
1282
}
1283