Completed
Push — stable8.2 ( e9036a...6707df )
by
unknown
59:39
created

Access::executeSearch()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 31
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42
Metric Value
dl 0
loc 31
ccs 0
cts 20
cp 0
rs 8.439
cc 6
eloc 19
nc 6
nop 5
crap 42
1
<?php
2
/**
3
 * @author Alexander Bergolth <[email protected]>
4
 * @author Andreas Fischer <[email protected]>
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Benjamin Diele <[email protected]>
8
 * @author Christopher Schäpers <[email protected]>
9
 * @author Donald Buczek <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[email protected]>
11
 * @author Lorenzo M. Catucci <[email protected]>
12
 * @author Lukas Reschke <[email protected]>
13
 * @author Lyonel Vincent <[email protected]>
14
 * @author Morris Jobke <[email protected]>
15
 * @author Nicolas Grekas <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Scrutinizer Auto-Fixer <[email protected]>
18
 *
19
 * @copyright Copyright (c) 2015, ownCloud, Inc.
20
 * @license AGPL-3.0
21
 *
22
 * This code is free software: you can redistribute it and/or modify
23
 * it under the terms of the GNU Affero General Public License, version 3,
24
 * as published by the Free Software Foundation.
25
 *
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29
 * GNU Affero General Public License for more details.
30
 *
31
 * You should have received a copy of the GNU Affero General Public License, version 3,
32
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33
 *
34
 */
35
36
namespace OCA\user_ldap\lib;
37
38
use OCA\user_ldap\lib\user\OfflineUser;
39
use OCA\User_LDAP\Mapping\AbstractMapping;
40
41
/**
42
 * Class Access
43
 * @package OCA\user_ldap\lib
44
 */
45
class Access extends LDAPUtility implements user\IUserTools {
46
	/**
47
	 * @var \OCA\user_ldap\lib\Connection
48
	 */
49
	public $connection;
50
	public $userManager;
51
	//never ever check this var directly, always use getPagedSearchResultState
52
	protected $pagedSearchedSuccessful;
53
54
	/**
55
	 * @var string[] $cookies an array of returned Paged Result cookies
56
	 */
57
	protected $cookies = array();
58
59
	/**
60
	 * @var string $lastCookie the last cookie returned from a Paged Results
61
	 * operation, defaults to an empty string
62
	 */
63
	protected $lastCookie = '';
64
65
	/**
66
	 * @var AbstractMapping $userMapper
67
	 */
68
	protected $userMapper;
69
70
	/**
71
	* @var AbstractMapping $userMapper
72
	*/
73
	protected $groupMapper;
74
75 91
	public function __construct(Connection $connection, ILDAPWrapper $ldap,
76
		user\Manager $userManager) {
77 91
		parent::__construct($ldap);
78 91
		$this->connection = $connection;
79 91
		$this->userManager = $userManager;
80 91
		$this->userManager->setLdapAccess($this);
81 91
	}
82
83
	/**
84
	 * sets the User Mapper
85
	 * @param AbstractMapping $mapper
86
	 */
87 1
	public function setUserMapper(AbstractMapping $mapper) {
88 1
		$this->userMapper = $mapper;
89 1
	}
90
91
	/**
92
	 * returns the User Mapper
93
	 * @throws \Exception
94
	 * @return AbstractMapping
95
	 */
96 1
	public function getUserMapper() {
97 1
		if(is_null($this->userMapper)) {
98
			throw new \Exception('UserMapper was not assigned to this Access instance.');
99
		}
100 1
		return $this->userMapper;
101
	}
102
103
	/**
104
	 * sets the Group Mapper
105
	 * @param AbstractMapping $mapper
106
	 */
107
	public function setGroupMapper(AbstractMapping $mapper) {
108
		$this->groupMapper = $mapper;
109
	}
110
111
	/**
112
	 * returns the Group Mapper
113
	 * @throws \Exception
114
	 * @return AbstractMapping
115
	 */
116
	public function getGroupMapper() {
117
		if(is_null($this->groupMapper)) {
118
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
119
		}
120
		return $this->groupMapper;
121
	}
122
123
	/**
124
	 * @return bool
125
	 */
126 4
	private function checkConnection() {
127 4
		return ($this->connection instanceof Connection);
128
	}
129
130
	/**
131
	 * returns the Connection instance
132
	 * @return \OCA\user_ldap\lib\Connection
133
	 */
134 46
	public function getConnection() {
135 46
		return $this->connection;
136
	}
137
138
	/**
139
	 * reads a given attribute for an LDAP record identified by a DN
140
	 * @param string $dn the record in question
141
	 * @param string $attr the attribute that shall be retrieved
142
	 *        if empty, just check the record's existence
143
	 * @param string $filter
144
	 * @return array|false an array of values on success or an empty
145
	 *          array if $attr is empty, false otherwise
146
	 */
147 4
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
148 4
		if(!$this->checkConnection()) {
149
			\OCP\Util::writeLog('user_ldap',
150
				'No LDAP Connector assigned, access impossible for readAttribute.',
151
				\OCP\Util::WARN);
152
			return false;
153
		}
154 4
		$cr = $this->connection->getConnectionResource();
155 4
		if(!$this->ldap->isResource($cr)) {
156
			//LDAP not available
157
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
158
			return false;
159
		}
160
		//Cancel possibly running Paged Results operation, otherwise we run in
161
		//LDAP protocol errors
162 4
		$this->abandonPagedSearch();
163
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
164
		// but does not hurt either.
165 4
		$pagingSize = intval($this->connection->ldapPagingSize);
166
		// 0 won't result in replies, small numbers may leave out groups
167
		// (cf. #12306), 500 is default for paging and should work everywhere.
168 4
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
169 4
		$this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0);
170 4
		$dn = $this->DNasBaseParameter($dn);
171 4
		$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...
172 4
		if(!$this->ldap->isResource($rr)) {
173
			if(!empty($attr)) {
174
				//do not throw this message on userExists check, irritates
175
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
176
			}
177
			//in case an error occurs , e.g. object does not exist
178
			return false;
179
		}
180 4
		if (empty($attr)) {
181
			\OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
182
			return array();
183
		}
184 4
		$er = $this->ldap->firstEntry($cr, $rr);
185 4
		if(!$this->ldap->isResource($er)) {
186
			//did not match the filter, return false
187
			return false;
188
		}
189
		//LDAP attributes are not case sensitive
