Completed
Pull Request — master (#2286)
by Lukas
14:18 queued 06:33
created

Access::groupname2dn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Aaron Wood <[email protected]>
6
 * @author Alexander Bergolth <[email protected]>
7
 * @author Andreas Fischer <[email protected]>
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bart Visscher <[email protected]>
10
 * @author Benjamin Diele <[email protected]>
11
 * @author Christopher Schäpers <[email protected]>
12
 * @author Joas Schilling <[email protected]>
13
 * @author Jörn Friedrich Dreyer <[email protected]>
14
 * @author Lorenzo M. Catucci <[email protected]>
15
 * @author Lukas Reschke <[email protected]>
16
 * @author Lyonel Vincent <[email protected]>
17
 * @author Mario Kolling <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Nicolas Grekas <[email protected]>
20
 * @author Ralph Krimmel <[email protected]>
21
 * @author Renaud Fortier <[email protected]>
22
 * @author Robin McCorkell <[email protected]>
23
 * @author Roger Szabo <[email protected]>
24
 *
25
 * @license AGPL-3.0
26
 *
27
 * This code is free software: you can redistribute it and/or modify
28
 * it under the terms of the GNU Affero General Public License, version 3,
29
 * as published by the Free Software Foundation.
30
 *
31
 * This program is distributed in the hope that it will be useful,
32
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
33
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34
 * GNU Affero General Public License for more details.
35
 *
36
 * You should have received a copy of the GNU Affero General Public License, version 3,
37
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
38
 *
39
 */
40
41
namespace OCA\User_LDAP;
42
43
use OC\HintException;
44
use OCA\User_LDAP\Exceptions\ConstraintViolationException;
45
use OCA\User_LDAP\User\IUserTools;
46
use OCA\User_LDAP\User\Manager;
47
use OCA\User_LDAP\User\OfflineUser;
48
use OCA\User_LDAP\Mapping\AbstractMapping;
49
50
/**
51
 * Class Access
52
 * @package OCA\User_LDAP
53
 */
54
class Access extends LDAPUtility implements IUserTools {
55
	/**
56
	 * @var \OCA\User_LDAP\Connection
57
	 */
58
	public $connection;
59
	/** @var Manager */
60
	public $userManager;
61
	//never ever check this var directly, always use getPagedSearchResultState
62
	protected $pagedSearchedSuccessful;
63
64
	/**
65
	 * @var string[] $cookies an array of returned Paged Result cookies
66
	 */
67
	protected $cookies = array();
68
69
	/**
70
	 * @var string $lastCookie the last cookie returned from a Paged Results
71
	 * operation, defaults to an empty string
72
	 */
73
	protected $lastCookie = '';
74
75
	/**
76
	 * @var AbstractMapping $userMapper
77
	 */
78
	protected $userMapper;
79
80
	/**
81
	* @var AbstractMapping $userMapper
82
	*/
83
	protected $groupMapper;
84
	
85
	/**
86
	 * @var \OCA\User_LDAP\Helper
87
	 */
88
	private $helper;
89
90
	public function __construct(Connection $connection, ILDAPWrapper $ldap,
91
		Manager $userManager, Helper $helper) {
92
		parent::__construct($ldap);
93
		$this->connection = $connection;
94
		$this->userManager = $userManager;
95
		$this->userManager->setLdapAccess($this);
96
		$this->helper = $helper;
97
	}
98
99
	/**
100
	 * sets the User Mapper
101
	 * @param AbstractMapping $mapper
102
	 */
103
	public function setUserMapper(AbstractMapping $mapper) {
104
		$this->userMapper = $mapper;
105
	}
106
107
	/**
108
	 * returns the User Mapper
109
	 * @throws \Exception
110
	 * @return AbstractMapping
111
	 */
112
	public function getUserMapper() {
113
		if(is_null($this->userMapper)) {
114
			throw new \Exception('UserMapper was not assigned to this Access instance.');
115
		}
116
		return $this->userMapper;
117
	}
118
119
	/**
120
	 * sets the Group Mapper
121
	 * @param AbstractMapping $mapper
122
	 */
123
	public function setGroupMapper(AbstractMapping $mapper) {
124
		$this->groupMapper = $mapper;
125
	}
126
127
	/**
128
	 * returns the Group Mapper
129
	 * @throws \Exception
130
	 * @return AbstractMapping
131
	 */
132
	public function getGroupMapper() {
133
		if(is_null($this->groupMapper)) {
134
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
135
		}
136
		return $this->groupMapper;
137
	}
138
139
	/**
140
	 * @return bool
141
	 */
142
	private function checkConnection() {
143
		return ($this->connection instanceof Connection);
144
	}
145
146
	/**
147
	 * returns the Connection instance
148
	 * @return \OCA\User_LDAP\Connection
149
	 */
150
	public function getConnection() {
151
		return $this->connection;
152
	}
153
154
	/**
155
	 * reads a given attribute for an LDAP record identified by a DN
156
	 * @param string $dn the record in question
157
	 * @param string $attr the attribute that shall be retrieved
158
	 *        if empty, just check the record's existence
159
	 * @param string $filter
160
	 * @return array|false an array of values on success or an empty
161
	 *          array if $attr is empty, false otherwise
162
	 */
163
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
164
		if(!$this->checkConnection()) {
165
			\OCP\Util::writeLog('user_ldap',
166
				'No LDAP Connector assigned, access impossible for readAttribute.',
167
				\OCP\Util::WARN);
168
			return false;
169
		}
170
		$cr = $this->connection->getConnectionResource();
171
		if(!$this->ldap->isResource($cr)) {
172
			//LDAP not available
173
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
174
			return false;
175
		}
176
		//Cancel possibly running Paged Results operation, otherwise we run in
177
		//LDAP protocol errors
178
		$this->abandonPagedSearch();
179
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
180
		// but does not hurt either.
181
		$pagingSize = intval($this->connection->ldapPagingSize);
182
		// 0 won't result in replies, small numbers may leave out groups
183
		// (cf. #12306), 500 is default for paging and should work everywhere.
184
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
185
		$this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0);
186
		$dn = $this->helper->DNasBaseParameter($dn);
187
		$rr = @$this->ldap->read($cr, $dn, $filter, array($attr));
0 ignored issues
show
Documentation introduced by
$dn is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
188
		if(!$this->ldap->isResource($rr)) {
189
			if ($attr !== '') {
190
				//do not throw this message on userExists check, irritates
191
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
192
			}
193
			//in case an error occurs , e.g. object does not exist
194
			return false;
195
		}
196
		if ($attr === '' && ($filter === 'objectclass=*' || $this->ldap->countEntries($cr, $rr) === 1)) {
197
			\OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
198
			return array();
199
		}
200
		$er = $this->ldap->firstEntry($cr, $rr);
201
		if(!$this->ldap->isResource($er)) {
202
			//did not match the filter, return false
203
			return false;
204
		}
205
		//LDAP attributes are not case sensitive
206
		$result = \OCP\Util::mb_array_change_key_case(
207
				$this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
208
		$attr = mb_strtolower($attr, 'UTF-8');
209
210
		if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
211
			$values = array();
212
			for($i=0;$i<$result[$attr]['count'];$i++) {
213
				if($this->resemblesDN($attr)) {
214
					$values[] = $this->helper->sanitizeDN($result[$attr][$i]);
215
				} elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
216
					$values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
217
				} else {
218
					$values[] = $result[$attr][$i];
219
				}
220
			}
221
			return $values;
222
		}
