Completed
Push — master ( ede649...d3e7dd )
by Lukas
16:27
created

Access::getUUID()   C

Complexity

Conditions 11
Paths 26

Size

Total Lines 28
Code Lines 20

Duplication

Lines 7
Ratio 25 %

Importance

Changes 0
Metric Value
cc 11
eloc 20
nc 26
nop 3
dl 7
loc 28
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	 */
531
	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
532
		$newlyMapped = false;
533
		if($isUser) {
534
			$mapper = $this->getUserMapper();
535
			$nameAttribute = $this->connection->ldapUserDisplayName;
536
		} else {
537
			$mapper = $this->getGroupMapper();
538
			$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...
539
		}
540
541
		//let's try to retrieve the Nextcloud name from the mappings table
542
		$ncName = $mapper->getNameByDN($fdn);
543
		if(is_string($ncName)) {
544
			return $ncName;
545
		}
546
547
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
548
		$uuid = $this->getUUID($fdn, $isUser, $record);
0 ignored issues
show
Bug introduced by
It seems like $record defined by parameter $record on line 531 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...
549
		if(is_string($uuid)) {
550
			$ncName = $mapper->getNameByUUID($uuid);
551
			if(is_string($ncName)) {
552
				$mapper->setDNbyUUID($fdn, $uuid);
553
				return $ncName;
554
			}
555
		} else {
556
			//If the UUID can't be detected something is foul.
557
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
558
			return false;
559
		}
560
561
		if(is_null($ldapName)) {
562
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
563
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
564
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
565
				return false;
566
			}
567
			$ldapName = $ldapName[0];
568
		}
569
570
		if($isUser) {
571
			$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...
572
			if ($usernameAttribute !== '') {
573
				$username = $this->readAttribute($fdn, $usernameAttribute);
574
				$username = $username[0];
575
			} else {
576
				$username = $uuid;
577
			}
578
			$intName = $this->sanitizeUsername($username);
579
		} else {
580
			$intName = $ldapName;
581
		}
582
583
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
584
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
585
		//NOTE: mind, disabling cache affects only this instance! Using it
586
		// outside of core user management will still cache the user as non-existing.
587
		$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...
588
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
589
		if(($isUser && !\OCP\User::userExists($intName))
590
			|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
591
			if($mapper->map($fdn, $intName, $uuid)) {
592
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
593
				$newlyMapped = true;
594
				return $intName;
595
			}
596
		}
597
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
598
599
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
600
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
601
			$newlyMapped = true;
602
			return $altName;
603
		}
604
605
		//if everything else did not help..
606
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
607
		return false;
608
	}
609
610
	/**
611
	 * gives back the user names as they are used ownClod internally
612
	 * @param array $ldapUsers as returned by fetchList()
613
	 * @return array an array with the user names to use in Nextcloud
614
	 *
615
	 * gives back the user names as they are used ownClod internally
616
	 */
617
	public function nextcloudUserNames($ldapUsers) {
618
		return $this->ldap2NextcloudNames($ldapUsers, true);
619
	}
620
621
	/**
622
	 * gives back the group names as they are used ownClod internally
623
	 * @param array $ldapGroups as returned by fetchList()
624
	 * @return array an array with the group names to use in Nextcloud
625
	 *
626
	 * gives back the group names as they are used ownClod internally
627
	 */
628
	public function nextcloudGroupNames($ldapGroups) {
629
		return $this->ldap2NextcloudNames($ldapGroups, false);
630
	}
631
632
	/**
633
	 * @param array $ldapObjects as returned by fetchList()
634
	 * @param bool $isUsers
635
	 * @return array
636
	 */
637
	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
638
		if($isUsers) {
639
			$nameAttribute = $this->connection->ldapUserDisplayName;
640
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
641
		} else {
642
			$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...
643
		}
644
		$nextcloudNames = array();
645
646
		foreach($ldapObjects as $ldapObject) {
647
			$nameByLDAP = null;
648
			if(    isset($ldapObject[$nameAttribute])
649
				&& is_array($ldapObject[$nameAttribute])
650
				&& isset($ldapObject[$nameAttribute][0])
651
			) {
652
				// might be set, but not necessarily. if so, we use it.
653
				$nameByLDAP = $ldapObject[$nameAttribute][0];
654
			}
655
656
			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
657
			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...
658
				$nextcloudNames[] = $ncName;
659
				if($isUsers) {
660
					//cache the user names so it does not need to be retrieved
661
					//again later (e.g. sharing dialogue).
662
					if(is_null($nameByLDAP)) {
663
						continue;
664
					}
665
					$sndName = isset($ldapObject[$sndAttribute][0])
666
						? $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...
667
					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
668
				}
669
			}
670
		}
671
		return $nextcloudNames;
672
	}
673
674
	/**
675
	 * caches the user display name
676
	 * @param string $ocName the internal Nextcloud username
677
	 * @param string|false $home the home directory path
678
	 */
679
	public function cacheUserHome($ocName, $home) {
680
		$cacheKey = 'getHome'.$ocName;
681
		$this->connection->writeToCache($cacheKey, $home);
682
	}
683
684
	/**
685
	 * caches a user as existing
686
	 * @param string $ocName the internal Nextcloud username
687
	 */
688
	public function cacheUserExists($ocName) {
689
		$this->connection->writeToCache('userExists'.$ocName, true);
690
	}
691
692
	/**
693
	 * caches the user display name
694
	 * @param string $ocName the internal Nextcloud username
695
	 * @param string $displayName the display name
696
	 * @param string $displayName2 the second display name
697
	 */
698
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
699
		$user = $this->userManager->get($ocName);