190 4
		$result = \OCP\Util::mb_array_change_key_case(
191 4
				$this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
192 4
		$attr = mb_strtolower($attr, 'UTF-8');
193
194 4
		if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
195 4
			$values = array();
196 4
			for($i=0;$i<$result[$attr]['count'];$i++) {
197 4
				if($this->resemblesDN($attr)) {
198 4
					$values[] = $this->sanitizeDN($result[$attr][$i]);
199 4
				} elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
200
					$values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
201
				} else {
202
					$values[] = $result[$attr][$i];
203
				}
204 4
			}
205 4
			return $values;
206
		}
207
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
208
		return false;
209
	}
210
211
	/**
212
	 * checks whether the given attributes value is probably a DN
213
	 * @param string $attr the attribute in question
214
	 * @return boolean if so true, otherwise false
215
	 */
216 4
	private function resemblesDN($attr) {
217
		$resemblingAttributes = array(
218 4
			'dn',
219 4
			'uniquemember',
220 4
			'member',
221
			// memberOf is an "operational" attribute, without a definition in any RFC
222
			'memberof'
223 4
		);
224 4
		return in_array($attr, $resemblingAttributes);
225
	}
226
227
	/**
228
	 * checks whether the given string is probably a DN
229
	 * @param string $string
230
	 * @return boolean
231
	 */
232 2
	public function stringResemblesDN($string) {
233 2
		$r = $this->ldap->explodeDN($string, 0);
234
		// if exploding a DN succeeds and does not end up in
235
		// an empty array except for $r[count] being 0.
236 2
		return (is_array($r) && count($r) > 1);
237
	}
238
239
	/**
240
	 * sanitizes a DN received from the LDAP server
241
	 * @param array $dn the DN in question
242
	 * @return array the sanitized DN
243
	 */