223
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
224
		return false;
225
	}
226
	
227
	/**
228
	 * Set password for an LDAP user identified by a DN
229
	 *
230
	 * @param string $userDN the user in question
231
	 * @param string $password the new password
232
	 * @return bool
233
	 * @throws HintException
234
	 * @throws \Exception
235
	 */
236
	public function setPassword($userDN, $password) {
237
		if(intval($this->connection->turnOnPasswordChange) !== 1) {
238
			throw new \Exception('LDAP password changes are disabled.');
239
		}
240
		$cr = $this->connection->getConnectionResource();
241
		if(!$this->ldap->isResource($cr)) {
242
			//LDAP not available
243
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
244
			return false;
245
		}
246
		
247
		try {
248
			return $this->ldap->modReplace($cr, $userDN, $password);
249
		} catch(ConstraintViolationException $e) {
250
			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
251
		}
252
	}
253
254
	/**
255
	 * checks whether the given attributes value is probably a DN
256
	 * @param string $attr the attribute in question
257
	 * @return boolean if so true, otherwise false
258
	 */
259
	private function resemblesDN($attr) {
260
		$resemblingAttributes = array(
261
			'dn',
262
			'uniquemember',
263
			'member',
264
			// memberOf is an "operational" attribute, without a definition in any RFC
265
			'memberof'
266
		);
267
		return in_array($attr, $resemblingAttributes);
268
	}
269
270
	/**
271
	 * checks whether the given string is probably a DN
272
	 * @param string $string
273
	 * @return boolean
274
	 */
275
	public function stringResemblesDN($string) {
276
		$r = $this->ldap->explodeDN($string, 0);
277
		// if exploding a DN succeeds and does not end up in
278
		// an empty array except for $r[count] being 0.
279
		return (is_array($r) && count($r) > 1);
280
	}
281
282
	/**
283
	 * returns a DN-string that is cleaned from not domain parts, e.g.
284
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
285
	 * becomes dc=foobar,dc=server,dc=org
286
	 * @param string $dn
287
	 * @return string
288
	 */
289
	public function getDomainDNFromDN($dn) {
290
		$allParts = $this->ldap->explodeDN($dn, 0);
291
		if($allParts === false) {
292
			//not a valid DN
293
			return '';
294
		}
295
		$domainParts = array();
296
		$dcFound = false;
297
		foreach($allParts as $part) {
298
			if(!$dcFound && strpos($part, 'dc=') === 0) {
299
				$dcFound = true;
300
			}
301
			if($dcFound) {
302
				$domainParts[] = $part;
303
			}
304
		}
305
		$domainDN = implode(',', $domainParts);
306
		return $domainDN;
307
	}
308
309
	/**
310
	 * returns the LDAP DN for the given internal ownCloud name of the group
311
	 * @param string $name the ownCloud name in question
312
	 * @return string|false LDAP DN on success, otherwise false
313
	 */
314
	public function groupname2dn($name) {
315
		return $this->groupMapper->getDNByName($name);
316
	}
317
318
	/**
319
	 * returns the LDAP DN for the given internal ownCloud name of the user
320
	 * @param string $name the ownCloud name in question
321
	 * @return string|false with the LDAP DN on success, otherwise false
322
	 */
323
	public function username2dn($name) {
324
		$fdn = $this->userMapper->getDNByName($name);
325
326
		//Check whether the DN belongs to the Base, to avoid issues on multi-
327
		//server setups
328
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
329
			return $fdn;
330
		}
331
332
		return false;
333
	}
334
335
	/**
336
	 * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
337
	 * @param string $fdn the dn of the group object
338
	 * @param string $ldapName optional, the display name of the object
339
	 * @return string|false with the name to use in ownCloud, false on DN outside of search DN
340
	 */
341 View Code Duplication
	public function dn2groupname($fdn, $ldapName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
342
		//To avoid bypassing the base DN settings under certain circumstances
343
		//with the group support, check whether the provided DN matches one of
344
		//the given Bases
345
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups 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...
346
			return false;
347
		}
348
349
		return $this->dn2ocname($fdn, $ldapName, false);
350
	}
351
352
	/**
353
	 * accepts an array of group DNs and tests whether they match the user
354
	 * filter by doing read operations against the group entries. Returns an
355
	 * array of DNs that match the filter.
356
	 *
357
	 * @param string[] $groupDNs
358
	 * @return string[]
359
	 */
360
	public function groupsMatchFilter($groupDNs) {
361
		$validGroupDNs = [];
362
		foreach($groupDNs as $dn) {
363
			$cacheKey = 'groupsMatchFilter-'.$dn;
364
			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
365
			if(!is_null($groupMatchFilter)) {
366
				if($groupMatchFilter) {
367
					$validGroupDNs[] = $dn;
368
				}
369
				continue;
370
			}
371
372
			// Check the base DN first. If this is not met already, we don't
373
			// need to ask the server at all.
374
			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups 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...
375
				$this->connection->writeToCache($cacheKey, false);
376
				continue;
377
			}
378
379
			$result = $this->readAttribute($dn, 'cn', $this->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...
380
			if(is_array($result)) {
381
				$this->connection->writeToCache($cacheKey, true);
382
				$validGroupDNs[] = $dn;
383
			} else {
384
				$this->connection->writeToCache($cacheKey, false);
385
			}
386
387
		}
388
		return $validGroupDNs;
389
	}
390
391
	/**
392
	 * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
393
	 * @param string $dn the dn of the user object
0 ignored issues
show
Bug introduced by
There is no parameter named $dn. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
394
	 * @param string $ldapName optional, the display name of the object
395
	 * @return string|false with with the name to use in ownCloud
396
	 */
397 View Code Duplication
	public function dn2username($fdn, $ldapName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
398
		//To avoid bypassing the base DN settings under certain circumstances
399
		//with the group support, check whether the provided DN matches one of
400
		//the given Bases
401
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
402
			return false;
403
		}
404
405
		return $this->dn2ocname($fdn, $ldapName, true);
406
	}
407
408
	/**
409
	 * returns an internal ownCloud name for the given LDAP DN, false on DN outside of search DN
410
	 * @param string $dn the dn of the user object
0 ignored issues
show
Bug introduced by
There is no parameter named $dn. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
411
	 * @param string $ldapName optional, the display name of the object
412
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
413
	 * @return string|false with with the name to use in ownCloud
414
	 */
415
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
416
		if($isUser) {
417
			$mapper = $this->getUserMapper();
418
			$nameAttribute = $this->connection->ldapUserDisplayName;
419
		} else {
420
			$mapper = $this->getGroupMapper();
421
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName 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...
422
		}
423
424
		//let's try to retrieve the ownCloud name from the mappings table
425
		$ocName = $mapper->getNameByDN($fdn);
426
		if(is_string($ocName)) {
427
			return $ocName;
428
		}
429
430
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
431
		$uuid = $this->getUUID($fdn, $isUser);
432
		if(is_string($uuid)) {
433
			$ocName = $mapper->getNameByUUID($uuid);
434
			if(is_string($ocName)) {
435
				$mapper->setDNbyUUID($fdn, $uuid);
436
				return $ocName;
437
			}
438
		} else {
439
			//If the UUID can't be detected something is foul.
440
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
441
			return false;
442
		}
443
444
		if(is_null($ldapName)) {
445
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
446
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
447
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
448
				return false;
449
			}
