Passed
Push — master ( c4a9bd...e7950a )
by Blizzz
17:30 queued 10s
created

Access::mapAndAnnounceIfApplicable()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 3
nop 5
dl 0
loc 15
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)) {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBaseGroups does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like $this->connection->ldapBaseGroups can also be of type boolean; however, parameter $bases of OCA\User_LDAP\Access::isDNPartOfBase() does only seem to accept string[], maybe add an additional type check? ( Ignorable by Annotation )

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

465
		if(!$this->isDNPartOfBase($fdn, /** @scrutinizer ignore-type */ $this->connection->ldapBaseGroups)) {
Loading history...
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)) {
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBaseGroups does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like $this->connection->ldapBaseGroups can also be of type boolean; however, parameter $bases of OCA\User_LDAP\Access::isDNPartOfBase() does only seem to accept string[], maybe add an additional type check? ( Ignorable by Annotation )

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

495
			if(!$this->isDNPartOfBase($dn, /** @scrutinizer ignore-type */ $this->connection->ldapBaseGroups)) {
Loading history...
496
				$this->connection->writeToCache($cacheKey, false);
497
				continue;
498
			}
499
500
			$result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilter does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupDisplayName does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
549
			$filter = $this->connection->ldapGroupFilter;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupFilter does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
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)) {
0 ignored issues
show
introduced by
The condition is_string($ncName) is always true.
Loading history...
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);
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);
576
				return false;
577
			}
578
			$ldapName = $ldapName[0];
579
		}
580
581
		if($isUser) {
582
			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
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;
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);
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;
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupDisplayName does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
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
				}
716
			}
717
		}
718
		return $nextcloudNames;
719
	}
720
721
	/**
722
	 * removes the deleted-flag of a user if it was set
723
	 *
724
	 * @param string $ncname
725
	 * @throws \Exception
726
	 */
727
	public function updateUserState($ncname) {
728
		$user = $this->userManager->get($ncname);
729
		if($user instanceof OfflineUser) {
730
			$user->unmark();
731
		}
732
	}
733
734
	/**
735
	 * caches the user display name
736
	 * @param string $ocName the internal Nextcloud username
737
	 * @param string|false $home the home directory path
738
	 */
739
	public function cacheUserHome($ocName, $home) {
740
		$cacheKey = 'getHome'.$ocName;
741
		$this->connection->writeToCache($cacheKey, $home);
742
	}
743
744
	/**
745
	 * caches a user as existing
746
	 * @param string $ocName the internal Nextcloud username
747
	 */
748
	public function cacheUserExists($ocName) {
749
		$this->connection->writeToCache('userExists'.$ocName, true);
750
	}
751
752
	/**
753
	 * caches the user display name
754
	 * @param string $ocName the internal Nextcloud username
755
	 * @param string $displayName the display name
756
	 * @param string $displayName2 the second display name
757
	 */
758
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
759
		$user = $this->userManager->get($ocName);
760
		if($user === null) {
761
			return;
762
		}
763
		$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

763
		/** @scrutinizer ignore-call */ 
764
  $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...
764
		$cacheKeyTrunk = 'getDisplayName';
765
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
766
	}
767
768
	/**
769
	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
770
	 * @param string $name the display name of the object
771
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
772
	 *
773
	 * Instead of using this method directly, call
774
	 * createAltInternalOwnCloudName($name, true)
775
	 */
776
	private function _createAltInternalOwnCloudNameForUsers($name) {
777
		$attempts = 0;
778
		//while loop is just a precaution. If a name is not generated within
779
		//20 attempts, something else is very wrong. Avoids infinite loop.
780
		while($attempts < 20){
781
			$altName = $name . '_' . rand(1000,9999);
782
			if(!$this->ncUserManager->userExists($altName)) {
783
				return $altName;
784
			}
785
			$attempts++;
786
		}
787
		return false;
788
	}
789
790
	/**
791
	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
792
	 * @param string $name the display name of the object
793
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
794
	 *
795
	 * Instead of using this method directly, call
796
	 * createAltInternalOwnCloudName($name, false)
797
	 *
798
	 * Group names are also used as display names, so we do a sequential
799
	 * numbering, e.g. Developers_42 when there are 41 other groups called
800
	 * "Developers"
801
	 */
802
	private function _createAltInternalOwnCloudNameForGroups($name) {
803
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
804
		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...
805
			$lastNo = 1; //will become name_2
806
		} else {
807
			natsort($usedNames);
808
			$lastName = array_pop($usedNames);
809
			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
810
		}
