Completed
Push — master ( 5bc8c9...07e638 )
by Morris
52:34 queued 36:57
created

Access::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 12
nc 1
nop 5
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Aaron Wood <[email protected]>
6
 * @author Alexander Bergolth <[email protected]>
7
 * @author Andreas Fischer <[email protected]>
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bart Visscher <[email protected]>
10
 * @author Benjamin Diele <[email protected]>
11
 * @author bline <[email protected]>
12
 * @author Christopher Schäpers <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Juan Pablo Villafáñez <[email protected]>
15
 * @author Jörn Friedrich Dreyer <[email protected]>
16
 * @author Lorenzo M. Catucci <[email protected]>
17
 * @author Lukas Reschke <[email protected]>
18
 * @author Lyonel Vincent <[email protected]>
19
 * @author Mario Kolling <[email protected]>
20
 * @author Morris Jobke <[email protected]>
21
 * @author Nicolas Grekas <[email protected]>
22
 * @author Ralph Krimmel <[email protected]>
23
 * @author Robin McCorkell <[email protected]>
24
 * @author Roger Szabo <[email protected]>
25
 * @author root <[email protected]>
26
 * @author Victor Dubiniuk <[email protected]>
27
 *
28
 * @license AGPL-3.0
29
 *
30
 * This code is free software: you can redistribute it and/or modify
31
 * it under the terms of the GNU Affero General Public License, version 3,
32
 * as published by the Free Software Foundation.
33
 *
34
 * This program is distributed in the hope that it will be useful,
35
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37
 * GNU Affero General Public License for more details.
38
 *
39
 * You should have received a copy of the GNU Affero General Public License, version 3,
40
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
41
 *
42
 */
43
44
namespace OCA\User_LDAP;
45
46
use OC\HintException;
47
use OCA\User_LDAP\Exceptions\ConstraintViolationException;
48
use OCA\User_LDAP\User\IUserTools;
49
use OCA\User_LDAP\User\Manager;
50
use OCA\User_LDAP\User\OfflineUser;
51
use OCA\User_LDAP\Mapping\AbstractMapping;
52
53
use OC\ServerNotAvailableException;
54
use OCP\IConfig;
55
56
/**
57
 * Class Access
58
 * @package OCA\User_LDAP
59
 */
60
class Access extends LDAPUtility implements IUserTools {
61
	const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
62
63
	/** @var \OCA\User_LDAP\Connection */
64
	public $connection;
65
	/** @var Manager */
66
	public $userManager;
67
	//never ever check this var directly, always use getPagedSearchResultState
68
	protected $pagedSearchedSuccessful;
69
70
	/**
71
	 * @var string[] $cookies an array of returned Paged Result cookies
72
	 */
73
	protected $cookies = array();
74
75
	/**
76
	 * @var string $lastCookie the last cookie returned from a Paged Results
77
	 * operation, defaults to an empty string
78
	 */
79
	protected $lastCookie = '';
80
81
	/**
82
	 * @var AbstractMapping $userMapper
83
	 */
84
	protected $userMapper;
85
86
	/**
87
	* @var AbstractMapping $userMapper
88
	*/
89
	protected $groupMapper;
90
91
	/**
92
	 * @var \OCA\User_LDAP\Helper
93
	 */
94
	private $helper;
95
	/** @var IConfig */
96
	private $config;
97
98
	public function __construct(
99
		Connection $connection,
100
		ILDAPWrapper $ldap,
101
		Manager $userManager,
102
		Helper $helper,
103
		IConfig $config
104
	) {
105
		parent::__construct($ldap);
106
		$this->connection = $connection;
107
		$this->userManager = $userManager;
108
		$this->userManager->setLdapAccess($this);
109
		$this->helper = $helper;
110
		$this->config = $config;
111
	}
112
113
	/**
114
	 * sets the User Mapper
115
	 * @param AbstractMapping $mapper
116
	 */
117
	public function setUserMapper(AbstractMapping $mapper) {
118
		$this->userMapper = $mapper;
119
	}
120
121
	/**
122
	 * returns the User Mapper
123
	 * @throws \Exception
124
	 * @return AbstractMapping
125
	 */
126
	public function getUserMapper() {
127
		if(is_null($this->userMapper)) {
128
			throw new \Exception('UserMapper was not assigned to this Access instance.');
129
		}
130
		return $this->userMapper;
131
	}
132
133
	/**
134
	 * sets the Group Mapper
135
	 * @param AbstractMapping $mapper
136
	 */
137
	public function setGroupMapper(AbstractMapping $mapper) {
138
		$this->groupMapper = $mapper;
139
	}
140
141
	/**
142
	 * returns the Group Mapper
143
	 * @throws \Exception
144
	 * @return AbstractMapping
145
	 */
146
	public function getGroupMapper() {
147
		if(is_null($this->groupMapper)) {
148
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
149
		}
150
		return $this->groupMapper;
151
	}
152
153
	/**
154
	 * @return bool
155
	 */
156
	private function checkConnection() {
157
		return ($this->connection instanceof Connection);
158
	}
159
160
	/**
161
	 * returns the Connection instance
162
	 * @return \OCA\User_LDAP\Connection
163
	 */
164
	public function getConnection() {
165
		return $this->connection;
166
	}
167
168
	/**
169
	 * reads a given attribute for an LDAP record identified by a DN
170
	 * @param string $dn the record in question
171
	 * @param string $attr the attribute that shall be retrieved
172
	 *        if empty, just check the record's existence
173
	 * @param string $filter
174
	 * @return array|false an array of values on success or an empty
175
	 *          array if $attr is empty, false otherwise
176
	 */
177
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
178
		if(!$this->checkConnection()) {
179
			\OCP\Util::writeLog('user_ldap',
180
				'No LDAP Connector assigned, access impossible for readAttribute.',
181
				\OCP\Util::WARN);
182
			return false;
183
		}
184
		$cr = $this->connection->getConnectionResource();
185
		if(!$this->ldap->isResource($cr)) {
186
			//LDAP not available
187
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
188
			return false;
189
		}
190
		//Cancel possibly running Paged Results operation, otherwise we run in
191
		//LDAP protocol errors
192
		$this->abandonPagedSearch();
193
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
194
		// but does not hurt either.
195
		$pagingSize = intval($this->connection->ldapPagingSize);
196
		// 0 won't result in replies, small numbers may leave out groups
197
		// (cf. #12306), 500 is default for paging and should work everywhere.
198
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
199
		$attr = mb_strtolower($attr, 'UTF-8');
200
		// the actual read attribute later may contain parameters on a ranged
201
		// request, e.g. member;range=99-199. Depends on server reply.
202
		$attrToRead = $attr;
203
204
		$values = [];
205
		$isRangeRequest = false;
206
		do {
207
			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
208
			if(is_bool($result)) {
209
				// when an exists request was run and it was successful, an empty
210
				// array must be returned
211
				return $result ? [] : false;
212
			}
213
214
			if (!$isRangeRequest) {
215
				$values = $this->extractAttributeValuesFromResult($result, $attr);
216
				if (!empty($values)) {
217
					return $values;
218
				}
219
			}
220
221
			$isRangeRequest = false;
222
			$result = $this->extractRangeData($result, $attr);
223
			if (!empty($result)) {
224
				$normalizedResult = $this->extractAttributeValuesFromResult(
225
					[ $attr => $result['values'] ],
226
					$attr
227
				);
228
				$values = array_merge($values, $normalizedResult);
229
230
				if($result['rangeHigh'] === '*') {
231
					// when server replies with * as high range value, there are
232
					// no more results left
233
					return $values;
234
				} else {
235
					$low  = $result['rangeHigh'] + 1;
236
					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
237
					$isRangeRequest = true;
238
				}
239
			}
240
		} while($isRangeRequest);
241
242
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
243
		return false;
244
	}