450
			$ldapName = $ldapName[0];
451
		}
452
453
		if($isUser) {
454
			$usernameAttribute = strval($this->connection->ldapExpertUsernameAttr);
0 ignored issues
show
Documentation introduced by
The property ldapExpertUsernameAttr 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...
455
			if ($usernameAttribute !== '') {
456
				$username = $this->readAttribute($fdn, $usernameAttribute);
457
				$username = $username[0];
458
			} else {
459
				$username = $uuid;
460
			}
461
			$intName = $this->sanitizeUsername($username);
462
		} else {
463
			$intName = $ldapName;
464
		}
465
466
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
467
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
468
		//NOTE: mind, disabling cache affects only this instance! Using it
469
		// outside of core user management will still cache the user as non-existing.
470
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL 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...
471
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
472
		if(($isUser && !\OCP\User::userExists($intName))
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::userExists() has been deprecated with message: 8.1.0 use method userExists() of \OCP\IUserManager - \OC::$server->getUserManager()

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

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

Loading history...
473
			|| (!$isUser && !\OC_Group::groupExists($intName))) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_Group::groupExists() has been deprecated with message: Use \OC::$server->getGroupManager->groupExists($gid)

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

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

Loading history...
474
			if($mapper->map($fdn, $intName, $uuid)) {
475
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
476
				return $intName;
477
			}
478
		}
479
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
480
481
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
482
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
483
			return $altName;
484
		}
485
486
		//if everything else did not help..
487
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
488
		return false;
489
	}
490
491
	/**
492
	 * gives back the user names as they are used ownClod internally
493
	 * @param array $ldapUsers as returned by fetchList()
494
	 * @return array an array with the user names to use in ownCloud
495
	 *
496
	 * gives back the user names as they are used ownClod internally
497
	 */
498
	public function ownCloudUserNames($ldapUsers) {
499
		return $this->ldap2ownCloudNames($ldapUsers, true);
500
	}
501
502
	/**
503
	 * gives back the group names as they are used ownClod internally
504
	 * @param array $ldapGroups as returned by fetchList()
505
	 * @return array an array with the group names to use in ownCloud
506
	 *
507
	 * gives back the group names as they are used ownClod internally
508
	 */
509
	public function ownCloudGroupNames($ldapGroups) {
510
		return $this->ldap2ownCloudNames($ldapGroups, false);
511
	}
512
513
	/**
514
	 * @param array $ldapObjects as returned by fetchList()
515
	 * @param bool $isUsers
516
	 * @return array
517
	 */
518
	private function ldap2ownCloudNames($ldapObjects, $isUsers) {
519
		if($isUsers) {
520
			$nameAttribute = $this->connection->ldapUserDisplayName;
521
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
522
		} else {
523
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName 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...
524
		}
525
		$ownCloudNames = array();
526
527
		foreach($ldapObjects as $ldapObject) {
528
			$nameByLDAP = null;
529
			if(    isset($ldapObject[$nameAttribute])
530
				&& is_array($ldapObject[$nameAttribute])
531
				&& isset($ldapObject[$nameAttribute][0])
532
			) {
533
				// might be set, but not necessarily. if so, we use it.
534
				$nameByLDAP = $ldapObject[$nameAttribute][0];
535
			}
536
537
			$ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
538
			if($ocName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ocName of type string|false is loosely compared to true; 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...
539
				$ownCloudNames[] = $ocName;
540
				if($isUsers) {
541
					//cache the user names so it does not need to be retrieved
542
					//again later (e.g. sharing dialogue).
543
					if(is_null($nameByLDAP)) {
544
						continue;
545
					}
546
					$sndName = isset($ldapObject[$sndAttribute][0])
547
						? $ldapObject[$sndAttribute][0] : '';
0 ignored issues
show
Bug introduced by
The variable $sndAttribute does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
548
					$this->cacheUserDisplayName($ocName, $nameByLDAP, $sndName);
549
				}
550
			}
551
		}
552
		return $ownCloudNames;
553
	}
554
555
	/**
556
	 * caches the user display name
557
	 * @param string $ocName the internal ownCloud username
558
	 * @param string|false $home the home directory path
559
	 */
560
	public function cacheUserHome($ocName, $home) {
561
		$cacheKey = 'getHome'.$ocName;
562
		$this->connection->writeToCache($cacheKey, $home);
563
	}
564
565
	/**
566
	 * caches a user as existing
567
	 * @param string $ocName the internal ownCloud username
568
	 */
569
	public function cacheUserExists($ocName) {
570
		$this->connection->writeToCache('userExists'.$ocName, true);
571
	}
572
573
	/**
574
	 * caches the user display name
575
	 * @param string $ocName the internal ownCloud username
576
	 * @param string $displayName the display name
577
	 * @param string $displayName2 the second display name
578
	 */
579
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
580
		$user = $this->userManager->get($ocName);
581
		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
0 ignored issues
show
Bug introduced by
The method composeAndStoreDisplayName does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
582
		$cacheKeyTrunk = 'getDisplayName';
583
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
584
	}
585
586
	/**
587
	 * creates a unique name for internal ownCloud use for users. Don't call it directly.
588
	 * @param string $name the display name of the object
589
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
590
	 *
591
	 * Instead of using this method directly, call
592
	 * createAltInternalOwnCloudName($name, true)
593
	 */
594
	private function _createAltInternalOwnCloudNameForUsers($name) {
595
		$attempts = 0;
596
		//while loop is just a precaution. If a name is not generated within
597
		//20 attempts, something else is very wrong. Avoids infinite loop.
598
		while($attempts < 20){
599
			$altName = $name . '_' . rand(1000,9999);
600
			if(!\OCP\User::userExists($altName)) {
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::userExists() has been deprecated with message: 8.1.0 use method userExists() of \OCP\IUserManager - \OC::$server->getUserManager()

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

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

Loading history...
601
				return $altName;
602
			}
603
			$attempts++;
604
		}
605
		return false;
606
	}
607
608
	/**
609
	 * creates a unique name for internal ownCloud use for groups. Don't call it directly.
610
	 * @param string $name the display name of the object
611
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful.
612
	 *
613
	 * Instead of using this method directly, call
614
	 * createAltInternalOwnCloudName($name, false)
615
	 *
616
	 * Group names are also used as display names, so we do a sequential
617
	 * numbering, e.g. Developers_42 when there are 41 other groups called
618
	 * "Developers"
619
	 */
620
	private function _createAltInternalOwnCloudNameForGroups($name) {
621
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
622
		if(!($usedNames) || count($usedNames) === 0) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $usedNames of type 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...
623
			$lastNo = 1; //will become name_2
624
		} else {
625
			natsort($usedNames);
626
			$lastName = array_pop($usedNames);
627
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
628
		}
629
		$altName = $name.'_'.strval($lastNo+1);
630
		unset($usedNames);
631
632
		$attempts = 1;
633
		while($attempts < 21){
634
			// Check to be really sure it is unique
635
			// while loop is just a precaution. If a name is not generated within
636
			// 20 attempts, something else is very wrong. Avoids infinite loop.
637
			if(!\OC_Group::groupExists($altName)) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_Group::groupExists() has been deprecated with message: Use \OC::$server->getGroupManager->groupExists($gid)

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

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

Loading history...
638
				return $altName;
639
			}
640
			$altName = $name . '_' . ($lastNo + $attempts);
641
			$attempts++;