700
		if($user === null) {
701
			return;
702
		}
703
		$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...
704
		$cacheKeyTrunk = 'getDisplayName';
705
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
706
	}
707
708
	/**
709
	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
710
	 * @param string $name the display name of the object
711
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
712
	 *
713
	 * Instead of using this method directly, call
714
	 * createAltInternalOwnCloudName($name, true)
715
	 */
716
	private function _createAltInternalOwnCloudNameForUsers($name) {
717
		$attempts = 0;
718
		//while loop is just a precaution. If a name is not generated within
719
		//20 attempts, something else is very wrong. Avoids infinite loop.
720
		while($attempts < 20){
721
			$altName = $name . '_' . rand(1000,9999);
722
			if(!\OCP\User::userExists($altName)) {
723
				return $altName;
724
			}
725
			$attempts++;
726
		}
727
		return false;
728
	}
729
730
	/**
731
	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
732
	 * @param string $name the display name of the object
733
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
734
	 *
735
	 * Instead of using this method directly, call
736
	 * createAltInternalOwnCloudName($name, false)
737
	 *
738
	 * Group names are also used as display names, so we do a sequential
739
	 * numbering, e.g. Developers_42 when there are 41 other groups called
740
	 * "Developers"
741
	 */
742
	private function _createAltInternalOwnCloudNameForGroups($name) {
743
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
744
		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...
745
			$lastNo = 1; //will become name_2
746
		} else {
747
			natsort($usedNames);
748
			$lastName = array_pop($usedNames);
749
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
750
		}
751
		$altName = $name.'_'.strval($lastNo+1);
752
		unset($usedNames);
753
754
		$attempts = 1;
755
		while($attempts < 21){
756
			// Check to be really sure it is unique
757
			// while loop is just a precaution. If a name is not generated within
758
			// 20 attempts, something else is very wrong. Avoids infinite loop.
759
			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
760
				return $altName;
761
			}
762
			$altName = $name . '_' . ($lastNo + $attempts);
763
			$attempts++;
764
		}
765
		return false;
766
	}
767
768
	/**
769
	 * creates a unique name for internal Nextcloud use.
770
	 * @param string $name the display name of the object
771
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
772
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
773
	 */
774
	private function createAltInternalOwnCloudName($name, $isUser) {
775
		$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...
776
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
777
		if($isUser) {
778
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
779
		} else {
780
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
781
		}
782
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
783
784
		return $altName;
785
	}
786
787
	/**
788
	 * fetches a list of users according to a provided loginName and utilizing
789
	 * the login filter.
790
	 *
791
	 * @param string $loginName
792
	 * @param array $attributes optional, list of attributes to read
793
	 * @return array
794
	 */
795 View Code Duplication
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
796
		$loginName = $this->escapeFilterPart($loginName);
797
		$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...
798
		$users = $this->fetchListOfUsers($filter, $attributes);
799
		return $users;
800
	}
801
802
	/**
803
	 * counts the number of users according to a provided loginName and
804
	 * utilizing the login filter.
805
	 *
806
	 * @param string $loginName
807
	 * @return int
808
	 */
809 View Code Duplication
	public function countUsersByLoginName($loginName) {
810
		$loginName = $this->escapeFilterPart($loginName);
811
		$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...
812
		$users = $this->countUsers($filter);
813
		return $users;
814
	}
815
816
	/**
817
	 * @param string $filter
818
	 * @param string|string[] $attr
819
	 * @param int $limit
820
	 * @param int $offset
821
	 * @param bool $forceApplyAttributes
822
	 * @return array
823
	 */
824
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
825
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
826
		$recordsToUpdate = $ldapRecords;
827
		if(!$forceApplyAttributes) {
828
			$isBackgroundJobModeAjax = $this->config
829
					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
830
			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
831
				$newlyMapped = false;
832
				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
833
				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
834
			});
835
		}
836
		$this->batchApplyUserAttributes($recordsToUpdate);
837
		return $this->fetchList($ldapRecords, (count($attr) > 1));
838
	}
839
840
	/**
841
	 * provided with an array of LDAP user records the method will fetch the
842
	 * user object and requests it to process the freshly fetched attributes and
843
	 * and their values
844
	 * @param array $ldapRecords
845
	 */
846
	public function batchApplyUserAttributes(array $ldapRecords){
847
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
848
		foreach($ldapRecords as $userRecord) {
849
			if(!isset($userRecord[$displayNameAttribute])) {
850
				// displayName is obligatory
851
				continue;
852
			}
853
			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
854
			if($ocName === false) {
855
				continue;
856
			}
857
			$this->cacheUserExists($ocName);
858
			$user = $this->userManager->get($ocName);
859
			if($user instanceof OfflineUser) {
860
				$user->unmark();
861
				$user = $this->userManager->get($ocName);
862
			}
863
			if ($user !== null) {
864
				$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...
865
			} else {
866
				\OC::$server->getLogger()->debug(
867
					"The ldap user manager returned null for $ocName",
868
					['app'=>'user_ldap']
869
				);
870
			}
871
		}
872
	}
873
874
	/**
875
	 * @param string $filter
876
	 * @param string|string[] $attr
877
	 * @param int $limit
878
	 * @param int $offset
879
	 * @return array
880
	 */
881
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
882
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
883
	}
884
885
	/**
886
	 * @param array $list
887
	 * @param bool $manyAttributes
888
	 * @return array
889
	 */
