Completed
Push — master ( c20409...a4266f )
by Lukas
07:37
created

Group_LDAP::primaryGroupID2Name()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
nc 4
nop 2
dl 0
loc 32
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Alexander Bergolth <[email protected]>
6
 * @author Alex Weirig <[email protected]>
7
 * @author alexweirig <[email protected]>
8
 * @author Andreas Fischer <[email protected]>
9
 * @author Arthur Schiwon <[email protected]>
10
 * @author Bart Visscher <[email protected]>
11
 * @author Christopher Schäpers <[email protected]>
12
 * @author Frédéric Fortier <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Lukas Reschke <[email protected]>
15
 * @author Morris Jobke <[email protected]>
16
 * @author Nicolas Grekas <[email protected]>
17
 * @author Robin McCorkell <[email protected]>
18
 * @author Roeland Jago Douma <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
38
namespace OCA\User_LDAP;
39
40
use OC\Cache\CappedMemoryCache;
41
42
class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
43
	protected $enabled = false;
44
45
	/**
46
	 * @var string[] $cachedGroupMembers array of users with gid as key
47
	 */
48
	protected $cachedGroupMembers;
49
50
	/**
51
	 * @var string[] $cachedGroupsByMember array of groups with uid as key
52
	 */
53
	protected $cachedGroupsByMember;
54
55
	public function __construct(Access $access) {
56
		parent::__construct($access);
57
		$filter = $this->access->connection->ldapGroupFilter;
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
58
		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
59
		if(!empty($filter) && !empty($gassoc)) {
60
			$this->enabled = true;
61
		}
62
63
		$this->cachedGroupMembers = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \OC\Cache\CappedMemoryCache() of type object<OC\Cache\CappedMemoryCache> is incompatible with the declared type array<integer,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...
64
		$this->cachedGroupsByMember = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \OC\Cache\CappedMemoryCache() of type object<OC\Cache\CappedMemoryCache> is incompatible with the declared type array<integer,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...
65
	}
66
67
	/**
68
	 * is user in group?
69
	 * @param string $uid uid of the user
70
	 * @param string $gid gid of the group
71
	 * @return bool
72
	 *
73
	 * Checks whether the user is member of a group or not.
74
	 */
75
	public function inGroup($uid, $gid) {
76
		if(!$this->enabled) {
77
			return false;
78
		}
79
		$cacheKey = 'inGroup'.$uid.':'.$gid;
80
		$inGroup = $this->access->connection->getFromCache($cacheKey);
81
		if(!is_null($inGroup)) {
82
			return (bool)$inGroup;
83
		}
84
85
		$userDN = $this->access->username2dn($uid);
86
87
		if(isset($this->cachedGroupMembers[$gid])) {
88
			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
89
			return $isInGroup;
90
		}
91
92
		$cacheKeyMembers = 'inGroup-members:'.$gid;
93
		$members = $this->access->connection->getFromCache($cacheKeyMembers);
94
		if(!is_null($members)) {
95
			$this->cachedGroupMembers[$gid] = $members;
96
			$isInGroup = in_array($userDN, $members);
97
			$this->access->connection->writeToCache($cacheKey, $isInGroup);
98
			return $isInGroup;
99
		}
100
101
		$groupDN = $this->access->groupname2dn($gid);
102
		// just in case
103 View Code Duplication
		if(!$groupDN || !$userDN) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupDN of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $userDN of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
104
			$this->access->connection->writeToCache($cacheKey, false);
105
			return false;
106
		}
107
108
		//check primary group first
109 View Code Duplication
		if($gid === $this->getUserPrimaryGroup($userDN)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
110
			$this->access->connection->writeToCache($cacheKey, true);
111
			return true;
112
		}
113
114
		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
115
		$members = $this->_groupMembers($groupDN);
116
		$members = array_keys($members); // uids are returned as keys