642
		}
643
		return false;
644
	}
645
646
	/**
647
	 * creates a unique name for internal ownCloud use.
648
	 * @param string $name the display name of the object
649
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
650
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
651
	 */
652
	private function createAltInternalOwnCloudName($name, $isUser) {
653
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL 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...
654
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
655
		if($isUser) {
656
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
657
		} else {
658
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
659
		}
660
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
661
662
		return $altName;
663
	}
664
665
	/**
666
	 * fetches a list of users according to a provided loginName and utilizing
667
	 * the login filter.
668
	 *
669
	 * @param string $loginName
670
	 * @param array $attributes optional, list of attributes to read
671
	 * @return array
672
	 */
673 View Code Duplication
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
674
		$loginName = $this->escapeFilterPart($loginName);
675
		$filter = str_replace('%uid', $loginName, $this->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...
676
		$users = $this->fetchListOfUsers($filter, $attributes);
677
		return $users;
678
	}
679
680
	/**
681
	 * counts the number of users according to a provided loginName and
682
	 * utilizing the login filter.
683
	 *
684
	 * @param string $loginName
685
	 * @return array
686
	 */
687 View Code Duplication
	public function countUsersByLoginName($loginName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
688
		$loginName = $this->escapeFilterPart($loginName);
689
		$filter = str_replace('%uid', $loginName, $this->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...
690
		$users = $this->countUsers($filter);
691
		return $users;
692
	}
693
694
	/**
695
	 * @param string $filter
696
	 * @param string|string[] $attr
697
	 * @param int $limit
698
	 * @param int $offset
699
	 * @return array
700
	 */
701
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
702
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
703
		$this->batchApplyUserAttributes($ldapRecords);
704
		return $this->fetchList($ldapRecords, (count($attr) > 1));
705
	}
706
707
	/**
708
	 * provided with an array of LDAP user records the method will fetch the
709
	 * user object and requests it to process the freshly fetched attributes and
710
	 * and their values
711
	 * @param array $ldapRecords
712
	 */
713
	public function batchApplyUserAttributes(array $ldapRecords){
714
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
715
		foreach($ldapRecords as $userRecord) {
716
			if(!isset($userRecord[$displayNameAttribute])) {
717
				// displayName is obligatory
718
				continue;
719
			}
720
			$ocName  = $this->dn2ocname($userRecord['dn'][0]);
721
			if($ocName === false) {
722
				continue;
723
			}
724
			$this->cacheUserExists($ocName);
725
			$user = $this->userManager->get($ocName);
726
			if($user instanceof OfflineUser) {
727
				$user->unmark();
728
				$user = $this->userManager->get($ocName);
729
			}
730
			if ($user !== null) {
731
				$user->processAttributes($userRecord);
0 ignored issues
show
Bug introduced by
The method processAttributes does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
732
			} else {
733
				\OC::$server->getLogger()->debug(
734
					"The ldap user manager returned null for $ocName",
735
					['app'=>'user_ldap']
736
				);
737
			}
738
		}
739
	}
740
741
	/**
742
	 * @param string $filter
743
	 * @param string|string[] $attr
744
	 * @param int $limit
745
	 * @param int $offset
746
	 * @return array
747
	 */
748
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
749
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
750
	}
751
752
	/**
753
	 * @param array $list
754
	 * @param bool $manyAttributes
755
	 * @return array
756
	 */
757
	private function fetchList($list, $manyAttributes) {
758
		if(is_array($list)) {
759
			if($manyAttributes) {
760
				return $list;
761
			} else {
762
				$list = array_reduce($list, function($carry, $item) {
763
					$attribute = array_keys($item)[0];
764
					$carry[] = $item[$attribute][0];
765
					return $carry;
766
				}, array());
767
				return array_unique($list, SORT_LOCALE_STRING);
768
			}
769
		}
770
771
		//error cause actually, maybe throw an exception in future.
772
		return array();
773
	}
774
775
	/**
776
	 * executes an LDAP search, optimized for Users
777
	 * @param string $filter the LDAP filter for the search
778
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
779
	 * @param integer $limit
780
	 * @param integer $offset
781
	 * @return array with the search result
782
	 *
783
	 * Executes an LDAP search
784
	 */
785
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
786
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
787
	}
788
789
	/**
790
	 * @param string $filter
791
	 * @param string|string[] $attr
792
	 * @param int $limit
793
	 * @param int $offset
794
	 * @return false|int
795
	 */
796
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
797
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
798
	}
799
800
	/**
801
	 * executes an LDAP search, optimized for Groups
802
	 * @param string $filter the LDAP filter for the search
803
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
804
	 * @param integer $limit
805
	 * @param integer $offset
806
	 * @return array with the search result
807
	 *
808
	 * Executes an LDAP search
809
	 */
810
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
811
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups 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...
812
	}
813
814
	/**
815
	 * returns the number of available groups
816
	 * @param string $filter the LDAP search filter
817
	 * @param string[] $attr optional
818
	 * @param int|null $limit
819
	 * @param int|null $offset
820
	 * @return int|bool
821
	 */
822
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
823
		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups 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...
824
	}
825
826
	/**
827
	 * returns the number of available objects on the base DN
828
	 *
829
	 * @param int|null $limit
830
	 * @param int|null $offset
831
	 * @return int|bool
832
	 */
833
	public function countObjects($limit = null, $offset = null) {
834
		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
0 ignored issues
show
Bug introduced by
The property ldapBase does not seem to exist. Did you mean ldapBaseUsers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
835
	}
836
837
	/**
838
	 * retrieved. Results will according to the order in the array.
839
	 * @param int $limit optional, maximum results to be counted
840
	 * @param int $offset optional, a starting point
841
	 * @return array|false array with the search result as first value and pagedSearchOK as
842
	 * second | false if not successful
843
	 */
844
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
845
		if(!is_null($attr) && !is_array($attr)) {
846
			$attr = array(mb_strtolower($attr, 'UTF-8'));
847
		}
848
849
		// See if we have a resource, in case not cancel with message
850
		$cr = $this->connection->getConnectionResource();
851
		if(!$this->ldap->isResource($cr)) {
852
			// Seems like we didn't find any resource.
853
			// Return an empty array just like before.
854
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
855
			return false;
856
		}
857
858
		//check whether paged search should be attempted
859
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset);
0 ignored issues
show
Documentation introduced by
$attr is of type null|array, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
860
861
		$linkResources = array_pad(array(), count($base), $cr);
862
		$sr = $this->ldap->search($linkResources, $base, $filter, $attr);
0 ignored issues
show
Documentation introduced by
$linkResources is of type array, but the function expects a resource.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $attr can also be of type null; however, OCA\User_LDAP\ILDAPWrapper::search() does only seem to accept array, 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...
863
		$error = $this->ldap->errno($cr);
864
		if(!is_array($sr) || $error !== 0) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $error (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
865
			\OCP\Util::writeLog('user_ldap',
866
				'Error when searching: '.$this->ldap->error($cr).
867
					' code '.$this->ldap->errno($cr),
868
				\OCP\Util::ERROR);
869
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
870
			return false;
871
		}
872
873
		return array($sr, $pagedSearchOK);
874
	}