890
	private function fetchList($list, $manyAttributes) {
891
		if(is_array($list)) {
892
			if($manyAttributes) {
893
				return $list;
894
			} else {
895
				$list = array_reduce($list, function($carry, $item) {
896
					$attribute = array_keys($item)[0];
897
					$carry[] = $item[$attribute][0];
898
					return $carry;
899
				}, array());
900
				return array_unique($list, SORT_LOCALE_STRING);
901
			}
902
		}
903
904
		//error cause actually, maybe throw an exception in future.
905
		return array();
906
	}
907
908
	/**
909
	 * executes an LDAP search, optimized for Users
910
	 * @param string $filter the LDAP filter for the search
911
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
912
	 * @param integer $limit
913
	 * @param integer $offset
914
	 * @return array with the search result
915
	 *
916
	 * Executes an LDAP search
917
	 */
918
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
919
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
920
	}
921
922
	/**
923
	 * @param string $filter
924
	 * @param string|string[] $attr
925
	 * @param int $limit
926
	 * @param int $offset
927
	 * @return false|int
928
	 */
929
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
930
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
931
	}
932
933
	/**
934
	 * executes an LDAP search, optimized for Groups
935
	 * @param string $filter the LDAP filter for the search
936
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
937
	 * @param integer $limit
938
	 * @param integer $offset
939
	 * @return array with the search result
940
	 *
941
	 * Executes an LDAP search
942
	 */
943
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
944
		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...
945
	}
946
947
	/**
948
	 * returns the number of available groups
949
	 * @param string $filter the LDAP search filter
950
	 * @param string[] $attr optional
951
	 * @param int|null $limit
952
	 * @param int|null $offset
953
	 * @return int|bool
954
	 */
955
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
956
		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...
957
	}
958
959
	/**
960
	 * returns the number of available objects on the base DN
961
	 *
962
	 * @param int|null $limit
963
	 * @param int|null $offset
964
	 * @return int|bool
965
	 */
966
	public function countObjects($limit = null, $offset = null) {
967
		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...
968
	}
969
970
	/**
971
	 * Returns the LDAP handler
972
	 * @throws \OC\ServerNotAvailableException
973
	 */
974
975
	/**
976
	 * @return mixed
977
	 * @throws \OC\ServerNotAvailableException
978
	 */
979
	private function invokeLDAPMethod() {
980
		$arguments = func_get_args();
981
		$command = array_shift($arguments);
982
		$cr = array_shift($arguments);
983
		if (!method_exists($this->ldap, $command)) {
984
			return null;
985
		}
986
		array_unshift($arguments, $cr);
987
		// php no longer supports call-time pass-by-reference
988
		// thus cannot support controlPagedResultResponse as the third argument
989
		// is a reference
990
		$doMethod = function () use ($command, &$arguments) {
991
			if ($command == 'controlPagedResultResponse') {
992
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
993
			} else {
994
				return call_user_func_array(array($this->ldap, $command), $arguments);
995
			}
996
		};
997
		try {
998
			$ret = $doMethod();
999
		} catch (ServerNotAvailableException $e) {
1000
			/* Server connection lost, attempt to reestablish it
1001
			 * Maybe implement exponential backoff?
1002
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1003
			 */
1004
			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG);
1005
			$this->connection->resetConnectionResource();
1006
			$cr = $this->connection->getConnectionResource();
1007
1008
			if(!$this->ldap->isResource($cr)) {
1009
				// Seems like we didn't find any resource.
1010
				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG);
1011
				throw $e;
1012
			}
1013
1014
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1015
			$ret = $doMethod();
1016
		}
1017
		return $ret;
1018
	}
1019
1020
	/**
1021
	 * retrieved. Results will according to the order in the array.
1022
	 * @param int $limit optional, maximum results to be counted
1023
	 * @param int $offset optional, a starting point
1024
	 * @return array|false array with the search result as first value and pagedSearchOK as
1025
	 * second | false if not successful
1026
	 * @throws \OC\ServerNotAvailableException
1027
	 */
1028
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1029
		if(!is_null($attr) && !is_array($attr)) {
1030
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1031
		}
1032
1033
		// See if we have a resource, in case not cancel with message
1034
		$cr = $this->connection->getConnectionResource();
1035
		if(!$this->ldap->isResource($cr)) {
1036
			// Seems like we didn't find any resource.
1037
			// Return an empty array just like before.
1038
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
1039
			return false;
1040
		}
1041
1042
		//check whether paged search should be attempted
1043
		$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...
1044
1045
		$linkResources = array_pad(array(), count($base), $cr);
1046
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1047
		// cannot use $cr anymore, might have changed in the previous call!
1048
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1049
		if(!is_array($sr) || $error !== 0) {
1050
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
1051
			return false;
1052
		}
1053
1054
		return array($sr, $pagedSearchOK);
1055
	}
1056
1057
	/**
1058
	 * processes an LDAP paged search operation
1059
	 * @param array $sr the array containing the LDAP search resources
1060
	 * @param string $filter the LDAP filter for the search
1061
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1062
	 * @param int $iFoundItems number of results in the single search operation
1063
	 * @param int $limit maximum results to be counted
1064
	 * @param int $offset a starting point
1065
	 * @param bool $pagedSearchOK whether a paged search has been executed
1066
	 * @param bool $skipHandling required for paged search when cookies to
1067
	 * prior results need to be gained
1068
	 * @return bool cookie validity, true if we have more pages, false otherwise.
1069
	 */
1070
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1071
		$cookie = null;
1072
		if($pagedSearchOK) {
1073
			$cr = $this->connection->getConnectionResource();
1074
			foreach($sr as $key => $res) {
1075
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1076
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1077
				}
1078
			}
1079
1080
			//browsing through prior pages to get the cookie for the new one
1081
			if($skipHandling) {
1082
				return false;
1083
			}
1084
			// if count is bigger, then the server does not support