245
246
	/**
247
	 * Runs an read operation against LDAP
248
	 *
249
	 * @param resource $cr the LDAP connection
250
	 * @param string $dn
251
	 * @param string $attribute
252
	 * @param string $filter
253
	 * @param int $maxResults
254
	 * @return array|bool false if there was any error, true if an exists check
255
	 *                    was performed and the requested DN found, array with the
256
	 *                    returned data on a successful usual operation
257
	 */
258
	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
259
		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
260
		$dn = $this->helper->DNasBaseParameter($dn);
261
		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
262
		if (!$this->ldap->isResource($rr)) {
263
			if ($attribute !== '') {
264
				//do not throw this message on userExists check, irritates
265
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, \OCP\Util::DEBUG);
266
			}
267
			//in case an error occurs , e.g. object does not exist
268
			return false;
269
		}
270
		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
271
			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', \OCP\Util::DEBUG);
272
			return true;
273
		}
274
		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
275
		if (!$this->ldap->isResource($er)) {
276
			//did not match the filter, return false
277
			return false;
278
		}
279
		//LDAP attributes are not case sensitive
280
		$result = \OCP\Util::mb_array_change_key_case(
281
			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
282
283
		return $result;
284
	}
285
286
	/**
287
	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
288
	 * data if present.
289
	 *
290
	 * @param array $result from ILDAPWrapper::getAttributes()
291
	 * @param string $attribute the attribute name that was read
292
	 * @return string[]
293
	 */
294
	public function extractAttributeValuesFromResult($result, $attribute) {
295
		$values = [];
296
		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
297
			$lowercaseAttribute = strtolower($attribute);
298
			for($i=0;$i<$result[$attribute]['count'];$i++) {
299
				if($this->resemblesDN($attribute)) {
300
					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
301
				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
302
					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
303
				} else {
304
					$values[] = $result[$attribute][$i];
305
				}
306
			}
307
		}
308
		return $values;
309
	}
310
311
	/**
312
	 * Attempts to find ranged data in a getAttribute results and extracts the
313
	 * returned values as well as information on the range and full attribute
314
	 * name for further processing.
315
	 *
316
	 * @param array $result from ILDAPWrapper::getAttributes()
317
	 * @param string $attribute the attribute name that was read. Without ";range=…"
318
	 * @return array If a range was detected with keys 'values', 'attributeName',
319
	 *               'attributeFull' and 'rangeHigh', otherwise empty.
320
	 */
321
	public function extractRangeData($result, $attribute) {
322
		$keys = array_keys($result);
323
		foreach($keys as $key) {
324
			if($key !== $attribute && strpos($key, $attribute) === 0) {
325
				$queryData = explode(';', $key);
326
				if(strpos($queryData[1], 'range=') === 0) {
327
					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
328
					$data = [
329
						'values' => $result[$key],
330
						'attributeName' => $queryData[0],
331
						'attributeFull' => $key,
332
						'rangeHigh' => $high,
333
					];
334
					return $data;
335
				}
336
			}
337
		}
338
		return [];
339
	}
340
	
341
	/**
342
	 * Set password for an LDAP user identified by a DN
343
	 *
344
	 * @param string $userDN the user in question
345
	 * @param string $password the new password
346
	 * @return bool
347
	 * @throws HintException
348
	 * @throws \Exception
349
	 */
350
	public function setPassword($userDN, $password) {
351
		if(intval($this->connection->turnOnPasswordChange) !== 1) {
352
			throw new \Exception('LDAP password changes are disabled.');
353
		}
354
		$cr = $this->connection->getConnectionResource();
355
		if(!$this->ldap->isResource($cr)) {
356
			//LDAP not available
357
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
358
			return false;
359
		}
360
		try {
361
			return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
362
		} catch(ConstraintViolationException $e) {
363
			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
364
		}
365
	}
366
367
	/**
368
	 * checks whether the given attributes value is probably a DN
369
	 * @param string $attr the attribute in question
370
	 * @return boolean if so true, otherwise false
371
	 */
372
	private function resemblesDN($attr) {
373
		$resemblingAttributes = array(
374
			'dn',
375
			'uniquemember',
376
			'member',
377
			// memberOf is an "operational" attribute, without a definition in any RFC
378
			'memberof'
379
		);
380
		return in_array($attr, $resemblingAttributes);
381
	}
382
383
	/**
384
	 * checks whether the given string is probably a DN
385
	 * @param string $string
386
	 * @return boolean
387
	 */
388
	public function stringResemblesDN($string) {
389
		$r = $this->ldap->explodeDN($string, 0);
390
		// if exploding a DN succeeds and does not end up in
391
		// an empty array except for $r[count] being 0.
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
392
		return (is_array($r) && count($r) > 1);
393
	}
394
395
	/**
396
	 * returns a DN-string that is cleaned from not domain parts, e.g.
397
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
398
	 * becomes dc=foobar,dc=server,dc=org
399
	 * @param string $dn
400
	 * @return string
401
	 */
402
	public function getDomainDNFromDN($dn) {
403
		$allParts = $this->ldap->explodeDN($dn, 0);
404
		if($allParts === false) {
405
			//not a valid DN
406
			return '';
407
		}
408
		$domainParts = array();
409
		$dcFound = false;
410
		foreach($allParts as $part) {
411
			if(!$dcFound && strpos($part, 'dc=') === 0) {
412
				$dcFound = true;
413
			}
414
			if($dcFound) {
415
				$domainParts[] = $part;
416
			}
417
		}
418
		$domainDN = implode(',', $domainParts);
419
		return $domainDN;
420
	}
421
422
	/**
423
	 * returns the LDAP DN for the given internal Nextcloud name of the group
424
	 * @param string $name the Nextcloud name in question
425
	 * @return string|false LDAP DN on success, otherwise false
426
	 */
427
	public function groupname2dn($name) {
428
		return $this->groupMapper->getDNByName($name);
429
	}
430
431
	/**
432
	 * returns the LDAP DN for the given internal Nextcloud name of the user
433
	 * @param string $name the Nextcloud name in question
434
	 * @return string|false with the LDAP DN on success, otherwise false
435
	 */
436
	public function username2dn($name) {
437
		$fdn = $this->userMapper->getDNByName($name);
438
439
		//Check whether the DN belongs to the Base, to avoid issues on multi-
440
		//server setups
441
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
442
			return $fdn;
443
		}
444
445
		return false;
446
	}
447
448
	/**
449
	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
450
	 * @param string $fdn the dn of the group object
451
	 * @param string $ldapName optional, the display name of the object
452
	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
453
	 */
454 View Code Duplication
	public function dn2groupname($fdn, $ldapName = null) {
455
		//To avoid bypassing the base DN settings under certain circumstances
456
		//with the group support, check whether the provided DN matches one of
457
		//the given Bases
458
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
459
			return false;
460
		}
461
462
		return $this->dn2ocname($fdn, $ldapName, false);
463
	}
464
465
	/**
466
	 * accepts an array of group DNs and tests whether they match the user
467
	 * filter by doing read operations against the group entries. Returns an
468
	 * array of DNs that match the filter.
469
	 *
470
	 * @param string[] $groupDNs
471
	 * @return string[]
472
	 */
473
	public function groupsMatchFilter($groupDNs) {
474
		$validGroupDNs = [];
475
		foreach($groupDNs as $dn) {
476
			$cacheKey = 'groupsMatchFilter-'.$dn;
477
			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
478
			if(!is_null($groupMatchFilter)) {
479
				if($groupMatchFilter) {
480
					$validGroupDNs[] = $dn;
481
				}
482
				continue;
483
			}
484
485
			// Check the base DN first. If this is not met already, we don't
486
			// need to ask the server at all.
487
			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
488
				$this->connection->writeToCache($cacheKey, false);
489
				continue;
490
			}
491
492
			$result = $this->readAttribute($dn, 'cn', $this->connection->ldapGroupFilter);
0 ignored issues
show
Documentation introduced by
The property ldapGroupFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
493
			if(is_array($result)) {
494
				$this->connection->writeToCache($cacheKey, true);
495
				$validGroupDNs[] = $dn;
496
			} else {
497
				$this->connection->writeToCache($cacheKey, false);
498
			}
499
500
		}
