Passed
Push — master ( bffb34...3e5174 )
by Blizzz
12:26 queued 12s
created

Access::dn2groupname()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Aaron Wood <[email protected]>
6
 * @author Alexander Bergolth <[email protected]>
7
 * @author Andreas Fischer <[email protected]>
8
 * @author Arthur Schiwon <[email protected]>
9
 * @author Bart Visscher <[email protected]>
10
 * @author Benjamin Diele <[email protected]>
11
 * @author 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 OC\Hooks\PublicEmitter;
48
use OCA\User_LDAP\Exceptions\ConstraintViolationException;
49
use OCA\User_LDAP\User\Manager;
50
use OCA\User_LDAP\User\OfflineUser;
51
use OCA\User_LDAP\Mapping\AbstractMapping;
52
use OC\ServerNotAvailableException;
53
use OCP\IConfig;
54
use OCP\ILogger;
55
use OCP\IUserManager;
56
57
/**
58
 * Class Access
59
 * @package OCA\User_LDAP
60
 */
61
class Access extends LDAPUtility {
62
	const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
63
64
	/** @var \OCA\User_LDAP\Connection */
65
	public $connection;
66
	/** @var Manager */
67
	public $userManager;
68
	//never ever check this var directly, always use getPagedSearchResultState
69
	protected $pagedSearchedSuccessful;
70
71
	/**
72
	 * @var string[] $cookies an array of returned Paged Result cookies
73
	 */
74
	protected $cookies = array();
75
76
	/**
77
	 * @var string $lastCookie the last cookie returned from a Paged Results
78
	 * operation, defaults to an empty string
79
	 */
80
	protected $lastCookie = '';
81
82
	/**
83
	 * @var AbstractMapping $userMapper
84
	 */
85
	protected $userMapper;
86
87
	/**
88
	* @var AbstractMapping $userMapper
89
	*/
90
	protected $groupMapper;
91
92
	/**
93
	 * @var \OCA\User_LDAP\Helper
94
	 */
95
	private $helper;
96
	/** @var IConfig */
97
	private $config;
98
	/** @var IUserManager */
99
	private $ncUserManager;
100
101
	public function __construct(
102
		Connection $connection,
103
		ILDAPWrapper $ldap,
104
		Manager $userManager,
105
		Helper $helper,
106
		IConfig $config,
107
		IUserManager $ncUserManager
108
	) {
109
		parent::__construct($ldap);
110
		$this->connection = $connection;
111
		$this->userManager = $userManager;
112
		$this->userManager->setLdapAccess($this);
113
		$this->helper = $helper;
114
		$this->config = $config;
115
		$this->ncUserManager = $ncUserManager;
116
	}
117
118
	/**
119
	 * sets the User Mapper
120
	 * @param AbstractMapping $mapper
121
	 */
122
	public function setUserMapper(AbstractMapping $mapper) {
123
		$this->userMapper = $mapper;
124
	}
125
126
	/**
127
	 * returns the User Mapper
128
	 * @throws \Exception
129
	 * @return AbstractMapping
130
	 */
131
	public function getUserMapper() {
132
		if(is_null($this->userMapper)) {
133
			throw new \Exception('UserMapper was not assigned to this Access instance.');
134
		}
135
		return $this->userMapper;
136
	}
137
138
	/**
139
	 * sets the Group Mapper
140
	 * @param AbstractMapping $mapper
141
	 */
142
	public function setGroupMapper(AbstractMapping $mapper) {
143
		$this->groupMapper = $mapper;
144
	}
145
146
	/**
147
	 * returns the Group Mapper
148
	 * @throws \Exception
149
	 * @return AbstractMapping
150
	 */
151
	public function getGroupMapper() {
152
		if(is_null($this->groupMapper)) {
153
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
154
		}
155
		return $this->groupMapper;
156
	}
157
158
	/**
159
	 * @return bool
160
	 */
161
	private function checkConnection() {
162
		return ($this->connection instanceof Connection);
163
	}
164
165
	/**
166
	 * returns the Connection instance
167
	 * @return \OCA\User_LDAP\Connection
168
	 */
169
	public function getConnection() {
170
		return $this->connection;
171
	}
172
173
	/**
174
	 * reads a given attribute for an LDAP record identified by a DN
175
	 *
176
	 * @param string $dn the record in question
177
	 * @param string $attr the attribute that shall be retrieved
178
	 *        if empty, just check the record's existence
179
	 * @param string $filter
180
	 * @return array|false an array of values on success or an empty
181
	 *          array if $attr is empty, false otherwise
182
	 * @throws ServerNotAvailableException
183
	 */
184
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
185
		if(!$this->checkConnection()) {
186
			\OCP\Util::writeLog('user_ldap',
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

186
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap',

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

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

Loading history...
187
				'No LDAP Connector assigned, access impossible for readAttribute.',
188
				ILogger::WARN);
189
			return false;
190
		}
191
		$cr = $this->connection->getConnectionResource();
192
		if(!$this->ldap->isResource($cr)) {
193
			//LDAP not available
194
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

194
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);

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

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

Loading history...
195
			return false;
196
		}
197
		//Cancel possibly running Paged Results operation, otherwise we run in
198
		//LDAP protocol errors
199
		$this->abandonPagedSearch();
200
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
201
		// but does not hurt either.
202
		$pagingSize = (int)$this->connection->ldapPagingSize;
203
		// 0 won't result in replies, small numbers may leave out groups
204
		// (cf. #12306), 500 is default for paging and should work everywhere.
205
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
206
		$attr = mb_strtolower($attr, 'UTF-8');
207
		// the actual read attribute later may contain parameters on a ranged
208
		// request, e.g. member;range=99-199. Depends on server reply.
209
		$attrToRead = $attr;
210
211
		$values = [];
212
		$isRangeRequest = false;
213
		do {
214
			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
215
			if(is_bool($result)) {
216
				// when an exists request was run and it was successful, an empty
217
				// array must be returned
218
				return $result ? [] : false;
219
			}
220
221
			if (!$isRangeRequest) {
222
				$values = $this->extractAttributeValuesFromResult($result, $attr);
223
				if (!empty($values)) {
224
					return $values;
225
				}
226
			}
227
228
			$isRangeRequest = false;
229
			$result = $this->extractRangeData($result, $attr);
230
			if (!empty($result)) {
231
				$normalizedResult = $this->extractAttributeValuesFromResult(
232
					[ $attr => $result['values'] ],
233
					$attr
234
				);
235
				$values = array_merge($values, $normalizedResult);
236
237
				if($result['rangeHigh'] === '*') {
238
					// when server replies with * as high range value, there are
239
					// no more results left
240
					return $values;
241
				} else {
242
					$low  = $result['rangeHigh'] + 1;
243
					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
244
					$isRangeRequest = true;
245
				}
246
			}
247
		} while($isRangeRequest);
248
249
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

249
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);

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

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

Loading history...
250
		return false;
251
	}
252
253
	/**
254
	 * Runs an read operation against LDAP
255
	 *
256
	 * @param resource $cr the LDAP connection
257
	 * @param string $dn
258
	 * @param string $attribute
259
	 * @param string $filter
260
	 * @param int $maxResults
261
	 * @return array|bool false if there was any error, true if an exists check
262
	 *                    was performed and the requested DN found, array with the
263
	 *                    returned data on a successful usual operation
264
	 * @throws ServerNotAvailableException
265
	 */
266
	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
267
		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
268
		$dn = $this->helper->DNasBaseParameter($dn);
269
		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
270
		if (!$this->ldap->isResource($rr)) {
271
			if ($attribute !== '') {
272
				//do not throw this message on userExists check, irritates
273
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

273
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);

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

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

Loading history...
274
			}
275
			//in case an error occurs , e.g. object does not exist
276
			return false;
277
		}
278
		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
279
			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

279
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);

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

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

Loading history...
280
			return true;
281
		}
282
		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
283
		if (!$this->ldap->isResource($er)) {
284
			//did not match the filter, return false
285
			return false;
286
		}
287
		//LDAP attributes are not case sensitive
288
		$result = \OCP\Util::mb_array_change_key_case(
289
			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
290
291
		return $result;
292
	}
