Passed
Push — master ( bffb34...3e5174 )
by Blizzz
12:26 queued 12s
created

Group_LDAP::getGroupPrimaryGroupID()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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\Group\Backend\IGetDisplayNameBackend;
46
use OCP\GroupInterface;
47
use OCP\ILogger;
48
49
class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
50
	protected $enabled = false;
51
52
	/**
53
	 * @var string[] $cachedGroupMembers array of users with gid as key
54
	 */
55
	protected $cachedGroupMembers;
56
57
	/**
58
	 * @var string[] $cachedGroupsByMember array of groups with uid as key
59
	 */
60
	protected $cachedGroupsByMember;
61
62
	/**
63
	 * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
64
	 */
65
	protected $cachedNestedGroups;
66
67
	/** @var GroupPluginManager */
68
	protected $groupPluginManager;
69
70
	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
71
		parent::__construct($access);
72
		$filter = $this->access->connection->ldapGroupFilter;
73
		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
74
		if(!empty($filter) && !empty($gassoc)) {
75
			$this->enabled = true;
76
		}
77
78
		$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...
79
		$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...
80
		$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...
81
		$this->groupPluginManager = $groupPluginManager;
82
	}
83
84
	/**
85
	 * is user in group?
86
	 * @param string $uid uid of the user
87
	 * @param string $gid gid of the group
88
	 * @return bool
89
	 *
90
	 * Checks whether the user is member of a group or not.
91
	 */
92
	public function inGroup($uid, $gid) {
93
		if(!$this->enabled) {
94
			return false;
95
		}
96
		$cacheKey = 'inGroup'.$uid.':'.$gid;
97
		$inGroup = $this->access->connection->getFromCache($cacheKey);
98
		if(!is_null($inGroup)) {
99
			return (bool)$inGroup;
100
		}
101
102
		$userDN = $this->access->username2dn($uid);
103
104
		if(isset($this->cachedGroupMembers[$gid])) {
105
			$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

105
			$isInGroup = in_array($userDN, /** @scrutinizer ignore-type */ $this->cachedGroupMembers[$gid]);
Loading history...
106
			return $isInGroup;
107
		}
108
109
		$cacheKeyMembers = 'inGroup-members:'.$gid;
110
		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111
		if(!is_null($members)) {
112
			$this->cachedGroupMembers[$gid] = $members;
113
			$isInGroup = in_array($userDN, $members);
114
			$this->access->connection->writeToCache($cacheKey, $isInGroup);
115
			return $isInGroup;
116
		}
117
118
		$groupDN = $this->access->groupname2dn($gid);
119
		// just in case
120
		if(!$groupDN || !$userDN) {
121
			$this->access->connection->writeToCache($cacheKey, false);
122
			return false;
123
		}
124
125
		//check primary group first
126
		if($gid === $this->getUserPrimaryGroup($userDN)) {
127
			$this->access->connection->writeToCache($cacheKey, true);
128
			return true;
129
		}
130
131
		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
		$members = $this->_groupMembers($groupDN);
133
		$members = array_keys($members); // uids are returned as keys
134
		if(!is_array($members) || count($members) === 0) {
0 ignored issues
show
introduced by
The condition is_array($members) is always true.
Loading history...
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
			}
744
			$uid = $result[0];
745
		} else {
746
			// just in case
747
			$uid = $userDN;
748
		}
749
750
		if(isset($this->cachedGroupsByMember[$uid])) {
751
			$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

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

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

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

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