875
876
	/**
877
	 * processes an LDAP paged search operation
878
	 * @param array $sr the array containing the LDAP search resources
879
	 * @param string $filter the LDAP filter for the search
880
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
881
	 * @param int $iFoundItems number of results in the search operation
882
	 * @param int $limit maximum results to be counted
883
	 * @param int $offset a starting point
884
	 * @param bool $pagedSearchOK whether a paged search has been executed
885
	 * @param bool $skipHandling required for paged search when cookies to
886
	 * prior results need to be gained
887
	 * @return bool cookie validity, true if we have more pages, false otherwise.
888
	 */
889
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
890
		$cookie = null;
891
		if($pagedSearchOK) {
892
			$cr = $this->connection->getConnectionResource();
893
			foreach($sr as $key => $res) {
894
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
895
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
896
				}
897
			}
898
899
			//browsing through prior pages to get the cookie for the new one
900
			if($skipHandling) {
901
				return false;
902
			}
903
			// if count is bigger, then the server does not support
904
			// paged search. Instead, he did a normal search. We set a
905
			// flag here, so the callee knows how to deal with it.
906
			if($iFoundItems <= $limit) {
907
				$this->pagedSearchedSuccessful = true;
908
			}
909
		} else {
910
			if(!is_null($limit)) {
911
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
912
			}
913
		}
914
		/* ++ Fixing RHDS searches with pages with zero results ++
915
		 * Return cookie status. If we don't have more pages, with RHDS
916
		 * cookie is null, with openldap cookie is an empty string and
917
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
918
		 */
919
		return !empty($cookie) || $cookie === '0';
920
	}
921
922
	/**
923
	 * executes an LDAP search, but counts the results only
924
	 * @param string $filter the LDAP filter for the search
925
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
926
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
927
	 * retrieved. Results will according to the order in the array.
928
	 * @param int $limit optional, maximum results to be counted
929
	 * @param int $offset optional, a starting point
930
	 * @param bool $skipHandling indicates whether the pages search operation is
931
	 * completed
932
	 * @return int|false Integer or false if the search could not be initialized
933
	 *
934
	 */
935
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
936
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
937
938
		$limitPerPage = intval($this->connection->ldapPagingSize);
939
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
940
			$limitPerPage = $limit;
941
		}
942
943
		$counter = 0;
944
		$count = null;
0 ignored issues
show
Unused Code introduced by
$count is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
945
		$this->connection->getConnectionResource();
946
947
		do {
948
			$search = $this->executeSearch($filter, $base, $attr,
949
										   $limitPerPage, $offset);
950
			if($search === false) {
951
				return $counter > 0 ? $counter : false;
952
			}
953
			list($sr, $pagedSearchOK) = $search;
954
955
			/* ++ Fixing RHDS searches with pages with zero results ++
956
			 * countEntriesInSearchResults() method signature changed
957
			 * by removing $limit and &$hasHitLimit parameters
958
			 */
959
			$count = $this->countEntriesInSearchResults($sr);
960
			$counter += $count;
961
962
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
963
										$offset, $pagedSearchOK, $skipHandling);
964
			$offset += $limitPerPage;
965
			/* ++ Fixing RHDS searches with pages with zero results ++
966
			 * Continue now depends on $hasMorePages value
967
			 */
968
			$continue = $pagedSearchOK && $hasMorePages;
969
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
970
971
		return $counter;
972
	}
973
974
	/**
975
	 * @param array $searchResults
976
	 * @return int
977
	 */
978
	private function countEntriesInSearchResults($searchResults) {
979
		$cr = $this->connection->getConnectionResource();
980
		$counter = 0;
981
982
		foreach($searchResults as $res) {
983
			$count = intval($this->ldap->countEntries($cr, $res));
984
			$counter += $count;
985
		}
986
987
		return $counter;
988
	}
989
990
	/**
991
	 * Executes an LDAP search
992
	 * @param string $filter the LDAP filter for the search
993
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
994
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
995
	 * @param int $limit
996
	 * @param int $offset
997
	 * @param bool $skipHandling
998
	 * @return array with the search result
999
	 */
1000
	private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1001
		if($limit <= 0) {
1002
			//otherwise search will fail
1003
			$limit = null;
1004
		}
1005
1006
		/* ++ Fixing RHDS searches with pages with zero results ++
1007
		 * As we can have pages with zero results and/or pages with less
1008
		 * than $limit results but with a still valid server 'cookie',
1009
		 * loops through until we get $continue equals true and
1010
		 * $findings['count'] < $limit
1011
		 */
1012
		$findings = array();
1013
		$savedoffset = $offset;
1014
		do {
1015
			$search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
1016
			if($search === false) {
1017
				return array();
1018
			}
1019
			list($sr, $pagedSearchOK) = $search;
1020
			$cr = $this->connection->getConnectionResource();
1021
1022
			if($skipHandling) {
1023
				//i.e. result do not need to be fetched, we just need the cookie
1024
				//thus pass 1 or any other value as $iFoundItems because it is not
1025
				//used
1026
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
1027
								$offset, $pagedSearchOK,
1028
								$skipHandling);
1029
				return array();
1030
			}
1031
1032
			foreach($sr as $res) {
1033
				$findings = array_merge($findings, $this->ldap->getEntries($cr	, $res ));
1034
			}
1035
1036
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
1037
								$limit, $offset, $pagedSearchOK,
1038
										$skipHandling);
1039
			$offset += $limit;
1040
		} while ($continue && $pagedSearchOK && $findings['count'] < $limit);
1041
		// reseting offset
1042
		$offset = $savedoffset;
1043
1044
		// if we're here, probably no connection resource is returned.
1045
		// to make ownCloud behave nicely, we simply give back an empty array.
1046
		if(is_null($findings)) {
1047
			return array();
1048
		}
1049
1050
		if(!is_null($attr)) {
1051
			$selection = array();
1052
			$i = 0;
1053
			foreach($findings as $item) {
1054
				if(!is_array($item)) {
1055
					continue;
1056
				}
1057
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1058
				foreach($attr as $key) {
1059
					$key = mb_strtolower($key, 'UTF-8');
1060
					if(isset($item[$key])) {
1061
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1062
							unset($item[$key]['count']);
1063
						}
1064
						if($key !== 'dn') {
1065
							$selection[$i][$key] = $this->resemblesDN($key) ?
1066
								$this->helper->sanitizeDN($item[$key])
1067
								: $item[$key];
1068
						} else {
1069
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1070
						}
1071
					}
1072
1073
				}
1074
				$i++;
1075
			}
1076
			$findings = $selection;
1077
		}
1078
		//we slice the findings, when
1079
		//a) paged search unsuccessful, though attempted
1080
		//b) no paged search, but limit set
1081
		if((!$this->getPagedSearchResultState()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getPagedSearchResultState() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1082
			&& $pagedSearchOK)
1083
			|| (
1084
				!$pagedSearchOK
1085
				&& !is_null($limit)
1086
			)
1087
		) {
1088
			$findings = array_slice($findings, intval($offset), $limit);
1089
		}
1090
		return $findings;
1091
	}
1092
1093
	/**
1094
	 * @param string $name
1095
	 * @return bool|mixed|string
1096
	 */
1097
	public function sanitizeUsername($name) {
1098
		if($this->connection->ldapIgnoreNamingRules) {
0 ignored issues
show
Documentation introduced by
The property ldapIgnoreNamingRules 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...
1099
			return $name;
1100
		}
1101
1102
		// Transliteration
1103
		// latin characters to ASCII
1104
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1105
1106
		// Replacements
1107
		$name = str_replace(' ', '_', $name);
1108
1109
		// Every remaining disallowed characters will be removed
1110
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1111
1112
		return $name;
1113
	}