1085
			// paged search. Instead, he did a normal search. We set a
1086
			// flag here, so the callee knows how to deal with it.
1087
			if($iFoundItems <= $limit) {
1088
				$this->pagedSearchedSuccessful = true;
1089
			}
1090
		} else {
1091
			if(!is_null($limit)) {
1092
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
1093
			}
1094
		}
1095
		/* ++ Fixing RHDS searches with pages with zero results ++
1096
		 * Return cookie status. If we don't have more pages, with RHDS
1097
		 * cookie is null, with openldap cookie is an empty string and
1098
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1099
		 */
1100
		return !empty($cookie) || $cookie === '0';
1101
	}
1102
1103
	/**
1104
	 * executes an LDAP search, but counts the results only
1105
	 * @param string $filter the LDAP filter for the search
1106
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1107
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1108
	 * retrieved. Results will according to the order in the array.
1109
	 * @param int $limit optional, maximum results to be counted
1110
	 * @param int $offset optional, a starting point
1111
	 * @param bool $skipHandling indicates whether the pages search operation is
1112
	 * completed
1113
	 * @return int|false Integer or false if the search could not be initialized
1114
	 *
1115
	 */
1116
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1117
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1118
1119
		$limitPerPage = intval($this->connection->ldapPagingSize);
1120 View Code Duplication
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1121
			$limitPerPage = $limit;
1122
		}
1123
1124
		$counter = 0;
1125
		$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...
1126
		$this->connection->getConnectionResource();
1127
1128
		do {
1129
			$search = $this->executeSearch($filter, $base, $attr,
1130
										   $limitPerPage, $offset);
1131
			if($search === false) {
1132
				return $counter > 0 ? $counter : false;
1133
			}
1134
			list($sr, $pagedSearchOK) = $search;
1135
1136
			/* ++ Fixing RHDS searches with pages with zero results ++
1137
			 * countEntriesInSearchResults() method signature changed
1138
			 * by removing $limit and &$hasHitLimit parameters
1139
			 */
1140
			$count = $this->countEntriesInSearchResults($sr);
1141
			$counter += $count;
1142
1143
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1144
										$offset, $pagedSearchOK, $skipHandling);
1145
			$offset += $limitPerPage;
1146
			/* ++ Fixing RHDS searches with pages with zero results ++
1147
			 * Continue now depends on $hasMorePages value
1148
			 */
1149
			$continue = $pagedSearchOK && $hasMorePages;
1150
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1151
1152
		return $counter;
1153
	}
1154
1155
	/**
1156
	 * @param array $searchResults
1157
	 * @return int
1158
	 */
1159
	private function countEntriesInSearchResults($searchResults) {
1160
		$counter = 0;
1161
1162
		foreach($searchResults as $res) {
1163
			$count = intval($this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res));
1164
			$counter += $count;
1165
		}
1166
1167
		return $counter;
1168
	}
1169
1170
	/**
1171
	 * Executes an LDAP search
1172
	 * @param string $filter the LDAP filter for the search
1173
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1174
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1175
	 * @param int $limit
1176
	 * @param int $offset
1177
	 * @param bool $skipHandling
1178
	 * @return array with the search result
1179
	 */
1180
	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1181
		$limitPerPage = intval($this->connection->ldapPagingSize);
1182 View Code Duplication
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1183
			$limitPerPage = $limit;
1184
		}
1185
1186
		/* ++ Fixing RHDS searches with pages with zero results ++
1187
		 * As we can have pages with zero results and/or pages with less
1188
		 * than $limit results but with a still valid server 'cookie',
1189
		 * loops through until we get $continue equals true and
1190
		 * $findings['count'] < $limit
1191
		 */
1192
		$findings = array();
1193
		$savedoffset = $offset;
1194
		do {
1195
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1196
			if($search === false) {
1197
				return array();
1198
			}
1199
			list($sr, $pagedSearchOK) = $search;
1200
			$cr = $this->connection->getConnectionResource();
1201
1202
			if($skipHandling) {
1203
				//i.e. result do not need to be fetched, we just need the cookie
1204
				//thus pass 1 or any other value as $iFoundItems because it is not
1205
				//used
1206
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1207
								$offset, $pagedSearchOK,
1208
								$skipHandling);
1209
				return array();
1210
			}
1211
1212
			$iFoundItems = 0;
1213
			foreach($sr as $res) {
1214
				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1215
				$iFoundItems = max($iFoundItems, $findings['count']);
1216
				unset($findings['count']);
1217
			}
1218
1219
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1220
				$limitPerPage, $offset, $pagedSearchOK,
1221
										$skipHandling);
1222
			$offset += $limitPerPage;
1223
		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1224
		// reseting offset
1225
		$offset = $savedoffset;
1226
1227
		// if we're here, probably no connection resource is returned.
1228
		// to make Nextcloud behave nicely, we simply give back an empty array.
1229
		if(is_null($findings)) {
1230
			return array();
1231
		}
1232
1233
		if(!is_null($attr)) {
1234
			$selection = array();
1235
			$i = 0;
1236
			foreach($findings as $item) {
1237
				if(!is_array($item)) {
1238
					continue;
1239
				}
1240
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1241
				foreach($attr as $key) {
1242
					$key = mb_strtolower($key, 'UTF-8');
1243
					if(isset($item[$key])) {
1244
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1245
							unset($item[$key]['count']);
1246
						}
1247
						if($key !== 'dn') {
1248
							$selection[$i][$key] = $this->resemblesDN($key) ?
1249
								$this->helper->sanitizeDN($item[$key])
1250
								: $key === 'objectguid' || $key === 'guid' ?
1251
									$selection[$i][$key] = $this->convertObjectGUID2Str($item[$key])
1252
									: $item[$key];
1253
						} else {
1254
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1255
						}
1256
					}
1257
1258
				}
1259
				$i++;
1260
			}