811
		$altName = $name.'_'. (string)($lastNo+1);
812
		unset($usedNames);
813
814
		$attempts = 1;
815
		while($attempts < 21){
816
			// Check to be really sure it is unique
817
			// while loop is just a precaution. If a name is not generated within
818
			// 20 attempts, something else is very wrong. Avoids infinite loop.
819
			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
820
				return $altName;
821
			}
822
			$altName = $name . '_' . ($lastNo + $attempts);
823
			$attempts++;
824
		}
825
		return false;
826
	}
827
828
	/**
829
	 * creates a unique name for internal Nextcloud use.
830
	 * @param string $name the display name of the object
831
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
832
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
833
	 */
834
	private function createAltInternalOwnCloudName($name, $isUser) {
835
		$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...
836
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
837
		if($isUser) {
838
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
839
		} else {
840
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
841
		}
842
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
843
844
		return $altName;
845
	}
846
847
	/**
848
	 * fetches a list of users according to a provided loginName and utilizing
849
	 * the login filter.
850
	 *
851
	 * @param string $loginName
852
	 * @param array $attributes optional, list of attributes to read
853
	 * @return array
854
	 */
855
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
856
		$loginName = $this->escapeFilterPart($loginName);
857
		$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...
858
		return $this->fetchListOfUsers($filter, $attributes);
859
	}
860
861
	/**
862
	 * counts the number of users according to a provided loginName and
863
	 * utilizing the login filter.
864
	 *
865
	 * @param string $loginName
866
	 * @return int
867
	 */
868
	public function countUsersByLoginName($loginName) {
869
		$loginName = $this->escapeFilterPart($loginName);
870
		$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...
871
		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...
872
	}
873
874
	/**
875
	 * @param string $filter
876
	 * @param string|string[] $attr
877
	 * @param int $limit
878
	 * @param int $offset
879
	 * @param bool $forceApplyAttributes
880
	 * @return array
881
	 */
882
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
883
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
884
		$recordsToUpdate = $ldapRecords;
885
		if(!$forceApplyAttributes) {
886
			$isBackgroundJobModeAjax = $this->config
887
					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
888
			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
889
				$newlyMapped = false;
890
				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
891
				if(is_string($uid)) {
0 ignored issues
show
introduced by
The condition is_string($uid) is always true.
Loading history...
892
					$this->cacheUserExists($uid);
893
				}
894
				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
895
			});
896
		}
897
		$this->batchApplyUserAttributes($recordsToUpdate);
898
		return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
899
	}
900
901
	/**
902
	 * provided with an array of LDAP user records the method will fetch the
903
	 * user object and requests it to process the freshly fetched attributes and
904
	 * and their values
905
	 *
906
	 * @param array $ldapRecords
907
	 * @throws \Exception
908
	 */
909
	public function batchApplyUserAttributes(array $ldapRecords){
910
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
911
		foreach($ldapRecords as $userRecord) {
912
			if(!isset($userRecord[$displayNameAttribute])) {
913
				// displayName is obligatory
914
				continue;
915
			}
916
			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
917
			if($ocName === false) {
918
				continue;
919
			}
920
			$this->updateUserState($ocName);
921
			$user = $this->userManager->get($ocName);
922
			if ($user !== null) {
923
				$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

923
				$user->/** @scrutinizer ignore-call */ 
924
           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...
924
			} else {
925
				\OC::$server->getLogger()->debug(
926
					"The ldap user manager returned null for $ocName",
927
					['app'=>'user_ldap']
928
				);
929
			}
930
		}
931
	}
932
933
	/**
934
	 * @param string $filter
935
	 * @param string|string[] $attr
936
	 * @param int $limit
937
	 * @param int $offset
938
	 * @return array
939
	 */
940
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
941
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), $this->manyAttributes($attr));
942
	}
943
944
	/**
945
	 * @param array $list
946
	 * @param bool $manyAttributes
947
	 * @return array
948
	 */
949
	private function fetchList($list, $manyAttributes) {
950
		if(is_array($list)) {
0 ignored issues
show
introduced by
The condition is_array($list) is always true.
Loading history...
951
			if($manyAttributes) {
952
				return $list;
953
			} else {
954
				$list = array_reduce($list, function($carry, $item) {
955
					$attribute = array_keys($item)[0];
956
					$carry[] = $item[$attribute][0];
957
					return $carry;
958
				}, array());
959
				return array_unique($list, SORT_LOCALE_STRING);
960
			}
961
		}
962
963
		//error cause actually, maybe throw an exception in future.
964
		return array();
965
	}