501
		return $validGroupDNs;
502
	}
503
504
	/**
505
	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
506
	 * @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...
507
	 * @param string $ldapName optional, the display name of the object
508
	 * @return string|false with with the name to use in Nextcloud
509
	 */
510 View Code Duplication
	public function dn2username($fdn, $ldapName = null) {
511
		//To avoid bypassing the base DN settings under certain circumstances
512
		//with the group support, check whether the provided DN matches one of
513
		//the given Bases
514
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
515
			return false;
516
		}
517
518
		return $this->dn2ocname($fdn, $ldapName, true);
519
	}
520
521
	/**
522
	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
523
	 *
524
	 * @param string $fdn the dn of the user object
525
	 * @param string|null $ldapName optional, the display name of the object
526
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
527
	 * @param bool|null $newlyMapped
528
	 * @param array|null $record
529
	 * @return false|string with with the name to use in Nextcloud
530
	 * @throws \Exception
531
	 */
532
	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
533
		$newlyMapped = false;
534
		if($isUser) {
535
			$mapper = $this->getUserMapper();
536
			$nameAttribute = $this->connection->ldapUserDisplayName;
537
		} else {
538
			$mapper = $this->getGroupMapper();
539
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
540
		}
541
542
		//let's try to retrieve the Nextcloud name from the mappings table
543
		$ncName = $mapper->getNameByDN($fdn);
544
		if(is_string($ncName)) {
545
			return $ncName;
546
		}
547
548
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
549
		$uuid = $this->getUUID($fdn, $isUser, $record);
0 ignored issues
show
Bug introduced by
It seems like $record defined by parameter $record on line 532 can also be of type array; however, OCA\User_LDAP\Access::getUUID() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
550
		if(is_string($uuid)) {
551
			$ncName = $mapper->getNameByUUID($uuid);
552
			if(is_string($ncName)) {
553
				$mapper->setDNbyUUID($fdn, $uuid);
554
				return $ncName;
555
			}
556
		} else {
557
			//If the UUID can't be detected something is foul.
558
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
559
			return false;
560
		}
561
562
		if(is_null($ldapName)) {
563
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
564
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
565
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
566
				return false;
567
			}
568
			$ldapName = $ldapName[0];
569
		}
570
571
		if($isUser) {
572
			$usernameAttribute = strval($this->connection->ldapExpertUsernameAttr);
0 ignored issues
show
Documentation introduced by
The property ldapExpertUsernameAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
573
			if ($usernameAttribute !== '') {
574
				$username = $this->readAttribute($fdn, $usernameAttribute);
575
				$username = $username[0];
576
			} else {
577
				$username = $uuid;
578
			}
579
			$intName = $this->sanitizeUsername($username);
580
		} else {
581
			$intName = $ldapName;
582
		}
583
584
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
585
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
586
		//NOTE: mind, disabling cache affects only this instance! Using it
587
		// outside of core user management will still cache the user as non-existing.
588
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
589
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
590
		if(($isUser && $intName !== '' && !\OCP\User::userExists($intName))
591
			|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
592
			if($mapper->map($fdn, $intName, $uuid)) {
593
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
594
				$newlyMapped = true;
595
				return $intName;
596
			}
597
		}
598
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
599
600
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
601
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
602
			$newlyMapped = true;
603
			return $altName;
604
		}
605
606
		//if everything else did not help..
607
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
608
		return false;
609
	}
610
611
	/**
612
	 * gives back the user names as they are used ownClod internally
613
	 * @param array $ldapUsers as returned by fetchList()
614
	 * @return array an array with the user names to use in Nextcloud
615
	 *
616
	 * gives back the user names as they are used ownClod internally
617
	 */
618
	public function nextcloudUserNames($ldapUsers) {
619
		return $this->ldap2NextcloudNames($ldapUsers, true);
620
	}
621
622
	/**
623
	 * gives back the group names as they are used ownClod internally
624
	 * @param array $ldapGroups as returned by fetchList()
625
	 * @return array an array with the group names to use in Nextcloud
626
	 *
627
	 * gives back the group names as they are used ownClod internally
628
	 */
629
	public function nextcloudGroupNames($ldapGroups) {
630
		return $this->ldap2NextcloudNames($ldapGroups, false);
631
	}
632
633
	/**
634
	 * @param array $ldapObjects as returned by fetchList()
635
	 * @param bool $isUsers
636
	 * @return array
637
	 */
638
	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
639
		if($isUsers) {
640
			$nameAttribute = $this->connection->ldapUserDisplayName;
641
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
642
		} else {
643
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
644
		}
645
		$nextcloudNames = array();
646
647
		foreach($ldapObjects as $ldapObject) {
648
			$nameByLDAP = null;
649
			if(    isset($ldapObject[$nameAttribute])
650
				&& is_array($ldapObject[$nameAttribute])
651
				&& isset($ldapObject[$nameAttribute][0])
652
			) {
653
				// might be set, but not necessarily. if so, we use it.
654
				$nameByLDAP = $ldapObject[$nameAttribute][0];
655
			}
656
657
			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
658
			if($ncName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ncName of type false|string 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...
659
				$nextcloudNames[] = $ncName;
660
				if($isUsers) {
661
					//cache the user names so it does not need to be retrieved
662
					//again later (e.g. sharing dialogue).
663
					if(is_null($nameByLDAP)) {
664
						continue;
665
					}
666
					$sndName = isset($ldapObject[$sndAttribute][0])
667
						? $ldapObject[$sndAttribute][0] : '';
0 ignored issues
show
Bug introduced by
The variable $sndAttribute does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
668
					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
669
				}
670
			}
671
		}
672
		return $nextcloudNames;
673
	}
674
675
	/**
676
	 * caches the user display name
677
	 * @param string $ocName the internal Nextcloud username
678
	 * @param string|false $home the home directory path
679
	 */
680
	public function cacheUserHome($ocName, $home) {
681
		$cacheKey = 'getHome'.$ocName;
682
		$this->connection->writeToCache($cacheKey, $home);
683
	}
684
685
	/**
686
	 * caches a user as existing
687
	 * @param string $ocName the internal Nextcloud username
688
	 */
689
	public function cacheUserExists($ocName) {
690
		$this->connection->writeToCache('userExists'.$ocName, true);
691
	}
692
693
	/**
694
	 * caches the user display name
695
	 * @param string $ocName the internal Nextcloud username
696
	 * @param string $displayName the display name
697
	 * @param string $displayName2 the second display name
698
	 */
699
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
700
		$user = $this->userManager->get($ocName);
701
		if($user === null) {
702
			return;
703
		}
704
		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
0 ignored issues
show
Bug introduced by
The method composeAndStoreDisplayName does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
705
		$cacheKeyTrunk = 'getDisplayName';
706
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
707
	}
708
709
	/**
710
	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
711
	 * @param string $name the display name of the object
712
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
713
	 *
714
	 * Instead of using this method directly, call
715
	 * createAltInternalOwnCloudName($name, true)
716
	 */
717
	private function _createAltInternalOwnCloudNameForUsers($name) {
718
		$attempts = 0;
719
		//while loop is just a precaution. If a name is not generated within
720
		//20 attempts, something else is very wrong. Avoids infinite loop.
721
		while($attempts < 20){
722
			$altName = $name . '_' . rand(1000,9999);
723
			if(!\OCP\User::userExists($altName)) {
724
				return $altName;
725
			}
726
			$attempts++;
727
		}
728
		return false;
729
	}