117
		if(!is_array($members) || count($members) === 0) {
118
			$this->access->connection->writeToCache($cacheKey, false);
119
			return false;
120
		}
121
122
		//extra work if we don't get back user DNs
123
		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
124
			$dns = array();
125
			$filterParts = array();
126
			$bytes = 0;
127
			foreach($members as $mid) {
128
				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
129
				$filterParts[] = $filter;
130
				$bytes += strlen($filter);
131
				if($bytes >= 9000000) {
132
					// AD has a default input buffer of 10 MB, we do not want
133
					// to take even the chance to exceed it
134
					$filter = $this->access->combineFilterWithOr($filterParts);
135
					$bytes = 0;
136
					$filterParts = array();
137
					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
138
					$dns = array_merge($dns, $users);
139
				}
140
			}
141
			if(count($filterParts) > 0) {
142
				$filter = $this->access->combineFilterWithOr($filterParts);
143
				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
144
				$dns = array_merge($dns, $users);
145
			}
146
			$members = $dns;
147
		}
148
149
		$isInGroup = in_array($userDN, $members);
150
		$this->access->connection->writeToCache($cacheKey, $isInGroup);
151
		$this->access->connection->writeToCache($cacheKeyMembers, $members);
152
		$this->cachedGroupMembers[$gid] = $members;
153
154
		return $isInGroup;
155
	}
156
157
	/**
158
	 * @param string $dnGroup
159
	 * @return array
160
	 *
161
	 * For a group that has user membership defined by an LDAP search url attribute returns the users
162
	 * that match the search url otherwise returns an empty array.
163
	 */
164
	public function getDynamicGroupMembers($dnGroup) {
165
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
0 ignored issues
show
Documentation introduced by
The property ldapDynamicGroupMemberURL does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
166
167
		if (empty($dynamicGroupMemberURL)) {
168
			return array();
169
		}
170
171
		$dynamicMembers = array();
172
		$memberURLs = $this->access->readAttribute(
173
			$dnGroup,
174
			$dynamicGroupMemberURL,
175
			$this->access->connection->ldapGroupFilter
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
176
		);
177
		if ($memberURLs !== false) {
178
			// this group has the 'memberURL' attribute so this is a dynamic group
179
			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
180
			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
181
			$pos = strpos($memberURLs[0], '(');
182
			if ($pos !== false) {
183
				$memberUrlFilter = substr($memberURLs[0], $pos);
184
				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
185
				$dynamicMembers = array();
186
				foreach($foundMembers as $value) {
187
					$dynamicMembers[$value['dn'][0]] = 1;
188
				}
189
			} else {
190
				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
191
					'of group ' . $dnGroup, \OCP\Util::DEBUG);
192
			}
193
		}
194
		return $dynamicMembers;
195
	}
196
197
	/**
198
	 * @param string $dnGroup
199
	 * @param array|null &$seen
200
	 * @return array|mixed|null
201
	 */
202
	private function _groupMembers($dnGroup, &$seen = null) {
203
		if ($seen === null) {
204
			$seen = array();
205
		}
206
		$allMembers = array();
207
		if (array_key_exists($dnGroup, $seen)) {
208
			// avoid loops
209
			return array();
210
		}
211
		// used extensively in cron job, caching makes sense for nested groups
212
		$cacheKey = '_groupMembers'.$dnGroup;
213
		$groupMembers = $this->access->connection->getFromCache($cacheKey);
214
		if(!is_null($groupMembers)) {
215
			return $groupMembers;
216
		}
217
		$seen[$dnGroup] = 1;
218
		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
219
												$this->access->connection->ldapGroupFilter);
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
220
		if (is_array($members)) {
221
			foreach ($members as $memberDN) {
222
				$allMembers[$memberDN] = 1;
223
				$nestedGroups = $this->access->connection->ldapNestedGroups;
0 ignored issues
show
Documentation introduced by
The property ldapNestedGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
224
				if (!empty($nestedGroups)) {
225
					$subMembers = $this->_groupMembers($memberDN, $seen);
226
					if ($subMembers) {
227
						$allMembers = array_merge($allMembers, $subMembers);
228
					}
229
				}
230
			}
231
		}