966
967
	/**
968
	 * executes an LDAP search, optimized for Users
969
	 * @param string $filter the LDAP filter for the search
970
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
971
	 * @param integer $limit
972
	 * @param integer $offset
973
	 * @return array with the search result
974
	 *
975
	 * Executes an LDAP search
976
	 */
977
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
978
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
979
	}
980
981
	/**
982
	 * @param string $filter
983
	 * @param string|string[] $attr
984
	 * @param int $limit
985
	 * @param int $offset
986
	 * @return false|int
987
	 */
988
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
989
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
990
	}
991
992
	/**
993
	 * executes an LDAP search, optimized for Groups
994
	 * @param string $filter the LDAP filter for the search
995
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
996
	 * @param integer $limit
997
	 * @param integer $offset
998
	 * @return array with the search result
999
	 *
1000
	 * Executes an LDAP search
1001
	 */
1002
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1003
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Bug introduced by
It seems like $this->connection->ldapBaseGroups can also be of type boolean; however, parameter $base of OCA\User_LDAP\Access::search() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1003
		return $this->search($filter, /** @scrutinizer ignore-type */ $this->connection->ldapBaseGroups, $attr, $limit, $offset);
Loading history...
Bug Best Practice introduced by
The property ldapBaseGroups does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1004
	}
1005
1006
	/**
1007
	 * returns the number of available groups
1008
	 * @param string $filter the LDAP search filter
1009
	 * @param string[] $attr optional
1010
	 * @param int|null $limit
1011
	 * @param int|null $offset
1012
	 * @return int|bool
1013
	 */
1014
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
1015
		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapBaseGroups does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like $this->connection->ldapBaseGroups can also be of type boolean; however, parameter $base of OCA\User_LDAP\Access::count() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1015
		return $this->count($filter, /** @scrutinizer ignore-type */ $this->connection->ldapBaseGroups, $attr, $limit, $offset);
Loading history...
1016
	}
1017
1018
	/**
1019
	 * returns the number of available objects on the base DN
1020
	 *
1021
	 * @param int|null $limit
1022
	 * @param int|null $offset
1023
	 * @return int|bool
1024
	 */
1025
	public function countObjects($limit = null, $offset = null) {
1026
		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
0 ignored issues
show
Bug introduced by
It seems like $this->connection->ldapBase can also be of type boolean; however, parameter $base of OCA\User_LDAP\Access::count() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

1026
		return $this->count('objectclass=*', /** @scrutinizer ignore-type */ $this->connection->ldapBase, array('dn'), $limit, $offset);
Loading history...
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...
1027
	}
1028
1029
	/**
1030
	 * Returns the LDAP handler
1031
	 * @throws \OC\ServerNotAvailableException
1032
	 */
1033
1034
	/**
1035
	 * @return mixed
1036
	 * @throws \OC\ServerNotAvailableException
1037
	 */