730
731
	/**
732
	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
733
	 * @param string $name the display name of the object
734
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
735
	 *
736
	 * Instead of using this method directly, call
737
	 * createAltInternalOwnCloudName($name, false)
738
	 *
739
	 * Group names are also used as display names, so we do a sequential
740
	 * numbering, e.g. Developers_42 when there are 41 other groups called
741
	 * "Developers"
742
	 */
743
	private function _createAltInternalOwnCloudNameForGroups($name) {
744
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
745
		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...
746
			$lastNo = 1; //will become name_2
747
		} else {
748
			natsort($usedNames);
749
			$lastName = array_pop($usedNames);
750
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
751
		}
752
		$altName = $name.'_'.strval($lastNo+1);
753
		unset($usedNames);
754
755
		$attempts = 1;
756
		while($attempts < 21){
757
			// Check to be really sure it is unique
758
			// while loop is just a precaution. If a name is not generated within
759
			// 20 attempts, something else is very wrong. Avoids infinite loop.
760
			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
761
				return $altName;
762
			}
763
			$altName = $name . '_' . ($lastNo + $attempts);
764
			$attempts++;
765
		}
766
		return false;
767
	}
768
769
	/**
770
	 * creates a unique name for internal Nextcloud use.
771
	 * @param string $name the display name of the object
772
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
773
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
774
	 */
775
	private function createAltInternalOwnCloudName($name, $isUser) {
776
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
777
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
778
		if($isUser) {
779
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
780
		} else {
781
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
782
		}
783
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
784
785
		return $altName;
786
	}
787
788
	/**
789
	 * fetches a list of users according to a provided loginName and utilizing
790
	 * the login filter.
791
	 *
792
	 * @param string $loginName
793
	 * @param array $attributes optional, list of attributes to read
794
	 * @return array
795
	 */
796 View Code Duplication
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
797
		$loginName = $this->escapeFilterPart($loginName);
798
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
799
		$users = $this->fetchListOfUsers($filter, $attributes);
800
		return $users;
801
	}
802
803
	/**
804
	 * counts the number of users according to a provided loginName and
805
	 * utilizing the login filter.
806
	 *
807
	 * @param string $loginName
808
	 * @return int
809
	 */
810 View Code Duplication
	public function countUsersByLoginName($loginName) {
811
		$loginName = $this->escapeFilterPart($loginName);
812
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
813
		$users = $this->countUsers($filter);
814
		return $users;
815
	}
816
817
	/**
818
	 * @param string $filter
819
	 * @param string|string[] $attr
820
	 * @param int $limit
821
	 * @param int $offset
822
	 * @param bool $forceApplyAttributes
823
	 * @return array
824
	 */
825
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
826
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
827
		$recordsToUpdate = $ldapRecords;
828
		if(!$forceApplyAttributes) {
829
			$isBackgroundJobModeAjax = $this->config
830
					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
831
			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
832
				$newlyMapped = false;
833
				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
834
				if(is_string($uid)) {
835
					$this->cacheUserExists($uid);
836
				}
837
				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
838
			});
839
		}
840
		$this->batchApplyUserAttributes($recordsToUpdate);
841
		return $this->fetchList($ldapRecords, (count($attr) > 1));
842
	}
843
844
	/**
845
	 * provided with an array of LDAP user records the method will fetch the
846
	 * user object and requests it to process the freshly fetched attributes and
847
	 * and their values
848
	 * @param array $ldapRecords
849
	 */
850
	public function batchApplyUserAttributes(array $ldapRecords){
851
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
852
		foreach($ldapRecords as $userRecord) {
853
			if(!isset($userRecord[$displayNameAttribute])) {
854
				// displayName is obligatory
855
				continue;
856
			}
857
			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
858
			if($ocName === false) {
859
				continue;
860
			}
861
			$user = $this->userManager->get($ocName);
862
			if($user instanceof OfflineUser) {
863
				$user->unmark();
864
				$user = $this->userManager->get($ocName);
865
			}
866
			if ($user !== null) {
867
				$user->processAttributes($userRecord);
0 ignored issues
show
Bug introduced by
The method processAttributes does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
868
			} else {
869
				\OC::$server->getLogger()->debug(
870
					"The ldap user manager returned null for $ocName",
871
					['app'=>'user_ldap']
872
				);
873
			}
874
		}
875
	}
876
877
	/**
878
	 * @param string $filter
879
	 * @param string|string[] $attr
880
	 * @param int $limit
881
	 * @param int $offset
882
	 * @return array
883
	 */
884
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
885
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
886
	}
887
888
	/**
889
	 * @param array $list
890
	 * @param bool $manyAttributes
891
	 * @return array
892
	 */
893
	private function fetchList($list, $manyAttributes) {
894
		if(is_array($list)) {
895
			if($manyAttributes) {
896
				return $list;
897
			} else {
898
				$list = array_reduce($list, function($carry, $item) {
899
					$attribute = array_keys($item)[0];
900
					$carry[] = $item[$attribute][0];
901
					return $carry;
902
				}, array());
903
				return array_unique($list, SORT_LOCALE_STRING);
904
			}
905
		}
906
907
		//error cause actually, maybe throw an exception in future.
908
		return array();
909
	}
910
911
	/**
912
	 * executes an LDAP search, optimized for Users
913
	 * @param string $filter the LDAP filter for the search
914
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
915
	 * @param integer $limit
916
	 * @param integer $offset
917
	 * @return array with the search result
918
	 *
919
	 * Executes an LDAP search
920
	 */
921
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
922
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
923
	}
924
925
	/**
926
	 * @param string $filter
927
	 * @param string|string[] $attr
928
	 * @param int $limit
929
	 * @param int $offset
930
	 * @return false|int
931
	 */
932
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
933
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
934
	}
935
936
	/**
937
	 * executes an LDAP search, optimized for Groups
938
	 * @param string $filter the LDAP filter for the search
939
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
940
	 * @param integer $limit
941
	 * @param integer $offset
942
	 * @return array with the search result
943
	 *
944
	 * Executes an LDAP search
945
	 */
946
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
947
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
948
	}
949
950
	/**
951
	 * returns the number of available groups
952
	 * @param string $filter the LDAP search filter
953
	 * @param string[] $attr optional
954
	 * @param int|null $limit
955
	 * @param int|null $offset
956
	 * @return int|bool
957
	 */
958
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
959
		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
960
	}
961
962
	/**
963
	 * returns the number of available objects on the base DN
964
	 *
965
	 * @param int|null $limit
966
	 * @param int|null $offset
967
	 * @return int|bool
968
	 */
969
	public function countObjects($limit = null, $offset = null) {
970
		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...
971
	}
972
973
	/**
974
	 * Returns the LDAP handler
975
	 * @throws \OC\ServerNotAvailableException
976
	 */
977
978
	/**
979
	 * @return mixed
980
	 * @throws \OC\ServerNotAvailableException
981
	 */
982
	private function invokeLDAPMethod() {
983
		$arguments = func_get_args();
984
		$command = array_shift($arguments);
985
		$cr = array_shift($arguments);
986
		if (!method_exists($this->ldap, $command)) {
987
			return null;
988
		}
989
		array_unshift($arguments, $cr);
990
		// php no longer supports call-time pass-by-reference
991
		// thus cannot support controlPagedResultResponse as the third argument
992
		// is a reference
993
		$doMethod = function () use ($command, &$arguments) {
994
			if ($command == 'controlPagedResultResponse') {
995
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
996
			} else {
997
				return call_user_func_array(array($this->ldap, $command), $arguments);
998
			}
999
		};
1000
		try {
1001
			$ret = $doMethod();
1002
		} catch (ServerNotAvailableException $e) {
1003
			/* Server connection lost, attempt to reestablish it
1004
			 * Maybe implement exponential backoff?
1005
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1006
			 */
1007
			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG);
1008
			$this->connection->resetConnectionResource();
1009
			$cr = $this->connection->getConnectionResource();
1010
1011
			if(!$this->ldap->isResource($cr)) {
1012
				// Seems like we didn't find any resource.
1013
				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG);
1014
				throw $e;
1015
			}