1261
			$findings = $selection;
1262
		}
1263
		//we slice the findings, when
1264
		//a) paged search unsuccessful, though attempted
1265
		//b) no paged search, but limit set
1266
		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...
1267
			&& $pagedSearchOK)
1268
			|| (
1269
				!$pagedSearchOK
1270
				&& !is_null($limit)
1271
			)
1272
		) {
1273
			$findings = array_slice($findings, intval($offset), $limit);
1274
		}
1275
		return $findings;
1276
	}
1277
1278
	/**
1279
	 * @param string $name
1280
	 * @return bool|mixed|string
1281
	 */
1282
	public function sanitizeUsername($name) {
1283
		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...
1284
			return $name;
1285
		}
1286
1287
		// Transliteration
1288
		// latin characters to ASCII
1289
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1290
1291
		// Replacements
1292
		$name = str_replace(' ', '_', $name);
1293
1294
		// Every remaining disallowed characters will be removed
1295
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1296
1297
		return $name;
1298
	}
1299
1300
	/**
1301
	* escapes (user provided) parts for LDAP filter
1302
	* @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...
1303
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1304
	* @return string the escaped string
1305
	*/
1306
	public function escapeFilterPart($input, $allowAsterisk = false) {
1307
		$asterisk = '';
1308
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1309
			$asterisk = '*';
1310
			$input = mb_substr($input, 1, null, 'UTF-8');
1311
		}
1312
		$search  = array('*', '\\', '(', ')');
1313
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1314
		return $asterisk . str_replace($search, $replace, $input);
1315
	}
1316
1317
	/**
1318
	 * combines the input filters with AND
1319
	 * @param string[] $filters the filters to connect
1320
	 * @return string the combined filter
1321
	 */
1322
	public function combineFilterWithAnd($filters) {
1323
		return $this->combineFilter($filters, '&');
1324
	}
1325
1326
	/**
1327
	 * combines the input filters with OR
1328
	 * @param string[] $filters the filters to connect
1329
	 * @return string the combined filter
1330
	 * Combines Filter arguments with OR
1331
	 */
1332
	public function combineFilterWithOr($filters) {
1333
		return $this->combineFilter($filters, '|');
1334
	}
1335
1336
	/**
1337
	 * combines the input filters with given operator
1338
	 * @param string[] $filters the filters to connect
1339
	 * @param string $operator either & or |
1340
	 * @return string the combined filter
1341
	 */
1342
	private function combineFilter($filters, $operator) {
1343
		$combinedFilter = '('.$operator;
1344
		foreach($filters as $filter) {
1345
			if ($filter !== '' && $filter[0] !== '(') {
1346
				$filter = '('.$filter.')';
1347
			}
1348
			$combinedFilter.=$filter;
1349
		}
1350
		$combinedFilter.=')';
1351
		return $combinedFilter;
1352
	}
1353
1354
	/**
1355
	 * creates a filter part for to perform search for users
1356
	 * @param string $search the search term
1357
	 * @return string the final filter part to use in LDAP searches
1358
	 */
1359
	public function getFilterPartForUserSearch($search) {
1360
		return $this->getFilterPartForSearch($search,
1361
			$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...
1362
			$this->connection->ldapUserDisplayName);
1363
	}
1364
1365
	/**
1366
	 * creates a filter part for to perform search for groups
1367
	 * @param string $search the search term
1368
	 * @return string the final filter part to use in LDAP searches
1369
	 */
1370
	public function getFilterPartForGroupSearch($search) {
1371
		return $this->getFilterPartForSearch($search,
1372
			$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...
1373
			$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...
1374
	}
1375
1376
	/**
1377
	 * creates a filter part for searches by splitting up the given search
1378
	 * string into single words
1379
	 * @param string $search the search term
1380
	 * @param string[] $searchAttributes needs to have at least two attributes,
1381
	 * otherwise it does not make sense :)
1382
	 * @return string the final filter part to use in LDAP searches
1383
	 * @throws \Exception
1384
	 */
1385
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1386
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1387
			throw new \Exception('searchAttributes must be an array with at least two string');
1388
		}
1389
		$searchWords = explode(' ', trim($search));
1390
		$wordFilters = array();
1391
		foreach($searchWords as $word) {
1392
			$word = $this->prepareSearchTerm($word);
1393
			//every word needs to appear at least once
1394
			$wordMatchOneAttrFilters = array();
1395
			foreach($searchAttributes as $attr) {
1396
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1397
			}
1398
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1399
		}
1400
		return $this->combineFilterWithAnd($wordFilters);
1401
	}
1402
1403
	/**
1404
	 * creates a filter part for searches
1405
	 * @param string $search the search term
1406
	 * @param string[]|null $searchAttributes
1407
	 * @param string $fallbackAttribute a fallback attribute in case the user
1408
	 * did not define search attributes. Typically the display name attribute.
1409
	 * @return string the final filter part to use in LDAP searches
1410
	 */
1411
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1412
		$filter = array();
1413
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1414
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1415
			try {
1416
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1417
			} catch(\Exception $e) {
1418
				\OCP\Util::writeLog(
1419
					'user_ldap',
1420
					'Creating advanced filter for search failed, falling back to simple method.',
1421
					\OCP\Util::INFO
1422
				);
1423
			}
1424
		}
1425
1426
		$search = $this->prepareSearchTerm($search);
1427
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1428
			if ($fallbackAttribute === '') {
1429
				return '';
1430
			}