232
		
233
		$allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
234
		
235
		$this->access->connection->writeToCache($cacheKey, $allMembers);
236
		return $allMembers;
237
	}
238
239
	/**
240
	 * @param string $DN
241
	 * @param array|null &$seen
242
	 * @return array
243
	 */
244
	private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
245
		if ($seen === null) {
246
			$seen = array();
247
		}
248
		if (array_key_exists($DN, $seen)) {
249
			// avoid loops
250
			return array();
251
		}
252
		$seen[$DN] = 1;
253
		$groups = $this->access->readAttribute($DN, 'memberOf');
254
		if (!is_array($groups)) {
255
			return array();
256
		}
257
		$groups = $this->access->groupsMatchFilter($groups);
258
		$allGroups =  $groups;
259
		$nestedGroups = $this->access->connection->ldapNestedGroups;
0 ignored issues
show
Documentation introduced by
The property ldapNestedGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
260
		if (intval($nestedGroups) === 1) {
261
			foreach ($groups as $group) {
262
				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
263
				$allGroups = array_merge($allGroups, $subGroups);
264
			}
265
		}
266
		return $allGroups;	
267
	}
268
269
	/**
270
	 * translates a primary group ID into an ownCloud internal name
271
	 * @param string $gid as given by primaryGroupID on AD
272
	 * @param string $dn a DN that belongs to the same domain as the group
273
	 * @return string|bool
274
	 */
275
	public function primaryGroupID2Name($gid, $dn) {
276
		$cacheKey = 'primaryGroupIDtoName';
277
		$groupNames = $this->access->connection->getFromCache($cacheKey);
278
		if(!is_null($groupNames) && isset($groupNames[$gid])) {
279
			return $groupNames[$gid];
280
		}
281
282
		$domainObjectSid = $this->access->getSID($dn);
283
		if($domainObjectSid === false) {
284
			return false;
285
		}
286
287
		//we need to get the DN from LDAP
288
		$filter = $this->access->combineFilterWithAnd(array(
289
			$this->access->connection->ldapGroupFilter,
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
290
			'objectsid=' . $domainObjectSid . '-' . $gid
291
		));
292
		$result = $this->access->searchGroups($filter, array('dn'), 1);
293
		if(empty($result)) {
294
			return false;
295
		}
296
		$dn = $result[0]['dn'][0];
297
298
		//and now the group name
299
		//NOTE once we have separate ownCloud group IDs and group names we can
300
		//directly read the display name attribute instead of the DN
301
		$name = $this->access->dn2groupname($dn);
302
303
		$this->access->connection->writeToCache($cacheKey, $name);
304
305
		return $name;
306
	}
307
308
	/**
309
	 * returns the entry's primary group ID
310
	 * @param string $dn
311
	 * @param string $attribute
312
	 * @return string|bool
313
	 */
314
	private function getEntryGroupID($dn, $attribute) {
315
		$value = $this->access->readAttribute($dn, $attribute);
316
		if(is_array($value) && !empty($value)) {
317
			return $value[0];
318
		}
319
		return false;
320
	}
321
322
	/**
323
	 * returns the group's primary ID
324
	 * @param string $dn
325
	 * @return string|bool
326
	 */
327
	public function getGroupPrimaryGroupID($dn) {
328
		return $this->getEntryGroupID($dn, 'primaryGroupToken');
329
	}
330
331
	/**
332
	 * returns the user's primary group ID
333
	 * @param string $dn
334
	 * @return string|bool
335
	 */
