Passed
Push — master ( ebd623...95857a )
by Morris
11:31
created

Access::manyAttributes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 5
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(($isUser && $intName !== '' && !$this->ncUserManager->userExists($intName))
613
			|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) {
614
			if($mapper->map($fdn, $intName, $uuid)) {
615
				$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
616
				if($this->ncUserManager instanceof PublicEmitter && $isUser) {
617
					$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]);
618
				}
619
				$newlyMapped = true;
620
				return $intName;
621
			}
622
		}
623
		$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
624
625
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
626
		if (is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
627
			if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
628
				$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$altName]);
629
			}
630
			$newlyMapped = true;
631
			return $altName;
632
		}
633
634
		//if everything else did not help..
635
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
636
		return false;
637
	}
638
639
	/**
640
	 * gives back the user names as they are used ownClod internally
641
	 * @param array $ldapUsers as returned by fetchList()
642
	 * @return array an array with the user names to use in Nextcloud
643
	 *
644
	 * gives back the user names as they are used ownClod internally
645
	 */
646
	public function nextcloudUserNames($ldapUsers) {
647
		return $this->ldap2NextcloudNames($ldapUsers, true);
648
	}
649
650
	/**
651
	 * gives back the group names as they are used ownClod internally
652
	 * @param array $ldapGroups as returned by fetchList()
653
	 * @return array an array with the group names to use in Nextcloud
654
	 *
655
	 * gives back the group names as they are used ownClod internally
656
	 */
657
	public function nextcloudGroupNames($ldapGroups) {
658
		return $this->ldap2NextcloudNames($ldapGroups, false);
659
	}
660
661
	/**
662
	 * @param array $ldapObjects as returned by fetchList()
663
	 * @param bool $isUsers
664
	 * @return array
665
	 * @throws \Exception
666
	 */
667
	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
668
		if($isUsers) {
669
			$nameAttribute = $this->connection->ldapUserDisplayName;
670
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
671
		} else {
672
			$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...
673
		}
674
		$nextcloudNames = [];
675
676
		foreach($ldapObjects as $ldapObject) {
677
			$nameByLDAP = null;
678
			if(    isset($ldapObject[$nameAttribute])
679
				&& is_array($ldapObject[$nameAttribute])
680
				&& isset($ldapObject[$nameAttribute][0])
681
			) {
682
				// might be set, but not necessarily. if so, we use it.
683
				$nameByLDAP = $ldapObject[$nameAttribute][0];
684
			}
685
686
			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
687
			if($ncName) {
688
				$nextcloudNames[] = $ncName;
689
				if($isUsers) {
690
					$this->updateUserState($ncName);
691
					//cache the user names so it does not need to be retrieved
692
					//again later (e.g. sharing dialogue).
693
					if(is_null($nameByLDAP)) {
694
						continue;
695
					}
696
					$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...
697
						? $ldapObject[$sndAttribute][0] : '';
698
					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
699
				}
700
			}
701
		}
702
		return $nextcloudNames;
703
	}
704
705
	/**
706
	 * removes the deleted-flag of a user if it was set
707
	 *
708
	 * @param string $ncname
709
	 * @throws \Exception
710
	 */
711
	public function updateUserState($ncname) {
712
		$user = $this->userManager->get($ncname);
713
		if($user instanceof OfflineUser) {
714
			$user->unmark();
715
		}
716
	}
717
718
	/**
719
	 * caches the user display name
720
	 * @param string $ocName the internal Nextcloud username
721
	 * @param string|false $home the home directory path
722
	 */
723
	public function cacheUserHome($ocName, $home) {
724
		$cacheKey = 'getHome'.$ocName;
725
		$this->connection->writeToCache($cacheKey, $home);
726
	}
727
728
	/**
729
	 * caches a user as existing
730
	 * @param string $ocName the internal Nextcloud username
731
	 */
732
	public function cacheUserExists($ocName) {
733
		$this->connection->writeToCache('userExists'.$ocName, true);
734
	}
735
736
	/**
737
	 * caches the user display name
738
	 * @param string $ocName the internal Nextcloud username
739
	 * @param string $displayName the display name
740
	 * @param string $displayName2 the second display name
741
	 */
742
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
743
		$user = $this->userManager->get($ocName);
744
		if($user === null) {
745
			return;
746
		}
747
		$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

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

907
				$user->/** @scrutinizer ignore-call */ 