244 4
	private function sanitizeDN($dn) {
245
		//treating multiple base DNs
246 4
		if(is_array($dn)) {
247
			$result = array();
248
			foreach($dn as $singleDN) {
249
				$result[] = $this->sanitizeDN($singleDN);
250
			}
251
			return $result;
252
		}
253
254
		//OID sometimes gives back DNs with whitespace after the comma
255
		// a la "uid=foo, cn=bar, dn=..." We need to tackle this!
256 4
		$dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
257
258
		//make comparisons and everything work
259 4
		$dn = mb_strtolower($dn, 'UTF-8');
260
261
		//escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
262
		//to use the DN in search filters, \ needs to be escaped to \5c additionally
263
		//to use them in bases, we convert them back to simple backslashes in readAttribute()
264
		$replacements = array(
265 4
			'\,' => '\5c2C',
266 4
			'\=' => '\5c3D',
267 4
			'\+' => '\5c2B',
268 4
			'\<' => '\5c3C',
269 4
			'\>' => '\5c3E',
270 4
			'\;' => '\5c3B',
271 4
			'\"' => '\5c22',
272 4
			'\#' => '\5c23',
273 4
			'('  => '\28',
274 4
			')'  => '\29',
275 4
			'*'  => '\2A',
276 4
		);
277 4
		$dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
278
279 4
		return $dn;
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 2
	public function getDomainDNFromDN($dn) {
290 2
		$allParts = $this->ldap->explodeDN($dn, 0);
291 2
		if($allParts === false) {
292
			//not a valid DN
293 1
			return '';
294
		}
295 1
		$domainParts = array();
296 1
		$dcFound = false;
297 1
		foreach($allParts as $part) {
298 1
			if(!$dcFound && strpos($part, 'dc=') === 0) {
299 1
				$dcFound = true;
300 1
			}
301 1
			if($dcFound) {
302 1
				$domainParts[] = $part;
303 1
			}
304 1
		}
305 1
		$domainDN = implode(',', $domainParts);
306 1
		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
	public function ocname2dn($name, $isUser) {
337
	 * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
338
	 * @param string $fdn the dn of the group object
339
	 * @param string $ldapName optional, the display name of the object
340
	 * @return string|false with the name to use in ownCloud, false on DN outside of search DN
341
	 */
342 View Code Duplication
	public function dn2groupname($fdn, $ldapName = null) {
343
		//To avoid bypassing the base DN settings under certain circumstances
344
		//with the group support, check whether the provided DN matches one of
345
		//the given Bases
346
		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\lib\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...
347
			return false;
348
		}
349
350
		return $this->dn2ocname($fdn, $ldapName, false);
351
	}
352
353
	/**
354
	 * accepts an array of group DNs and tests whether they match the user
355
	 * filter by doing read operations against the group entries. Returns an
356
	 * array of DNs that match the filter.
357
	 *
358
	 * @param string[] $groupDNs
359
	 * @return string[]
360
	 */
361
	public function groupsMatchFilter($groupDNs) {
362
		$validGroupDNs = [];
363
		foreach($groupDNs as $dn) {
364
			$cacheKey = 'groupsMatchFilter-'.$dn;
365
			if($this->connection->isCached($cacheKey)) {
366
				if($this->connection->getFromCache($cacheKey)) {
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\lib\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\lib\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) {
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 1
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
416 1
		if($isUser) {
417 1
			$mapper = $this->getUserMapper();
418 1
			$nameAttribute = $this->connection->ldapUserDisplayName;
419 1
		} 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\lib\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 1
		$ocName = $mapper->getNameByDN($fdn);
426 1
		if(is_string($ocName)) {
427 1
			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 = $this->connection->ldapExpertUsernameAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUsernameAttr does not exist on object<OCA\user_ldap\lib\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...
455
			if(!empty($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\lib\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))
473
			|| (!$isUser && !\OC_Group::groupExists($intName))) {
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
		} else {
522
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\user_ldap\lib\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...
523
		}
524
		$ownCloudNames = array();
525
526
		foreach($ldapObjects as $ldapObject) {
527
			$nameByLDAP = null;
528
			if(    isset($ldapObject[$nameAttribute])
529
				&& is_array($ldapObject[$nameAttribute])
530
				&& isset($ldapObject[$nameAttribute][0])
531
			) {
532
				// might be set, but not necessarily. if so, we use it.
533
				$nameByLDAP = $ldapObject[$nameAttribute][0];
534
			}
535
536
			$ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
537
			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...
538
				$ownCloudNames[] = $ocName;
539
				if($isUsers) {
540
					//cache the user names so it does not need to be retrieved
541
					//again later (e.g. sharing dialogue).
542
					$this->cacheUserExists($ocName);
543
					if(!is_null($nameByLDAP)) {
544
						$this->cacheUserDisplayName($ocName, $nameByLDAP);
545
					}
546
				}
547
			}
548
			continue;
549
		}
550
		return $ownCloudNames;
551
	}
552
553
	/**
554
	 * caches the user display name
555
	 * @param string $ocName the internal ownCloud username
556
	 * @param string|false $home the home directory path
557
	 */
558 1
	public function cacheUserHome($ocName, $home) {
559 1
		$cacheKey = 'getHome'.$ocName;
560 1
		$this->connection->writeToCache($cacheKey, $home);
561 1
	}
562
563
	/**
564
	 * caches a user as existing
565
	 * @param string $ocName the internal ownCloud username
566
	 */
567 1
	public function cacheUserExists($ocName) {
568 1
		$this->connection->writeToCache('userExists'.$ocName, true);
569 1
	}
570
571
	/**
572
	 * caches the user display name
573
	 * @param string $ocName the internal ownCloud username
574
	 * @param string $displayName the display name
575
	 */
576
	public function cacheUserDisplayName($ocName, $displayName) {
577
		$cacheKeyTrunk = 'getDisplayName';
578
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
579
	}
580
581
	/**
582
	 * creates a unique name for internal ownCloud use for users. Don't call it directly.
583
	 * @param string $name the display name of the object
584
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
585
	 *
586
	 * Instead of using this method directly, call
587
	 * createAltInternalOwnCloudName($name, true)
588
	 */
589
	private function _createAltInternalOwnCloudNameForUsers($name) {
590
		$attempts = 0;
591
		//while loop is just a precaution. If a name is not generated within
592
		//20 attempts, something else is very wrong. Avoids infinite loop.
593
		while($attempts < 20){
594
			$altName = $name . '_' . rand(1000,9999);
595
			if(!\OCP\User::userExists($altName)) {
596
				return $altName;
597
			}
598
			$attempts++;
599
		}
600
		return false;
601
	}
602
603
	/**
604
	 * creates a unique name for internal ownCloud use for groups. Don't call it directly.
605
	 * @param string $name the display name of the object
606
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful.
607
	 *
608
	 * Instead of using this method directly, call
609
	 * createAltInternalOwnCloudName($name, false)
610
	 *
611
	 * Group names are also used as display names, so we do a sequential
612
	 * numbering, e.g. Developers_42 when there are 41 other groups called
613
	 * "Developers"
614
	 */
615
	private function _createAltInternalOwnCloudNameForGroups($name) {
616
		$usedNames = $this->groupMapper->getNamesBySearch($name.'_%');
617
		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...
618
			$lastNo = 1; //will become name_2
619
		} else {
620
			natsort($usedNames);
621
			$lastName = array_pop($usedNames);
622
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
623
		}
624
		$altName = $name.'_'.strval($lastNo+1);
625
		unset($usedNames);
626
627
		$attempts = 1;
628
		while($attempts < 21){
629
			// Check to be really sure it is unique
630
			// while loop is just a precaution. If a name is not generated within
631
			// 20 attempts, something else is very wrong. Avoids infinite loop.
632
			if(!\OC_Group::groupExists($altName)) {
633
				return $altName;
634
			}
635
			$altName = $name . '_' . ($lastNo + $attempts);
636
			$attempts++;
637
		}
638
		return false;
639
	}
640
641
	/**
642
	 * creates a unique name for internal ownCloud use.
643
	 * @param string $name the display name of the object
644
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
645
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
646
	 */
647
	private function createAltInternalOwnCloudName($name, $isUser) {
648
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL does not exist on object<OCA\user_ldap\lib\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...
649
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
650
		if($isUser) {
651
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
652
		} else {
653
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
654
		}
655
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
656
657
		return $altName;
658
	}
659
660
	/**
661
	 * fetches a list of users according to a provided loginName and utilizing
662
	 * the login filter.
663
	 *
664
	 * @param string $loginName
665
	 * @param array $attributes optional, list of attributes to read
666
	 * @return array
667
	 */
668 View Code Duplication
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
669
		$loginName = $this->escapeFilterPart($loginName);
670
		$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\lib\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...
671
		$users = $this->fetchListOfUsers($filter, $attributes);
672
		return $users;
673
	}
674
675
	/**
676
	 * counts the number of users according to a provided loginName and
677
	 * utilizing the login filter.
678
	 *
679
	 * @param string $loginName
680
	 * @return array
681
	 */
682 View Code Duplication
	public function countUsersByLoginName($loginName) {
683
		$loginName = $this->escapeFilterPart($loginName);
684
		$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\lib\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...
685
		$users = $this->countUsers($filter);
686
		return $users;
687
	}
688
689
	/**
690
	 * @param string $filter
691
	 * @param string|string[] $attr
692
	 * @param int $limit
693
	 * @param int $offset
694
	 * @return array
695
	 */
696
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
697
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
698
		$this->batchApplyUserAttributes($ldapRecords);
699
		return $this->fetchList($ldapRecords, (count($attr) > 1));
700
	}
701
702
	/**
703
	 * provided with an array of LDAP user records the method will fetch the
704
	 * user object and requests it to process the freshly fetched attributes and
705
	 * and their values
706
	 * @param array $ldapRecords
707
	 */
708 1
	public function batchApplyUserAttributes(array $ldapRecords){
709 1
		foreach($ldapRecords as $userRecord) {
710 1
			$ocName  = $this->dn2ocname($userRecord['dn'][0]);
711 1
			$this->cacheUserExists($ocName);
0 ignored issues
show
Security Bug introduced by
It seems like $ocName defined by $this->dn2ocname($userRecord['dn'][0]) on line 710 can also be of type false; however, OCA\user_ldap\lib\Access::cacheUserExists() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
712 1
			$user = $this->userManager->get($ocName);
713 1
			if($user instanceof OfflineUser) {
714
				$user->unmark();
715
				$user = $this->userManager->get($ocName);
716
			}
717 1
			$user->processAttributes($userRecord);
0 ignored issues
show
Bug introduced by
The method processAttributes does only exist in OCA\user_ldap\lib\user\User, but not in OCA\user_ldap\lib\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...
718 1
		}
719 1
	}
720
721
	/**
722
	 * @param string $filter
723
	 * @param string|string[] $attr
724
	 * @param int $limit
725
	 * @param int $offset
726
	 * @return array
727
	 */
728
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
729
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
730
	}
731
732
	/**
733
	 * @param array $list
734
	 * @param bool $manyAttributes
735
	 * @return array
736
	 */