336
	public function getUserPrimaryGroupIDs($dn) {
337
		$primaryGroupID = false;
338
		if($this->access->connection->hasPrimaryGroups) {
339
			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
340
			if($primaryGroupID === false) {
341
				$this->access->connection->hasPrimaryGroups = false;
342
			}
343
		}
344
		return $primaryGroupID;
345
	}
346
347
	/**
348
	 * returns a filter for a "users in primary group" search or count operation
349
	 *
350
	 * @param string $groupDN
351
	 * @param string $search
352
	 * @return string
353
	 * @throws \Exception
354
	 */
355
	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
356
		$groupID = $this->getGroupPrimaryGroupID($groupDN);
357
		if($groupID === false) {
358
			throw new \Exception('Not a valid group');
359
		}
360
361
		$filterParts = [];
362
		$filterParts[] = $this->access->getFilterForUserCount();
363
		if ($search !== '') {
364
			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
365
		}
366
		$filterParts[] = 'primaryGroupID=' . $groupID;
367
368
		$filter = $this->access->combineFilterWithAnd($filterParts);
369
370
		return $filter;
371
	}
372
373
	/**
374
	 * returns a list of users that have the given group as primary group
375
	 *
376
	 * @param string $groupDN
377
	 * @param string $search
378
	 * @param int $limit
379
	 * @param int $offset
380
	 * @return string[]
381
	 */
382
	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
383
		try {
384
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
385
			$users = $this->access->fetchListOfUsers(
386
				$filter,
387
				array($this->access->connection->ldapUserDisplayName, 'dn'),
388
				$limit,
389
				$offset
390
			);
391
			return $this->access->ownCloudUserNames($users);
392
		} catch (\Exception $e) {
393
			return array();
394
		}
395
	}
396
397
	/**
398
	 * returns the number of users that have the given group as primary group
399
	 *
400
	 * @param string $groupDN
401
	 * @param string $search
402
	 * @param int $limit
403
	 * @param int $offset
404
	 * @return int
405
	 */
406
	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
407
		try {
408
			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
409
			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
410
			return (int)$users;
411
		} catch (\Exception $e) {
412
			return 0;
413
		}
414
	}
415
416
	/**
417
	 * gets the primary group of a user
418
	 * @param string $dn
419
	 * @return string
420
	 */
421
	public function getUserPrimaryGroup($dn) {
422
		$groupID = $this->getUserPrimaryGroupIDs($dn);
423
		if($groupID !== false) {
424
			$groupName = $this->primaryGroupID2Name($groupID, $dn);
0 ignored issues
show
Bug introduced by
It seems like $groupID defined by $this->getUserPrimaryGroupIDs($dn) on line 422 can also be of type boolean; however, OCA\User_LDAP\Group_LDAP::primaryGroupID2Name() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
425
			if($groupName !== false) {
426
				return $groupName;
427
			}
428
		}
429
430
		return false;
431
	}
432
433
	/**
434
	 * Get all groups a user belongs to
435
	 * @param string $uid Name of the user
436
	 * @return array with group names
437
	 *
438
	 * This function fetches all groups a user belongs to. It does not check
439
	 * if the user exists at all.
440
	 *
441
	 * This function includes groups based on dynamic group membership.
442
	 */
443
	public function getUserGroups($uid) {
444
		if(!$this->enabled) {
445
			return array();
446
		}
447
		$cacheKey = 'getUserGroups'.$uid;
448
		$userGroups = $this->access->connection->getFromCache($cacheKey);
449
		if(!is_null($userGroups)) {
450
			return $userGroups;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $userGroups; (object|integer|double|string|array|boolean) is incompatible with the return type declared by the interface OCP\GroupInterface::getUserGroups of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
451
		}
452
		$userDN = $this->access->username2dn($uid);
453
		if(!$userDN) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userDN of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
454
			$this->access->connection->writeToCache($cacheKey, array());
455
			return array();
456
		}
457
458
		$groups = [];
459
		$primaryGroup = $this->getUserPrimaryGroup($userDN);