1038
	private function invokeLDAPMethod() {
1039
		$arguments = func_get_args();
1040
		$command = array_shift($arguments);
1041
		$cr = array_shift($arguments);
1042
		if (!method_exists($this->ldap, $command)) {
1043
			return null;
1044
		}
1045
		array_unshift($arguments, $cr);
1046
		// php no longer supports call-time pass-by-reference
1047
		// thus cannot support controlPagedResultResponse as the third argument
1048
		// is a reference
1049
		$doMethod = function () use ($command, &$arguments) {
1050
			if ($command == 'controlPagedResultResponse') {
1051
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1052
			} else {
1053
				return call_user_func_array(array($this->ldap, $command), $arguments);
1054
			}
1055
		};
1056
		try {
1057
			$ret = $doMethod();
1058
		} catch (ServerNotAvailableException $e) {
1059
			/* Server connection lost, attempt to reestablish it
1060
			 * Maybe implement exponential backoff?
1061
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1062
			 */
1063
			\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

1063
			/** @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...
1064
			$this->connection->resetConnectionResource();
1065
			$cr = $this->connection->getConnectionResource();
1066
1067
			if(!$this->ldap->isResource($cr)) {
1068
				// Seems like we didn't find any resource.
1069
				\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

1069
				/** @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...
1070
				throw $e;
1071
			}
1072
1073
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1074
			$ret = $doMethod();
1075
		}
1076
		return $ret;
1077
	}
1078
1079
	/**
1080
	 * retrieved. Results will according to the order in the array.
1081
	 *
1082
	 * @param $filter
1083
	 * @param $base
1084
	 * @param string[]|string|null $attr
1085
	 * @param int $limit optional, maximum results to be counted
1086
	 * @param int $offset optional, a starting point
1087
	 * @return array|false array with the search result as first value and pagedSearchOK as
1088
	 * second | false if not successful
1089
	 * @throws ServerNotAvailableException
1090
	 */
1091
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1092
		if(!is_null($attr) && !is_array($attr)) {
1093
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1094
		}
1095
1096
		// See if we have a resource, in case not cancel with message
1097
		$cr = $this->connection->getConnectionResource();
1098
		if(!$this->ldap->isResource($cr)) {
1099
			// Seems like we didn't find any resource.
1100
			// Return an empty array just like before.
1101
			\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

1101
			/** @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...
1102
			return false;
1103
		}
1104
1105
		//check whether paged search should be attempted
1106
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1107
1108
		$linkResources = array_pad(array(), count($base), $cr);
1109
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1110
		// cannot use $cr anymore, might have changed in the previous call!
1111
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1112
		if(!is_array($sr) || $error !== 0) {
1113
			\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

1113
			/** @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...
1114
			return false;
1115
		}
1116
1117
		return array($sr, $pagedSearchOK);
1118
	}
1119
1120
	/**
1121
	 * processes an LDAP paged search operation
1122
	 * @param array $sr the array containing the LDAP search resources
1123
	 * @param string $filter the LDAP filter for the search
1124
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1125
	 * @param int $iFoundItems number of results in the single search operation
1126
	 * @param int $limit maximum results to be counted
1127
	 * @param int $offset a starting point
1128
	 * @param bool $pagedSearchOK whether a paged search has been executed
1129
	 * @param bool $skipHandling required for paged search when cookies to
1130
	 * prior results need to be gained
1131
	 * @return bool cookie validity, true if we have more pages, false otherwise.
1132
	 */
1133
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1134
		$cookie = null;
1135
		if($pagedSearchOK) {
1136
			$cr = $this->connection->getConnectionResource();
1137
			foreach($sr as $key => $res) {
1138
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1139
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1140
				}
1141
			}
1142
1143
			//browsing through prior pages to get the cookie for the new one
1144
			if($skipHandling) {
1145
				return false;
1146
			}
1147
			// if count is bigger, then the server does not support
1148
			// paged search. Instead, he did a normal search. We set a
1149
			// flag here, so the callee knows how to deal with it.
1150
			if($iFoundItems <= $limit) {
1151
				$this->pagedSearchedSuccessful = true;
1152
			}
1153
		} else {
1154
			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...
1155
				\OC::$server->getLogger()->debug(
1156
					'Paged search was not available',
1157
					[ 'app' => 'user_ldap' ]
1158
				);
1159
			}
1160
		}
1161
		/* ++ Fixing RHDS searches with pages with zero results ++
1162
		 * Return cookie status. If we don't have more pages, with RHDS
1163
		 * cookie is null, with openldap cookie is an empty string and
1164
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1165
		 */
1166
		return !empty($cookie) || $cookie === '0';
1167
	}
1168
1169
	/**
1170
	 * executes an LDAP search, but counts the results only
1171
	 *
1172
	 * @param string $filter the LDAP filter for the search
1173
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1174
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1175
	 * retrieved. Results will according to the order in the array.
1176
	 * @param int $limit optional, maximum results to be counted
1177
	 * @param int $offset optional, a starting point
1178
	 * @param bool $skipHandling indicates whether the pages search operation is
1179
	 * completed
1180
	 * @return int|false Integer or false if the search could not be initialized
1181
	 * @throws ServerNotAvailableException
1182
	 */
1183
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1184
		\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

1184
		/** @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...
1185
1186
		$limitPerPage = (int)$this->connection->ldapPagingSize;
1187
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1188
			$limitPerPage = $limit;
1189
		}
1190
1191
		$counter = 0;
1192
		$count = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $count is dead and can be removed.
Loading history...
1193
		$this->connection->getConnectionResource();
1194
1195
		do {
1196
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1197
			if($search === false) {
1198
				return $counter > 0 ? $counter : false;
1199
			}
1200
			list($sr, $pagedSearchOK) = $search;
1201
1202
			/* ++ Fixing RHDS searches with pages with zero results ++
1203
			 * countEntriesInSearchResults() method signature changed
1204
			 * by removing $limit and &$hasHitLimit parameters
1205
			 */
1206
			$count = $this->countEntriesInSearchResults($sr);