908
           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...
908
			} else {
909
				\OC::$server->getLogger()->debug(
910
					"The ldap user manager returned null for $ocName",
911
					['app'=>'user_ldap']
912
				);
913
			}
914
		}
915
	}
916
917
	/**
918
	 * @param string $filter
919
	 * @param string|string[] $attr
920
	 * @param int $limit
921
	 * @param int $offset
922
	 * @return array
923
	 */
924
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
925
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), $this->manyAttributes($attr));
926
	}
927
928
	/**
929
	 * @param array $list
930
	 * @param bool $manyAttributes
931
	 * @return array
932
	 */
933
	private function fetchList($list, $manyAttributes) {
934
		if(is_array($list)) {
0 ignored issues
show
introduced by
The condition is_array($list) is always true.
Loading history...
935
			if($manyAttributes) {
936
				return $list;
937
			} else {
938
				$list = array_reduce($list, function($carry, $item) {
939
					$attribute = array_keys($item)[0];
940
					$carry[] = $item[$attribute][0];
941
					return $carry;
942
				}, array());
943
				return array_unique($list, SORT_LOCALE_STRING);
944
			}
945
		}
946
947
		//error cause actually, maybe throw an exception in future.
948
		return array();
949
	}
950
951
	/**
952
	 * executes an LDAP search, optimized for Users
953
	 * @param string $filter the LDAP filter for the search
954
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
955
	 * @param integer $limit
956
	 * @param integer $offset
957
	 * @return array with the search result
958
	 *
959
	 * Executes an LDAP search
960
	 */
961
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
962
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
963
	}
964
965
	/**
966
	 * @param string $filter
967
	 * @param string|string[] $attr
968
	 * @param int $limit
969
	 * @param int $offset
970
	 * @return false|int
971
	 */
972
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
973
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
974
	}
975
976
	/**
977
	 * executes an LDAP search, optimized for Groups
978
	 * @param string $filter the LDAP filter for the search
979
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
980
	 * @param integer $limit
981
	 * @param integer $offset
982
	 * @return array with the search result
983
	 *
984
	 * Executes an LDAP search
985
	 */
986
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
987
		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

987
		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...
988
	}
989
990
	/**
991
	 * returns the number of available groups
992
	 * @param string $filter the LDAP search filter
993
	 * @param string[] $attr optional
994
	 * @param int|null $limit
995
	 * @param int|null $offset
996
	 * @return int|bool
997
	 */
998
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
999
		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

999
		return $this->count($filter, /** @scrutinizer ignore-type */ $this->connection->ldapBaseGroups, $attr, $limit, $offset);
Loading history...
1000
	}
1001
1002
	/**
1003
	 * returns the number of available objects on the base DN
1004
	 *
1005
	 * @param int|null $limit
1006
	 * @param int|null $offset
1007
	 * @return int|bool
1008
	 */
1009
	public function countObjects($limit = null, $offset = null) {
1010
		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

1010
		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...
1011
	}
1012
1013
	/**
1014
	 * Returns the LDAP handler
1015
	 * @throws \OC\ServerNotAvailableException
1016
	 */
1017
1018
	/**
1019
	 * @return mixed
1020
	 * @throws \OC\ServerNotAvailableException
1021
	 */