1431
			$filter[] = $fallbackAttribute . '=' . $search;
1432
		} else {
1433
			foreach($searchAttributes as $attribute) {
1434
				$filter[] = $attribute . '=' . $search;
1435
			}
1436
		}
1437
		if(count($filter) === 1) {
1438
			return '('.$filter[0].')';
1439
		}
1440
		return $this->combineFilterWithOr($filter);
1441
	}
1442
1443
	/**
1444
	 * returns the search term depending on whether we are allowed
1445
	 * list users found by ldap with the current input appended by
1446
	 * a *
1447
	 * @return string
1448
	 */
1449
	private function prepareSearchTerm($term) {
1450
		$config = \OC::$server->getConfig();
1451
1452
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1453
1454
		$result = $term;
1455
		if ($term === '') {
1456
			$result = '*';
1457
		} else if ($allowEnum !== 'no') {
1458
			$result = $term . '*';
1459
		}
1460
		return $result;
1461
	}
1462
1463
	/**
1464
	 * returns the filter used for counting users
1465
	 * @return string
1466
	 */
1467
	public function getFilterForUserCount() {
1468
		$filter = $this->combineFilterWithAnd(array(
1469
			$this->connection->ldapUserFilter,
1470
			$this->connection->ldapUserDisplayName . '=*'
1471
		));
1472
1473
		return $filter;
1474
	}
1475
1476
	/**
1477
	 * @param string $name
1478
	 * @param string $password
1479
	 * @return bool
1480
	 */
1481
	public function areCredentialsValid($name, $password) {
1482
		$name = $this->helper->DNasBaseParameter($name);
1483
		$testConnection = clone $this->connection;
1484
		$credentials = array(
1485
			'ldapAgentName' => $name,
1486
			'ldapAgentPassword' => $password
1487
		);
1488
		if(!$testConnection->setConfiguration($credentials)) {
1489
			return false;
1490
		}
1491
		return $testConnection->bind();
1492
	}
1493
1494
	/**
1495
	 * reverse lookup of a DN given a known UUID
1496
	 *
1497
	 * @param string $uuid
1498
	 * @return string
1499
	 * @throws \Exception
1500
	 */
1501
	public function getUserDnByUuid($uuid) {
1502
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1503
		$filter       = $this->connection->ldapUserFilter;
1504
		$base         = $this->connection->ldapBaseUsers;
1505
1506
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1507
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1508
			// existing DN to be able to reliably detect it.
1509
			$result = $this->search($filter, $base, ['dn'], 1);
1510
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1511
				throw new \Exception('Cannot determine UUID attribute');
1512
			}
1513
			$dn = $result[0]['dn'][0];
1514
			if(!$this->detectUuidAttribute($dn, true)) {
1515
				throw new \Exception('Cannot determine UUID attribute');
1516
			}
1517
		} else {
1518
			// The UUID attribute is either known or an override is given.
1519
			// By calling this method we ensure that $this->connection->$uuidAttr
1520
			// is definitely set
1521
			if(!$this->detectUuidAttribute('', true)) {
1522
				throw new \Exception('Cannot determine UUID attribute');
1523
			}
1524
		}
1525
1526
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1527
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1528
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1529
		}
1530
1531
		$filter = $uuidAttr . '=' . $uuid;
1532
		$result = $this->searchUsers($filter, ['dn'], 2);
1533
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1534
			// we put the count into account to make sure that this is
1535
			// really unique
1536
			return $result[0]['dn'][0];
1537
		}
1538
1539
		throw new \Exception('Cannot determine UUID attribute');
1540
	}
1541
1542
	/**
1543
	 * auto-detects the directory's UUID attribute
1544
	 *
1545
	 * @param string $dn a known DN used to check against
1546
	 * @param bool $isUser
1547
	 * @param bool $force the detection should be run, even if it is not set to auto
1548
	 * @param array|null $ldapRecord
1549
	 * @return bool true on success, false otherwise
1550
	 */
1551
	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1552 View Code Duplication
		if($isUser) {
1553
			$uuidAttr     = 'ldapUuidUserAttribute';
1554
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1555
		} else {
1556
			$uuidAttr     = 'ldapUuidGroupAttribute';
1557
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1558
		}
1559
1560
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1561
			return true;
1562
		}
1563
1564
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1565
			$this->connection->$uuidAttr = $uuidOverride;
1566
			return true;
1567
		}
1568
1569
		foreach(self::UUID_ATTRIBUTES as $attribute) {
1570
			if($ldapRecord !== null) {
1571
				// we have the info from LDAP already, we don't need to talk to the server again
1572
				if(isset($ldapRecord[$attribute])) {
1573
					$this->connection->$uuidAttr = $attribute;
1574
					return true;
1575
				} else {
1576
					continue;
1577
				}
1578
			}
1579
1580
			$value = $this->readAttribute($dn, $attribute);
1581
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1582
				\OCP\Util::writeLog('user_ldap',
1583
									'Setting '.$attribute.' as '.$uuidAttr,
1584
									\OCP\Util::DEBUG);
1585
				$this->connection->$uuidAttr = $attribute;
1586
				return true;
1587
			}
1588
		}
1589
		\OCP\Util::writeLog('user_ldap',
1590
							'Could not autodetect the UUID attribute',
1591
							\OCP\Util::ERROR);
1592
1593
		return false;
1594
	}
1595
1596
	/**
1597
	 * @param string $dn
1598
	 * @param bool $isUser
1599
	 * @param null $ldapRecord
1600
	 * @return bool|string
1601
	 */