737
	private function fetchList($list, $manyAttributes) {
738
		if(is_array($list)) {
739
			if($manyAttributes) {
740
				return $list;
741
			} else {
742
				$list = array_reduce($list, function($carry, $item) {
743
					$attribute = array_keys($item)[0];
744
					$carry[] = $item[$attribute][0];
745
					return $carry;
746
				}, array());
747
				return array_unique($list, SORT_LOCALE_STRING);
748
			}
749
		}
750
751
		//error cause actually, maybe throw an exception in future.
752
		return array();
753
	}
754
755
	/**
756
	 * executes an LDAP search, optimized for Users
757
	 * @param string $filter the LDAP filter for the search
758
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
759
	 * @param integer $limit
760
	 * @param integer $offset
761
	 * @return array with the search result
762
	 *
763
	 * Executes an LDAP search
764
	 */
765
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
766
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
767
	}
768
769
	/**
770
	 * @param string $filter
771
	 * @param string|string[] $attr
772
	 * @param int $limit
773
	 * @param int $offset
774
	 * @return false|int
775
	 */
776
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
777
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
778
	}
779
780
	/**
781
	 * executes an LDAP search, optimized for Groups
782
	 * @param string $filter the LDAP filter for the search
783
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
784
	 * @param integer $limit
785
	 * @param integer $offset
786
	 * @return array with the search result
787
	 *
788
	 * Executes an LDAP search
789
	 */
790
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
791
		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\lib\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...
792
	}
793
794
	/**
795
	 * returns the number of available groups
796
	 * @param string $filter the LDAP search filter
797
	 * @param string[] $attr optional
798
	 * @param int|null $limit
799
	 * @param int|null $offset
800
	 * @return int|bool
801
	 */
802
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
803
		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\lib\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...
804
	}
805
806
	/**
807
	 * returns the number of available objects on the base DN
808
	 *
809
	 * @param int|null $limit
810
	 * @param int|null $offset
811
	 * @return int|bool
812
	 */
813
	public function countObjects($limit = null, $offset = null) {
814
		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...
815
	}
816
817
	/**
818
	 * retrieved. Results will according to the order in the array.
819
	 * @param int $limit optional, maximum results to be counted
820
	 * @param int $offset optional, a starting point
821
	 * @return array|false array with the search result as first value and pagedSearchOK as
822
	 * second | false if not successful
823
	 */
824
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
825
		if(!is_null($attr) && !is_array($attr)) {
826
			$attr = array(mb_strtolower($attr, 'UTF-8'));
827
		}
828
829
		// See if we have a resource, in case not cancel with message
830
		$cr = $this->connection->getConnectionResource();
831
		if(!$this->ldap->isResource($cr)) {
832
			// Seems like we didn't find any resource.
833
			// Return an empty array just like before.
834
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
835
			return false;
836
		}
837
838
		//check whether paged search should be attempted
839
		$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...
840
841
		$linkResources = array_pad(array(), count($base), $cr);
842
		$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\lib\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...
843
		$error = $this->ldap->errno($cr);
844
		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...
845
			\OCP\Util::writeLog('user_ldap',
846
				'Error when searching: '.$this->ldap->error($cr).
847
					' code '.$this->ldap->errno($cr),
848
				\OCP\Util::ERROR);
849
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
850
			return false;
851
		}
852
853
		return array($sr, $pagedSearchOK);
854
	}
855
856
	/**
857
	 * processes an LDAP paged search operation
858
	 * @param array $sr the array containing the LDAP search resources
859
	 * @param string $filter the LDAP filter for the search
860
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
861
	 * @param int $iFoundItems number of results in the search operation
862
	 * @param int $limit maximum results to be counted
863
	 * @param int $offset a starting point
864
	 * @param bool $pagedSearchOK whether a paged search has been executed
865
	 * @param bool $skipHandling required for paged search when cookies to
866
	 * prior results need to be gained
867
	 * @return array|false array with the search result as first value and pagedSearchOK as
868
	 * second | false if not successful
869
	 */
870
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
871
		if($pagedSearchOK) {
872
			$cr = $this->connection->getConnectionResource();
873
			foreach($sr as $key => $res) {
874
				$cookie = null;
875
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
876
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
877
				}
878
			}
879
880
			//browsing through prior pages to get the cookie for the new one
881
			if($skipHandling) {
882
				return;
883
			}
884
			// if count is bigger, then the server does not support
885
			// paged search. Instead, he did a normal search. We set a
886
			// flag here, so the callee knows how to deal with it.
887
			if($iFoundItems <= $limit) {
888
				$this->pagedSearchedSuccessful = true;
889
			}
890
		} else {
891
			if(!is_null($limit)) {
892
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
893
			}
894
		}
895
	}
896
897
	/**
898
	 * executes an LDAP search, but counts the results only
899
	 * @param string $filter the LDAP filter for the search
900
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
901
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
902
	 * retrieved. Results will according to the order in the array.
903
	 * @param int $limit optional, maximum results to be counted
904
	 * @param int $offset optional, a starting point
905
	 * @param bool $skipHandling indicates whether the pages search operation is
906
	 * completed
907
	 * @return int|false Integer or false if the search could not be initialized
908
	 *
909
	 */
910
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
911
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
912
913
		$limitPerPage = intval($this->connection->ldapPagingSize);
914
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
915
			$limitPerPage = $limit;
916
		}
917
918
		$counter = 0;
919
		$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...
920
		$this->connection->getConnectionResource();
921
922
		do {
923
			$continue = false;
924
			$search = $this->executeSearch($filter, $base, $attr,
925
										   $limitPerPage, $offset);
926
			if($search === false) {
927
				return $counter > 0 ? $counter : false;
928
			}
929
			list($sr, $pagedSearchOK) = $search;
930
931
			$count = $this->countEntriesInSearchResults($sr, $limitPerPage, $continue);
932
			$counter += $count;
933
934
			$this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
935
										$offset, $pagedSearchOK, $skipHandling);
936
			$offset += $limitPerPage;
937
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
938
939
		return $counter;
940
	}
941
942
	/**
943
	 * @param array $searchResults
944
	 * @param int $limit
945
	 * @param bool $hasHitLimit
946
	 * @return int
947
	 */
948
	private function countEntriesInSearchResults($searchResults, $limit, &$hasHitLimit) {
949
		$cr = $this->connection->getConnectionResource();
950
		$counter = 0;
951
952
		foreach($searchResults as $res) {
953
			$count = intval($this->ldap->countEntries($cr, $res));
954
			$counter += $count;
955
			if($count > 0 && $count === $limit) {
956
				$hasHitLimit = true;
957
			}
958
		}
959
960
		return $counter;
961
	}