293
294
	/**
295
	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
296
	 * data if present.
297
	 *
298
	 * @param array $result from ILDAPWrapper::getAttributes()
299
	 * @param string $attribute the attribute name that was read
300
	 * @return string[]
301
	 */
302
	public function extractAttributeValuesFromResult($result, $attribute) {
303
		$values = [];
304
		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
305
			$lowercaseAttribute = strtolower($attribute);
306
			for($i=0;$i<$result[$attribute]['count'];$i++) {
307
				if($this->resemblesDN($attribute)) {
308
					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
309
				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
310
					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
311
				} else {
312
					$values[] = $result[$attribute][$i];
313
				}
314
			}
315
		}
316
		return $values;
317
	}
318
319
	/**
320
	 * Attempts to find ranged data in a getAttribute results and extracts the
321
	 * returned values as well as information on the range and full attribute
322
	 * name for further processing.
323
	 *
324
	 * @param array $result from ILDAPWrapper::getAttributes()
325
	 * @param string $attribute the attribute name that was read. Without ";range=…"
326
	 * @return array If a range was detected with keys 'values', 'attributeName',
327
	 *               'attributeFull' and 'rangeHigh', otherwise empty.
328
	 */
329
	public function extractRangeData($result, $attribute) {
330
		$keys = array_keys($result);
331
		foreach($keys as $key) {
332
			if($key !== $attribute && strpos($key, $attribute) === 0) {
333
				$queryData = explode(';', $key);
334
				if(strpos($queryData[1], 'range=') === 0) {
335
					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
336
					$data = [
337
						'values' => $result[$key],
338
						'attributeName' => $queryData[0],
339
						'attributeFull' => $key,
340
						'rangeHigh' => $high,
341
					];
342
					return $data;
343
				}
344
			}
345
		}
346
		return [];
347
	}
348
	
349
	/**
350
	 * Set password for an LDAP user identified by a DN
351
	 *
352
	 * @param string $userDN the user in question
353
	 * @param string $password the new password
354
	 * @return bool
355
	 * @throws HintException
356
	 * @throws \Exception
357
	 */
358
	public function setPassword($userDN, $password) {
359
		if((int)$this->connection->turnOnPasswordChange !== 1) {
360
			throw new \Exception('LDAP password changes are disabled.');
361
		}
362
		$cr = $this->connection->getConnectionResource();
363
		if(!$this->ldap->isResource($cr)) {
364
			//LDAP not available
365
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

365
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);

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

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

Loading history...
366
			return false;
367
		}
368
		try {
369
			return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
370
		} catch(ConstraintViolationException $e) {
371
			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
372
		}
373
	}
374
375
	/**
376
	 * checks whether the given attributes value is probably a DN
377
	 * @param string $attr the attribute in question
378
	 * @return boolean if so true, otherwise false
379
	 */
380
	private function resemblesDN($attr) {
381
		$resemblingAttributes = array(
382
			'dn',
383
			'uniquemember',
384
			'member',
385
			// memberOf is an "operational" attribute, without a definition in any RFC
386
			'memberof'
387
		);
388
		return in_array($attr, $resemblingAttributes);
389
	}
390
391
	/**
392
	 * checks whether the given string is probably a DN
393
	 * @param string $string
394
	 * @return boolean
395
	 */
396
	public function stringResemblesDN($string) {
397
		$r = $this->ldap->explodeDN($string, 0);
398
		// if exploding a DN succeeds and does not end up in
399
		// an empty array except for $r[count] being 0.
400
		return (is_array($r) && count($r) > 1);
401
	}
402
403
	/**
404
	 * returns a DN-string that is cleaned from not domain parts, e.g.
405
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
406
	 * becomes dc=foobar,dc=server,dc=org
407
	 * @param string $dn
408
	 * @return string
409
	 */
410
	public function getDomainDNFromDN($dn) {
411
		$allParts = $this->ldap->explodeDN($dn, 0);
412
		if($allParts === false) {
413
			//not a valid DN
414
			return '';
415
		}
416
		$domainParts = array();
417
		$dcFound = false;
418
		foreach($allParts as $part) {
419
			if(!$dcFound && strpos($part, 'dc=') === 0) {
420
				$dcFound = true;
421
			}
422
			if($dcFound) {
423
				$domainParts[] = $part;
424
			}
425
		}
426
		return implode(',', $domainParts);
427
	}
428
429
	/**
430
	 * returns the LDAP DN for the given internal Nextcloud name of the group
431
	 * @param string $name the Nextcloud name in question
432
	 * @return string|false LDAP DN on success, otherwise false
433
	 */
434
	public function groupname2dn($name) {
435
		return $this->groupMapper->getDNByName($name);
436
	}
437
438
	/**
439
	 * returns the LDAP DN for the given internal Nextcloud name of the user
440
	 * @param string $name the Nextcloud name in question
441
	 * @return string|false with the LDAP DN on success, otherwise false
442
	 */
443
	public function username2dn($name) {
444
		$fdn = $this->userMapper->getDNByName($name);
445
446
		//Check whether the DN belongs to the Base, to avoid issues on multi-
447
		//server setups
448
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
449
			return $fdn;
450
		}
451
452
		return false;
453
	}
454
455
	/**
456
	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
457
	 * @param string $fdn the dn of the group object
458
	 * @param string $ldapName optional, the display name of the object
459
	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
460
	 */
461
	public function dn2groupname($fdn, $ldapName = null) {
462
		//To avoid bypassing the base DN settings under certain circumstances
463
		//with the group support, check whether the provided DN matches one of
464
		//the given Bases
465
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
466
			return false;
467
		}
468
469
		return $this->dn2ocname($fdn, $ldapName, false);
470
	}
471
472
	/**
473
	 * accepts an array of group DNs and tests whether they match the user
474
	 * filter by doing read operations against the group entries. Returns an
475
	 * array of DNs that match the filter.
476
	 *
477
	 * @param string[] $groupDNs
478
	 * @return string[]
479
	 * @throws ServerNotAvailableException
480
	 */
481
	public function groupsMatchFilter($groupDNs) {
482
		$validGroupDNs = [];
483
		foreach($groupDNs as $dn) {
484
			$cacheKey = 'groupsMatchFilter-'.$dn;
485
			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
486
			if(!is_null($groupMatchFilter)) {
487
				if($groupMatchFilter) {
488
					$validGroupDNs[] = $dn;
489
				}
490
				continue;
491
			}
492
493
			// Check the base DN first. If this is not met already, we don't
494
			// need to ask the server at all.
495
			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
496
				$this->connection->writeToCache($cacheKey, false);
497
				continue;
498
			}
499
500
			$result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
501
			if(is_array($result)) {
502
				$this->connection->writeToCache($cacheKey, true);
503
				$validGroupDNs[] = $dn;
504
			} else {
505
				$this->connection->writeToCache($cacheKey, false);
506
			}
507
508
		}
509
		return $validGroupDNs;
510
	}
511
512
	/**
513
	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
514
	 * @param string $dn the dn of the user object
515
	 * @param string $ldapName optional, the display name of the object
516
	 * @return string|false with with the name to use in Nextcloud
517
	 */
518
	public function dn2username($fdn, $ldapName = null) {
519
		//To avoid bypassing the base DN settings under certain circumstances
520
		//with the group support, check whether the provided DN matches one of
521
		//the given Bases
522
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
523
			return false;
524
		}
525
526
		return $this->dn2ocname($fdn, $ldapName, true);
527
	}
528
529
	/**
530
	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
531
	 *
532
	 * @param string $fdn the dn of the user object
533
	 * @param string|null $ldapName optional, the display name of the object
534
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
535
	 * @param bool|null $newlyMapped
536
	 * @param array|null $record
537
	 * @return false|string with with the name to use in Nextcloud
538
	 * @throws \Exception
539
	 */
540
	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
541
		$newlyMapped = false;
542
		if($isUser) {
543
			$mapper = $this->getUserMapper();
544
			$nameAttribute = $this->connection->ldapUserDisplayName;
545
			$filter = $this->connection->ldapUserFilter;
546
		} else {
547
			$mapper = $this->getGroupMapper();
548
			$nameAttribute = $this->connection->ldapGroupDisplayName;
549
			$filter = $this->connection->ldapGroupFilter;
550
		}
551
552
		//let's try to retrieve the Nextcloud name from the mappings table