1114
1115
	/**
1116
	* escapes (user provided) parts for LDAP filter
1117
	* @param string $input, the provided value
0 ignored issues
show
Documentation introduced by
There is no parameter named $input,. Did you maybe mean $input?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1118
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1119
	* @return string the escaped string
1120
	*/
1121
	public function escapeFilterPart($input, $allowAsterisk = false) {
1122
		$asterisk = '';
1123
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1124
			$asterisk = '*';
1125
			$input = mb_substr($input, 1, null, 'UTF-8');
1126
		}
1127
		$search  = array('*', '\\', '(', ')');
1128
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1129
		return $asterisk . str_replace($search, $replace, $input);
1130
	}
1131
1132
	/**
1133
	 * combines the input filters with AND
1134
	 * @param string[] $filters the filters to connect
1135
	 * @return string the combined filter
1136
	 */
1137
	public function combineFilterWithAnd($filters) {
1138
		return $this->combineFilter($filters, '&');
1139
	}
1140
1141
	/**
1142
	 * combines the input filters with OR
1143
	 * @param string[] $filters the filters to connect
1144
	 * @return string the combined filter
1145
	 * Combines Filter arguments with OR
1146
	 */
1147
	public function combineFilterWithOr($filters) {
1148
		return $this->combineFilter($filters, '|');
1149
	}
1150
1151
	/**
1152
	 * combines the input filters with given operator
1153
	 * @param string[] $filters the filters to connect
1154
	 * @param string $operator either & or |
1155
	 * @return string the combined filter
1156
	 */
1157
	private function combineFilter($filters, $operator) {
1158
		$combinedFilter = '('.$operator;
1159
		foreach($filters as $filter) {
1160
			if ($filter !== '' && $filter[0] !== '(') {
1161
				$filter = '('.$filter.')';
1162
			}
1163
			$combinedFilter.=$filter;
1164
		}
1165
		$combinedFilter.=')';
1166
		return $combinedFilter;
1167
	}
1168
1169
	/**
1170
	 * creates a filter part for to perform search for users
1171
	 * @param string $search the search term
1172
	 * @return string the final filter part to use in LDAP searches
1173
	 */
1174
	public function getFilterPartForUserSearch($search) {
1175
		return $this->getFilterPartForSearch($search,
1176
			$this->connection->ldapAttributesForUserSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForUserSearch 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...
1177
			$this->connection->ldapUserDisplayName);
1178
	}
1179
1180
	/**
1181
	 * creates a filter part for to perform search for groups
1182
	 * @param string $search the search term
1183
	 * @return string the final filter part to use in LDAP searches
1184
	 */
1185
	public function getFilterPartForGroupSearch($search) {
1186
		return $this->getFilterPartForSearch($search,
1187
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForGroupSearch 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...
1188
			$this->connection->ldapGroupDisplayName);
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...
1189
	}
1190
1191
	/**
1192
	 * creates a filter part for searches by splitting up the given search
1193
	 * string into single words
1194
	 * @param string $search the search term
1195
	 * @param string[] $searchAttributes needs to have at least two attributes,
1196
	 * otherwise it does not make sense :)
1197
	 * @return string the final filter part to use in LDAP searches
1198
	 * @throws \Exception
1199
	 */
1200
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1201
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1202
			throw new \Exception('searchAttributes must be an array with at least two string');
1203
		}
1204
		$searchWords = explode(' ', trim($search));
1205
		$wordFilters = array();
1206
		foreach($searchWords as $word) {
1207
			$word = $this->prepareSearchTerm($word);
1208
			//every word needs to appear at least once
1209
			$wordMatchOneAttrFilters = array();
1210
			foreach($searchAttributes as $attr) {
1211
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1212
			}
1213
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1214
		}
1215
		return $this->combineFilterWithAnd($wordFilters);
1216
	}
1217
1218
	/**
1219
	 * creates a filter part for searches
1220
	 * @param string $search the search term
1221
	 * @param string[]|null $searchAttributes
1222
	 * @param string $fallbackAttribute a fallback attribute in case the user
1223
	 * did not define search attributes. Typically the display name attribute.
1224
	 * @return string the final filter part to use in LDAP searches
1225
	 */
1226
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1227
		$filter = array();
1228
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1229
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1230
			try {
1231
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1232
			} catch(\Exception $e) {
1233
				\OCP\Util::writeLog(
1234
					'user_ldap',
1235
					'Creating advanced filter for search failed, falling back to simple method.',
1236
					\OCP\Util::INFO
1237
				);
1238
			}
1239
		}
1240
1241
		$search = $this->prepareSearchTerm($search);
1242
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1243
			if ($fallbackAttribute === '') {
1244
				return '';
1245
			}
1246
			$filter[] = $fallbackAttribute . '=' . $search;
1247
		} else {
1248
			foreach($searchAttributes as $attribute) {
1249
				$filter[] = $attribute . '=' . $search;
1250
			}
1251
		}
1252
		if(count($filter) === 1) {
1253
			return '('.$filter[0].')';
1254
		}
1255
		return $this->combineFilterWithOr($filter);
1256
	}
1257
1258
	/**
1259
	 * returns the search term depending on whether we are allowed
1260
	 * list users found by ldap with the current input appended by
1261
	 * a *
1262
	 * @return string
1263
	 */
1264
	private function prepareSearchTerm($term) {
1265
		$config = \OC::$server->getConfig();
1266
1267
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1268
1269
		$result = $term;
1270
		if ($term === '') {
1271
			$result = '*';
1272
		} else if ($allowEnum !== 'no') {
1273
			$result = $term . '*';
1274
		}
1275
		return $result;
1276
	}
1277
1278
	/**
1279
	 * returns the filter used for counting users
1280
	 * @return string
1281
	 */
1282
	public function getFilterForUserCount() {
1283
		$filter = $this->combineFilterWithAnd(array(
1284
			$this->connection->ldapUserFilter,
1285
			$this->connection->ldapUserDisplayName . '=*'
1286
		));
1287
1288
		return $filter;
1289
	}
1290
1291
	/**
1292
	 * @param string $name
1293
	 * @param string $password
1294
	 * @return bool
1295
	 */
1296
	public function areCredentialsValid($name, $password) {
1297
		$name = $this->helper->DNasBaseParameter($name);
1298
		$testConnection = clone $this->connection;
1299
		$credentials = array(
1300
			'ldapAgentName' => $name,
1301
			'ldapAgentPassword' => $password
1302
		);
1303
		if(!$testConnection->setConfiguration($credentials)) {
1304
			return false;
1305
		}
1306
		return $testConnection->bind();
1307
	}
1308
1309
	/**
1310
	 * reverse lookup of a DN given a known UUID
1311
	 *
1312
	 * @param string $uuid
1313
	 * @return string
1314
	 * @throws \Exception
1315
	 */
1316
	public function getUserDnByUuid($uuid) {
1317
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr 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...
1318
		$filter       = $this->connection->ldapUserFilter;
1319
		$base         = $this->connection->ldapBaseUsers;
1320
1321
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
0 ignored issues
show
Documentation introduced by
The property ldapUuidUserAttribute 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...
1322
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1323
			// existing DN to be able to reliably detect it.
1324
			$result = $this->search($filter, $base, ['dn'], 1);
1325
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1326
				throw new \Exception('Cannot determine UUID attribute');
1327
			}