460
461
		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
0 ignored issues
show
Documentation introduced by
The property ldapDynamicGroupMemberURL does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
462
463
		if (!empty($dynamicGroupMemberURL)) {
464
			// look through dynamic groups to add them to the result array if needed
465
			$groupsToMatch = $this->access->fetchListOfGroups(
466
				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
467
			foreach($groupsToMatch as $dynamicGroup) {
468
				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
469
					continue;
470
				}
471
				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
472
				if ($pos !== false) {
473
					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
474
					// apply filter via ldap search to see if this user is in this
475
					// dynamic group
476
					$userMatch = $this->access->readAttribute(
477
						$userDN,
478
						$this->access->connection->ldapUserDisplayName,
479
						$memberUrlFilter
480
					);
481
					if ($userMatch !== false) {
482
						// match found so this user is in this group
483
						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
484
						if(is_string($groupName)) {
485
							// be sure to never return false if the dn could not be
486
							// resolved to a name, for whatever reason.
487
							$groups[] = $groupName;
488
						}
489
					}
490
				} else {
491
					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
492
						'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
493
				}
494
			}
495
		}
496
497
		// if possible, read out membership via memberOf. It's far faster than
498
		// performing a search, which still is a fallback later.
499
		if(intval($this->access->connection->hasMemberOfFilterSupport) === 1
0 ignored issues
show
Documentation introduced by
The property hasMemberOfFilterSupport does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
500
			&& intval($this->access->connection->useMemberOfToDetectMembership) === 1
0 ignored issues
show
Documentation introduced by
The property useMemberOfToDetectMembership does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
501
		) {
502
			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
503
			if (is_array($groupDNs)) {
504
				foreach ($groupDNs as $dn) {
505
					$groupName = $this->access->dn2groupname($dn);
506
					if(is_string($groupName)) {
507
						// be sure to never return false if the dn could not be
508
						// resolved to a name, for whatever reason.
509
						$groups[] = $groupName;
510
					}
511
				}
512
			}
513
			
514
			if($primaryGroup !== false) {
515
				$groups[] = $primaryGroup;
516
			}
517
			$this->access->connection->writeToCache($cacheKey, $groups);
518
			return $groups;
519
		}
520
521
		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
522
		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
523
			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
524
		) {
525
			$uid = $userDN;
526
		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
527
			$result = $this->access->readAttribute($userDN, 'uid');
528
			if ($result === false) {
529
				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
530
					$this->access->connection->ldapHost, \OCP\Util::DEBUG);
531
			}
532
			$uid = $result[0];
533
		} else {
534
			// just in case
535
			$uid = $userDN;
536
		}
537
538
		if(isset($this->cachedGroupsByMember[$uid])) {
539
			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
540
		} else {
541
			$groupsByMember = array_values($this->getGroupsByMember($uid));
542
			$groupsByMember = $this->access->ownCloudGroupNames($groupsByMember);
543
			$this->cachedGroupsByMember[$uid] = $groupsByMember;
544
			$groups = array_merge($groups, $groupsByMember);
545
		}
546
547
		if($primaryGroup !== false) {
548
			$groups[] = $primaryGroup;
549
		}
550
551
		$groups = array_unique($groups, SORT_LOCALE_STRING);
552
		$this->access->connection->writeToCache($cacheKey, $groups);
553
554
		return $groups;
555
	}
556
557
	/**
558
	 * @param string $dn
559
	 * @param array|null &$seen
560
	 * @return array
561
	 */