1016
1017
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1018
			$ret = $doMethod();
1019
		}
1020
		return $ret;
1021
	}
1022
1023
	/**
1024
	 * retrieved. Results will according to the order in the array.
1025
	 *
1026
	 * @param $filter
1027
	 * @param $base
1028
	 * @param string[]|string|null $attr
1029
	 * @param int $limit optional, maximum results to be counted
1030
	 * @param int $offset optional, a starting point
1031
	 * @return array|false array with the search result as first value and pagedSearchOK as
1032
	 * second | false if not successful
1033
	 * @throws ServerNotAvailableException
1034
	 */
1035
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1036
		if(!is_null($attr) && !is_array($attr)) {
1037
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1038
		}
1039
1040
		// See if we have a resource, in case not cancel with message
1041
		$cr = $this->connection->getConnectionResource();
1042
		if(!$this->ldap->isResource($cr)) {
1043
			// Seems like we didn't find any resource.
1044
			// Return an empty array just like before.
1045
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
1046
			return false;
1047
		}
1048
1049
		//check whether paged search should be attempted
1050
		$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...
1051
1052
		$linkResources = array_pad(array(), count($base), $cr);
1053
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1054
		// cannot use $cr anymore, might have changed in the previous call!
1055
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1056
		if(!is_array($sr) || $error !== 0) {
1057
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
1058
			return false;
1059
		}
1060
1061
		return array($sr, $pagedSearchOK);
1062
	}
1063
1064
	/**
1065
	 * processes an LDAP paged search operation
1066
	 * @param array $sr the array containing the LDAP search resources
1067
	 * @param string $filter the LDAP filter for the search
1068
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1069
	 * @param int $iFoundItems number of results in the single search operation
1070
	 * @param int $limit maximum results to be counted
1071
	 * @param int $offset a starting point
1072
	 * @param bool $pagedSearchOK whether a paged search has been executed
1073
	 * @param bool $skipHandling required for paged search when cookies to
1074
	 * prior results need to be gained
1075
	 * @return bool cookie validity, true if we have more pages, false otherwise.
1076
	 */
1077
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1078
		$cookie = null;
1079
		if($pagedSearchOK) {
1080
			$cr = $this->connection->getConnectionResource();
1081
			foreach($sr as $key => $res) {
1082
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1083
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1084
				}
1085
			}
1086
1087
			//browsing through prior pages to get the cookie for the new one
1088
			if($skipHandling) {
1089
				return false;
1090
			}
1091
			// if count is bigger, then the server does not support
1092
			// paged search. Instead, he did a normal search. We set a
1093
			// flag here, so the callee knows how to deal with it.
1094
			if($iFoundItems <= $limit) {
1095
				$this->pagedSearchedSuccessful = true;
1096
			}
1097
		} else {
1098
			if(!is_null($limit)) {
1099
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
1100
			}
1101
		}
1102
		/* ++ Fixing RHDS searches with pages with zero results ++
1103
		 * Return cookie status. If we don't have more pages, with RHDS
1104
		 * cookie is null, with openldap cookie is an empty string and
1105
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1106
		 */
1107
		return !empty($cookie) || $cookie === '0';
1108
	}
1109
1110
	/**
1111
	 * executes an LDAP search, but counts the results only
1112
	 *
1113
	 * @param string $filter the LDAP filter for the search
1114
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1115
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1116
	 * retrieved. Results will according to the order in the array.
1117
	 * @param int $limit optional, maximum results to be counted
1118
	 * @param int $offset optional, a starting point
1119
	 * @param bool $skipHandling indicates whether the pages search operation is
1120
	 * completed
1121
	 * @return int|false Integer or false if the search could not be initialized
1122
	 * @throws ServerNotAvailableException
1123
	 */
1124
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1125
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1126
1127
		$limitPerPage = intval($this->connection->ldapPagingSize);
1128 View Code Duplication
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1129
			$limitPerPage = $limit;
1130
		}
1131
1132
		$counter = 0;
1133
		$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...
1134
		$this->connection->getConnectionResource();
1135
1136
		do {
1137
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1138
			if($search === false) {
1139
				return $counter > 0 ? $counter : false;
1140
			}
1141
			list($sr, $pagedSearchOK) = $search;
1142
1143
			/* ++ Fixing RHDS searches with pages with zero results ++
1144
			 * countEntriesInSearchResults() method signature changed
1145
			 * by removing $limit and &$hasHitLimit parameters
1146
			 */
1147
			$count = $this->countEntriesInSearchResults($sr);
1148
			$counter += $count;
1149
1150
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1151
										$offset, $pagedSearchOK, $skipHandling);
1152
			$offset += $limitPerPage;
1153
			/* ++ Fixing RHDS searches with pages with zero results ++
1154
			 * Continue now depends on $hasMorePages value
1155
			 */
1156
			$continue = $pagedSearchOK && $hasMorePages;
1157
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1158
1159
		return $counter;
1160
	}
1161
1162
	/**
1163
	 * @param array $searchResults
1164
	 * @return int
1165
	 */
1166
	private function countEntriesInSearchResults($searchResults) {
1167
		$counter = 0;
1168
1169
		foreach($searchResults as $res) {
1170
			$count = intval($this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res));
1171
			$counter += $count;
1172
		}
1173
1174
		return $counter;
1175
	}
1176
1177
	/**
1178
	 * Executes an LDAP search
1179
	 *
1180
	 * @param string $filter the LDAP filter for the search
1181
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1182
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1183
	 * @param int $limit
1184
	 * @param int $offset
1185
	 * @param bool $skipHandling
1186
	 * @return array with the search result
1187
	 * @throws ServerNotAvailableException
1188
	 */
1189
	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1190
		$limitPerPage = intval($this->connection->ldapPagingSize);
1191 View Code Duplication
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1192
			$limitPerPage = $limit;
1193
		}
1194
1195
		/* ++ Fixing RHDS searches with pages with zero results ++
1196
		 * As we can have pages with zero results and/or pages with less
1197
		 * than $limit results but with a still valid server 'cookie',
1198
		 * loops through until we get $continue equals true and
1199
		 * $findings['count'] < $limit
1200
		 */
1201
		$findings = [];
1202
		$savedoffset = $offset;
1203
		do {
1204
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1205
			if($search === false) {
1206
				return [];
1207
			}
1208
			list($sr, $pagedSearchOK) = $search;
1209
			$cr = $this->connection->getConnectionResource();
1210
1211
			if($skipHandling) {
1212
				//i.e. result do not need to be fetched, we just need the cookie
1213
				//thus pass 1 or any other value as $iFoundItems because it is not
1214
				//used
1215
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1216
								$offset, $pagedSearchOK,
1217
								$skipHandling);
1218
				return array();
1219
			}
1220
1221
			$iFoundItems = 0;
1222
			foreach($sr as $res) {
1223
				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1224
				$iFoundItems = max($iFoundItems, $findings['count']);
1225
				unset($findings['count']);
1226
			}
1227
1228
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1229
				$limitPerPage, $offset, $pagedSearchOK,
1230
										$skipHandling);
1231
			$offset += $limitPerPage;
1232
		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1233
		// reseting offset
1234
		$offset = $savedoffset;
1235
1236
		// if we're here, probably no connection resource is returned.
1237
		// to make Nextcloud behave nicely, we simply give back an empty array.
1238
		if(is_null($findings)) {
1239
			return array();
1240
		}