553
		$ncName = $mapper->getNameByDN($fdn);
554
		if(is_string($ncName)) {
555
			return $ncName;
556
		}
557
558
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
559
		$uuid = $this->getUUID($fdn, $isUser, $record);
560
		if(is_string($uuid)) {
561
			$ncName = $mapper->getNameByUUID($uuid);
562
			if(is_string($ncName)) {
563
				$mapper->setDNbyUUID($fdn, $uuid);
564
				return $ncName;
565
			}
566
		} else {
567
			//If the UUID can't be detected something is foul.
568
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

568
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);

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

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

Loading history...
569
			return false;
570
		}
571
572
		if(is_null($ldapName)) {
573
			$ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
574
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
575
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

575
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);

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

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

Loading history...
576
				return false;
577
			}
578
			$ldapName = $ldapName[0];
579
		}
580
581
		if($isUser) {
582
			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapExpertUsernameAttr does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
583
			if ($usernameAttribute !== '') {
584
				$username = $this->readAttribute($fdn, $usernameAttribute);
585
				$username = $username[0];
586
			} else {
587
				$username = $uuid;
588
			}
589
			try {
590
				$intName = $this->sanitizeUsername($username);
591
			} catch (\InvalidArgumentException $e) {
592
				\OC::$server->getLogger()->logException($e, [
593
					'app' => 'user_ldap',
594
					'level' => ILogger::WARN,
595
				]);
596
				// we don't attempt to set a username here. We can go for
597
				// for an alternative 4 digit random number as we would append
598
				// otherwise, however it's likely not enough space in bigger
599
				// setups, and most importantly: this is not intended.
600
				return false;
601
			}
602
		} else {
603
			$intName = $ldapName;
604
		}
605
606
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
607
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
608
		//NOTE: mind, disabling cache affects only this instance! Using it
609
		// outside of core user management will still cache the user as non-existing.
610
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapCacheTTL does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
611
		$this->connection->setConfiguration(['ldapCacheTTL' => 0]);
612
		if( $intName !== ''
613
			&& (($isUser && !$this->ncUserManager->userExists($intName))
614
				|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
615
			)
616
		) {
617
			$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
618
			$newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
619
			if($newlyMapped) {
620
				return $intName;
621
			}
622
		}
623
624
		$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
625
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
626
		if (is_string($altName)) {
627
			if($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
628
				$newlyMapped = true;
629
				return $altName;
630
			}
631
		}
632
633
		//if everything else did not help..
634
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

634
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);

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

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

Loading history...
635
		return false;
636
	}
637
638
	protected function mapAndAnnounceIfApplicable(
639
		AbstractMapping $mapper,
640
		string $fdn,
641
		string $name,
642
		string $uuid,
643
		bool $isUser
644
	) :bool {
645
		if($mapper->map($fdn, $name, $uuid)) {
646
			if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
647
				$this->cacheUserExists($name);
648
				$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
649
			}
650
			return true;
651
		}
652
		return false;
653
	}
654
655
	/**
656
	 * gives back the user names as they are used ownClod internally
657
	 * @param array $ldapUsers as returned by fetchList()
658
	 * @return array an array with the user names to use in Nextcloud
659
	 *
660
	 * gives back the user names as they are used ownClod internally
661
	 */
662
	public function nextcloudUserNames($ldapUsers) {
663
		return $this->ldap2NextcloudNames($ldapUsers, true);
664
	}
665
666
	/**
667
	 * gives back the group names as they are used ownClod internally
668
	 * @param array $ldapGroups as returned by fetchList()
669
	 * @return array an array with the group names to use in Nextcloud
670
	 *
671
	 * gives back the group names as they are used ownClod internally
672
	 */
673
	public function nextcloudGroupNames($ldapGroups) {
674
		return $this->ldap2NextcloudNames($ldapGroups, false);
675
	}
676
677
	/**
678
	 * @param array $ldapObjects as returned by fetchList()
679
	 * @param bool $isUsers
680
	 * @return array
681
	 * @throws \Exception
682
	 */
683
	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
684
		if($isUsers) {
685
			$nameAttribute = $this->connection->ldapUserDisplayName;
686
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
687
		} else {
688
			$nameAttribute = $this->connection->ldapGroupDisplayName;
689
		}
690
		$nextcloudNames = [];
691
692
		foreach($ldapObjects as $ldapObject) {
693
			$nameByLDAP = null;
694
			if(    isset($ldapObject[$nameAttribute])
695
				&& is_array($ldapObject[$nameAttribute])
696
				&& isset($ldapObject[$nameAttribute][0])
697
			) {
698
				// might be set, but not necessarily. if so, we use it.
699
				$nameByLDAP = $ldapObject[$nameAttribute][0];
700
			}
701
702
			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
703
			if($ncName) {
704
				$nextcloudNames[] = $ncName;
705
				if($isUsers) {
706
					$this->updateUserState($ncName);
707
					//cache the user names so it does not need to be retrieved
708
					//again later (e.g. sharing dialogue).
709
					if(is_null($nameByLDAP)) {
710
						continue;
711
					}
712
					$sndName = isset($ldapObject[$sndAttribute][0])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sndAttribute does not seem to be defined for all execution paths leading up to this point.
Loading history...
713
						? $ldapObject[$sndAttribute][0] : '';
714
					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
715
				} else if($nameByLDAP !== null) {
716
					$this->cacheGroupDisplayName($ncName, $nameByLDAP);
717
				}
718
			}
719
		}
720
		return $nextcloudNames;
721
	}
722
723
	/**
724
	 * removes the deleted-flag of a user if it was set
725
	 *
726
	 * @param string $ncname
727
	 * @throws \Exception
728
	 */
729
	public function updateUserState($ncname) {
730
		$user = $this->userManager->get($ncname);
731
		if($user instanceof OfflineUser) {
732
			$user->unmark();
733
		}
734
	}
735
736
	/**
737
	 * caches the user display name
738
	 * @param string $ocName the internal Nextcloud username
739
	 * @param string|false $home the home directory path
740
	 */
741
	public function cacheUserHome($ocName, $home) {
742
		$cacheKey = 'getHome'.$ocName;
743
		$this->connection->writeToCache($cacheKey, $home);
744
	}
745
746
	/**
747
	 * caches a user as existing
748
	 * @param string $ocName the internal Nextcloud username
749
	 */
750
	public function cacheUserExists($ocName) {
751
		$this->connection->writeToCache('userExists'.$ocName, true);
752
	}
753
754
	/**
755
	 * caches the user display name
756
	 * @param string $ocName the internal Nextcloud username
757
	 * @param string $displayName the display name
758
	 * @param string $displayName2 the second display name
759
	 */
760
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
761
		$user = $this->userManager->get($ocName);
762
		if($user === null) {
763
			return;
764
		}
765
		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
0 ignored issues
show
Bug introduced by
The method composeAndStoreDisplayName() does not exist on OCA\User_LDAP\User\OfflineUser. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

765
		/** @scrutinizer ignore-call */ 
766
  $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
766
		$cacheKeyTrunk = 'getDisplayName';
767
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
768
	}
769
770
	public function cacheGroupDisplayName(string $ncName, string $displayName): void {
771
		$cacheKey = 'group_getDisplayName' . $ncName;
772
		$this->connection->writeToCache($cacheKey, $displayName);
773
	}
774
775
	/**
776
	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
777
	 * @param string $name the display name of the object
778
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
779
	 *
780
	 * Instead of using this method directly, call
781
	 * createAltInternalOwnCloudName($name, true)
782
	 */
783
	private function _createAltInternalOwnCloudNameForUsers($name) {
784
		$attempts = 0;
785
		//while loop is just a precaution. If a name is not generated within
786
		//20 attempts, something else is very wrong. Avoids infinite loop.
787
		while($attempts < 20){
788
			$altName = $name . '_' . rand(1000,9999);
789
			if(!$this->ncUserManager->userExists($altName)) {
790
				return $altName;
791
			}
792
			$attempts++;
793
		}
794
		return false;
795
	}
796
797
	/**
798
	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
799
	 * @param string $name the display name of the object
800
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
801
	 *
802
	 * Instead of using this method directly, call
803
	 * createAltInternalOwnCloudName($name, false)
804
	 *
805
	 * Group names are also used as display names, so we do a sequential
806
	 * numbering, e.g. Developers_42 when there are 41 other groups called
807
	 * "Developers"
808
	 */