562
	private function getGroupsByMember($dn, &$seen = null) {
563
		if ($seen === null) {
564
			$seen = array();
565
		}
566
		$allGroups = array();
567
		if (array_key_exists($dn, $seen)) {
568
			// avoid loops
569
			return array();
570
		}
571
		$seen[$dn] = true;
572
		$filter = $this->access->combineFilterWithAnd(array(
573
			$this->access->connection->ldapGroupFilter,
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
574
			$this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
575
		));
576
		$groups = $this->access->fetchListOfGroups($filter,
577
			array($this->access->connection->ldapGroupDisplayName, 'dn'));
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
578
		if (is_array($groups)) {
579
			foreach ($groups as $groupobj) {
580
				$groupDN = $groupobj['dn'][0];
581
				$allGroups[$groupDN] = $groupobj;
582
				$nestedGroups = $this->access->connection->ldapNestedGroups;
0 ignored issues
show
Documentation introduced by
The property ldapNestedGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
583
				if (!empty($nestedGroups)) {
584
					$supergroups = $this->getGroupsByMember($groupDN, $seen);
585
					if (is_array($supergroups) && (count($supergroups)>0)) {
586
						$allGroups = array_merge($allGroups, $supergroups);
587
					}
588
				}
589
			}
590
		}
591
		return $allGroups;
592
	}
593
594
	/**
595
	 * get a list of all users in a group
596
	 *
597
	 * @param string $gid
598
	 * @param string $search
599
	 * @param int $limit
600
	 * @param int $offset
601
	 * @return array with user ids
602
	 */
603
	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
604
		if(!$this->enabled) {
605
			return array();
606
		}
607
		if(!$this->groupExists($gid)) {
608
			return array();
609
		}
610
		$search = $this->access->escapeFilterPart($search, true);
611
		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
612
		// check for cache of the exact query
613
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
614
		if(!is_null($groupUsers)) {
615
			return $groupUsers;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $groupUsers; (object|integer|double|string|array|boolean) is incompatible with the return type declared by the interface OCP\GroupInterface::usersInGroup of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
616
		}
617
618
		// check for cache of the query without limit and offset
619
		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
620
		if(!is_null($groupUsers)) {
621
			$groupUsers = array_slice($groupUsers, $offset, $limit);
622
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
623
			return $groupUsers;
624
		}
625
626
		if($limit === -1) {
627
			$limit = null;
628
		}
629
		$groupDN = $this->access->groupname2dn($gid);
630
		if(!$groupDN) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupDN of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
631
			// group couldn't be found, return empty resultset
632
			$this->access->connection->writeToCache($cacheKey, array());
633
			return array();
634
		}
635
636
		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
637
		$members = array_keys($this->_groupMembers($groupDN));
638
		if(!$members && empty($primaryUsers)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $members of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
639
			//in case users could not be retrieved, return empty result set
640
			$this->access->connection->writeToCache($cacheKey, array());
641
			return array();
642
		}
643
644
		$groupUsers = array();
645
		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
646
		$attrs = $this->access->userManager->getAttributes(true);
647
		foreach($members as $member) {
648
			if($isMemberUid) {
649
				//we got uids, need to get their DNs to 'translate' them to user names
650
				$filter = $this->access->combineFilterWithAnd(array(
651
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
652
					$this->access->getFilterPartForUserSearch($search)
653
				));
654
				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
655
				if(count($ldap_users) < 1) {
656
					continue;
657
				}
658
				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
659
			} else {
660
				//we got DNs, check if we need to filter by search or we can give back all of them
661
				if ($search !== '') {
662
					if(!$this->access->readAttribute($member,
663
						$this->access->connection->ldapUserDisplayName,
664
						$this->access->getFilterPartForUserSearch($search))) {
665
						continue;
666
					}
667
				}
668
				// dn2username will also check if the users belong to the allowed base
669
				if($ocname = $this->access->dn2username($member)) {
670
					$groupUsers[] = $ocname;
671
				}
672
			}
673
		}
674
675
		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers));
676
		natsort($groupUsers);
677
		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
678
		$groupUsers = array_slice($groupUsers, $offset, $limit);
679
680
681
		$this->access->connection->writeToCache($cacheKey, $groupUsers);
682
683
		return $groupUsers;
684
	}