1207
			$counter += $count;
1208
1209
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1210
										$offset, $pagedSearchOK, $skipHandling);
1211
			$offset += $limitPerPage;
1212
			/* ++ Fixing RHDS searches with pages with zero results ++
1213
			 * Continue now depends on $hasMorePages value
1214
			 */
1215
			$continue = $pagedSearchOK && $hasMorePages;
1216
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1217
1218
		return $counter;
1219
	}
1220
1221
	/**
1222
	 * @param array $searchResults
1223
	 * @return int
1224
	 */
1225
	private function countEntriesInSearchResults($searchResults) {
1226
		$counter = 0;
1227
1228
		foreach($searchResults as $res) {
1229
			$count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1230
			$counter += $count;
1231
		}
1232
1233
		return $counter;
1234
	}
1235
1236
	/**
1237
	 * Executes an LDAP search
1238
	 *
1239
	 * @param string $filter the LDAP filter for the search
1240
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1241
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1242
	 * @param int $limit
1243
	 * @param int $offset
1244
	 * @param bool $skipHandling
1245
	 * @return array with the search result
1246
	 * @throws ServerNotAvailableException
1247
	 */
1248
	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1249
		$limitPerPage = (int)$this->connection->ldapPagingSize;
1250
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1251
			$limitPerPage = $limit;
1252
		}
1253
1254
		/* ++ Fixing RHDS searches with pages with zero results ++
1255
		 * As we can have pages with zero results and/or pages with less
1256
		 * than $limit results but with a still valid server 'cookie',
1257
		 * loops through until we get $continue equals true and
1258
		 * $findings['count'] < $limit
1259
		 */
1260
		$findings = [];
1261
		$savedoffset = $offset;
1262
		do {
1263
			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1264
			if($search === false) {
1265
				return [];
1266
			}
1267
			list($sr, $pagedSearchOK) = $search;
1268
			$cr = $this->connection->getConnectionResource();
1269
1270
			if($skipHandling) {
1271
				//i.e. result do not need to be fetched, we just need the cookie
1272
				//thus pass 1 or any other value as $iFoundItems because it is not
1273
				//used
1274
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1275
								$offset, $pagedSearchOK,
1276
								$skipHandling);
1277
				return array();
1278
			}
1279
1280
			$iFoundItems = 0;
1281
			foreach($sr as $res) {
1282
				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1283
				$iFoundItems = max($iFoundItems, $findings['count']);
1284
				unset($findings['count']);
1285
			}
1286
1287
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1288
				$limitPerPage, $offset, $pagedSearchOK,
1289
										$skipHandling);
1290
			$offset += $limitPerPage;
1291
		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1292
		// reseting offset
1293
		$offset = $savedoffset;
1294
1295
		// if we're here, probably no connection resource is returned.
1296
		// to make Nextcloud behave nicely, we simply give back an empty array.
1297
		if(is_null($findings)) {
0 ignored issues
show
introduced by
The condition is_null($findings) is always false.
Loading history...
1298
			return array();
1299
		}
1300
1301
		if(!is_null($attr)) {
1302
			$selection = [];
1303
			$i = 0;
1304
			foreach($findings as $item) {
1305
				if(!is_array($item)) {
1306
					continue;
1307
				}
1308
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1309
				foreach($attr as $key) {
1310
					if(isset($item[$key])) {
1311
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1312
							unset($item[$key]['count']);
1313
						}
1314
						if($key !== 'dn') {
1315
							if($this->resemblesDN($key)) {
1316
								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1317
							} else if($key === 'objectguid' || $key === 'guid') {
1318
								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1319
							} else {
1320
								$selection[$i][$key] = $item[$key];
1321
							}
1322
						} else {
1323
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1324
						}
1325
					}
1326
1327
				}
1328
				$i++;
1329
			}
1330
			$findings = $selection;
1331
		}
1332
		//we slice the findings, when
1333
		//a) paged search unsuccessful, though attempted
1334
		//b) no paged search, but limit set
1335
		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...
1336
			&& $pagedSearchOK)
1337
			|| (
1338
				!$pagedSearchOK
1339
				&& !is_null($limit)
1340
			)
1341
		) {
1342
			$findings = array_slice($findings, (int)$offset, $limit);
1343
		}
1344
		return $findings;
1345
	}
1346
1347
	/**
1348
	 * @param string $name
1349
	 * @return string
1350
	 * @throws \InvalidArgumentException
1351
	 */