1328
			$dn = $result[0]['dn'][0];
1329
			if(!$this->detectUuidAttribute($dn, true)) {
1330
				throw new \Exception('Cannot determine UUID attribute');
1331
			}
1332
		} else {
1333
			// The UUID attribute is either known or an override is given.
1334
			// By calling this method we ensure that $this->connection->$uuidAttr
1335
			// is definitely set
1336
			if(!$this->detectUuidAttribute('', true)) {
1337
				throw new \Exception('Cannot determine UUID attribute');
1338
			}
1339
		}
1340
1341
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
0 ignored issues
show
Documentation introduced by
The property ldapUuidUserAttribute 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...
1342
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1343
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1344
		}
1345
1346
		$filter = $uuidAttr . '=' . $uuid;
1347
		$result = $this->searchUsers($filter, ['dn'], 2);
1348
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1349
			// we put the count into account to make sure that this is
1350
			// really unique
1351
			return $result[0]['dn'][0];
1352
		}
1353
1354
		throw new \Exception('Cannot determine UUID attribute');
1355
	}
1356
1357
	/**
1358
	 * auto-detects the directory's UUID attribute
1359
	 * @param string $dn a known DN used to check against
1360
	 * @param bool $isUser
1361
	 * @param bool $force the detection should be run, even if it is not set to auto
1362
	 * @return bool true on success, false otherwise
1363
	 */
1364
	private function detectUuidAttribute($dn, $isUser = true, $force = false) {
1365 View Code Duplication
		if($isUser) {
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...
1366
			$uuidAttr     = 'ldapUuidUserAttribute';
1367
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr 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...
1368
		} else {
1369
			$uuidAttr     = 'ldapUuidGroupAttribute';
1370
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr 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...
1371
		}
1372
1373
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1374
			return true;
1375
		}
1376
1377
		if ($uuidOverride !== '' && !$force) {
1378
			$this->connection->$uuidAttr = $uuidOverride;
1379
			return true;
1380
		}
1381
1382
		// for now, supported attributes are entryUUID, nsuniqueid, objectGUID, ipaUniqueID
1383
		$testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid');
1384
1385
		foreach($testAttributes as $attribute) {
1386
			$value = $this->readAttribute($dn, $attribute);
1387
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1388
				\OCP\Util::writeLog('user_ldap',
1389
									'Setting '.$attribute.' as '.$uuidAttr,
1390
									\OCP\Util::DEBUG);
1391
				$this->connection->$uuidAttr = $attribute;
1392
				return true;
1393
			}
1394
		}
1395
		\OCP\Util::writeLog('user_ldap',
1396
							'Could not autodetect the UUID attribute',
1397
							\OCP\Util::ERROR);
1398
1399
		return false;
1400
	}
1401
1402
	/**
1403
	 * @param string $dn
1404
	 * @param bool $isUser
1405
	 * @return string|bool
1406
	 */
1407
	public function getUUID($dn, $isUser = true) {
1408 View Code Duplication
		if($isUser) {
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...
1409
			$uuidAttr     = 'ldapUuidUserAttribute';
1410
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr 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...
1411
		} else {
1412
			$uuidAttr     = 'ldapUuidGroupAttribute';
1413
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr 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...
1414
		}
1415
1416
		$uuid = false;
1417
		if($this->detectUuidAttribute($dn, $isUser)) {
1418
			$uuid = $this->readAttribute($dn, $this->connection->$uuidAttr);
1419
			if( !is_array($uuid)
1420
				&& $uuidOverride !== ''
1421
				&& $this->detectUuidAttribute($dn, $isUser, true)) {
1422
					$uuid = $this->readAttribute($dn,
1423
												 $this->connection->$uuidAttr);
1424
			}
1425
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1426
				$uuid = $uuid[0];
1427
			}
1428
		}
1429
1430
		return $uuid;
1431
	}
1432
1433
	/**
1434
	 * converts a binary ObjectGUID into a string representation
1435
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1436
	 * @return string
1437
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1438
	 */
1439
	private function convertObjectGUID2Str($oguid) {
1440
		$hex_guid = bin2hex($oguid);
1441
		$hex_guid_to_guid_str = '';
1442 View Code Duplication
		for($k = 1; $k <= 4; ++$k) {
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...
1443
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1444
		}
1445
		$hex_guid_to_guid_str .= '-';
1446 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
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...
1447
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1448
		}
1449
		$hex_guid_to_guid_str .= '-';
1450 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
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...
1451
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1452
		}
1453
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1454
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1455
1456
		return strtoupper($hex_guid_to_guid_str);
1457
	}
1458
1459
	/**
1460
	 * the first three blocks of the string-converted GUID happen to be in
1461
	 * reverse order. In order to use it in a filter, this needs to be
1462
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1463
	 * to every two hax figures.
1464
	 *
1465
	 * If an invalid string is passed, it will be returned without change.
1466
	 *
1467
	 * @param string $guid
1468
	 * @return string
1469
	 */
1470
	public function formatGuid2ForFilterUser($guid) {
1471
		if(!is_string($guid)) {
1472
			throw new \InvalidArgumentException('String expected');
1473
		}
1474
		$blocks = explode('-', $guid);
1475 View Code Duplication
		if(count($blocks) !== 5) {
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...
1476
			/*
1477
			 * Why not throw an Exception instead? This method is a utility
1478
			 * called only when trying to figure out whether a "missing" known
1479
			 * LDAP user was or was not renamed on the LDAP server. And this
1480
			 * even on the use case that a reverse lookup is needed (UUID known,
1481
			 * not DN), i.e. when finding users (search dialog, users page,
1482
			 * login, …) this will not be fired. This occurs only if shares from
1483
			 * a users are supposed to be mounted who cannot be found. Throwing
1484
			 * an exception here would kill the experience for a valid, acting
1485
			 * user. Instead we write a log message.
1486
			 */
1487
			\OC::$server->getLogger()->info(
1488
				'Passed string does not resemble a valid GUID. Known UUID ' .
1489
				'({uuid}) probably does not match UUID configuration.',
1490
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1491
			);
1492
			return $guid;
1493
		}
1494 View Code Duplication
		for($i=0; $i < 3; $i++) {
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...
1495
			$pairs = str_split($blocks[$i], 2);
1496
			$pairs = array_reverse($pairs);
1497
			$blocks[$i] = implode('', $pairs);
1498
		}
1499 View Code Duplication
		for($i=0; $i < 5; $i++) {
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...
1500
			$pairs = str_split($blocks[$i], 2);
1501
			$blocks[$i] = '\\' . implode('\\', $pairs);
1502
		}
1503
		return implode('', $blocks);
1504
	}
1505
1506
	/**
1507
	 * gets a SID of the domain of the given dn
1508
	 * @param string $dn
1509
	 * @return string|bool
1510
	 */
1511
	public function getSID($dn) {
1512
		$domainDN = $this->getDomainDNFromDN($dn);
1513
		$cacheKey = 'getSID-'.$domainDN;
1514
		$sid = $this->connection->getFromCache($cacheKey);
1515
		if(!is_null($sid)) {
1516
			return $sid;
1517
		}
1518
1519
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1520
		if(!is_array($objectSid) || empty($objectSid)) {
1521
			$this->connection->writeToCache($cacheKey, false);
1522
			return false;
1523
		}
1524
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1525
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1526
1527
		return $domainObjectSid;
1528
	}