962
963
	/**
964
	 * Executes an LDAP search
965
	 * @param string $filter the LDAP filter for the search
966
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
967
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
968
	 * @param int $limit
969
	 * @param int $offset
970
	 * @param bool $skipHandling
971
	 * @return array with the search result
972
	 */
973
	private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
974
		if($limit <= 0) {
975
			//otherwise search will fail
976
			$limit = null;
977
		}
978
		$search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
979
		if($search === false) {
980
			return array();
981
		}
982
		list($sr, $pagedSearchOK) = $search;
983
		$cr = $this->connection->getConnectionResource();
984
985
		if($skipHandling) {
986
			//i.e. result do not need to be fetched, we just need the cookie
987
			//thus pass 1 or any other value as $iFoundItems because it is not
988
			//used
989
			$this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
990
											$offset, $pagedSearchOK,
991
											$skipHandling);
992
			return array();
993
		}
994
995
		// Do the server-side sorting
996
		foreach(array_reverse($attr) as $sortAttr){
997
			foreach($sr as $searchResource) {
998
				$this->ldap->sort($cr, $searchResource, $sortAttr);
999
			}
1000
		}
1001
1002
		$findings = array();
1003
		foreach($sr as $res) {
1004
			$findings = array_merge($findings, $this->ldap->getEntries($cr	, $res ));
1005
		}
1006
1007
		$this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
1008
										$limit, $offset, $pagedSearchOK,
1009
										$skipHandling);
1010
1011
		// if we're here, probably no connection resource is returned.
1012
		// to make ownCloud behave nicely, we simply give back an empty array.
1013
		if(is_null($findings)) {
1014
			return array();
1015
		}
1016
1017
		if(!is_null($attr)) {
1018
			$selection = array();
1019
			$i = 0;
1020
			foreach($findings as $item) {
1021
				if(!is_array($item)) {
1022
					continue;
1023
				}
1024
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1025
				foreach($attr as $key) {
1026
					$key = mb_strtolower($key, 'UTF-8');
1027
					if(isset($item[$key])) {
1028
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1029
							unset($item[$key]['count']);
1030
						}
1031
						if($key !== 'dn') {
1032
							$selection[$i][$key] = $this->resemblesDN($key) ?
1033
								$this->sanitizeDN($item[$key])
1034
								: $item[$key];
1035
						} else {
1036
							$selection[$i][$key] = [$this->sanitizeDN($item[$key])];
1037
						}
1038
					}
1039
1040
				}
1041
				$i++;
1042
			}
1043
			$findings = $selection;
1044
		}
1045
		//we slice the findings, when
1046
		//a) paged search unsuccessful, though attempted
1047
		//b) no paged search, but limit set
1048
		if((!$this->getPagedSearchResultState()
1049
			&& $pagedSearchOK)
1050
			|| (
1051
				!$pagedSearchOK
1052
				&& !is_null($limit)
1053
			)
1054
		) {
1055
			$findings = array_slice($findings, intval($offset), $limit);
1056
		}
1057
		return $findings;
1058
	}
1059
1060
	/**
1061
	 * @param string $name
1062
	 * @return bool|mixed|string
1063
	 */
1064
	public function sanitizeUsername($name) {
1065
		if($this->connection->ldapIgnoreNamingRules) {
0 ignored issues
show
Documentation introduced by
The property ldapIgnoreNamingRules does not exist on object<OCA\user_ldap\lib\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...
1066
			return $name;
1067
		}
1068
1069
		// Transliteration
1070
		// latin characters to ASCII
1071
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1072
1073
		// Replacements
1074
		$name = str_replace(' ', '_', $name);
1075
1076
		// Every remaining disallowed characters will be removed
1077
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1078
1079
		return $name;
1080
	}
1081
1082
	/**
1083
	* escapes (user provided) parts for LDAP filter
1084
	* @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...
1085
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1086
	* @return string the escaped string
1087
	*/
1088 3
	public function escapeFilterPart($input, $allowAsterisk = false) {
1089 3
		$asterisk = '';
1090 3
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1091
			$asterisk = '*';
1092
			$input = mb_substr($input, 1, null, 'UTF-8');
1093
		}
1094 3
		$search  = array('*', '\\', '(', ')');
1095 3
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1096 3
		return $asterisk . str_replace($search, $replace, $input);
1097
	}
1098
1099
	/**
1100
	 * combines the input filters with AND
1101
	 * @param string[] $filters the filters to connect
1102
	 * @return string the combined filter
1103
	 */
1104
	public function combineFilterWithAnd($filters) {
1105
		return $this->combineFilter($filters, '&');
1106
	}
1107
1108
	/**
1109
	 * combines the input filters with OR
1110
	 * @param string[] $filters the filters to connect
1111
	 * @return string the combined filter
1112
	 * Combines Filter arguments with OR
1113
	 */
1114
	public function combineFilterWithOr($filters) {
1115
		return $this->combineFilter($filters, '|');
1116
	}
1117
1118
	/**
1119
	 * combines the input filters with given operator
1120
	 * @param string[] $filters the filters to connect
1121
	 * @param string $operator either & or |
1122
	 * @return string the combined filter
1123
	 */
1124
	private function combineFilter($filters, $operator) {
1125
		$combinedFilter = '('.$operator;
1126
		foreach($filters as $filter) {
1127
			if(!empty($filter) && $filter[0] !== '(') {
1128
				$filter = '('.$filter.')';
1129
			}
1130
			$combinedFilter.=$filter;
1131
		}
1132
		$combinedFilter.=')';
1133
		return $combinedFilter;
1134
	}
1135
1136
	/**
1137
	 * creates a filter part for to perform search for users
1138
	 * @param string $search the search term
1139
	 * @return string the final filter part to use in LDAP searches
1140
	 */
1141
	public function getFilterPartForUserSearch($search) {
1142
		return $this->getFilterPartForSearch($search,
1143
			$this->connection->ldapAttributesForUserSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForUserSearch does not exist on object<OCA\user_ldap\lib\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...
1144
			$this->connection->ldapUserDisplayName);
1145
	}
1146
1147
	/**
1148
	 * creates a filter part for to perform search for groups
1149
	 * @param string $search the search term
1150
	 * @return string the final filter part to use in LDAP searches
1151
	 */