1602
	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1603 View Code Duplication
		if($isUser) {
1604
			$uuidAttr     = 'ldapUuidUserAttribute';
1605
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1606
		} else {
1607
			$uuidAttr     = 'ldapUuidGroupAttribute';
1608
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1609
		}
1610
1611
		$uuid = false;
1612
		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1613
			$attr = $this->connection->$uuidAttr;
1614
			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1615
			if( !is_array($uuid)
1616
				&& $uuidOverride !== ''
1617
				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1618
			{
1619
				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1620
					? $ldapRecord[$this->connection->$uuidAttr]
1621
					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1622
			}
1623
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1624
				$uuid = $uuid[0];
1625
			}
1626
		}
1627
1628
		return $uuid;
1629
	}
1630
1631
	/**
1632
	 * converts a binary ObjectGUID into a string representation
1633
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1634
	 * @return string
1635
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1636
	 */
1637
	private function convertObjectGUID2Str($oguid) {
1638
		$hex_guid = bin2hex($oguid);
1639
		$hex_guid_to_guid_str = '';
1640 View Code Duplication
		for($k = 1; $k <= 4; ++$k) {
1641
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1642
		}
1643
		$hex_guid_to_guid_str .= '-';
1644 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1645
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1646
		}
1647
		$hex_guid_to_guid_str .= '-';
1648 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1649
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1650
		}
1651
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1652
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1653
1654
		return strtoupper($hex_guid_to_guid_str);
1655
	}
1656
1657
	/**
1658
	 * the first three blocks of the string-converted GUID happen to be in
1659
	 * reverse order. In order to use it in a filter, this needs to be
1660
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1661
	 * to every two hax figures.
1662
	 *
1663
	 * If an invalid string is passed, it will be returned without change.
1664
	 *
1665
	 * @param string $guid
1666
	 * @return string
1667
	 */
1668
	public function formatGuid2ForFilterUser($guid) {
1669
		if(!is_string($guid)) {
1670
			throw new \InvalidArgumentException('String expected');
1671
		}
1672
		$blocks = explode('-', $guid);
1673 View Code Duplication
		if(count($blocks) !== 5) {
1674
			/*
1675
			 * Why not throw an Exception instead? This method is a utility
1676
			 * called only when trying to figure out whether a "missing" known
1677
			 * LDAP user was or was not renamed on the LDAP server. And this
1678
			 * even on the use case that a reverse lookup is needed (UUID known,
1679
			 * not DN), i.e. when finding users (search dialog, users page,
1680
			 * login, …) this will not be fired. This occurs only if shares from
1681
			 * a users are supposed to be mounted who cannot be found. Throwing
1682
			 * an exception here would kill the experience for a valid, acting
1683
			 * user. Instead we write a log message.
1684
			 */
1685
			\OC::$server->getLogger()->info(
1686
				'Passed string does not resemble a valid GUID. Known UUID ' .
1687
				'({uuid}) probably does not match UUID configuration.',
1688
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1689
			);
1690
			return $guid;
1691
		}
1692 View Code Duplication
		for($i=0; $i < 3; $i++) {
1693
			$pairs = str_split($blocks[$i], 2);
1694
			$pairs = array_reverse($pairs);
1695
			$blocks[$i] = implode('', $pairs);
1696
		}
1697 View Code Duplication
		for($i=0; $i < 5; $i++) {
1698
			$pairs = str_split($blocks[$i], 2);
1699
			$blocks[$i] = '\\' . implode('\\', $pairs);
1700
		}
1701
		return implode('', $blocks);
1702
	}
1703
1704
	/**
1705
	 * gets a SID of the domain of the given dn
1706
	 * @param string $dn
1707
	 * @return string|bool
1708
	 */
1709
	public function getSID($dn) {
1710
		$domainDN = $this->getDomainDNFromDN($dn);
1711
		$cacheKey = 'getSID-'.$domainDN;
1712
		$sid = $this->connection->getFromCache($cacheKey);
1713
		if(!is_null($sid)) {
1714
			return $sid;
1715
		}
1716
1717
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1718
		if(!is_array($objectSid) || empty($objectSid)) {
1719
			$this->connection->writeToCache($cacheKey, false);
1720
			return false;
1721
		}
1722
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1723
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1724
1725
		return $domainObjectSid;
1726
	}
1727
1728
	/**
1729
	 * converts a binary SID into a string representation
1730
	 * @param string $sid
1731
	 * @return string
1732
	 */
1733
	public function convertSID2Str($sid) {
1734
		// The format of a SID binary string is as follows:
1735
		// 1 byte for the revision level
1736
		// 1 byte for the number n of variable sub-ids
1737
		// 6 bytes for identifier authority value
1738
		// n*4 bytes for n sub-ids
1739
		//
1740
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1741
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1742
		$revision = ord($sid[0]);
1743
		$numberSubID = ord($sid[1]);
1744
1745
		$subIdStart = 8; // 1 + 1 + 6
1746
		$subIdLength = 4;
1747
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1748
			// Incorrect number of bytes present.
1749
			return '';
1750
		}
1751
1752
		// 6 bytes = 48 bits can be represented using floats without loss of
1753
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1754
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1755
1756
		$subIDs = array();
1757
		for ($i = 0; $i < $numberSubID; $i++) {
1758
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1759
			$subIDs[] = sprintf('%u', $subID[1]);
1760
		}
1761
1762
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1763
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1764
	}
1765
1766
	/**
1767
	 * checks if the given DN is part of the given base DN(s)
1768
	 * @param string $dn the DN
1769
	 * @param string[] $bases array containing the allowed base DN or DNs
1770
	 * @return bool
1771
	 */