1241
1242
		if(!is_null($attr)) {
1243
			$selection = [];
1244
			$i = 0;
1245
			foreach($findings as $item) {
1246
				if(!is_array($item)) {
1247
					continue;
1248
				}
1249
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1250
				foreach($attr as $key) {
0 ignored issues
show
Bug introduced by
The expression $attr of type array<integer,string>|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...
1251
					if(isset($item[$key])) {
1252
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1253
							unset($item[$key]['count']);
1254
						}
1255
						if($key !== 'dn') {
1256
							$selection[$i][$key] = $this->resemblesDN($key) ?
1257
								$this->helper->sanitizeDN($item[$key])
1258
								: $key === 'objectguid' || $key === 'guid' ?
1259
									$selection[$i][$key] = $this->convertObjectGUID2Str($item[$key])
1260
									: $item[$key];
1261
						} else {
1262
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1263
						}
1264
					}
1265
1266
				}
1267
				$i++;
1268
			}
1269
			$findings = $selection;
1270
		}
1271
		//we slice the findings, when
1272
		//a) paged search unsuccessful, though attempted
1273
		//b) no paged search, but limit set
1274
		if((!$this->getPagedSearchResultState()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getPagedSearchResultState() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

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

$a = canBeFalseAndNull();

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

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
1275
			&& $pagedSearchOK)
1276
			|| (
1277
				!$pagedSearchOK
1278
				&& !is_null($limit)
1279
			)
1280
		) {
1281
			$findings = array_slice($findings, intval($offset), $limit);
1282
		}
1283
		return $findings;
1284
	}
1285
1286
	/**
1287
	 * @param string $name
1288
	 * @return bool|mixed|string
1289
	 */
1290
	public function sanitizeUsername($name) {
1291
		if($this->connection->ldapIgnoreNamingRules) {
0 ignored issues
show
Documentation introduced by
The property ldapIgnoreNamingRules does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1292
			return trim($name);
1293
		}
1294
1295
		// Transliteration
1296
		// latin characters to ASCII
1297
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1298
1299
		// Replacements
1300
		$name = str_replace(' ', '_', $name);
1301
1302
		// Every remaining disallowed characters will be removed
1303
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1304
1305
		return $name;
1306
	}
1307
1308
	/**
1309
	* escapes (user provided) parts for LDAP filter
1310
	* @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...
1311
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1312
	* @return string the escaped string
1313
	*/
1314
	public function escapeFilterPart($input, $allowAsterisk = false) {
1315
		$asterisk = '';
1316
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1317
			$asterisk = '*';
1318
			$input = mb_substr($input, 1, null, 'UTF-8');
1319
		}
1320
		$search  = array('*', '\\', '(', ')');
1321
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1322
		return $asterisk . str_replace($search, $replace, $input);
1323
	}
1324
1325
	/**
1326
	 * combines the input filters with AND
1327
	 * @param string[] $filters the filters to connect
1328
	 * @return string the combined filter
1329
	 */
1330
	public function combineFilterWithAnd($filters) {
1331
		return $this->combineFilter($filters, '&');
1332
	}
1333
1334
	/**
1335
	 * combines the input filters with OR
1336
	 * @param string[] $filters the filters to connect
1337
	 * @return string the combined filter
1338
	 * Combines Filter arguments with OR
1339
	 */
1340
	public function combineFilterWithOr($filters) {
1341
		return $this->combineFilter($filters, '|');
1342
	}
1343
1344
	/**
1345
	 * combines the input filters with given operator
1346
	 * @param string[] $filters the filters to connect
1347
	 * @param string $operator either & or |
1348
	 * @return string the combined filter
1349
	 */
1350
	private function combineFilter($filters, $operator) {
1351
		$combinedFilter = '('.$operator;
1352
		foreach($filters as $filter) {
1353
			if ($filter !== '' && $filter[0] !== '(') {
1354
				$filter = '('.$filter.')';
1355
			}
1356
			$combinedFilter.=$filter;
1357
		}
1358
		$combinedFilter.=')';
1359
		return $combinedFilter;
1360
	}
1361
1362
	/**
1363
	 * creates a filter part for to perform search for users
1364
	 * @param string $search the search term
1365
	 * @return string the final filter part to use in LDAP searches
1366
	 */
1367
	public function getFilterPartForUserSearch($search) {
1368
		return $this->getFilterPartForSearch($search,
1369
			$this->connection->ldapAttributesForUserSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForUserSearch does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1370
			$this->connection->ldapUserDisplayName);
1371
	}
1372
1373
	/**
1374
	 * creates a filter part for to perform search for groups
1375
	 * @param string $search the search term
1376
	 * @return string the final filter part to use in LDAP searches
1377
	 */
1378
	public function getFilterPartForGroupSearch($search) {
1379
		return $this->getFilterPartForSearch($search,
1380
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForGroupSearch does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1381
			$this->connection->ldapGroupDisplayName);
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1382
	}
1383
1384
	/**
1385
	 * creates a filter part for searches by splitting up the given search
1386
	 * string into single words
1387
	 * @param string $search the search term
1388
	 * @param string[] $searchAttributes needs to have at least two attributes,
1389
	 * otherwise it does not make sense :)
1390
	 * @return string the final filter part to use in LDAP searches
1391
	 * @throws \Exception
1392
	 */
1393
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1394
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1395
			throw new \Exception('searchAttributes must be an array with at least two string');
1396
		}
1397
		$searchWords = explode(' ', trim($search));
1398
		$wordFilters = array();
1399
		foreach($searchWords as $word) {
1400
			$word = $this->prepareSearchTerm($word);
1401
			//every word needs to appear at least once
1402
			$wordMatchOneAttrFilters = array();
1403
			foreach($searchAttributes as $attr) {
1404
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1405
			}
1406
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1407
		}
1408
		return $this->combineFilterWithAnd($wordFilters);
1409
	}
1410
1411
	/**
1412
	 * creates a filter part for searches
1413
	 * @param string $search the search term
1414
	 * @param string[]|null $searchAttributes
1415
	 * @param string $fallbackAttribute a fallback attribute in case the user
1416
	 * did not define search attributes. Typically the display name attribute.
1417
	 * @return string the final filter part to use in LDAP searches
1418
	 */
1419
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1420
		$filter = array();
1421
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1422
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1423
			try {
1424
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1425
			} catch(\Exception $e) {
1426
				\OCP\Util::writeLog(
1427
					'user_ldap',
1428
					'Creating advanced filter for search failed, falling back to simple method.',
1429
					\OCP\Util::INFO
1430
				);
1431
			}
1432
		}
1433
1434
		$search = $this->prepareSearchTerm($search);
1435
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1436
			if ($fallbackAttribute === '') {
1437
				return '';
1438
			}
1439
			$filter[] = $fallbackAttribute . '=' . $search;
1440
		} else {
1441
			foreach($searchAttributes as $attribute) {
1442
				$filter[] = $attribute . '=' . $search;
1443
			}
1444
		}
1445
		if(count($filter) === 1) {
1446
			return '('.$filter[0].')';
1447
		}
1448
		return $this->combineFilterWithOr($filter);
1449
	}
1450
1451
	/**
1452
	 * returns the search term depending on whether we are allowed
1453
	 * list users found by ldap with the current input appended by
1454
	 * a *
1455
	 * @return string
1456
	 */
1457
	private function prepareSearchTerm($term) {
1458
		$config = \OC::$server->getConfig();
1459
1460
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1461
1462
		$result = $term;
1463
		if ($term === '') {
1464
			$result = '*';
1465
		} else if ($allowEnum !== 'no') {
1466
			$result = $term . '*';
1467
		}
1468
		return $result;
1469
	}
1470
1471
	/**
1472
	 * returns the filter used for counting users
1473
	 * @return string
1474
	 */