1022
	private function invokeLDAPMethod() {
1023
		$arguments = func_get_args();
1024
		$command = array_shift($arguments);
1025
		$cr = array_shift($arguments);
1026
		if (!method_exists($this->ldap, $command)) {
1027
			return null;
1028
		}
1029
		array_unshift($arguments, $cr);
1030
		// php no longer supports call-time pass-by-reference
1031
		// thus cannot support controlPagedResultResponse as the third argument
1032
		// is a reference
1033
		$doMethod = function () use ($command, &$arguments) {
1034
			if ($command == 'controlPagedResultResponse') {
1035
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1036
			} else {
1037
				return call_user_func_array(array($this->ldap, $command), $arguments);
1038
			}
1039
		};
1040
		try {
1041
			$ret = $doMethod();
1042
		} catch (ServerNotAvailableException $e) {
1043
			/* Server connection lost, attempt to reestablish it
1044
			 * Maybe implement exponential backoff?
1045
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1046
			 */
1047
			\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

1047
			/** @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...
1048
			$this->connection->resetConnectionResource();
1049
			$cr = $this->connection->getConnectionResource();
1050
1051
			if(!$this->ldap->isResource($cr)) {
1052
				// Seems like we didn't find any resource.
1053
				\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

1053
				/** @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...
1054
				throw $e;
1055
			}
1056
1057
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1058
			$ret = $doMethod();
1059
		}
1060
		return $ret;
1061
	}
1062
1063
	/**
1064
	 * retrieved. Results will according to the order in the array.
1065
	 *
1066
	 * @param $filter
1067
	 * @param $base
1068
	 * @param string[]|string|null $attr
1069
	 * @param int $limit optional, maximum results to be counted
1070
	 * @param int $offset optional, a starting point
1071
	 * @return array|false array with the search result as first value and pagedSearchOK as
1072
	 * second | false if not successful
1073
	 * @throws ServerNotAvailableException
1074
	 */
1075
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1076
		if(!is_null($attr) && !is_array($attr)) {
1077
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1078
		}
1079
1080
		// See if we have a resource, in case not cancel with message
1081
		$cr = $this->connection->getConnectionResource();
1082
		if(!$this->ldap->isResource($cr)) {
1083
			// Seems like we didn't find any resource.
1084
			// Return an empty array just like before.
1085
			\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

1085
			/** @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...
1086
			return false;
1087
		}
1088
1089
		//check whether paged search should be attempted
1090
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1091
1092
		$linkResources = array_pad(array(), count($base), $cr);
1093
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1094
		// cannot use $cr anymore, might have changed in the previous call!
1095
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1096
		if(!is_array($sr) || $error !== 0) {
1097
			\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

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

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

1424
			/** @scrutinizer ignore-type */ $this->connection->ldapAttributesForUserSearch,
Loading history...
1425
			$this->connection->ldapUserDisplayName);
1426
	}
1427
1428
	/**
1429
	 * creates a filter part for to perform search for groups
1430
	 * @param string $search the search term
1431
	 * @return string the final filter part to use in LDAP searches
1432
	 */
1433
	public function getFilterPartForGroupSearch($search) {
1434
		return $this->getFilterPartForSearch($search,
1435
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Bug Best Practice introduced by
The property ldapAttributesForGroupSearch does not exist on OCA\User_LDAP\Connection. Since you implemented __get, consider adding a @property annotation.
Loading history...
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

1435
			/** @scrutinizer ignore-type */ $this->connection->ldapAttributesForGroupSearch,
Loading history...
1436
			$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...
1437
	}
1438
1439
	/**
1440
	 * creates a filter part for searches by splitting up the given search
1441
	 * string into single words
1442
	 * @param string $search the search term
1443
	 * @param string[] $searchAttributes needs to have at least two attributes,
1444
	 * otherwise it does not make sense :)
1445
	 * @return string the final filter part to use in LDAP searches
1446
	 * @throws \Exception
1447
	 */
1448
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1449
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
0 ignored issues
show
introduced by
The condition is_array($searchAttributes) is always true.
Loading history...
1450
			throw new \Exception('searchAttributes must be an array with at least two string');
1451
		}
1452
		$searchWords = explode(' ', trim($search));
1453
		$wordFilters = array();
1454
		foreach($searchWords as $word) {
1455
			$word = $this->prepareSearchTerm($word);
1456
			//every word needs to appear at least once
1457
			$wordMatchOneAttrFilters = array();
1458
			foreach($searchAttributes as $attr) {
1459
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1460
			}
1461
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1462
		}
1463
		return $this->combineFilterWithAnd($wordFilters);
1464
	}
1465
1466
	/**
1467
	 * creates a filter part for searches
1468
	 * @param string $search the search term
1469
	 * @param string[]|null $searchAttributes
1470
	 * @param string $fallbackAttribute a fallback attribute in case the user
1471
	 * did not define search attributes. Typically the display name attribute.
1472
	 * @return string the final filter part to use in LDAP searches
1473
	 */
1474
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1475
		$filter = array();
1476
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1477
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1478
			try {
1479
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1480
			} catch(\Exception $e) {
1481
				\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

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

1645
				/** @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...
1646
					'user_ldap',
1647
					'Setting '.$attribute.' as '.$uuidAttr,
1648
					ILogger::DEBUG
1649
				);
1650
				$this->connection->$uuidAttr = $attribute;
1651
				return true;
1652
			}
1653
		}
1654
		\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

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

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

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