1352
	public function sanitizeUsername($name) {
1353
		$name = trim($name);
1354
1355
		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...
1356
			return $name;
1357
		}
1358
1359
		// Transliteration to ASCII
1360
		$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1361
		if($transliterated !== false) {
1362
			// depending on system config iconv can work or not
1363
			$name = $transliterated;
1364
		}
1365
1366
		// Replacements
1367
		$name = str_replace(' ', '_', $name);
1368
1369
		// Every remaining disallowed characters will be removed
1370
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1371
1372
		if($name === '') {
1373
			throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1374
		}
1375
1376
		return $name;
1377
	}
1378
1379
	/**
1380
	* escapes (user provided) parts for LDAP filter
1381
	* @param string $input, the provided value
1382
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1383
	* @return string the escaped string
1384
	*/
1385
	public function escapeFilterPart($input, $allowAsterisk = false) {
1386
		$asterisk = '';
1387
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1388
			$asterisk = '*';
1389
			$input = mb_substr($input, 1, null, 'UTF-8');
1390
		}
1391
		$search  = array('*', '\\', '(', ')');
1392
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1393
		return $asterisk . str_replace($search, $replace, $input);
1394
	}
1395
1396
	/**
1397
	 * combines the input filters with AND
1398
	 * @param string[] $filters the filters to connect
1399
	 * @return string the combined filter
1400
	 */
1401
	public function combineFilterWithAnd($filters) {
1402
		return $this->combineFilter($filters, '&');
1403
	}
1404
1405
	/**
1406
	 * combines the input filters with OR
1407
	 * @param string[] $filters the filters to connect
1408
	 * @return string the combined filter
1409
	 * Combines Filter arguments with OR
1410
	 */
1411
	public function combineFilterWithOr($filters) {
1412
		return $this->combineFilter($filters, '|');
1413
	}
1414
1415
	/**
1416
	 * combines the input filters with given operator
1417
	 * @param string[] $filters the filters to connect
1418
	 * @param string $operator either & or |
1419
	 * @return string the combined filter
1420
	 */
1421
	private function combineFilter($filters, $operator) {
1422
		$combinedFilter = '('.$operator;
1423
		foreach($filters as $filter) {
1424
			if ($filter !== '' && $filter[0] !== '(') {
1425
				$filter = '('.$filter.')';
1426
			}
1427
			$combinedFilter.=$filter;
1428
		}
1429
		$combinedFilter.=')';
1430
		return $combinedFilter;
1431
	}
1432
1433
	/**
1434
	 * creates a filter part for to perform search for users
1435
	 * @param string $search the search term
1436
	 * @return string the final filter part to use in LDAP searches
1437
	 */
1438
	public function getFilterPartForUserSearch($search) {
1439
		return $this->getFilterPartForSearch($search,
1440
			$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...
Bug introduced by
It seems like $this->connection->ldapAttributesForUserSearch can also be of type boolean; however, parameter $searchAttributes of OCA\User_LDAP\Access::getFilterPartForSearch() does only seem to accept null|string[], maybe add an additional type check? ( Ignorable by Annotation )

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

1440
			/** @scrutinizer ignore-type */ $this->connection->ldapAttributesForUserSearch,
Loading history...
1441
			$this->connection->ldapUserDisplayName);
1442
	}
1443
1444
	/**
1445
	 * creates a filter part for to perform search for groups
1446
	 * @param string $search the search term
1447
	 * @return string the final filter part to use in LDAP searches
1448
	 */
1449
	public function getFilterPartForGroupSearch($search) {
1450
		return $this->getFilterPartForSearch($search,
1451
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Bug introduced by
It seems like $this->connection->ldapAttributesForGroupSearch can also be of type boolean; however, parameter $searchAttributes of OCA\User_LDAP\Access::getFilterPartForSearch() does only seem to accept null|string[], maybe add an additional type check? ( Ignorable by Annotation )

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

1451
			/** @scrutinizer ignore-type */ $this->connection->ldapAttributesForGroupSearch,
Loading history...
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...
1452
			$this->connection->ldapGroupDisplayName);
0 ignored issues
show
Bug Best Practice introduced by
The property ldapGroupDisplayName does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
1453
	}
1454
1455
	/**
1456
	 * creates a filter part for searches by splitting up the given search
1457
	 * string into single words
1458
	 * @param string $search the search term
1459
	 * @param string[] $searchAttributes needs to have at least two attributes,
1460
	 * otherwise it does not make sense :)
1461
	 * @return string the final filter part to use in LDAP searches
1462
	 * @throws \Exception
1463
	 */