1529
1530
	/**
1531
	 * converts a binary SID into a string representation
1532
	 * @param string $sid
1533
	 * @return string
1534
	 */
1535
	public function convertSID2Str($sid) {
1536
		// The format of a SID binary string is as follows:
1537
		// 1 byte for the revision level
1538
		// 1 byte for the number n of variable sub-ids
1539
		// 6 bytes for identifier authority value
1540
		// n*4 bytes for n sub-ids
1541
		//
1542
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1543
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1544
		$revision = ord($sid[0]);
1545
		$numberSubID = ord($sid[1]);
1546
1547
		$subIdStart = 8; // 1 + 1 + 6
1548
		$subIdLength = 4;
1549
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1550
			// Incorrect number of bytes present.
1551
			return '';
1552
		}
1553
1554
		// 6 bytes = 48 bits can be represented using floats without loss of
1555
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1556
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1557
1558
		$subIDs = array();
1559
		for ($i = 0; $i < $numberSubID; $i++) {
1560
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1561
			$subIDs[] = sprintf('%u', $subID[1]);
1562
		}
1563
1564
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1565
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1566
	}
1567
1568
	/**
1569
	 * checks if the given DN is part of the given base DN(s)
1570
	 * @param string $dn the DN
1571
	 * @param string[] $bases array containing the allowed base DN or DNs
1572
	 * @return bool
1573
	 */
1574
	public function isDNPartOfBase($dn, $bases) {
1575
		$belongsToBase = false;
1576
		$bases = $this->helper->sanitizeDN($bases);
1577
1578
		foreach($bases as $base) {
0 ignored issues
show
Bug introduced by
The expression $bases of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1579
			$belongsToBase = true;
1580
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1581
				$belongsToBase = false;
1582
			}
1583
			if($belongsToBase) {
1584
				break;
1585
			}
1586
		}
1587
		return $belongsToBase;
1588
	}
1589
1590
	/**
1591
	 * resets a running Paged Search operation
1592
	 */
1593
	private function abandonPagedSearch() {
1594
		if($this->connection->hasPagedResultSupport) {
1595
			$cr = $this->connection->getConnectionResource();
1596
			$this->ldap->controlPagedResult($cr, 0, false, $this->lastCookie);
1597
			$this->getPagedSearchResultState();
1598
			$this->lastCookie = '';
1599
			$this->cookies = array();
1600
		}
1601
	}
1602
1603
	/**
1604
	 * get a cookie for the next LDAP paged search
1605
	 * @param string $base a string with the base DN for the search
1606
	 * @param string $filter the search filter to identify the correct search
1607
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1608
	 * @param int $offset the offset for the new search to identify the correct search really good
1609
	 * @return string containing the key or empty if none is cached
1610
	 */
1611
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1612
		if($offset === 0) {
1613
			return '';
1614
		}
1615
		$offset -= $limit;
1616
		//we work with cache here
1617
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1618
		$cookie = '';
1619
		if(isset($this->cookies[$cacheKey])) {
1620
			$cookie = $this->cookies[$cacheKey];
1621
			if(is_null($cookie)) {
1622
				$cookie = '';
1623
			}
1624
		}
1625
		return $cookie;
1626
	}
1627
1628
	/**
1629
	 * checks whether an LDAP paged search operation has more pages that can be
1630
	 * retrieved, typically when offset and limit are provided.
1631
	 *
1632
	 * Be very careful to use it: the last cookie value, which is inspected, can
1633
	 * be reset by other operations. Best, call it immediately after a search(),
1634
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1635
	 * well. Don't rely on it with any fetchList-method.
1636
	 * @return bool
1637
	 */
1638
	public function hasMoreResults() {
1639
		if(!$this->connection->hasPagedResultSupport) {
1640
			return false;
1641
		}
1642
1643
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1644
			// as in RFC 2696, when all results are returned, the cookie will
1645
			// be empty.
1646
			return false;
1647
		}
1648
1649
		return true;
1650
	}
1651
1652
	/**
1653
	 * set a cookie for LDAP paged search run
1654
	 * @param string $base a string with the base DN for the search
1655
	 * @param string $filter the search filter to identify the correct search
1656
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1657
	 * @param int $offset the offset for the run search to identify the correct search really good
1658
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1659
	 * @return void
1660
	 */
1661
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1662
		// allow '0' for 389ds
1663
		if(!empty($cookie) || $cookie === '0') {
1664
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1665
			$this->cookies[$cacheKey] = $cookie;
1666
			$this->lastCookie = $cookie;
1667
		}
1668
	}
1669
1670
	/**
1671
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1672
	 * @return boolean|null true on success, null or false otherwise
1673
	 */
1674
	public function getPagedSearchResultState() {
1675
		$result = $this->pagedSearchedSuccessful;
1676
		$this->pagedSearchedSuccessful = null;
1677
		return $result;
1678
	}
1679
1680
	/**
1681
	 * Prepares a paged search, if possible
1682
	 * @param string $filter the LDAP filter for the search
1683
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1684
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1685
	 * @param int $limit
1686
	 * @param int $offset
1687
	 * @return bool|true
1688
	 */
1689
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1690
		$pagedSearchOK = false;
1691
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1692
			$offset = intval($offset); //can be null
1693
			\OCP\Util::writeLog('user_ldap',
1694
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1695
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1696
				\OCP\Util::DEBUG);
1697
			//get the cookie from the search for the previous search, required by LDAP
1698
			foreach($bases as $base) {
1699
1700
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1701
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1702
					// no cookie known, although the offset is not 0. Maybe cache run out. We need
1703
					// to start all over *sigh* (btw, Dear Reader, did you know LDAP paged
1704
					// searching was designed by MSFT?)
1705
					// 		Lukas: No, but thanks to reading that source I finally know!
1706
					// '0' is valid, because 389ds
1707
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1708
					//a bit recursive, $offset of 0 is the exit
1709
					\OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
1710
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1711
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1712
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1713
					//TODO: remember this, probably does not change in the next request...
1714
					if(empty($cookie) && $cookie !== '0') {
1715
						// '0' is valid, because 389ds
1716
						$cookie = null;
1717
					}
1718
				}
1719
				if(!is_null($cookie)) {
1720
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1721
					$this->abandonPagedSearch();
1722
					$pagedSearchOK = $this->ldap->controlPagedResult(
1723
						$this->connection->getConnectionResource(), $limit,
1724
						false, $cookie);
1725
					if(!$pagedSearchOK) {
1726
						return false;
1727
					}
1728
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1729
				} else {
1730
					\OCP\Util::writeLog('user_ldap',
1731
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1732
						\OCP\Util::INFO);
1733
				}
1734
1735
			}
1736
		/* ++ Fixing RHDS searches with pages with zero results ++
1737
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1738
		 * due to pages with zero results.
1739
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1740
		 * if we don't have a previous paged search.
1741
		 */
1742
		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1743
			// a search without limit was requested. However, if we do use
1744
			// Paged Search once, we always must do it. This requires us to
1745
			// initialize it with the configured page size.
1746
			$this->abandonPagedSearch();
1747
			// in case someone set it to 0 … use 500, otherwise no results will
1748
			// be returned.
1749
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1750
			$pagedSearchOK = $this->ldap->controlPagedResult(
1751
				$this->connection->getConnectionResource(), $pageSize, false, ''
1752
			);
1753
		}
1754
1755
		return $pagedSearchOK;
1756
	}
1757
1758
}
1759