1152
	public function getFilterPartForGroupSearch($search) {
1153
		return $this->getFilterPartForSearch($search,
1154
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForGroupSearch does not exist on object<OCA\user_ldap\lib\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...
1155
			$this->connection->ldapGroupDisplayName);
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\user_ldap\lib\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...
1156
	}
1157
1158
	/**
1159
	 * creates a filter part for searches by splitting up the given search
1160
	 * string into single words
1161
	 * @param string $search the search term
1162
	 * @param string[] $searchAttributes needs to have at least two attributes,
1163
	 * otherwise it does not make sense :)
1164
	 * @return string the final filter part to use in LDAP searches
1165
	 * @throws \Exception
1166
	 */
1167
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1168
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1169
			throw new \Exception('searchAttributes must be an array with at least two string');
1170
		}
1171
		$searchWords = explode(' ', trim($search));
1172
		$wordFilters = array();
1173
		foreach($searchWords as $word) {
1174
			$word .= '*';
1175
			//every word needs to appear at least once
1176
			$wordMatchOneAttrFilters = array();
1177
			foreach($searchAttributes as $attr) {
1178
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1179
			}
1180
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1181
		}
1182
		return $this->combineFilterWithAnd($wordFilters);
1183
	}
1184
1185
	/**
1186
	 * creates a filter part for searches
1187
	 * @param string $search the search term
1188
	 * @param string[]|null $searchAttributes
1189
	 * @param string $fallbackAttribute a fallback attribute in case the user
1190
	 * did not define search attributes. Typically the display name attribute.
1191
	 * @return string the final filter part to use in LDAP searches
1192
	 */
1193
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1194
		$filter = array();
1195
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1196
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1197
			try {
1198
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1199
			} catch(\Exception $e) {
1200
				\OCP\Util::writeLog(
1201
					'user_ldap',
1202
					'Creating advanced filter for search failed, falling back to simple method.',
1203
					\OCP\Util::INFO
1204
				);
1205
			}
1206
		}
1207
		$search = empty($search) ? '*' : $search.'*';
1208
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1209
			if(empty($fallbackAttribute)) {
1210
				return '';
1211
			}
1212
			$filter[] = $fallbackAttribute . '=' . $search;
1213
		} else {
1214
			foreach($searchAttributes as $attribute) {
1215
				$filter[] = $attribute . '=' . $search;
1216
			}
1217
		}
1218
		if(count($filter) === 1) {
1219
			return '('.$filter[0].')';
1220
		}
1221
		return $this->combineFilterWithOr($filter);
1222
	}
1223
1224
	/**
1225
	 * returns the filter used for counting users
1226
	 * @return string
1227
	 */
1228
	public function getFilterForUserCount() {
1229
		$filter = $this->combineFilterWithAnd(array(
1230
			$this->connection->ldapUserFilter,
1231
			$this->connection->ldapUserDisplayName . '=*'
1232
		));
1233
1234
		return $filter;
1235
	}
1236
1237
	/**
1238
	 * @param string $name
1239
	 * @param string $password
1240
	 * @return bool
1241
	 */
1242
	public function areCredentialsValid($name, $password) {
1243
		$name = $this->DNasBaseParameter($name);
1244
		$testConnection = clone $this->connection;
1245
		$credentials = array(
1246
			'ldapAgentName' => $name,
1247
			'ldapAgentPassword' => $password
1248
		);
1249
		if(!$testConnection->setConfiguration($credentials)) {
1250
			return false;
1251
		}
1252
		$result=$testConnection->bind();
1253
		$this->connection->bind();
1254
		return $result;
1255
	}
1256
1257
	/**
1258
	 * reverse lookup of a DN given a known UUID
1259
	 *
1260
	 * @param string $uuid
1261
	 * @return string
1262
	 * @throws \Exception
1263
	 */
1264
	public function getUserDnByUuid($uuid) {
1265
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\user_ldap\lib\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...
1266
		$filter       = $this->connection->ldapUserFilter;
1267
		$base         = $this->connection->ldapBaseUsers;
1268
1269
		if($this->connection->ldapUuidUserAttribute === 'auto' && empty($uuidOverride)) {
0 ignored issues
show
Documentation introduced by
The property ldapUuidUserAttribute does not exist on object<OCA\user_ldap\lib\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...
1270
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1271
			// existing DN to be able to reliably detect it.
1272
			$result = $this->search($filter, $base, ['dn'], 1);
1273
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1274
				throw new \Exception('Cannot determine UUID attribute');
1275
			}
1276
			$dn = $result[0]['dn'][0];
1277
			if(!$this->detectUuidAttribute($dn, true)) {
1278
				throw new \Exception('Cannot determine UUID attribute');
1279
			}
1280
		} else {
1281
			// The UUID attribute is either known or an override is given.
1282
			// By calling this method we ensure that $this->connection->$uuidAttr
1283
			// is definitely set
1284
			if(!$this->detectUuidAttribute('', true)) {
1285
				throw new \Exception('Cannot determine UUID attribute');
1286
			}
1287
		}
1288
1289
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
0 ignored issues
show
Documentation introduced by
The property ldapUuidUserAttribute does not exist on object<OCA\user_ldap\lib\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...
1290
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1291
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1292
		}
1293
1294
		$filter = $uuidAttr . '=' . $uuid;
1295
		$result = $this->searchUsers($filter, ['dn'], 2);
1296
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1297
			// we put the count into account to make sure that this is
1298
			// really unique
1299
			return $result[0]['dn'][0];
1300
		}
1301
1302
		throw new \Exception('Cannot determine UUID attribute');
1303
	}
1304
1305
	/**
1306
	 * auto-detects the directory's UUID attribute
1307
	 * @param string $dn a known DN used to check against
1308
	 * @param bool $isUser
1309
	 * @param bool $force the detection should be run, even if it is not set to auto
1310
	 * @return bool true on success, false otherwise
1311
	 */
1312
	private function detectUuidAttribute($dn, $isUser = true, $force = false) {
1313 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...
1314
			$uuidAttr     = 'ldapUuidUserAttribute';
1315
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\user_ldap\lib\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...
1316
		} else {
1317
			$uuidAttr     = 'ldapUuidGroupAttribute';
1318
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr does not exist on object<OCA\user_ldap\lib\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...
1319
		}
1320
1321
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1322
			return true;
1323
		}
1324
1325
		if(!empty($uuidOverride) && !$force) {
1326
			$this->connection->$uuidAttr = $uuidOverride;
1327
			return true;
1328
		}