1464
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1465
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
0 ignored issues
show
introduced by
The condition is_array($searchAttributes) is always true.
Loading history...
1466
			throw new \Exception('searchAttributes must be an array with at least two string');
1467
		}
1468
		$searchWords = explode(' ', trim($search));
1469
		$wordFilters = array();
1470
		foreach($searchWords as $word) {
1471
			$word = $this->prepareSearchTerm($word);
1472
			//every word needs to appear at least once
1473
			$wordMatchOneAttrFilters = array();
1474
			foreach($searchAttributes as $attr) {
1475
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1476
			}
1477
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1478
		}
1479
		return $this->combineFilterWithAnd($wordFilters);
1480
	}
1481
1482
	/**
1483
	 * creates a filter part for searches
1484
	 * @param string $search the search term
1485
	 * @param string[]|null $searchAttributes
1486
	 * @param string $fallbackAttribute a fallback attribute in case the user
1487
	 * did not define search attributes. Typically the display name attribute.
1488
	 * @return string the final filter part to use in LDAP searches
1489
	 */
1490
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1491
		$filter = array();
1492
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1493
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1494
			try {
1495
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1496
			} catch(\Exception $e) {
1497
				\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

1497
				/** @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...
1498
					'user_ldap',
1499
					'Creating advanced filter for search failed, falling back to simple method.',
1500
					ILogger::INFO
1501
				);
1502
			}
1503
		}
1504
1505
		$search = $this->prepareSearchTerm($search);
1506
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1507
			if ($fallbackAttribute === '') {
1508
				return '';
1509
			}
1510
			$filter[] = $fallbackAttribute . '=' . $search;
1511
		} else {
1512
			foreach($searchAttributes as $attribute) {
1513
				$filter[] = $attribute . '=' . $search;
1514
			}
1515
		}
1516
		if(count($filter) === 1) {
1517
			return '('.$filter[0].')';
1518
		}
1519
		return $this->combineFilterWithOr($filter);
1520
	}
1521
1522
	/**
1523
	 * returns the search term depending on whether we are allowed
1524
	 * list users found by ldap with the current input appended by
1525
	 * a *
1526
	 * @return string
1527
	 */
1528
	private function prepareSearchTerm($term) {
1529
		$config = \OC::$server->getConfig();
1530
1531
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1532
1533
		$result = $term;
1534
		if ($term === '') {
1535
			$result = '*';
1536
		} else if ($allowEnum !== 'no') {
1537
			$result = $term . '*';
1538
		}
1539
		return $result;
1540
	}
1541
1542
	/**
1543
	 * returns the filter used for counting users
1544
	 * @return string
1545
	 */
1546
	public function getFilterForUserCount() {
1547
		$filter = $this->combineFilterWithAnd(array(
1548
			$this->connection->ldapUserFilter,
1549
			$this->connection->ldapUserDisplayName . '=*'
1550
		));
1551
1552
		return $filter;
1553
	}
1554
1555
	/**
1556
	 * @param string $name
1557
	 * @param string $password
1558
	 * @return bool
1559
	 */
1560
	public function areCredentialsValid($name, $password) {
1561
		$name = $this->helper->DNasBaseParameter($name);
1562
		$testConnection = clone $this->connection;
1563
		$credentials = array(
1564
			'ldapAgentName' => $name,
1565
			'ldapAgentPassword' => $password
1566
		);
1567
		if(!$testConnection->setConfiguration($credentials)) {
1568
			return false;
1569
		}
1570
		return $testConnection->bind();
1571
	}
1572
1573
	/**
1574
	 * reverse lookup of a DN given a known UUID
1575
	 *
1576
	 * @param string $uuid
1577
	 * @return string
1578
	 * @throws \Exception
1579
	 */
1580
	public function getUserDnByUuid($uuid) {
1581
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1582
		$filter       = $this->connection->ldapUserFilter;
1583
		$base         = $this->connection->ldapBaseUsers;
1584
1585
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1586
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1587
			// existing DN to be able to reliably detect it.
1588
			$result = $this->search($filter, $base, ['dn'], 1);
1589
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1590
				throw new \Exception('Cannot determine UUID attribute');
1591
			}
1592
			$dn = $result[0]['dn'][0];
1593
			if(!$this->detectUuidAttribute($dn, true)) {
1594
				throw new \Exception('Cannot determine UUID attribute');
1595
			}
1596
		} else {
1597
			// The UUID attribute is either known or an override is given.
1598
			// By calling this method we ensure that $this->connection->$uuidAttr
1599
			// is definitely set
1600
			if(!$this->detectUuidAttribute('', true)) {
1601
				throw new \Exception('Cannot determine UUID attribute');
1602
			}
1603
		}