809
	private function _createAltInternalOwnCloudNameForGroups($name) {
810
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
811
		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...
812
			$lastNo = 1; //will become name_2
813
		} else {
814
			natsort($usedNames);
815
			$lastName = array_pop($usedNames);
816
			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
817
		}
818
		$altName = $name.'_'. (string)($lastNo+1);
819
		unset($usedNames);
820
821
		$attempts = 1;
822
		while($attempts < 21){
823
			// Check to be really sure it is unique
824
			// while loop is just a precaution. If a name is not generated within
825
			// 20 attempts, something else is very wrong. Avoids infinite loop.
826
			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
827
				return $altName;
828
			}
829
			$altName = $name . '_' . ($lastNo + $attempts);
830
			$attempts++;
831
		}
832
		return false;
833
	}
834
835
	/**
836
	 * creates a unique name for internal Nextcloud use.
837
	 * @param string $name the display name of the object
838
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
839
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
840
	 */
841
	private function createAltInternalOwnCloudName($name, $isUser) {
842
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapCacheTTL does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
843
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
844
		if($isUser) {
845
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
846
		} else {
847
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
848
		}
849
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
850
851
		return $altName;
852
	}
853
854
	/**
855
	 * fetches a list of users according to a provided loginName and utilizing
856
	 * the login filter.
857
	 *
858
	 * @param string $loginName
859
	 * @param array $attributes optional, list of attributes to read
860
	 * @return array
861
	 */
862
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
863
		$loginName = $this->escapeFilterPart($loginName);
864
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilter does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
865
		return $this->fetchListOfUsers($filter, $attributes);
866
	}
867
868
	/**
869
	 * counts the number of users according to a provided loginName and
870
	 * utilizing the login filter.
871
	 *
872
	 * @param string $loginName
873
	 * @return int
874
	 */
875
	public function countUsersByLoginName($loginName) {
876
		$loginName = $this->escapeFilterPart($loginName);
877
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapLoginFilter does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
878
		return $this->countUsers($filter);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->countUsers($filter) could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
879
	}
880
881
	/**
882
	 * @param string $filter
883
	 * @param string|string[] $attr
884
	 * @param int $limit
885
	 * @param int $offset
886
	 * @param bool $forceApplyAttributes
887
	 * @return array
888
	 */
889
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
890
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
891
		$recordsToUpdate = $ldapRecords;
892
		if(!$forceApplyAttributes) {
893
			$isBackgroundJobModeAjax = $this->config
894
					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
895
			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
896
				$newlyMapped = false;
897
				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
898
				if(is_string($uid)) {
899
					$this->cacheUserExists($uid);
900
				}
901
				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
902
			});
903
		}
904
		$this->batchApplyUserAttributes($recordsToUpdate);
905
		return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
906
	}
907
908
	/**
909
	 * provided with an array of LDAP user records the method will fetch the
910
	 * user object and requests it to process the freshly fetched attributes and
911
	 * and their values
912
	 *
913
	 * @param array $ldapRecords
914
	 * @throws \Exception
915
	 */
916
	public function batchApplyUserAttributes(array $ldapRecords){
917
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
918
		foreach($ldapRecords as $userRecord) {
919
			if(!isset($userRecord[$displayNameAttribute])) {
920
				// displayName is obligatory
921
				continue;
922
			}
923
			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
924
			if($ocName === false) {
925
				continue;
926
			}
927
			$this->updateUserState($ocName);
928
			$user = $this->userManager->get($ocName);
929
			if ($user !== null) {
930
				$user->processAttributes($userRecord);
0 ignored issues
show
Bug introduced by
The method processAttributes() does not exist on OCA\User_LDAP\User\OfflineUser. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

930
				$user->/** @scrutinizer ignore-call */ 
931
           processAttributes($userRecord);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
931
			} else {
932
				\OC::$server->getLogger()->debug(
933
					"The ldap user manager returned null for $ocName",
934
					['app'=>'user_ldap']
935
				);
936
			}
937
		}
938
	}
939
940
	/**
941
	 * @param string $filter
942
	 * @param string|string[] $attr
943
	 * @param int $limit
944
	 * @param int $offset
945
	 * @return array
946
	 */
947
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
948
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), $this->manyAttributes($attr));
949
	}
950
951
	/**
952
	 * @param array $list
953
	 * @param bool $manyAttributes
954
	 * @return array
955
	 */
956
	private function fetchList($list, $manyAttributes) {
957
		if(is_array($list)) {
0 ignored issues
show
introduced by
The condition is_array($list) is always true.
Loading history...
958
			if($manyAttributes) {
959
				return $list;
960
			} else {
961
				$list = array_reduce($list, function($carry, $item) {
962
					$attribute = array_keys($item)[0];
963
					$carry[] = $item[$attribute][0];
964
					return $carry;
965
				}, array());
966
				return array_unique($list, SORT_LOCALE_STRING);
967
			}
968
		}
969
970
		//error cause actually, maybe throw an exception in future.
971
		return array();
972
	}
973
974
	/**
975
	 * executes an LDAP search, optimized for Users
976
	 * @param string $filter the LDAP filter for the search
977
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
978
	 * @param integer $limit
979
	 * @param integer $offset
980
	 * @return array with the search result
981
	 *
982
	 * Executes an LDAP search
983
	 */
984
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
985
		$result = [];
986
		foreach($this->connection->ldapBaseUsers as $base) {
987
			$result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
988
		}
989
		return $result;
990
	}
991
992
	/**
993
	 * @param string $filter
994
	 * @param string|string[] $attr
995
	 * @param int $limit
996
	 * @param int $offset
997
	 * @return false|int
998
	 */
999
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
1000
		$result = false;
1001
		foreach($this->connection->ldapBaseUsers as $base) {
1002
			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1003
			$result = is_int($count) ? (int)$result + $count : $result;
1004
		}
1005
		return $result;
1006
	}
1007
1008
	/**
1009
	 * executes an LDAP search, optimized for Groups
1010
	 * @param string $filter the LDAP filter for the search
1011
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
1012
	 * @param integer $limit
1013
	 * @param integer $offset
1014
	 * @return array with the search result
1015
	 *
1016
	 * Executes an LDAP search
1017
	 */
1018
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1019
		$result = [];
1020
		foreach($this->connection->ldapBaseGroups as $base) {
1021
			$result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
1022
		}
1023
		return $result;
1024
	}
1025
1026
	/**
1027
	 * returns the number of available groups
1028
	 * @param string $filter the LDAP search filter
1029
	 * @param string[] $attr optional
1030
	 * @param int|null $limit
1031
	 * @param int|null $offset
1032
	 * @return int|bool
1033
	 */
1034
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
1035
		$result = false;
1036
		foreach($this->connection->ldapBaseGroups as $base) {
1037
			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1038
			$result = is_int($count) ? (int)$result + $count : $result;
1039
		}
1040
		return $result;
1041
	}
1042
1043
	/**
1044
	 * returns the number of available objects on the base DN
1045
	 *
1046
	 * @param int|null $limit
1047
	 * @param int|null $offset
1048
	 * @return int|bool
1049
	 */
1050
	public function countObjects($limit = null, $offset = null) {
1051
		$result = false;
1052
		foreach($this->connection->ldapBase as $base) {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBase does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1053
			$count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
1054
			$result = is_int($count) ? (int)$result + $count : $result;
1055
		}
1056
		return $result;
1057
	}
1058
1059
	/**
1060
	 * Returns the LDAP handler
1061
	 * @throws \OC\ServerNotAvailableException
1062
	 */
1063
1064
	/**
1065
	 * @return mixed
1066
	 * @throws \OC\ServerNotAvailableException
1067
	 */