1475
	public function getFilterForUserCount() {
1476
		$filter = $this->combineFilterWithAnd(array(
1477
			$this->connection->ldapUserFilter,
1478
			$this->connection->ldapUserDisplayName . '=*'
1479
		));
1480
1481
		return $filter;
1482
	}
1483
1484
	/**
1485
	 * @param string $name
1486
	 * @param string $password
1487
	 * @return bool
1488
	 */
1489
	public function areCredentialsValid($name, $password) {
1490
		$name = $this->helper->DNasBaseParameter($name);
1491
		$testConnection = clone $this->connection;
1492
		$credentials = array(
1493
			'ldapAgentName' => $name,
1494
			'ldapAgentPassword' => $password
1495
		);
1496
		if(!$testConnection->setConfiguration($credentials)) {
1497
			return false;
1498
		}
1499
		return $testConnection->bind();
1500
	}
1501
1502
	/**
1503
	 * reverse lookup of a DN given a known UUID
1504
	 *
1505
	 * @param string $uuid
1506
	 * @return string
1507
	 * @throws \Exception
1508
	 */
1509
	public function getUserDnByUuid($uuid) {
1510
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1511
		$filter       = $this->connection->ldapUserFilter;
1512
		$base         = $this->connection->ldapBaseUsers;
1513
1514
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1515
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1516
			// existing DN to be able to reliably detect it.
1517
			$result = $this->search($filter, $base, ['dn'], 1);
1518
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1519
				throw new \Exception('Cannot determine UUID attribute');
1520
			}
1521
			$dn = $result[0]['dn'][0];
1522
			if(!$this->detectUuidAttribute($dn, true)) {
1523
				throw new \Exception('Cannot determine UUID attribute');
1524
			}
1525
		} else {
1526
			// The UUID attribute is either known or an override is given.
1527
			// By calling this method we ensure that $this->connection->$uuidAttr
1528
			// is definitely set
1529
			if(!$this->detectUuidAttribute('', true)) {
1530
				throw new \Exception('Cannot determine UUID attribute');
1531
			}
1532
		}
1533
1534
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1535
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1536
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1537
		}
1538
1539
		$filter = $uuidAttr . '=' . $uuid;
1540
		$result = $this->searchUsers($filter, ['dn'], 2);
1541
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1542
			// we put the count into account to make sure that this is
1543
			// really unique
1544
			return $result[0]['dn'][0];
1545
		}
1546
1547
		throw new \Exception('Cannot determine UUID attribute');
1548
	}
1549
1550
	/**
1551
	 * auto-detects the directory's UUID attribute
1552
	 *
1553
	 * @param string $dn a known DN used to check against
1554
	 * @param bool $isUser
1555
	 * @param bool $force the detection should be run, even if it is not set to auto
1556
	 * @param array|null $ldapRecord
1557
	 * @return bool true on success, false otherwise
1558
	 */
1559
	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1560 View Code Duplication
		if($isUser) {
1561
			$uuidAttr     = 'ldapUuidUserAttribute';
1562
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1563
		} else {
1564
			$uuidAttr     = 'ldapUuidGroupAttribute';
1565
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1566
		}
1567
1568
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1569
			return true;
1570
		}
1571
1572
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1573
			$this->connection->$uuidAttr = $uuidOverride;
1574
			return true;
1575
		}
1576
1577
		foreach(self::UUID_ATTRIBUTES as $attribute) {
1578
			if($ldapRecord !== null) {
1579
				// we have the info from LDAP already, we don't need to talk to the server again
1580
				if(isset($ldapRecord[$attribute])) {
1581
					$this->connection->$uuidAttr = $attribute;
1582
					return true;
1583
				} else {
1584
					continue;
1585
				}
1586
			}
1587
1588
			$value = $this->readAttribute($dn, $attribute);
1589
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1590
				\OCP\Util::writeLog('user_ldap',
1591
									'Setting '.$attribute.' as '.$uuidAttr,
1592
									\OCP\Util::DEBUG);
1593
				$this->connection->$uuidAttr = $attribute;
1594
				return true;
1595
			}
1596
		}
1597
		\OCP\Util::writeLog('user_ldap',
1598
							'Could not autodetect the UUID attribute',
1599
							\OCP\Util::ERROR);
1600
1601
		return false;
1602
	}
1603
1604
	/**
1605
	 * @param string $dn
1606
	 * @param bool $isUser
1607
	 * @param null $ldapRecord
1608
	 * @return bool|string
1609
	 */
1610
	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1611 View Code Duplication
		if($isUser) {
1612
			$uuidAttr     = 'ldapUuidUserAttribute';
1613
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1614
		} else {
1615
			$uuidAttr     = 'ldapUuidGroupAttribute';
1616
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1617
		}
1618
1619
		$uuid = false;
1620
		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1621
			$attr = $this->connection->$uuidAttr;
1622
			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1623
			if( !is_array($uuid)
1624
				&& $uuidOverride !== ''
1625
				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1626
			{
1627
				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1628
					? $ldapRecord[$this->connection->$uuidAttr]
1629
					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1630
			}
1631
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1632
				$uuid = $uuid[0];
1633
			}
1634
		}
1635
1636
		return $uuid;
1637
	}
1638
1639
	/**
1640
	 * converts a binary ObjectGUID into a string representation
1641
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1642
	 * @return string
1643
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1644
	 */
1645
	private function convertObjectGUID2Str($oguid) {
1646
		$hex_guid = bin2hex($oguid);
1647
		$hex_guid_to_guid_str = '';
1648 View Code Duplication
		for($k = 1; $k <= 4; ++$k) {
1649
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1650
		}
1651
		$hex_guid_to_guid_str .= '-';
1652 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1653
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1654
		}
1655
		$hex_guid_to_guid_str .= '-';
1656 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1657
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1658
		}
1659
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1660
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1661
1662
		return strtoupper($hex_guid_to_guid_str);
1663
	}
1664
1665
	/**
1666
	 * the first three blocks of the string-converted GUID happen to be in
1667
	 * reverse order. In order to use it in a filter, this needs to be
1668
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1669
	 * to every two hax figures.
1670
	 *
1671
	 * If an invalid string is passed, it will be returned without change.
1672
	 *
1673
	 * @param string $guid
1674
	 * @return string
1675
	 */
1676
	public function formatGuid2ForFilterUser($guid) {
1677
		if(!is_string($guid)) {
1678
			throw new \InvalidArgumentException('String expected');
1679
		}
1680
		$blocks = explode('-', $guid);
1681 View Code Duplication
		if(count($blocks) !== 5) {
1682
			/*
1683
			 * Why not throw an Exception instead? This method is a utility
1684
			 * called only when trying to figure out whether a "missing" known
1685
			 * LDAP user was or was not renamed on the LDAP server. And this
1686
			 * even on the use case that a reverse lookup is needed (UUID known,
1687
			 * not DN), i.e. when finding users (search dialog, users page,
1688
			 * login, …) this will not be fired. This occurs only if shares from
1689
			 * a users are supposed to be mounted who cannot be found. Throwing
1690
			 * an exception here would kill the experience for a valid, acting
1691
			 * user. Instead we write a log message.
1692
			 */
1693
			\OC::$server->getLogger()->info(
1694
				'Passed string does not resemble a valid GUID. Known UUID ' .
1695
				'({uuid}) probably does not match UUID configuration.',
1696
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1697
			);
1698
			return $guid;
1699
		}
1700 View Code Duplication
		for($i=0; $i < 3; $i++) {
1701
			$pairs = str_split($blocks[$i], 2);
1702
			$pairs = array_reverse($pairs);
1703
			$blocks[$i] = implode('', $pairs);
1704
		}
1705 View Code Duplication
		for($i=0; $i < 5; $i++) {
1706
			$pairs = str_split($blocks[$i], 2);
1707
			$blocks[$i] = '\\' . implode('\\', $pairs);
1708
		}
1709
		return implode('', $blocks);
1710
	}