1329
1330
		// for now, supported attributes are entryUUID, nsuniqueid, objectGUID, ipaUniqueID
1331
		$testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid');
1332
1333
		foreach($testAttributes as $attribute) {
1334
			$value = $this->readAttribute($dn, $attribute);
1335
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1336
				\OCP\Util::writeLog('user_ldap',
1337
									'Setting '.$attribute.' as '.$uuidAttr,
1338
									\OCP\Util::DEBUG);
1339
				$this->connection->$uuidAttr = $attribute;
1340
				return true;
1341
			}
1342
		}
1343
		\OCP\Util::writeLog('user_ldap',
1344
							'Could not autodetect the UUID attribute',
1345
							\OCP\Util::ERROR);
1346
1347
		return false;
1348
	}
1349
1350
	/**
1351
	 * @param string $dn
1352
	 * @param bool $isUser
1353
	 * @return string|bool
1354
	 */
1355
	public function getUUID($dn, $isUser = true) {
1356 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...
1357
			$uuidAttr     = 'ldapUuidUserAttribute';
1358
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\user_ldap\lib\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...
1359
		} else {
1360
			$uuidAttr     = 'ldapUuidGroupAttribute';
1361
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr does not exist on object<OCA\user_ldap\lib\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...
1362
		}
1363
1364
		$uuid = false;
1365
		if($this->detectUuidAttribute($dn, $isUser)) {
1366
			$uuid = $this->readAttribute($dn, $this->connection->$uuidAttr);
1367
			if( !is_array($uuid)
1368
				&& !empty($uuidOverride)
1369
				&& $this->detectUuidAttribute($dn, $isUser, true)) {
1370
					$uuid = $this->readAttribute($dn,
1371
												 $this->connection->$uuidAttr);
1372
			}
1373
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1374
				$uuid = $uuid[0];
1375
			}
1376
		}
1377
1378
		return $uuid;
1379
	}
1380
1381
	/**
1382
	 * converts a binary ObjectGUID into a string representation
1383
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1384
	 * @return string
1385
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1386
	 */
1387
	private function convertObjectGUID2Str($oguid) {
1388
		$hex_guid = bin2hex($oguid);
1389
		$hex_guid_to_guid_str = '';
1390 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...
1391
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1392
		}
1393
		$hex_guid_to_guid_str .= '-';
1394 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...
1395
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1396
		}
1397
		$hex_guid_to_guid_str .= '-';
1398 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...
1399
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1400
		}
1401
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1402
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1403
1404
		return strtoupper($hex_guid_to_guid_str);
1405
	}
1406
1407
	/**
1408
	 * the first three blocks of the string-converted GUID happen to be in
1409
	 * reverse order. In order to use it in a filter, this needs to be
1410
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1411
	 * to every two hax figures.
1412
	 *
1413
	 * If an invalid string is passed, it will be returned without change.
1414
	 *
1415
	 * @param string $guid
1416
	 * @return string
1417
	 */
1418
	public function formatGuid2ForFilterUser($guid) {
1419
		if(!is_string($guid)) {
1420
			throw new \InvalidArgumentException('String expected');
1421
		}
1422
		$blocks = explode('-', $guid);
1423 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...
1424
			/*
1425
			 * Why not throw an Exception instead? This method is a utility
1426
			 * called only when trying to figure out whether a "missing" known
1427
			 * LDAP user was or was not renamed on the LDAP server. And this
1428
			 * even on the use case that a reverse lookup is needed (UUID known,
1429
			 * not DN), i.e. when finding users (search dialog, users page,
1430
			 * login, …) this will not be fired. This occurs only if shares from
1431
			 * a users are supposed to be mounted who cannot be found. Throwing
1432
			 * an exception here would kill the experience for a valid, acting
1433
			 * user. Instead we write a log message.
1434
			 */
1435
			\OC::$server->getLogger()->info(
1436
				'Passed string does not resemble a valid GUID. Known UUID ' .
1437
				'({uuid}) probably does not match UUID configuration.',
1438
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1439
			);
1440
			return $guid;
1441
		}
1442 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...
1443
			$pairs = str_split($blocks[$i], 2);
1444
			$pairs = array_reverse($pairs);
1445
			$blocks[$i] = implode('', $pairs);
1446
		}
1447 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...
1448
			$pairs = str_split($blocks[$i], 2);
1449
			$blocks[$i] = '\\' . implode('\\', $pairs);
1450
		}
1451
		return implode('', $blocks);
1452
	}
1453
1454
	/**
1455
	 * gets a SID of the domain of the given dn
1456
	 * @param string $dn
1457
	 * @return string|bool
1458
	 */
1459
	public function getSID($dn) {
1460
		$domainDN = $this->getDomainDNFromDN($dn);
1461
		$cacheKey = 'getSID-'.$domainDN;
1462
		if($this->connection->isCached($cacheKey)) {
1463
			return $this->connection->getFromCache($cacheKey);
1464
		}
1465
1466
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1467
		if(!is_array($objectSid) || empty($objectSid)) {
1468
			$this->connection->writeToCache($cacheKey, false);
1469
			return false;
1470
		}
1471
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1472
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1473
1474
		return $domainObjectSid;
1475
	}
1476
1477
	/**
1478
	 * converts a binary SID into a string representation
1479
	 * @param string $sid
1480
	 * @return string
1481
	 */
1482 3
	public function convertSID2Str($sid) {
1483
		// The format of a SID binary string is as follows:
1484
		// 1 byte for the revision level
1485
		// 1 byte for the number n of variable sub-ids
1486
		// 6 bytes for identifier authority value
1487
		// n*4 bytes for n sub-ids
1488
		//
1489
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1490
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1491 3
		$revision = ord($sid[0]);
1492 3
		$numberSubID = ord($sid[1]);
1493
1494 3
		$subIdStart = 8; // 1 + 1 + 6
1495 3
		$subIdLength = 4;
1496 3
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1497
			// Incorrect number of bytes present.
1498 1
			return '';
1499
		}
1500
1501
		// 6 bytes = 48 bits can be represented using floats without loss of
1502
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1503 2
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1504
1505 2
		$subIDs = array();
1506 2
		for ($i = 0; $i < $numberSubID; $i++) {
1507 2
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1508 2
			$subIDs[] = sprintf('%u', $subID[1]);
1509 2
		}
1510
1511
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1512 2
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1513
	}
