Passed
Push — master ( 7ff1f8...3af7f2 )
by Blizzz
10:42 queued 10s
created

Group_LDAP::walkNestedGroups()   C

Complexity

Conditions 12
Paths 48

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 18
nc 48
nop 3
dl 0
loc 32
rs 6.9666
c 0
b 0
f 0

How to fix   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 Alex Weirig <[email protected]>
6
 * @author Alexander Bergolth <[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 Thomas Müller <[email protected]>
21
 * @author Victor Dubiniuk <[email protected]>
22
 * @author Vincent Petry <[email protected]>
23
 * @author Vinicius Cubas Brand <[email protected]>
24
 * @author Xuanwo <[email protected]>
25
 *
26
 * @license AGPL-3.0
27
 *
28
 * This code is free software: you can redistribute it and/or modify
29
 * it under the terms of the GNU Affero General Public License, version 3,
30
 * as published by the Free Software Foundation.
31
 *
32
 * This program is distributed in the hope that it will be useful,
33
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35
 * GNU Affero General Public License for more details.
36
 *
37
 * You should have received a copy of the GNU Affero General Public License, version 3,
38
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
39
 *
40
 */
41
42
namespace OCA\User_LDAP;
43
44
use OC\Cache\CappedMemoryCache;
45
use OCP\GroupInterface;
46
use OCP\ILogger;
47
48
class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP {
49
	protected $enabled = false;
50
51
	/**
52
	 * @var string[] $cachedGroupMembers array of users with gid as key
53
	 */
54
	protected $cachedGroupMembers;
55
56
	/**
57
	 * @var string[] $cachedGroupsByMember array of groups with uid as key
58
	 */
59
	protected $cachedGroupsByMember;
60
61
	/**
62
	 * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
63
	 */
64
	protected $cachedNestedGroups;
65
66
	/** @var GroupPluginManager */
67
	protected $groupPluginManager;
68
69
	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
70
		parent::__construct($access);
71
		$filter = $this->access->connection->ldapGroupFilter;
72
		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
73
		if(!empty($filter) && !empty($gassoc)) {
74
			$this->enabled = true;
75
		}
76
77
		$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...
78
		$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...
79
		$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...
80
		$this->groupPluginManager = $groupPluginManager;
81
	}
82
83
	/**
84
	 * is user in group?
85
	 * @param string $uid uid of the user
86
	 * @param string $gid gid of the group
87
	 * @return bool
88
	 *
89
	 * Checks whether the user is member of a group or not.
90
	 */
91
	public function inGroup($uid, $gid) {
92
		if(!$this->enabled) {
93
			return false;
94
		}
95
		$cacheKey = 'inGroup'.$uid.':'.$gid;
96
		$inGroup = $this->access->connection->getFromCache($cacheKey);
97
		if(!is_null($inGroup)) {
98
			return (bool)$inGroup;
99
		}
100
101
		$userDN = $this->access->username2dn($uid);
102
103
		if(isset($this->cachedGroupMembers[$gid])) {
104
			$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

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

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

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

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

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

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

697
					/** @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...
698
						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
699
				}
700
			}
701
		}
702
703
		// if possible, read out membership via memberOf. It's far faster than
704
		// performing a search, which still is a fallback later.
705
		// memberof doesn't support memberuid, so skip it here.
706
		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...
707
			&& (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...
708
		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
709
		    ) {
710
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
711
			if (is_array($groupDNs)) {
0 ignored issues
show
introduced by
The condition is_array($groupDNs) is always true.
Loading history...
712
				foreach ($groupDNs as $dn) {
713
					$groupName = $this->access->dn2groupname($dn);
714
					if(is_string($groupName)) {
715
						// be sure to never return false if the dn could not be
716
						// resolved to a name, for whatever reason.
717
						$groups[] = $groupName;
718
					}
719
				}
720
			}
721
722
			if($primaryGroup !== false) {
0 ignored issues
show
introduced by
The condition $primaryGroup !== false is always true.
Loading history...
723
				$groups[] = $primaryGroup;
724
			}
725
			if($gidGroupName !== false) {
0 ignored issues
show
introduced by
The condition $gidGroupName !== false is always true.
Loading history...
726
				$groups[] = $gidGroupName;
727
			}
728
			$this->access->connection->writeToCache($cacheKey, $groups);
729
			return $groups;
730
		}
731
732
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
733
		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
734
			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
735
		) {
736
			$uid = $userDN;
737
		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
738
			$result = $this->access->readAttribute($userDN, 'uid');
739
			if ($result === false) {
740
				\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

740
				/** @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...
741
					$this->access->connection->ldapHost, ILogger::DEBUG);
742
			}
743
			$uid = $result[0];
744
		} else {
745
			// just in case
746
			$uid = $userDN;
747
		}
748
749
		if(isset($this->cachedGroupsByMember[$uid])) {
750
			$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

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

997
		/** @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...
998
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
		if(!is_null($ldap_groups)) {
1000
			return $ldap_groups;
1001
		}
1002
1003
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
		// error. With a limit of 0, we get 0 results. So we pass null.
1005
		if($limit <= 0) {
1006
			$limit = null;
1007
		}
1008
		$filter = $this->access->combineFilterWithAnd(array(
1009
			$this->access->connection->ldapGroupFilter,
1010
			$this->access->getFilterPartForGroupSearch($search)
1011
		));
1012
		\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

1012
		/** @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...
1013
		$ldap_groups = $this->access->fetchListOfGroups($filter,
1014
				array($this->access->connection->ldapGroupDisplayName, 'dn'),
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupDisplayName does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1015
				$limit,
1016
				$offset);
1017
		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1018
1019
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1020
		return $ldap_groups;
1021
	}
1022
1023
	/**
1024
	 * get a list of all groups using a paged search
1025
	 *
1026
	 * @param string $search
1027
	 * @param int $limit
1028
	 * @param int $offset
1029
	 * @return array with group names
1030
	 *
1031
	 * Returns a list with all groups
1032
	 * Uses a paged search if available to override a
1033
	 * server side search limit.
1034
	 * (active directory has a limit of 1000 by default)
1035
	 */
1036
	public function getGroups($search = '', $limit = -1, $offset = 0) {
1037
		if(!$this->enabled) {
1038
			return array();
1039
		}
1040
		$search = $this->access->escapeFilterPart($search, true);
1041
		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1042
		if ($pagingSize <= 0) {
1043
			return $this->getGroupsChunk($search, $limit, $offset);
1044
		}
1045
		$maxGroups = 100000; // limit max results (just for safety reasons)
1046
		if ($limit > -1) {
1047
		   $overallLimit = min($limit + $offset, $maxGroups);
1048
		} else {
1049
		   $overallLimit = $maxGroups;
1050
		}
1051
		$chunkOffset = $offset;
1052
		$allGroups = array();
1053
		while ($chunkOffset < $overallLimit) {
1054
			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1055
			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1056
			$nread = count($ldapGroups);
1057
			\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

1057
			/** @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...
1058
			if ($nread) {
1059
				$allGroups = array_merge($allGroups, $ldapGroups);
1060
				$chunkOffset += $nread;
1061
			}
1062
			if ($nread < $chunkLimit) {
1063
				break;
1064
			}
1065
		}
1066
		return $allGroups;
1067
	}
1068
1069
	/**
1070
	 * @param string $group
1071
	 * @return bool
1072
	 */
1073
	public function groupMatchesFilter($group) {
1074
		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...
1075
	}
1076
1077
	/**
1078
	 * check if a group exists
1079
	 * @param string $gid
1080
	 * @return bool
1081
	 */
1082
	public function groupExists($gid) {
1083
		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1084
		if(!is_null($groupExists)) {
1085
			return (bool)$groupExists;
1086
		}
1087
1088
		//getting dn, if false the group does not exist. If dn, it may be mapped
1089
		//only, requires more checking.
1090
		$dn = $this->access->groupname2dn($gid);
1091
		if(!$dn) {
1092
			$this->access->connection->writeToCache('groupExists'.$gid, false);
1093
			return false;
1094
		}
1095
1096
		//if group really still exists, we will be able to read its objectclass
1097
		if(!is_array($this->access->readAttribute($dn, ''))) {
1098
			$this->access->connection->writeToCache('groupExists'.$gid, false);
1099
			return false;
1100
		}
1101
1102
		$this->access->connection->writeToCache('groupExists'.$gid, true);
1103
		return true;
1104
	}
1105
1106
	/**
1107
	* Check if backend implements actions
1108
	* @param int $actions bitwise-or'ed actions
1109
	* @return boolean
1110
	*
1111
	* Returns the supported actions as int to be
1112
	* compared with GroupInterface::CREATE_GROUP etc.
1113
	*/
1114
	public function implementsActions($actions) {
1115
		return (bool)((GroupInterface::COUNT_USERS |
1116
				$this->groupPluginManager->getImplementedActions()) & $actions);
1117
	}
1118
1119
	/**
1120
	 * Return access for LDAP interaction.
1121
	 * @return Access instance of Access for LDAP interaction
1122
	 */
1123
	public function getLDAPAccess($gid) {
1124
		return $this->access;
1125
	}
1126
1127
	/**
1128
	 * create a group
1129
	 * @param string $gid
1130
	 * @return bool
1131
	 * @throws \Exception
1132
	 */
1133
	public function createGroup($gid) {
1134
		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1135
			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1136
				//updates group mapping
1137
				$this->access->dn2ocname($dn, $gid, false);
1138
				$this->access->connection->writeToCache("groupExists".$gid, true);
1139
			}
1140
			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...
1141
		}
1142
		throw new \Exception('Could not create group in LDAP backend.');
1143
	}
1144
1145
	/**
1146
	 * delete a group
1147
	 * @param string $gid gid of the group to delete
1148
	 * @return bool
1149
	 * @throws \Exception
1150
	 */
1151
	public function deleteGroup($gid) {
1152
		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1153
			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1154
				#delete group in nextcloud internal db
1155
				$this->access->getGroupMapper()->unmap($gid);
1156
				$this->access->connection->writeToCache("groupExists".$gid, false);
1157
			}
1158
			return $ret;
1159
		}
1160
		throw new \Exception('Could not delete group in LDAP backend.');
1161
	}
1162
1163
	/**
1164
	 * Add a user to a group
1165
	 * @param string $uid Name of the user to add to group
1166
	 * @param string $gid Name of the group in which add the user
1167
	 * @return bool
1168
	 * @throws \Exception
1169
	 */
1170
	public function addToGroup($uid, $gid) {
1171
		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1172
			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1173
				$this->access->connection->clearCache();
1174
			}
1175
			return $ret;
1176
		}