1068
	private function invokeLDAPMethod() {
1069
		$arguments = func_get_args();
1070
		$command = array_shift($arguments);
1071
		$cr = array_shift($arguments);
1072
		if (!method_exists($this->ldap, $command)) {
1073
			return null;
1074
		}
1075
		array_unshift($arguments, $cr);
1076
		// php no longer supports call-time pass-by-reference
1077
		// thus cannot support controlPagedResultResponse as the third argument
1078
		// is a reference
1079
		$doMethod = function () use ($command, &$arguments) {
1080
			if ($command == 'controlPagedResultResponse') {
1081
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1082
			} else {
1083
				return call_user_func_array(array($this->ldap, $command), $arguments);
1084
			}
1085
		};
1086
		try {
1087
			$ret = $doMethod();
1088
		} catch (ServerNotAvailableException $e) {
1089
			/* Server connection lost, attempt to reestablish it
1090
			 * Maybe implement exponential backoff?
1091
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1092
			 */
1093
			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1093
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);

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

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

Loading history...
1094
			$this->connection->resetConnectionResource();
1095
			$cr = $this->connection->getConnectionResource();
1096
1097
			if(!$this->ldap->isResource($cr)) {
1098
				// Seems like we didn't find any resource.
1099
				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1099
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);

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

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

Loading history...
1100
				throw $e;
1101
			}
1102
1103
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1104
			$ret = $doMethod();
1105
		}
1106
		return $ret;
1107
	}
1108
1109
	/**
1110
	 * retrieved. Results will according to the order in the array.
1111
	 *
1112
	 * @param $filter
1113
	 * @param $base
1114
	 * @param string[]|string|null $attr
1115
	 * @param int $limit optional, maximum results to be counted
1116
	 * @param int $offset optional, a starting point
1117
	 * @return array|false array with the search result as first value and pagedSearchOK as
1118
	 * second | false if not successful
1119
	 * @throws ServerNotAvailableException
1120
	 */
1121
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1122
		if(!is_null($attr) && !is_array($attr)) {
1123
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1124
		}
1125
1126
		// See if we have a resource, in case not cancel with message
1127
		$cr = $this->connection->getConnectionResource();
1128
		if(!$this->ldap->isResource($cr)) {
1129
			// Seems like we didn't find any resource.
1130
			// Return an empty array just like before.
1131
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1131
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);

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

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

Loading history...
1132
			return false;
1133
		}
1134
1135
		//check whether paged search should be attempted
1136
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1137
1138
		$linkResources = array_pad(array(), count($base), $cr);
1139
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1140
		// cannot use $cr anymore, might have changed in the previous call!
1141
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1142
		if(!is_array($sr) || $error !== 0) {
1143
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1143
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);

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

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

Loading history...
1144
			return false;
1145
		}
1146
1147
		return array($sr, $pagedSearchOK);
1148
	}
1149
1150
	/**
1151
	 * processes an LDAP paged search operation
1152
	 * @param array $sr the array containing the LDAP search resources
1153
	 * @param string $filter the LDAP filter for the search
1154
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1155
	 * @param int $iFoundItems number of results in the single search operation
1156
	 * @param int $limit maximum results to be counted
1157
	 * @param int $offset a starting point
1158
	 * @param bool $pagedSearchOK whether a paged search has been executed
1159
	 * @param bool $skipHandling required for paged search when cookies to
1160
	 * prior results need to be gained
1161
	 * @return bool cookie validity, true if we have more pages, false otherwise.
1162
	 */
1163
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1164
		$cookie = null;
1165
		if($pagedSearchOK) {
1166
			$cr = $this->connection->getConnectionResource();
1167
			foreach($sr as $key => $res) {
1168
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1169
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1170
				}
1171
			}
1172
1173
			//browsing through prior pages to get the cookie for the new one
1174
			if($skipHandling) {
1175
				return false;
1176
			}
1177
			// if count is bigger, then the server does not support
1178
			// paged search. Instead, he did a normal search. We set a
1179
			// flag here, so the callee knows how to deal with it.
1180
			if($iFoundItems <= $limit) {
1181
				$this->pagedSearchedSuccessful = true;
1182
			}
1183
		} else {
1184
			if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
0 ignored issues
show
introduced by
The condition is_null($limit) is always false.
Loading history...
1185
				\OC::$server->getLogger()->debug(
1186
					'Paged search was not available',
1187
					[ 'app' => 'user_ldap' ]
1188
				);
1189
			}
1190
		}
1191
		/* ++ Fixing RHDS searches with pages with zero results ++
1192
		 * Return cookie status. If we don't have more pages, with RHDS
1193
		 * cookie is null, with openldap cookie is an empty string and
1194
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1195
		 */
1196
		return !empty($cookie) || $cookie === '0';
1197
	}
1198
1199
	/**
1200
	 * executes an LDAP search, but counts the results only
1201
	 *
1202
	 * @param string $filter the LDAP filter for the search
1203
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1204
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1205
	 * retrieved. Results will according to the order in the array.
1206
	 * @param int $limit optional, maximum results to be counted
1207
	 * @param int $offset optional, a starting point
1208
	 * @param bool $skipHandling indicates whether the pages search operation is
1209
	 * completed
1210
	 * @return int|false Integer or false if the search could not be initialized
1211
	 * @throws ServerNotAvailableException
1212
	 */
1213
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1214
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1214
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);

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

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

Loading history...
1215
1216
		$limitPerPage = (int)$this->connection->ldapPagingSize;
1217
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1218
			$limitPerPage = $limit;
1219
		}
1220
1221
		$counter = 0;
1222
		$count = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
1223
		$this->connection->getConnectionResource();
1224
1225
		do {
1226
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1227
			if($search === false) {
1228
				return $counter > 0 ? $counter : false;
1229
			}
1230
			list($sr, $pagedSearchOK) = $search;
1231
1232
			/* ++ Fixing RHDS searches with pages with zero results ++
1233
			 * countEntriesInSearchResults() method signature changed
1234
			 * by removing $limit and &$hasHitLimit parameters
1235
			 */
1236
			$count = $this->countEntriesInSearchResults($sr);
1237
			$counter += $count;
1238
1239
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1240
										$offset, $pagedSearchOK, $skipHandling);
1241
			$offset += $limitPerPage;
1242
			/* ++ Fixing RHDS searches with pages with zero results ++
1243
			 * Continue now depends on $hasMorePages value
1244
			 */
1245
			$continue = $pagedSearchOK && $hasMorePages;
1246
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1247
1248
		return $counter;
1249
	}
1250
1251
	/**
1252
	 * @param array $searchResults
1253
	 * @return int
1254
	 */
1255
	private function countEntriesInSearchResults($searchResults) {
1256
		$counter = 0;
1257
1258
		foreach($searchResults as $res) {
1259
			$count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1260
			$counter += $count;
1261
		}
1262
1263
		return $counter;
1264
	}
1265
1266
	/**
1267
	 * Executes an LDAP search
1268
	 *
1269
	 * @param string $filter the LDAP filter for the search
1270
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1271
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1272
	 * @param int $limit
1273
	 * @param int $offset
1274
	 * @param bool $skipHandling
1275
	 * @return array with the search result
1276
	 * @throws ServerNotAvailableException
1277
	 */
1278
	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1279
		$limitPerPage = (int)$this->connection->ldapPagingSize;
1280
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1281
			$limitPerPage = $limit;
1282
		}
1283
1284
		/* ++ Fixing RHDS searches with pages with zero results ++
1285
		 * As we can have pages with zero results and/or pages with less
1286
		 * than $limit results but with a still valid server 'cookie',
1287
		 * loops through until we get $continue equals true and
1288
		 * $findings['count'] < $limit
1289
		 */
1290
		$findings = [];
1291
		$savedoffset = $offset;
1292
		do {
1293
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1294
			if($search === false) {
1295
				return [];
1296
			}
1297
			list($sr, $pagedSearchOK) = $search;
1298
			$cr = $this->connection->getConnectionResource();
1299
1300
			if($skipHandling) {
1301
				//i.e. result do not need to be fetched, we just need the cookie
1302
				//thus pass 1 or any other value as $iFoundItems because it is not
1303
				//used
1304
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1305
								$offset, $pagedSearchOK,
1306
								$skipHandling);
1307
				return array();
1308
			}
1309
1310
			$iFoundItems = 0;
1311
			foreach($sr as $res) {
1312
				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1313
				$iFoundItems = max($iFoundItems, $findings['count']);
1314
				unset($findings['count']);
1315
			}
1316
1317
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1318
				$limitPerPage, $offset, $pagedSearchOK,
1319
										$skipHandling);
1320
			$offset += $limitPerPage;
1321
		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1322
		// reseting offset