1604
1605
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1606
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1607
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1608
		}
1609
1610
		$filter = $uuidAttr . '=' . $uuid;
1611
		$result = $this->searchUsers($filter, ['dn'], 2);
1612
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1613
			// we put the count into account to make sure that this is
1614
			// really unique
1615
			return $result[0]['dn'][0];
1616
		}
1617
1618
		throw new \Exception('Cannot determine UUID attribute');
1619
	}
1620
1621
	/**
1622
	 * auto-detects the directory's UUID attribute
1623
	 *
1624
	 * @param string $dn a known DN used to check against
1625
	 * @param bool $isUser
1626
	 * @param bool $force the detection should be run, even if it is not set to auto
1627
	 * @param array|null $ldapRecord
1628
	 * @return bool true on success, false otherwise
1629
	 */
1630
	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1631
		if($isUser) {
1632
			$uuidAttr     = 'ldapUuidUserAttribute';
1633
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1634
		} else {
1635
			$uuidAttr     = 'ldapUuidGroupAttribute';
1636
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1637
		}
1638
1639
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1640
			return true;
1641
		}
1642
1643
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1644
			$this->connection->$uuidAttr = $uuidOverride;
1645
			return true;
1646
		}
1647
1648
		foreach(self::UUID_ATTRIBUTES as $attribute) {
1649
			if($ldapRecord !== null) {
1650
				// we have the info from LDAP already, we don't need to talk to the server again
1651
				if(isset($ldapRecord[$attribute])) {
1652
					$this->connection->$uuidAttr = $attribute;
1653
					return true;
1654
				} else {
1655
					continue;
1656
				}
1657
			}
1658
1659
			$value = $this->readAttribute($dn, $attribute);
1660
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1661
				\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

1661
				/** @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...
1662
					'user_ldap',
1663
					'Setting '.$attribute.' as '.$uuidAttr,
1664
					ILogger::DEBUG
1665
				);
1666
				$this->connection->$uuidAttr = $attribute;
1667
				return true;
1668
			}
1669
		}
1670
		\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

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

1970
			/** @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...
1971
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1972
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1973
				ILogger::DEBUG);
1974
			//get the cookie from the search for the previous search, required by LDAP
1975
			foreach($bases as $base) {
1976
1977
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1978
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1979
					// no cookie known from a potential previous search. We need
1980
					// to start from 0 to come to the desired page. cookie value
1981
					// of '0' is valid, because 389ds
1982
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1983
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1984
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1985
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1986
					// '0' is valid, because 389ds
1987
					//TODO: remember this, probably does not change in the next request...
1988
					if(empty($cookie) && $cookie !== '0') {
1989
						$cookie = null;
1990
					}
1991
				}
1992
				if(!is_null($cookie)) {
1993
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1994
					$this->abandonPagedSearch();
1995
					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1996
						$this->connection->getConnectionResource(), $limit,
1997
						false, $cookie);
1998
					if(!$pagedSearchOK) {
1999
						return false;
2000
					}
2001
					\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

2001
					/** @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...
2002
				} else {
2003
					$e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
2004
					\OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
2005
				}
2006
2007
			}
2008
		/* ++ Fixing RHDS searches with pages with zero results ++
2009
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
2010
		 * due to pages with zero results.
2011
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
2012
		 * if we don't have a previous paged search.
2013
		 */
2014
		} else if ($limit === 0 && !empty($this->lastCookie)) {
2015
			// a search without limit was requested. However, if we do use
2016
			// Paged Search once, we always must do it. This requires us to
2017
			// initialize it with the configured page size.
2018
			$this->abandonPagedSearch();
2019
			// in case someone set it to 0 … use 500, otherwise no results will
2020
			// be returned.
2021
			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2022
			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2023
				$this->connection->getConnectionResource(),
2024
				$pageSize, false, '');
2025
		}
2026
2027
		return $pagedSearchOK;
2028
	}
2029
2030
	/**
2031
	 * Is more than one $attr used for search?
2032
	 *
2033
	 * @param string|string[]|null $attr
2034
	 * @return bool
2035
	 */
2036
	private function manyAttributes($attr): bool {
2037
		if (\is_array($attr)) {
2038
			return \count($attr) > 1;
2039
		}
2040
		return false;
2041
	}
2042
2043
}
2044