685
686
	/**
687
	 * returns the number of users in a group, who match the search term
688
	 * @param string $gid the internal group name
689
	 * @param string $search optional, a search string
690
	 * @return int|bool
691
	 */
692
	public function countUsersInGroup($gid, $search = '') {
693
		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
694
		if(!$this->enabled || !$this->groupExists($gid)) {
695
			return false;
696
		}
697
		$groupUsers = $this->access->connection->getFromCache($cacheKey);
698
		if(!is_null($groupUsers)) {
699
			return $groupUsers;
700
		}
701
702
		$groupDN = $this->access->groupname2dn($gid);
703
		if(!$groupDN) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupDN of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
704
			// group couldn't be found, return empty result set
705
			$this->access->connection->writeToCache($cacheKey, false);
706
			return false;
707
		}
708
709
		$members = array_keys($this->_groupMembers($groupDN));
710
		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
711
		if(!$members && $primaryUserCount === 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $members of type array<integer|string> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
712
			//in case users could not be retrieved, return empty result set
713
			$this->access->connection->writeToCache($cacheKey, false);
714
			return false;
715
		}
716
717
		if ($search === '') {
718
			$groupUsers = count($members) + $primaryUserCount;
719
			$this->access->connection->writeToCache($cacheKey, $groupUsers);
720
			return $groupUsers;
721
		}
722
		$search = $this->access->escapeFilterPart($search, true);
723
		$isMemberUid =
724
			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
725
			=== 'memberuid');
726
727
		//we need to apply the search filter
728
		//alternatives that need to be checked:
729
		//a) get all users by search filter and array_intersect them
730
		//b) a, but only when less than 1k 10k ?k users like it is
731
		//c) put all DNs|uids in a LDAP filter, combine with the search string
732
		//   and let it count.
733
		//For now this is not important, because the only use of this method
734
		//does not supply a search string
735
		$groupUsers = array();
736
		foreach($members as $member) {
737
			if($isMemberUid) {
738
				//we got uids, need to get their DNs to 'translate' them to user names
739
				$filter = $this->access->combineFilterWithAnd(array(
740
					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
741
					$this->access->getFilterPartForUserSearch($search)
742
				));
743
				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
744
				if(count($ldap_users) < 1) {
745
					continue;
746
				}
747
				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
748
			} else {
749
				//we need to apply the search filter now
750
				if(!$this->access->readAttribute($member,
751
					$this->access->connection->ldapUserDisplayName,
752
					$this->access->getFilterPartForUserSearch($search))) {
753
					continue;
754
				}
755
				// dn2username will also check if the users belong to the allowed base
756
				if($ocname = $this->access->dn2username($member)) {
757
					$groupUsers[] = $ocname;
758
				}
759
			}
760
		}
761
762
		//and get users that have the group as primary
763
		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
764
765
		return count($groupUsers) + $primaryUsers;
766
	}
767
768
	/**
769
	 * get a list of all groups
770
	 *
771
	 * @param string $search
772
	 * @param $limit
773
	 * @param int $offset
774
	 * @return array with group names
775
	 *
776
	 * Returns a list with all groups (used by getGroups)
777
	 */
778
	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
779
		if(!$this->enabled) {
780
			return array();
781
		}
782
		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
783
784
		//Check cache before driving unnecessary searches
785
		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
786
		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
787
		if(!is_null($ldap_groups)) {
788
			return $ldap_groups;
789
		}
790
791
		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
792
		// error. With a limit of 0, we get 0 results. So we pass null.
793
		if($limit <= 0) {
794
			$limit = null;
795
		}
796
		$filter = $this->access->combineFilterWithAnd(array(
797
			$this->access->connection->ldapGroupFilter,
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
798
			$this->access->getFilterPartForGroupSearch($search)
799
		));
800
		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG);