1323
		$offset = $savedoffset;
1324
1325
		// if we're here, probably no connection resource is returned.
1326
		// to make Nextcloud behave nicely, we simply give back an empty array.
1327
		if(is_null($findings)) {
0 ignored issues
show
introduced by
The condition is_null($findings) is always false.
Loading history...
1328
			return array();
1329
		}
1330
1331
		if(!is_null($attr)) {
1332
			$selection = [];
1333
			$i = 0;
1334
			foreach($findings as $item) {
1335
				if(!is_array($item)) {
1336
					continue;
1337
				}
1338
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1339
				foreach($attr as $key) {
1340
					if(isset($item[$key])) {
1341
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1342
							unset($item[$key]['count']);
1343
						}
1344
						if($key !== 'dn') {
1345
							if($this->resemblesDN($key)) {
1346
								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1347
							} else if($key === 'objectguid' || $key === 'guid') {
1348
								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1349
							} else {
1350
								$selection[$i][$key] = $item[$key];
1351
							}
1352
						} else {
1353
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1354
						}
1355
					}
1356
1357
				}
1358
				$i++;
1359
			}
1360
			$findings = $selection;
1361
		}
1362
		//we slice the findings, when
1363
		//a) paged search unsuccessful, though attempted
1364
		//b) no paged search, but limit set
1365
		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...
1366
			&& $pagedSearchOK)
1367
			|| (
1368
				!$pagedSearchOK
1369
				&& !is_null($limit)
1370
			)
1371
		) {
1372
			$findings = array_slice($findings, (int)$offset, $limit);
1373
		}
1374
		return $findings;
1375
	}
1376
1377
	/**
1378
	 * @param string $name
1379
	 * @return string
1380
	 * @throws \InvalidArgumentException
1381
	 */
1382
	public function sanitizeUsername($name) {
1383
		$name = trim($name);
1384
1385
		if($this->connection->ldapIgnoreNamingRules) {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapIgnoreNamingRules does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1386
			return $name;
1387
		}
1388
1389
		// Transliteration to ASCII
1390
		$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1391
		if($transliterated !== false) {
1392
			// depending on system config iconv can work or not
1393
			$name = $transliterated;
1394
		}
1395
1396
		// Replacements
1397
		$name = str_replace(' ', '_', $name);
1398
1399
		// Every remaining disallowed characters will be removed
1400
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1401
1402
		if($name === '') {
1403
			throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1404
		}
1405
1406
		return $name;
1407
	}
1408
1409
	/**
1410
	* escapes (user provided) parts for LDAP filter
1411
	* @param string $input, the provided value
1412
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1413
	* @return string the escaped string
1414
	*/
1415
	public function escapeFilterPart($input, $allowAsterisk = false) {
1416
		$asterisk = '';
1417
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1418
			$asterisk = '*';
1419
			$input = mb_substr($input, 1, null, 'UTF-8');
1420
		}
1421
		$search  = array('*', '\\', '(', ')');
1422
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1423
		return $asterisk . str_replace($search, $replace, $input);
1424
	}
1425
1426
	/**
1427
	 * combines the input filters with AND
1428
	 * @param string[] $filters the filters to connect
1429
	 * @return string the combined filter
1430
	 */
1431
	public function combineFilterWithAnd($filters) {
1432
		return $this->combineFilter($filters, '&');
1433
	}
1434
1435
	/**
1436
	 * combines the input filters with OR
1437
	 * @param string[] $filters the filters to connect
1438
	 * @return string the combined filter
1439
	 * Combines Filter arguments with OR
1440
	 */
1441
	public function combineFilterWithOr($filters) {
1442
		return $this->combineFilter($filters, '|');
1443
	}
1444
1445
	/**
1446
	 * combines the input filters with given operator
1447
	 * @param string[] $filters the filters to connect
1448
	 * @param string $operator either & or |
1449
	 * @return string the combined filter
1450
	 */
1451
	private function combineFilter($filters, $operator) {
1452
		$combinedFilter = '('.$operator;
1453
		foreach($filters as $filter) {
1454
			if ($filter !== '' && $filter[0] !== '(') {
1455
				$filter = '('.$filter.')';
1456
			}
1457
			$combinedFilter.=$filter;
1458
		}
1459
		$combinedFilter.=')';
1460
		return $combinedFilter;
1461
	}
1462
1463
	/**
1464
	 * creates a filter part for to perform search for users
1465
	 * @param string $search the search term
1466
	 * @return string the final filter part to use in LDAP searches
1467
	 */
1468
	public function getFilterPartForUserSearch($search) {
1469
		return $this->getFilterPartForSearch($search,
1470
			$this->connection->ldapAttributesForUserSearch,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAttributesForUserSearch does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1471
			$this->connection->ldapUserDisplayName);
1472
	}
1473
1474
	/**
1475
	 * creates a filter part for to perform search for groups
1476
	 * @param string $search the search term
1477
	 * @return string the final filter part to use in LDAP searches
1478
	 */
1479
	public function getFilterPartForGroupSearch($search) {
1480
		return $this->getFilterPartForSearch($search,
1481
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAttributesForGroupSearch does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1482
			$this->connection->ldapGroupDisplayName);
1483
	}
1484
1485
	/**
1486
	 * creates a filter part for searches by splitting up the given search
1487
	 * string into single words
1488
	 * @param string $search the search term
1489
	 * @param string[] $searchAttributes needs to have at least two attributes,
1490
	 * otherwise it does not make sense :)
1491
	 * @return string the final filter part to use in LDAP searches
1492
	 * @throws \Exception
1493
	 */
1494
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1495
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
0 ignored issues
show
introduced by
The condition is_array($searchAttributes) is always true.
Loading history...
1496
			throw new \Exception('searchAttributes must be an array with at least two string');
1497
		}
1498
		$searchWords = explode(' ', trim($search));
1499
		$wordFilters = array();
1500
		foreach($searchWords as $word) {
1501
			$word = $this->prepareSearchTerm($word);
1502
			//every word needs to appear at least once
1503
			$wordMatchOneAttrFilters = array();
1504
			foreach($searchAttributes as $attr) {
1505
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1506
			}
1507
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1508
		}
1509
		return $this->combineFilterWithAnd($wordFilters);
1510
	}
1511
1512
	/**
1513
	 * creates a filter part for searches
1514
	 * @param string $search the search term
1515
	 * @param string[]|null $searchAttributes
1516
	 * @param string $fallbackAttribute a fallback attribute in case the user
1517
	 * did not define search attributes. Typically the display name attribute.
1518
	 * @return string the final filter part to use in LDAP searches
1519
	 */
1520
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1521
		$filter = array();
1522
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1523
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1524
			try {
1525
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1526
			} catch(\Exception $e) {
1527
				\OCP\Util::writeLog(
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1527
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog(

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

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

Loading history...
1528
					'user_ldap',
1529
					'Creating advanced filter for search failed, falling back to simple method.',
1530
					ILogger::INFO
1531
				);
1532
			}
1533
		}
1534
1535
		$search = $this->prepareSearchTerm($search);
1536
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1537
			if ($fallbackAttribute === '') {
1538
				return '';
1539
			}
1540
			$filter[] = $fallbackAttribute . '=' . $search;
1541
		} else {
1542
			foreach($searchAttributes as $attribute) {
1543
				$filter[] = $attribute . '=' . $search;
1544
			}
1545
		}
1546
		if(count($filter) === 1) {
1547
			return '('.$filter[0].')';
1548
		}
1549
		return $this->combineFilterWithOr($filter);
1550
	}
1551
1552
	/**
1553
	 * returns the search term depending on whether we are allowed
1554
	 * list users found by ldap with the current input appended by
1555
	 * a *
1556
	 * @return string
1557
	 */
1558
	private function prepareSearchTerm($term) {
1559
		$config = \OC::$server->getConfig();
1560
1561
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1562
1563
		$result = $term;
1564
		if ($term === '') {
1565
			$result = '*';
1566
		} else if ($allowEnum !== 'no') {
1567
			$result = $term . '*';
1568
		}
1569
		return $result;
1570
	}
1571
1572
	/**
1573
	 * returns the filter used for counting users
1574
	 * @return string
1575
	 */