1772
	public function isDNPartOfBase($dn, $bases) {
1773
		$belongsToBase = false;
1774
		$bases = $this->helper->sanitizeDN($bases);
1775
1776
		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...
1777
			$belongsToBase = true;
1778
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1779
				$belongsToBase = false;
1780
			}
1781
			if($belongsToBase) {
1782
				break;
1783
			}
1784
		}
1785
		return $belongsToBase;
1786
	}
1787
1788
	/**
1789
	 * resets a running Paged Search operation
1790
	 */
1791
	private function abandonPagedSearch() {
1792
		if($this->connection->hasPagedResultSupport) {
1793
			$cr = $this->connection->getConnectionResource();
1794
			$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1795
			$this->getPagedSearchResultState();
1796
			$this->lastCookie = '';
1797
			$this->cookies = array();
1798
		}
1799
	}
1800
1801
	/**
1802
	 * get a cookie for the next LDAP paged search
1803
	 * @param string $base a string with the base DN for the search
1804
	 * @param string $filter the search filter to identify the correct search
1805
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1806
	 * @param int $offset the offset for the new search to identify the correct search really good
1807
	 * @return string containing the key or empty if none is cached
1808
	 */
1809
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1810
		if($offset === 0) {
1811
			return '';
1812
		}
1813
		$offset -= $limit;
1814
		//we work with cache here
1815
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1816
		$cookie = '';
1817
		if(isset($this->cookies[$cacheKey])) {
1818
			$cookie = $this->cookies[$cacheKey];
1819
			if(is_null($cookie)) {
1820
				$cookie = '';
1821
			}
1822
		}
1823
		return $cookie;
1824
	}
1825
1826
	/**
1827
	 * checks whether an LDAP paged search operation has more pages that can be
1828
	 * retrieved, typically when offset and limit are provided.
1829
	 *
1830
	 * Be very careful to use it: the last cookie value, which is inspected, can
1831
	 * be reset by other operations. Best, call it immediately after a search(),
1832
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1833
	 * well. Don't rely on it with any fetchList-method.
1834
	 * @return bool
1835
	 */
1836
	public function hasMoreResults() {
1837
		if(!$this->connection->hasPagedResultSupport) {
1838
			return false;
1839
		}
1840
1841
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1842
			// as in RFC 2696, when all results are returned, the cookie will
1843
			// be empty.
1844
			return false;
1845
		}
1846
1847
		return true;
1848
	}
1849
1850
	/**
1851
	 * set a cookie for LDAP paged search run
1852
	 * @param string $base a string with the base DN for the search
1853
	 * @param string $filter the search filter to identify the correct search
1854
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1855
	 * @param int $offset the offset for the run search to identify the correct search really good
1856
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1857
	 * @return void
1858
	 */
1859
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1860
		// allow '0' for 389ds
1861
		if(!empty($cookie) || $cookie === '0') {
1862
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1863
			$this->cookies[$cacheKey] = $cookie;
1864
			$this->lastCookie = $cookie;
1865
		}
1866
	}
1867
1868
	/**
1869
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1870
	 * @return boolean|null true on success, null or false otherwise
1871
	 */
1872
	public function getPagedSearchResultState() {
1873
		$result = $this->pagedSearchedSuccessful;
1874
		$this->pagedSearchedSuccessful = null;
1875
		return $result;
1876
	}
1877
1878
	/**
1879
	 * Prepares a paged search, if possible
1880
	 * @param string $filter the LDAP filter for the search
1881
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1882
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1883
	 * @param int $limit
1884
	 * @param int $offset
1885
	 * @return bool|true
1886
	 */
1887
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1888
		$pagedSearchOK = false;
1889
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1890
			$offset = intval($offset); //can be null
1891
			\OCP\Util::writeLog('user_ldap',
1892
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1893
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1894
				\OCP\Util::DEBUG);
1895
			//get the cookie from the search for the previous search, required by LDAP
1896
			foreach($bases as $base) {
1897
1898
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1899
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1900
					// no cookie known from a potential previous search. We need
1901
					// to start from 0 to come to the desired page. cookie value
1902
					// of '0' is valid, because 389ds
1903
					$reOffset = 0;
1904
					while($reOffset < $offset) {
1905
						$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1906
						$reOffset += $limit;
1907
					}
1908
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1909
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1910
					// '0' is valid, because 389ds
1911
					//TODO: remember this, probably does not change in the next request...
1912
					if(empty($cookie) && $cookie !== '0') {
1913
						$cookie = null;
1914
					}
1915
				}
1916
				if(!is_null($cookie)) {
1917
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1918
					$this->abandonPagedSearch();
1919
					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1920
						$this->connection->getConnectionResource(), $limit,
1921
						false, $cookie);
1922
					if(!$pagedSearchOK) {
1923
						return false;
1924
					}
1925
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1926
				} else {
1927
					\OCP\Util::writeLog('user_ldap',
1928
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1929
						\OCP\Util::INFO);
1930
				}
1931
1932
			}
1933
		/* ++ Fixing RHDS searches with pages with zero results ++
1934
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1935
		 * due to pages with zero results.
1936
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1937
		 * if we don't have a previous paged search.
1938
		 */
1939
		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1940
			// a search without limit was requested. However, if we do use
1941
			// Paged Search once, we always must do it. This requires us to
1942
			// initialize it with the configured page size.
1943
			$this->abandonPagedSearch();
1944
			// in case someone set it to 0 … use 500, otherwise no results will
1945
			// be returned.
1946
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1947
			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1948
				$this->connection->getConnectionResource(),
1949
				$pageSize, false, '');
1950
		}
1951
1952
		return $pagedSearchOK;
1953
	}
1954
1955
}
1956