1514
1515
	/**
1516
	 * converts a stored DN so it can be used as base parameter for LDAP queries, internally we store them for usage in LDAP filters
1517
	 * @param string $dn the DN
1518
	 * @return string
1519
	 */
1520 4
	private function DNasBaseParameter($dn) {
1521 4
		return str_ireplace('\\5c', '\\', $dn);
1522
	}
1523
1524
	/**
1525
	 * checks if the given DN is part of the given base DN(s)
1526
	 * @param string $dn the DN
1527
	 * @param string[] $bases array containing the allowed base DN or DNs
1528
	 * @return bool
1529
	 */
1530
	public function isDNPartOfBase($dn, $bases) {
1531
		$belongsToBase = false;
1532
		$bases = $this->sanitizeDN($bases);
1533
1534
		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...
1535
			$belongsToBase = true;
1536
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1537
				$belongsToBase = false;
1538
			}
1539
			if($belongsToBase) {
1540
				break;
1541
			}
1542
		}
1543
		return $belongsToBase;
1544
	}
1545
1546
	/**
1547
	 * resets a running Paged Search operation
1548
	 */
1549 4
	private function abandonPagedSearch() {
1550 4
		if($this->connection->hasPagedResultSupport) {
1551
			$cr = $this->connection->getConnectionResource();
1552
			$this->ldap->controlPagedResult($cr, 0, false, $this->lastCookie);
1553
			$this->getPagedSearchResultState();
1554
			$this->lastCookie = '';
1555
			$this->cookies = array();
1556
		}
1557 4
	}
1558
1559
	/**
1560
	 * get a cookie for the next LDAP paged search
1561
	 * @param string $base a string with the base DN for the search
1562
	 * @param string $filter the search filter to identify the correct search
1563
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1564
	 * @param int $offset the offset for the new search to identify the correct search really good
1565
	 * @return string containing the key or empty if none is cached
1566
	 */
1567
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1568
		if($offset === 0) {
1569
			return '';
1570
		}
1571
		$offset -= $limit;
1572
		//we work with cache here
1573
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1574
		$cookie = '';
1575
		if(isset($this->cookies[$cacheKey])) {
1576
			$cookie = $this->cookies[$cacheKey];
1577
			if(is_null($cookie)) {
1578
				$cookie = '';
1579
			}
1580
		}
1581
		return $cookie;
1582
	}
1583
1584
	/**
1585
	 * checks whether an LDAP paged search operation has more pages that can be
1586
	 * retrieved, typically when offset and limit are provided.
1587
	 *
1588
	 * Be very careful to use it: the last cookie value, which is inspected, can
1589
	 * be reset by other operations. Best, call it immediately after a search(),
1590
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1591
	 * well. Don't rely on it with any fetchList-method.
1592
	 * @return bool
1593
	 */
1594
	public function hasMoreResults() {
1595
		if(!$this->connection->hasPagedResultSupport) {
1596
			return false;
1597
		}
1598
1599
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1600
			// as in RFC 2696, when all results are returned, the cookie will
1601
			// be empty.
1602
			return false;
1603
		}
1604
1605
		return true;
1606
	}
1607
1608
	/**
1609
	 * set a cookie for LDAP paged search run
1610
	 * @param string $base a string with the base DN for the search
1611
	 * @param string $filter the search filter to identify the correct search
1612
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1613
	 * @param int $offset the offset for the run search to identify the correct search really good
1614
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1615
	 * @return void
1616
	 */
1617
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1618
		// allow '0' for 389ds
1619
		if(!empty($cookie) || $cookie === '0') {
1620
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1621
			$this->cookies[$cacheKey] = $cookie;
1622
			$this->lastCookie = $cookie;
1623
		}
1624
	}
1625
1626
	/**
1627
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1628
	 * @return boolean|null true on success, null or false otherwise
1629
	 */
1630
	public function getPagedSearchResultState() {
1631
		$result = $this->pagedSearchedSuccessful;
1632
		$this->pagedSearchedSuccessful = null;
1633
		return $result;
1634
	}
1635
1636
	/**
1637
	 * Prepares a paged search, if possible
1638
	 * @param string $filter the LDAP filter for the search
1639
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1640
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1641
	 * @param int $limit
1642
	 * @param int $offset
1643
	 * @return bool|true
1644
	 */
1645 4
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1646 4
		$pagedSearchOK = false;
1647 4
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1648
			$offset = intval($offset); //can be null
1649
			\OCP\Util::writeLog('user_ldap',
1650
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1651
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1652
				\OCP\Util::DEBUG);
1653
			//get the cookie from the search for the previous search, required by LDAP
1654
			foreach($bases as $base) {
1655
1656
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1657
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1658
					// no cookie known, although the offset is not 0. Maybe cache run out. We need
1659
					// to start all over *sigh* (btw, Dear Reader, did you know LDAP paged
1660
					// searching was designed by MSFT?)
1661
					// 		Lukas: No, but thanks to reading that source I finally know!
1662
					// '0' is valid, because 389ds
1663
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1664
					//a bit recursive, $offset of 0 is the exit
1665
					\OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
1666
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1667
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1668
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1669
					//TODO: remember this, probably does not change in the next request...
1670
					if(empty($cookie) && $cookie !== '0') {
1671
						// '0' is valid, because 389ds
1672
						$cookie = null;
1673
					}
1674
				}
1675
				if(!is_null($cookie)) {
1676
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1677
					$this->abandonPagedSearch();
1678
					$pagedSearchOK = $this->ldap->controlPagedResult(
1679
						$this->connection->getConnectionResource(), $limit,
1680
						false, $cookie);
1681
					if(!$pagedSearchOK) {
1682
						return false;
1683
					}
1684
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1685
				} else {
1686
					\OCP\Util::writeLog('user_ldap',
1687
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1688
						\OCP\Util::INFO);
1689
				}
1690
1691
			}
1692 4
		} else if($this->connection->hasPagedResultSupport && $limit === 0) {
1693
			// a search without limit was requested. However, if we do use
1694
			// Paged Search once, we always must do it. This requires us to
1695
			// initialize it with the configured page size.
1696
			$this->abandonPagedSearch();
1697
			// in case someone set it to 0 … use 500, otherwise no results will
1698
			// be returned.
1699
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1700
			$pagedSearchOK = $this->ldap->controlPagedResult(
1701
				$this->connection->getConnectionResource(), $pageSize, false, ''
1702
			);
1703
		}
1704
1705 4
		return $pagedSearchOK;
1706
	}
1707
1708
}
1709