1576
	public function getFilterForUserCount() {
1577
		$filter = $this->combineFilterWithAnd(array(
1578
			$this->connection->ldapUserFilter,
1579
			$this->connection->ldapUserDisplayName . '=*'
1580
		));
1581
1582
		return $filter;
1583
	}
1584
1585
	/**
1586
	 * @param string $name
1587
	 * @param string $password
1588
	 * @return bool
1589
	 */
1590
	public function areCredentialsValid($name, $password) {
1591
		$name = $this->helper->DNasBaseParameter($name);
1592
		$testConnection = clone $this->connection;
1593
		$credentials = array(
1594
			'ldapAgentName' => $name,
1595
			'ldapAgentPassword' => $password
1596
		);
1597
		if(!$testConnection->setConfiguration($credentials)) {
1598
			return false;
1599
		}
1600
		return $testConnection->bind();
1601
	}
1602
1603
	/**
1604
	 * reverse lookup of a DN given a known UUID
1605
	 *
1606
	 * @param string $uuid
1607
	 * @return string
1608
	 * @throws \Exception
1609
	 */
1610
	public function getUserDnByUuid($uuid) {
1611
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1612
		$filter       = $this->connection->ldapUserFilter;
1613
		$base         = $this->connection->ldapBaseUsers;
1614
1615
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1616
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1617
			// existing DN to be able to reliably detect it.
1618
			$result = $this->search($filter, $base, ['dn'], 1);
1619
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1620
				throw new \Exception('Cannot determine UUID attribute');
1621
			}
1622
			$dn = $result[0]['dn'][0];
1623
			if(!$this->detectUuidAttribute($dn, true)) {
1624
				throw new \Exception('Cannot determine UUID attribute');
1625
			}
1626
		} else {
1627
			// The UUID attribute is either known or an override is given.
1628
			// By calling this method we ensure that $this->connection->$uuidAttr
1629
			// is definitely set
1630
			if(!$this->detectUuidAttribute('', true)) {
1631
				throw new \Exception('Cannot determine UUID attribute');
1632
			}
1633
		}
1634
1635
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1636
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1637
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1638
		}
1639
1640
		$filter = $uuidAttr . '=' . $uuid;
1641
		$result = $this->searchUsers($filter, ['dn'], 2);
1642
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1643
			// we put the count into account to make sure that this is
1644
			// really unique
1645
			return $result[0]['dn'][0];
1646
		}
1647
1648
		throw new \Exception('Cannot determine UUID attribute');
1649
	}
1650
1651
	/**
1652
	 * auto-detects the directory's UUID attribute
1653
	 *
1654
	 * @param string $dn a known DN used to check against
1655
	 * @param bool $isUser
1656
	 * @param bool $force the detection should be run, even if it is not set to auto
1657
	 * @param array|null $ldapRecord
1658
	 * @return bool true on success, false otherwise
1659
	 */
1660
	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1661
		if($isUser) {
1662
			$uuidAttr     = 'ldapUuidUserAttribute';
1663
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1664
		} else {
1665
			$uuidAttr     = 'ldapUuidGroupAttribute';
1666
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1667
		}
1668
1669
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1670
			return true;
1671
		}
1672
1673
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1674
			$this->connection->$uuidAttr = $uuidOverride;
1675
			return true;
1676
		}
1677
1678
		foreach(self::UUID_ATTRIBUTES as $attribute) {
1679
			if($ldapRecord !== null) {
1680
				// we have the info from LDAP already, we don't need to talk to the server again
1681
				if(isset($ldapRecord[$attribute])) {
1682
					$this->connection->$uuidAttr = $attribute;
1683
					return true;
1684
				} else {
1685
					continue;
1686
				}
1687
			}
1688
1689
			$value = $this->readAttribute($dn, $attribute);
1690
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1691
				\OCP\Util::writeLog(
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1691
				/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog(

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

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

Loading history...
1692
					'user_ldap',
1693
					'Setting '.$attribute.' as '.$uuidAttr,
1694
					ILogger::DEBUG
1695
				);
1696
				$this->connection->$uuidAttr = $attribute;
1697
				return true;
1698
			}
1699
		}
1700
		\OCP\Util::writeLog(
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1700
		/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog(

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

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

Loading history...
1701
			'user_ldap',
1702
			'Could not autodetect the UUID attribute',
1703
			ILogger::ERROR
1704
		);
1705
1706
		return false;
1707
	}
1708
1709
	/**
1710
	 * @param string $dn
1711
	 * @param bool $isUser
1712
	 * @param null $ldapRecord
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $ldapRecord is correct as it would always require null to be passed?
Loading history...
1713
	 * @return bool|string
1714
	 */
1715
	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1716
		if($isUser) {
1717
			$uuidAttr     = 'ldapUuidUserAttribute';
1718
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1719
		} else {
1720
			$uuidAttr     = 'ldapUuidGroupAttribute';
1721
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1722
		}
1723
1724
		$uuid = false;
1725
		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1726
			$attr = $this->connection->$uuidAttr;
1727
			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1728
			if( !is_array($uuid)
1729
				&& $uuidOverride !== ''
1730
				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1731
			{
1732
				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1733
					? $ldapRecord[$this->connection->$uuidAttr]
1734
					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1735
			}
1736
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1737
				$uuid = $uuid[0];
1738
			}
1739
		}
1740
1741
		return $uuid;
1742
	}
1743
1744
	/**
1745
	 * converts a binary ObjectGUID into a string representation
1746
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1747
	 * @return string
1748
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1749
	 */
1750
	private function convertObjectGUID2Str($oguid) {
1751
		$hex_guid = bin2hex($oguid);
1752
		$hex_guid_to_guid_str = '';
1753
		for($k = 1; $k <= 4; ++$k) {
1754
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1755
		}
1756
		$hex_guid_to_guid_str .= '-';
1757
		for($k = 1; $k <= 2; ++$k) {
1758
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1759
		}
1760
		$hex_guid_to_guid_str .= '-';
1761
		for($k = 1; $k <= 2; ++$k) {
1762
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1763
		}
1764
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1765
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1766
1767
		return strtoupper($hex_guid_to_guid_str);
1768
	}
1769
1770
	/**
1771
	 * the first three blocks of the string-converted GUID happen to be in
1772
	 * reverse order. In order to use it in a filter, this needs to be
1773
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1774
	 * to every two hax figures.
1775
	 *
1776
	 * If an invalid string is passed, it will be returned without change.
1777
	 *
1778
	 * @param string $guid
1779
	 * @return string
1780
	 */
1781
	public function formatGuid2ForFilterUser($guid) {
1782
		if(!is_string($guid)) {
0 ignored issues
show
introduced by
The condition is_string($guid) is always true.
Loading history...
1783
			throw new \InvalidArgumentException('String expected');
1784
		}
1785
		$blocks = explode('-', $guid);
1786
		if(count($blocks) !== 5) {
1787
			/*
1788
			 * Why not throw an Exception instead? This method is a utility
1789
			 * called only when trying to figure out whether a "missing" known
1790
			 * LDAP user was or was not renamed on the LDAP server. And this
1791
			 * even on the use case that a reverse lookup is needed (UUID known,
1792
			 * not DN), i.e. when finding users (search dialog, users page,
1793
			 * login, …) this will not be fired. This occurs only if shares from
1794
			 * a users are supposed to be mounted who cannot be found. Throwing
1795
			 * an exception here would kill the experience for a valid, acting
1796
			 * user. Instead we write a log message.
1797
			 */
1798
			\OC::$server->getLogger()->info(
1799
				'Passed string does not resemble a valid GUID. Known UUID ' .
1800
				'({uuid}) probably does not match UUID configuration.',
1801
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1802
			);
1803
			return $guid;
1804
		}
1805
		for($i=0; $i < 3; $i++) {
1806
			$pairs = str_split($blocks[$i], 2);
1807
			$pairs = array_reverse($pairs);
1808
			$blocks[$i] = implode('', $pairs);
1809
		}
1810
		for($i=0; $i < 5; $i++) {
1811
			$pairs = str_split($blocks[$i], 2);
1812
			$blocks[$i] = '\\' . implode('\\', $pairs);
1813
		}
1814
		return implode('', $blocks);
1815
	}
1816
1817
	/**
1818
	 * gets a SID of the domain of the given dn
1819
	 * @param string $dn
1820
	 * @return string|bool
1821
	 */