1711
1712
	/**
1713
	 * gets a SID of the domain of the given dn
1714
	 * @param string $dn
1715
	 * @return string|bool
1716
	 */
1717
	public function getSID($dn) {
1718
		$domainDN = $this->getDomainDNFromDN($dn);
1719
		$cacheKey = 'getSID-'.$domainDN;
1720
		$sid = $this->connection->getFromCache($cacheKey);
1721
		if(!is_null($sid)) {
1722
			return $sid;
1723
		}
1724
1725
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1726
		if(!is_array($objectSid) || empty($objectSid)) {
1727
			$this->connection->writeToCache($cacheKey, false);
1728
			return false;
1729
		}
1730
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1731
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1732
1733
		return $domainObjectSid;
1734
	}
1735
1736
	/**
1737
	 * converts a binary SID into a string representation
1738
	 * @param string $sid
1739
	 * @return string
1740
	 */
1741
	public function convertSID2Str($sid) {
1742
		// The format of a SID binary string is as follows:
1743
		// 1 byte for the revision level
1744
		// 1 byte for the number n of variable sub-ids
1745
		// 6 bytes for identifier authority value
1746
		// n*4 bytes for n sub-ids
1747
		//
1748
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1749
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1750
		$revision = ord($sid[0]);
1751
		$numberSubID = ord($sid[1]);
1752
1753
		$subIdStart = 8; // 1 + 1 + 6
1754
		$subIdLength = 4;
1755
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1756
			// Incorrect number of bytes present.
1757
			return '';
1758
		}
1759
1760
		// 6 bytes = 48 bits can be represented using floats without loss of
1761
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1762
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1763
1764
		$subIDs = array();
1765
		for ($i = 0; $i < $numberSubID; $i++) {
1766
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1767
			$subIDs[] = sprintf('%u', $subID[1]);
1768
		}
1769
1770
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1771
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1772
	}
1773
1774
	/**
1775
	 * checks if the given DN is part of the given base DN(s)
1776
	 * @param string $dn the DN
1777
	 * @param string[] $bases array containing the allowed base DN or DNs
1778
	 * @return bool
1779
	 */
1780
	public function isDNPartOfBase($dn, $bases) {
1781
		$belongsToBase = false;
1782
		$bases = $this->helper->sanitizeDN($bases);
1783
1784
		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...
1785
			$belongsToBase = true;
1786
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1787
				$belongsToBase = false;
1788
			}
1789
			if($belongsToBase) {
1790
				break;
1791
			}
1792
		}
1793
		return $belongsToBase;
1794
	}
1795
1796
	/**
1797
	 * resets a running Paged Search operation
1798
	 */
1799
	private function abandonPagedSearch() {
1800
		if($this->connection->hasPagedResultSupport) {
1801
			$cr = $this->connection->getConnectionResource();
1802
			$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1803
			$this->getPagedSearchResultState();
1804
			$this->lastCookie = '';
1805
			$this->cookies = array();
1806
		}
1807
	}
1808
1809
	/**
1810
	 * get a cookie for the next LDAP paged search
1811
	 * @param string $base a string with the base DN for the search
1812
	 * @param string $filter the search filter to identify the correct search
1813
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1814
	 * @param int $offset the offset for the new search to identify the correct search really good
1815
	 * @return string containing the key or empty if none is cached
1816
	 */
1817
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1818
		if($offset === 0) {
1819
			return '';
1820
		}
1821
		$offset -= $limit;
1822
		//we work with cache here
1823
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1824
		$cookie = '';
1825
		if(isset($this->cookies[$cacheKey])) {
1826
			$cookie = $this->cookies[$cacheKey];
1827
			if(is_null($cookie)) {
1828
				$cookie = '';
1829
			}
1830
		}
1831
		return $cookie;
1832
	}
1833
1834
	/**
1835
	 * checks whether an LDAP paged search operation has more pages that can be
1836
	 * retrieved, typically when offset and limit are provided.
1837
	 *
1838
	 * Be very careful to use it: the last cookie value, which is inspected, can
1839
	 * be reset by other operations. Best, call it immediately after a search(),
1840
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1841
	 * well. Don't rely on it with any fetchList-method.
1842
	 * @return bool
1843
	 */
1844
	public function hasMoreResults() {
1845
		if(!$this->connection->hasPagedResultSupport) {
1846
			return false;
1847
		}
1848
1849
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1850
			// as in RFC 2696, when all results are returned, the cookie will
1851
			// be empty.
1852
			return false;
1853
		}
1854
1855
		return true;
1856
	}
1857
1858
	/**
1859
	 * set a cookie for LDAP paged search run
1860
	 * @param string $base a string with the base DN for the search
1861
	 * @param string $filter the search filter to identify the correct search
1862
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1863
	 * @param int $offset the offset for the run search to identify the correct search really good
1864
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1865
	 * @return void
1866
	 */
1867
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1868
		// allow '0' for 389ds
1869
		if(!empty($cookie) || $cookie === '0') {
1870
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1871
			$this->cookies[$cacheKey] = $cookie;
1872
			$this->lastCookie = $cookie;
1873
		}
1874
	}
1875
1876
	/**
1877
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1878
	 * @return boolean|null true on success, null or false otherwise
1879
	 */
1880
	public function getPagedSearchResultState() {
1881
		$result = $this->pagedSearchedSuccessful;
1882
		$this->pagedSearchedSuccessful = null;
1883
		return $result;
1884
	}
1885
1886
	/**
1887
	 * Prepares a paged search, if possible
1888
	 * @param string $filter the LDAP filter for the search
1889
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1890
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1891
	 * @param int $limit
1892
	 * @param int $offset
1893
	 * @return bool|true
1894
	 */
1895
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1896
		$pagedSearchOK = false;
1897
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1898
			$offset = intval($offset); //can be null
1899
			\OCP\Util::writeLog('user_ldap',
1900
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1901
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1902
				\OCP\Util::DEBUG);
1903
			//get the cookie from the search for the previous search, required by LDAP
1904
			foreach($bases as $base) {
1905
1906
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1907
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1908
					// no cookie known from a potential previous search. We need
1909
					// to start from 0 to come to the desired page. cookie value
1910
					// of '0' is valid, because 389ds
1911
					$reOffset = 0;
1912
					while($reOffset < $offset) {
1913
						$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1914
						$reOffset += $limit;
1915
					}
1916
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1917
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1918
					// '0' is valid, because 389ds
1919
					//TODO: remember this, probably does not change in the next request...
1920
					if(empty($cookie) && $cookie !== '0') {
1921
						$cookie = null;
1922
					}
1923
				}
1924
				if(!is_null($cookie)) {
1925
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1926
					$this->abandonPagedSearch();
1927
					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1928
						$this->connection->getConnectionResource(), $limit,
1929
						false, $cookie);
1930
					if(!$pagedSearchOK) {
1931
						return false;
1932
					}
1933
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1934
				} else {
1935
					\OCP\Util::writeLog('user_ldap',
1936
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1937
						\OCP\Util::INFO);
1938
				}
1939
1940
			}
1941
		/* ++ Fixing RHDS searches with pages with zero results ++
1942
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1943
		 * due to pages with zero results.
1944
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1945
		 * if we don't have a previous paged search.
1946
		 */
1947
		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1948
			// a search without limit was requested. However, if we do use
1949
			// Paged Search once, we always must do it. This requires us to
1950
			// initialize it with the configured page size.
1951
			$this->abandonPagedSearch();
1952
			// in case someone set it to 0 … use 500, otherwise no results will
1953
			// be returned.
1954
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1955
			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1956
				$this->connection->getConnectionResource(),
1957
				$pageSize, false, '');
1958
		}
1959
1960
		return $pagedSearchOK;
1961
	}
1962
1963
}
1964