801
		$ldap_groups = $this->access->fetchListOfGroups($filter,
802
				array($this->access->connection->ldapGroupDisplayName, 'dn'),
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
803
				$limit,
804
				$offset);
805
		$ldap_groups = $this->access->ownCloudGroupNames($ldap_groups);
806
807
		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
808
		return $ldap_groups;
809
	}
810
811
	/**
812
	 * get a list of all groups using a paged search
813
	 *
814
	 * @param string $search
815
	 * @param int $limit
816
	 * @param int $offset
817
	 * @return array with group names
818
	 *
819
	 * Returns a list with all groups
820
	 * Uses a paged search if available to override a
821
	 * server side search limit.
822
	 * (active directory has a limit of 1000 by default)
823
	 */
824
	public function getGroups($search = '', $limit = -1, $offset = 0) {
825
		if(!$this->enabled) {
826
			return array();
827
		}
828
		$search = $this->access->escapeFilterPart($search, true);
829
		$pagingSize = intval($this->access->connection->ldapPagingSize);
830
		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
831
			return $this->getGroupsChunk($search, $limit, $offset);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getGroupsC...arch, $limit, $offset); (array|object|integer|double|string|boolean) is incompatible with the return type declared by the interface OCP\GroupInterface::getGroups of type array.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
832
		}
833
		$maxGroups = 100000; // limit max results (just for safety reasons)
834
		if ($limit > -1) {
835
		   $overallLimit = min($limit + $offset, $maxGroups);
836
		} else {
837
		   $overallLimit = $maxGroups;
838
		}
839
		$chunkOffset = $offset;
840
		$allGroups = array();
841
		while ($chunkOffset < $overallLimit) {
842
			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
843
			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
844
			$nread = count($ldapGroups);
845
			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', \OCP\Util::DEBUG);
846
			if ($nread) {
847
				$allGroups = array_merge($allGroups, $ldapGroups);
848
				$chunkOffset += $nread;
849
			}
850
			if ($nread < $chunkLimit) {
851
				break;
852
			}
853
		}
854
		return $allGroups;
855
	}
856
857
	/**
858
	 * @param string $group
859
	 * @return bool
860
	 */
861
	public function groupMatchesFilter($group) {
862
		return (strripos($group, $this->groupSearch) !== false);
0 ignored issues
show
Bug introduced by
The property groupSearch does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
863
	}
864
865
	/**
866
	 * check if a group exists
867
	 * @param string $gid
868
	 * @return bool
869
	 */
870
	public function groupExists($gid) {
871
		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
872
		if(!is_null($groupExists)) {
873
			return (bool)$groupExists;
874
		}
875
876
		//getting dn, if false the group does not exist. If dn, it may be mapped
877
		//only, requires more checking.
878
		$dn = $this->access->groupname2dn($gid);
879
		if(!$dn) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dn of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
880
			$this->access->connection->writeToCache('groupExists'.$gid, false);
881
			return false;
882
		}
883
884
		//if group really still exists, we will be able to read its objectclass
885 View Code Duplication
		if(!is_array($this->access->readAttribute($dn, ''))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
886
			$this->access->connection->writeToCache('groupExists'.$gid, false);
887
			return false;
888
		}
889
890
		$this->access->connection->writeToCache('groupExists'.$gid, true);
891
		return true;
892
	}
893
894
	/**
895
	* Check if backend implements actions
896
	* @param int $actions bitwise-or'ed actions
897
	* @return boolean
898
	*
899
	* Returns the supported actions as int to be
900
	* compared with OC_USER_BACKEND_CREATE_USER etc.
901
	*/
902
	public function implementsActions($actions) {
903
		return (bool)(\OC\Group\Backend::COUNT_USERS & $actions);
904
	}
905
906
	/**
907
	 * Return access for LDAP interaction.
908
	 * @return Access instance of Access for LDAP interaction
909
	 */
910
	public function getLDAPAccess() {
911
		return $this->access;
912
	}
913
}
914