1822
	public function getSID($dn) {
1823
		$domainDN = $this->getDomainDNFromDN($dn);
1824
		$cacheKey = 'getSID-'.$domainDN;
1825
		$sid = $this->connection->getFromCache($cacheKey);
1826
		if(!is_null($sid)) {
1827
			return $sid;
1828
		}
1829
1830
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1831
		if(!is_array($objectSid) || empty($objectSid)) {
1832
			$this->connection->writeToCache($cacheKey, false);
1833
			return false;
1834
		}
1835
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1836
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1837
1838
		return $domainObjectSid;
1839
	}
1840
1841
	/**
1842
	 * converts a binary SID into a string representation
1843
	 * @param string $sid
1844
	 * @return string
1845
	 */
1846
	public function convertSID2Str($sid) {
1847
		// The format of a SID binary string is as follows:
1848
		// 1 byte for the revision level
1849
		// 1 byte for the number n of variable sub-ids
1850
		// 6 bytes for identifier authority value
1851
		// n*4 bytes for n sub-ids
1852
		//
1853
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1854
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1855
		$revision = ord($sid[0]);
1856
		$numberSubID = ord($sid[1]);
1857
1858
		$subIdStart = 8; // 1 + 1 + 6
1859
		$subIdLength = 4;
1860
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1861
			// Incorrect number of bytes present.
1862
			return '';
1863
		}
1864
1865
		// 6 bytes = 48 bits can be represented using floats without loss of
1866
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1867
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1868
1869
		$subIDs = array();
1870
		for ($i = 0; $i < $numberSubID; $i++) {
1871
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1872
			$subIDs[] = sprintf('%u', $subID[1]);
1873
		}
1874
1875
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1876
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1877
	}
1878
1879
	/**
1880
	 * checks if the given DN is part of the given base DN(s)
1881
	 * @param string $dn the DN
1882
	 * @param string[] $bases array containing the allowed base DN or DNs
1883
	 * @return bool
1884
	 */
1885
	public function isDNPartOfBase($dn, $bases) {
1886
		$belongsToBase = false;
1887
		$bases = $this->helper->sanitizeDN($bases);
1888
1889
		foreach($bases as $base) {
1890
			$belongsToBase = true;
1891
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1892
				$belongsToBase = false;
1893
			}
1894
			if($belongsToBase) {
1895
				break;
1896
			}
1897
		}
1898
		return $belongsToBase;
1899
	}
1900
1901
	/**
1902
	 * resets a running Paged Search operation
1903
	 *
1904
	 * @throws ServerNotAvailableException
1905
	 */
1906
	private function abandonPagedSearch() {
1907
		$cr = $this->connection->getConnectionResource();
1908
		$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1909
		$this->getPagedSearchResultState();
1910
		$this->lastCookie = '';
1911
		$this->cookies = [];
1912
	}
1913
1914
	/**
1915
	 * get a cookie for the next LDAP paged search
1916
	 * @param string $base a string with the base DN for the search
1917
	 * @param string $filter the search filter to identify the correct search
1918
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1919
	 * @param int $offset the offset for the new search to identify the correct search really good
1920
	 * @return string containing the key or empty if none is cached
1921
	 */
1922
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1923
		if($offset === 0) {
1924
			return '';
1925
		}
1926
		$offset -= $limit;
1927
		//we work with cache here
1928
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1929
		$cookie = '';
1930
		if(isset($this->cookies[$cacheKey])) {
1931
			$cookie = $this->cookies[$cacheKey];
1932
			if(is_null($cookie)) {
0 ignored issues
show
introduced by
The condition is_null($cookie) is always false.
Loading history...
1933
				$cookie = '';
1934
			}
1935
		}
1936
		return $cookie;
1937
	}
1938
1939
	/**
1940
	 * checks whether an LDAP paged search operation has more pages that can be
1941
	 * retrieved, typically when offset and limit are provided.
1942
	 *
1943
	 * Be very careful to use it: the last cookie value, which is inspected, can
1944
	 * be reset by other operations. Best, call it immediately after a search(),
1945
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1946
	 * well. Don't rely on it with any fetchList-method.
1947
	 * @return bool
1948
	 */
1949
	public function hasMoreResults() {
1950
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1951
			// as in RFC 2696, when all results are returned, the cookie will
1952
			// be empty.
1953
			return false;
1954
		}
1955
1956
		return true;
1957
	}
1958
1959
	/**
1960
	 * set a cookie for LDAP paged search run
1961
	 * @param string $base a string with the base DN for the search
1962
	 * @param string $filter the search filter to identify the correct search
1963
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1964
	 * @param int $offset the offset for the run search to identify the correct search really good
1965
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1966
	 * @return void
1967
	 */
1968
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1969
		// allow '0' for 389ds
1970
		if(!empty($cookie) || $cookie === '0') {
1971
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1972
			$this->cookies[$cacheKey] = $cookie;
1973
			$this->lastCookie = $cookie;
1974
		}
1975
	}
1976
1977
	/**
1978
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1979
	 * @return boolean|null true on success, null or false otherwise
1980
	 */
1981
	public function getPagedSearchResultState() {
1982
		$result = $this->pagedSearchedSuccessful;
1983
		$this->pagedSearchedSuccessful = null;
1984
		return $result;
1985
	}
1986
1987
	/**
1988
	 * Prepares a paged search, if possible
1989
	 * @param string $filter the LDAP filter for the search
1990
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1991
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1992
	 * @param int $limit
1993
	 * @param int $offset
1994
	 * @return bool|true
1995
	 */
1996
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1997
		$pagedSearchOK = false;
1998
		if ($limit !== 0) {
1999
			$offset = (int)$offset; //can be null
2000
			\OCP\Util::writeLog('user_ldap',
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2000
			/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap',

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

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

Loading history...
2001
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
2002
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
2003
				ILogger::DEBUG);
2004
			//get the cookie from the search for the previous search, required by LDAP
2005
			foreach($bases as $base) {
2006
2007
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2008
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
2009
					// no cookie known from a potential previous search. We need
2010
					// to start from 0 to come to the desired page. cookie value
2011
					// of '0' is valid, because 389ds
2012
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
2013
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
2014
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2015
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
2016
					// '0' is valid, because 389ds
2017
					//TODO: remember this, probably does not change in the next request...
2018
					if(empty($cookie) && $cookie !== '0') {
2019
						$cookie = null;
2020
					}
2021
				}
2022
				if(!is_null($cookie)) {
2023
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
2024
					$this->abandonPagedSearch();
2025
					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2026
						$this->connection->getConnectionResource(), $limit,
2027
						false, $cookie);
2028
					if(!$pagedSearchOK) {
2029
						return false;
2030
					}
2031
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);
0 ignored issues
show
Deprecated Code introduced by
The function OCP\Util::writeLog() has been deprecated: 13.0.0 use log of \OCP\ILogger ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2031
					/** @scrutinizer ignore-deprecated */ \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);

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

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

Loading history...
2032
				} else {
2033
					$e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
2034
					\OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
2035
				}
2036
2037
			}
2038
		/* ++ Fixing RHDS searches with pages with zero results ++
2039
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
2040
		 * due to pages with zero results.
2041
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
2042
		 * if we don't have a previous paged search.
2043
		 */
2044
		} else if ($limit === 0 && !empty($this->lastCookie)) {
2045
			// a search without limit was requested. However, if we do use
2046
			// Paged Search once, we always must do it. This requires us to
2047
			// initialize it with the configured page size.
2048
			$this->abandonPagedSearch();
2049
			// in case someone set it to 0 … use 500, otherwise no results will
2050
			// be returned.
2051
			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2052
			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2053
				$this->connection->getConnectionResource(),
2054
				$pageSize, false, '');
2055
		}
2056
2057
		return $pagedSearchOK;
2058
	}
2059
2060
	/**
2061
	 * Is more than one $attr used for search?
2062
	 *
2063
	 * @param string|string[]|null $attr
2064
	 * @return bool
2065
	 */
2066
	private function manyAttributes($attr): bool {
2067
		if (\is_array($attr)) {
2068
			return \count($attr) > 1;
2069
		}
2070
		return false;
2071
	}
2072
2073
}
2074