1177
		throw new \Exception('Could not add user to group in LDAP backend.');
1178
	}
1179
1180
	/**
1181
	 * Removes a user from a group
1182
	 * @param string $uid Name of the user to remove from group
1183
	 * @param string $gid Name of the group from which remove the user
1184
	 * @return bool
1185
	 * @throws \Exception
1186
	 */
1187
	public function removeFromGroup($uid, $gid) {
1188
		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1189
			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1190
				$this->access->connection->clearCache();
1191
			}
1192
			return $ret;
1193
		}
1194
		throw new \Exception('Could not remove user from group in LDAP backend.');
1195
	}
1196
1197
	/**
1198
	 * Gets group details
1199
	 * @param string $gid Name of the group
1200
	 * @return array | false
1201
	 * @throws \Exception
1202
	 */
1203
	public function getGroupDetails($gid) {
1204
		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1205
			return $this->groupPluginManager->getGroupDetails($gid);
1206
		}
1207
		throw new \Exception('Could not get group details in LDAP backend.');
1208
	}
1209
1210
	/**
1211
	 * Return LDAP connection resource from a cloned connection.
1212
	 * The cloned connection needs to be closed manually.
1213
	 * of the current access.
1214
	 * @param string $gid
1215
	 * @return resource of the LDAP connection
1216
	 */
1217
	public function getNewLDAPConnection($gid) {
1218
		$connection = clone $this->access->getConnection();
1219
		return $connection->getConnectionResource();
1220
	}
1221
1222
}
1223