Completed
Push — master ( a0f16c...8318b2 )
by Blizzz
14:13
created

Access::invokeLDAPMethod()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 40
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

Loading history...
380
		return (is_array($r) && count($r) > 1);
381
	}
382
383
	/**
384
	 * returns a DN-string that is cleaned from not domain parts, e.g.
385
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
386
	 * becomes dc=foobar,dc=server,dc=org
387
	 * @param string $dn
388
	 * @return string
389
	 */
390
	public function getDomainDNFromDN($dn) {
391
		$allParts = $this->ldap->explodeDN($dn, 0);
392
		if($allParts === false) {
393
			//not a valid DN
394
			return '';
395
		}
396
		$domainParts = array();
397
		$dcFound = false;
398
		foreach($allParts as $part) {
399
			if(!$dcFound && strpos($part, 'dc=') === 0) {
400
				$dcFound = true;
401
			}
402
			if($dcFound) {
403
				$domainParts[] = $part;
404
			}
405
		}
406
		$domainDN = implode(',', $domainParts);
407
		return $domainDN;
408
	}
409
410
	/**
411
	 * returns the LDAP DN for the given internal Nextcloud name of the group
412
	 * @param string $name the Nextcloud name in question
413
	 * @return string|false LDAP DN on success, otherwise false
414
	 */
415
	public function groupname2dn($name) {
416
		return $this->groupMapper->getDNByName($name);
417
	}
418
419
	/**
420
	 * returns the LDAP DN for the given internal Nextcloud name of the user
421
	 * @param string $name the Nextcloud name in question
422
	 * @return string|false with the LDAP DN on success, otherwise false
423
	 */
424
	public function username2dn($name) {
425
		$fdn = $this->userMapper->getDNByName($name);
426
427
		//Check whether the DN belongs to the Base, to avoid issues on multi-
428
		//server setups
429
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
430
			return $fdn;
431
		}
432
433
		return false;
434
	}
435
436
	/**
437
	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
438
	 * @param string $fdn the dn of the group object
439
	 * @param string $ldapName optional, the display name of the object
440
	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
441
	 */
442 View Code Duplication
	public function dn2groupname($fdn, $ldapName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
443
		//To avoid bypassing the base DN settings under certain circumstances
444
		//with the group support, check whether the provided DN matches one of
445
		//the given Bases
446
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
481
			if(is_array($result)) {
482
				$this->connection->writeToCache($cacheKey, true);
483
				$validGroupDNs[] = $dn;
484
			} else {
485
				$this->connection->writeToCache($cacheKey, false);
486
			}
487
488
		}
489
		return $validGroupDNs;
490
	}
491
492
	/**
493
	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
494
	 * @param string $dn the dn of the user object
0 ignored issues
show
Bug introduced by
There is no parameter named $dn. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
495
	 * @param string $ldapName optional, the display name of the object
496
	 * @return string|false with with the name to use in Nextcloud
497
	 */
498 View Code Duplication
	public function dn2username($fdn, $ldapName = null) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
499
		//To avoid bypassing the base DN settings under certain circumstances
500
		//with the group support, check whether the provided DN matches one of
501
		//the given Bases
502
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
503
			return false;
504
		}
505
506
		return $this->dn2ocname($fdn, $ldapName, true);
507
	}
508
509
	/**
510
	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
511
	 * @param string $dn the dn of the user object
0 ignored issues
show
Bug introduced by
There is no parameter named $dn. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
512
	 * @param string $ldapName optional, the display name of the object
513
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
514
	 * @return string|false with with the name to use in Nextcloud
515
	 */
516
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
517
		if($isUser) {
518
			$mapper = $this->getUserMapper();
519
			$nameAttribute = $this->connection->ldapUserDisplayName;
520
		} else {
521
			$mapper = $this->getGroupMapper();
522
			$nameAttribute = $this->connection->ldapGroupDisplayName;
0 ignored issues
show
Documentation introduced by
The property ldapGroupDisplayName does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
523
		}
524
525
		//let's try to retrieve the Nextcloud name from the mappings table
526
		$ocName = $mapper->getNameByDN($fdn);
527
		if(is_string($ocName)) {
528
			return $ocName;
529
		}
530
531
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
532
		$uuid = $this->getUUID($fdn, $isUser);
533
		if(is_string($uuid)) {
534
			$ocName = $mapper->getNameByUUID($uuid);
535
			if(is_string($ocName)) {
536
				$mapper->setDNbyUUID($fdn, $uuid);
537
				return $ocName;
538
			}
539
		} else {
540
			//If the UUID can't be detected something is foul.
541
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
542
			return false;
543
		}
544
545
		if(is_null($ldapName)) {
546
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
547
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
548
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
549
				return false;
550
			}
551
			$ldapName = $ldapName[0];
552
		}
553
554
		if($isUser) {
555
			$usernameAttribute = strval($this->connection->ldapExpertUsernameAttr);
0 ignored issues
show
Documentation introduced by
The property ldapExpertUsernameAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
556
			if ($usernameAttribute !== '') {
557
				$username = $this->readAttribute($fdn, $usernameAttribute);
558
				$username = $username[0];
559
			} else {
560
				$username = $uuid;
561
			}
562
			$intName = $this->sanitizeUsername($username);
563
		} else {
564
			$intName = $ldapName;
565
		}
566
567
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
568
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
569
		//NOTE: mind, disabling cache affects only this instance! Using it
570
		// outside of core user management will still cache the user as non-existing.
571
		$originalTTL = $this->connection->ldapCacheTTL;
0 ignored issues
show
Documentation introduced by
The property ldapCacheTTL does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
572
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
573
		if(($isUser && !\OCP\User::userExists($intName))
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::userExists() has been deprecated with message: 8.1.0 use method userExists() of \OCP\IUserManager - \OC::$server->getUserManager()

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
625
		}
626
		$nextcloudNames = array();
627
628
		foreach($ldapObjects as $ldapObject) {
629
			$nameByLDAP = null;
630
			if(    isset($ldapObject[$nameAttribute])
631
				&& is_array($ldapObject[$nameAttribute])
632
				&& isset($ldapObject[$nameAttribute][0])
633
			) {
634
				// might be set, but not necessarily. if so, we use it.
635
				$nameByLDAP = $ldapObject[$nameAttribute][0];
636
			}
637
638
			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
639
			if($ncName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ncName of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
640
				$nextcloudNames[] = $ncName;
641
				if($isUsers) {
642
					//cache the user names so it does not need to be retrieved
643
					//again later (e.g. sharing dialogue).
644
					if(is_null($nameByLDAP)) {
645
						continue;
646
					}
647
					$sndName = isset($ldapObject[$sndAttribute][0])
648
						? $ldapObject[$sndAttribute][0] : '';
0 ignored issues
show
Bug introduced by
The variable $sndAttribute does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
649
					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
650
				}
651
			}
652
		}
653
		return $nextcloudNames;
654
	}
655
656
	/**
657
	 * caches the user display name
658
	 * @param string $ocName the internal Nextcloud username
659
	 * @param string|false $home the home directory path
660
	 */
661
	public function cacheUserHome($ocName, $home) {
662
		$cacheKey = 'getHome'.$ocName;
663
		$this->connection->writeToCache($cacheKey, $home);
664
	}
665
666
	/**
667
	 * caches a user as existing
668
	 * @param string $ocName the internal Nextcloud username
669
	 */
670
	public function cacheUserExists($ocName) {
671
		$this->connection->writeToCache('userExists'.$ocName, true);
672
	}
673
674
	/**
675
	 * caches the user display name
676
	 * @param string $ocName the internal Nextcloud username
677
	 * @param string $displayName the display name
678
	 * @param string $displayName2 the second display name
679
	 */
680
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
681
		$user = $this->userManager->get($ocName);
682
		if($user === null) {
683
			return;
684
		}
685
		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
0 ignored issues
show
Bug introduced by
The method composeAndStoreDisplayName does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
686
		$cacheKeyTrunk = 'getDisplayName';
687
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
688
	}
689
690
	/**
691
	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
692
	 * @param string $name the display name of the object
693
	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
694
	 *
695
	 * Instead of using this method directly, call
696
	 * createAltInternalOwnCloudName($name, true)
697
	 */
698
	private function _createAltInternalOwnCloudNameForUsers($name) {
699
		$attempts = 0;
700
		//while loop is just a precaution. If a name is not generated within
701
		//20 attempts, something else is very wrong. Avoids infinite loop.
702
		while($attempts < 20){
703
			$altName = $name . '_' . rand(1000,9999);
704
			if(!\OCP\User::userExists($altName)) {
0 ignored issues
show
Deprecated Code introduced by
The method OCP\User::userExists() has been deprecated with message: 8.1.0 use method userExists() of \OCP\IUserManager - \OC::$server->getUserManager()

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

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

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
758
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
759
		if($isUser) {
760
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
761
		} else {
762
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
763
		}
764
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
765
766
		return $altName;
767
	}
768
769
	/**
770
	 * fetches a list of users according to a provided loginName and utilizing
771
	 * the login filter.
772
	 *
773
	 * @param string $loginName
774
	 * @param array $attributes optional, list of attributes to read
775
	 * @return array
776
	 */
777 View Code Duplication
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
778
		$loginName = $this->escapeFilterPart($loginName);
779
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
780
		$users = $this->fetchListOfUsers($filter, $attributes);
781
		return $users;
782
	}
783
784
	/**
785
	 * counts the number of users according to a provided loginName and
786
	 * utilizing the login filter.
787
	 *
788
	 * @param string $loginName
789
	 * @return array
790
	 */
791 View Code Duplication
	public function countUsersByLoginName($loginName) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
792
		$loginName = $this->escapeFilterPart($loginName);
793
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
0 ignored issues
show
Documentation introduced by
The property ldapLoginFilter does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
794
		$users = $this->countUsers($filter);
795
		return $users;
796
	}
797
798
	/**
799
	 * @param string $filter
800
	 * @param string|string[] $attr
801
	 * @param int $limit
802
	 * @param int $offset
803
	 * @return array
804
	 */
805
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
806
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
807
		$this->batchApplyUserAttributes($ldapRecords);
808
		return $this->fetchList($ldapRecords, (count($attr) > 1));
809
	}
810
811
	/**
812
	 * provided with an array of LDAP user records the method will fetch the
813
	 * user object and requests it to process the freshly fetched attributes and
814
	 * and their values
815
	 * @param array $ldapRecords
816
	 */
817
	public function batchApplyUserAttributes(array $ldapRecords){
818
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
819
		foreach($ldapRecords as $userRecord) {
820
			if(!isset($userRecord[$displayNameAttribute])) {
821
				// displayName is obligatory
822
				continue;
823
			}
824
			$ocName  = $this->dn2ocname($userRecord['dn'][0]);
825
			if($ocName === false) {
826
				continue;
827
			}
828
			$this->cacheUserExists($ocName);
829
			$user = $this->userManager->get($ocName);
830
			if($user instanceof OfflineUser) {
831
				$user->unmark();
832
				$user = $this->userManager->get($ocName);
833
			}
834
			if ($user !== null) {
835
				$user->processAttributes($userRecord);
0 ignored issues
show
Bug introduced by
The method processAttributes does only exist in OCA\User_LDAP\User\User, but not in OCA\User_LDAP\User\OfflineUser.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
836
			} else {
837
				\OC::$server->getLogger()->debug(
838
					"The ldap user manager returned null for $ocName",
839
					['app'=>'user_ldap']
840
				);
841
			}
842
		}
843
	}
844
845
	/**
846
	 * @param string $filter
847
	 * @param string|string[] $attr
848
	 * @param int $limit
849
	 * @param int $offset
850
	 * @return array
851
	 */
852
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
853
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
854
	}
855
856
	/**
857
	 * @param array $list
858
	 * @param bool $manyAttributes
859
	 * @return array
860
	 */
861
	private function fetchList($list, $manyAttributes) {
862
		if(is_array($list)) {
863
			if($manyAttributes) {
864
				return $list;
865
			} else {
866
				$list = array_reduce($list, function($carry, $item) {
867
					$attribute = array_keys($item)[0];
868
					$carry[] = $item[$attribute][0];
869
					return $carry;
870
				}, array());
871
				return array_unique($list, SORT_LOCALE_STRING);
872
			}
873
		}
874
875
		//error cause actually, maybe throw an exception in future.
876
		return array();
877
	}
878
879
	/**
880
	 * executes an LDAP search, optimized for Users
881
	 * @param string $filter the LDAP filter for the search
882
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
883
	 * @param integer $limit
884
	 * @param integer $offset
885
	 * @return array with the search result
886
	 *
887
	 * Executes an LDAP search
888
	 */
889
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
890
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
891
	}
892
893
	/**
894
	 * @param string $filter
895
	 * @param string|string[] $attr
896
	 * @param int $limit
897
	 * @param int $offset
898
	 * @return false|int
899
	 */
900
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
901
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
902
	}
903
904
	/**
905
	 * executes an LDAP search, optimized for Groups
906
	 * @param string $filter the LDAP filter for the search
907
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
908
	 * @param integer $limit
909
	 * @param integer $offset
910
	 * @return array with the search result
911
	 *
912
	 * Executes an LDAP search
913
	 */
914
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
915
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
916
	}
917
918
	/**
919
	 * returns the number of available groups
920
	 * @param string $filter the LDAP search filter
921
	 * @param string[] $attr optional
922
	 * @param int|null $limit
923
	 * @param int|null $offset
924
	 * @return int|bool
925
	 */
926
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
927
		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
0 ignored issues
show
Documentation introduced by
The property ldapBaseGroups does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
928
	}
929
930
	/**
931
	 * returns the number of available objects on the base DN
932
	 *
933
	 * @param int|null $limit
934
	 * @param int|null $offset
935
	 * @return int|bool
936
	 */
937
	public function countObjects($limit = null, $offset = null) {
938
		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
0 ignored issues
show
Bug introduced by
The property ldapBase does not seem to exist. Did you mean ldapBaseUsers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
939
	}
940
941
	/**
942
	 * Returns the LDAP handler
943
	 * @throws \OC\ServerNotAvailableException
944
	 */
945
946
	/**
947
	 * @return mixed
948
	 * @throws \OC\ServerNotAvailableException
949
	 */
950
	private function invokeLDAPMethod() {
951
		$arguments = func_get_args();
952
		$command = array_shift($arguments);
953
		$cr = array_shift($arguments);
954
		if (!method_exists($this->ldap, $command)) {
955
			return null;
956
		}
957
		array_unshift($arguments, $cr);
958
		// php no longer supports call-time pass-by-reference
959
		// thus cannot support controlPagedResultResponse as the third argument
960
		// is a reference
961
		$doMethod = function () use ($command, &$arguments) {
962
			if ($command == 'controlPagedResultResponse') {
963
				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
964
			} else {
965
				return call_user_func_array(array($this->ldap, $command), $arguments);
966
			}
967
		};
968
		try {
969
			$ret = $doMethod();
970
		} catch (ServerNotAvailableException $e) {
971
			/* Server connection lost, attempt to reestablish it
972
			 * Maybe implement exponential backoff?
973
			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
974
			 */
975
			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", \OCP\Util::DEBUG);
976
			$this->connection->resetConnectionResource();
977
			$cr = $this->connection->getConnectionResource();
978
979
			if(!$this->ldap->isResource($cr)) {
980
				// Seems like we didn't find any resource.
981
				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", \OCP\Util::DEBUG);
982
				throw $e;
983
			}
984
985
			$arguments[0] = array_pad([], count($arguments[0]), $cr);
986
			$ret = $doMethod();
987
		}
988
		return $ret;
989
	}
990
991
	/**
992
	 * retrieved. Results will according to the order in the array.
993
	 * @param int $limit optional, maximum results to be counted
994
	 * @param int $offset optional, a starting point
995
	 * @return array|false array with the search result as first value and pagedSearchOK as
996
	 * second | false if not successful
997
	 * @throws \OC\ServerNotAvailableException
998
	 */
999
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1000
		if(!is_null($attr) && !is_array($attr)) {
1001
			$attr = array(mb_strtolower($attr, 'UTF-8'));
1002
		}
1003
1004
		// See if we have a resource, in case not cancel with message
1005
		$cr = $this->connection->getConnectionResource();
1006
		if(!$this->ldap->isResource($cr)) {
1007
			// Seems like we didn't find any resource.
1008
			// Return an empty array just like before.
1009
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
1010
			return false;
1011
		}
1012
1013
		//check whether paged search should be attempted
1014
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset);
0 ignored issues
show
Documentation introduced by
$attr is of type null|array, but the function expects a array<integer,string>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1015
1016
		$linkResources = array_pad(array(), count($base), $cr);
1017
		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1018
		// cannot use $cr anymore, might have changed in the previous call!
1019
		$error = $this->ldap->errno($this->connection->getConnectionResource());
1020
		if(!is_array($sr) || $error !== 0) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison !== seems to always evaluate to true as the types of $error (string) and 0 (integer) can never be identical. Maybe you want to use a loose comparison != instead?
Loading history...
1021
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
1022
			return false;
1023
		}
1024
1025
		return array($sr, $pagedSearchOK);
1026
	}
1027
1028
	/**
1029
	 * processes an LDAP paged search operation
1030
	 * @param array $sr the array containing the LDAP search resources
1031
	 * @param string $filter the LDAP filter for the search
1032
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1033
	 * @param int $iFoundItems number of results in the search operation
1034
	 * @param int $limit maximum results to be counted
1035
	 * @param int $offset a starting point
1036
	 * @param bool $pagedSearchOK whether a paged search has been executed
1037
	 * @param bool $skipHandling required for paged search when cookies to
1038
	 * prior results need to be gained
1039
	 * @return bool cookie validity, true if we have more pages, false otherwise.
1040
	 */
1041
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1042
		$cookie = null;
1043
		if($pagedSearchOK) {
1044
			$cr = $this->connection->getConnectionResource();
1045
			foreach($sr as $key => $res) {
1046
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1047
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1048
				}
1049
			}
1050
1051
			//browsing through prior pages to get the cookie for the new one
1052
			if($skipHandling) {
1053
				return false;
1054
			}
1055
			// if count is bigger, then the server does not support
1056
			// paged search. Instead, he did a normal search. We set a
1057
			// flag here, so the callee knows how to deal with it.
1058
			if($iFoundItems <= $limit) {
1059
				$this->pagedSearchedSuccessful = true;
1060
			}
1061
		} else {
1062
			if(!is_null($limit)) {
1063
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
1064
			}
1065
		}
1066
		/* ++ Fixing RHDS searches with pages with zero results ++
1067
		 * Return cookie status. If we don't have more pages, with RHDS
1068
		 * cookie is null, with openldap cookie is an empty string and
1069
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1070
		 */
1071
		return !empty($cookie) || $cookie === '0';
1072
	}
1073
1074
	/**
1075
	 * executes an LDAP search, but counts the results only
1076
	 * @param string $filter the LDAP filter for the search
1077
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1078
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1079
	 * retrieved. Results will according to the order in the array.
1080
	 * @param int $limit optional, maximum results to be counted
1081
	 * @param int $offset optional, a starting point
1082
	 * @param bool $skipHandling indicates whether the pages search operation is
1083
	 * completed
1084
	 * @return int|false Integer or false if the search could not be initialized
1085
	 *
1086
	 */
1087
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1088
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1089
1090
		$limitPerPage = intval($this->connection->ldapPagingSize);
1091
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1092
			$limitPerPage = $limit;
1093
		}
1094
1095
		$counter = 0;
1096
		$count = null;
0 ignored issues
show
Unused Code introduced by
$count is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1097
		$this->connection->getConnectionResource();
1098
1099
		do {
1100
			$search = $this->executeSearch($filter, $base, $attr,
1101
										   $limitPerPage, $offset);
1102
			if($search === false) {
1103
				return $counter > 0 ? $counter : false;
1104
			}
1105
			list($sr, $pagedSearchOK) = $search;
1106
1107
			/* ++ Fixing RHDS searches with pages with zero results ++
1108
			 * countEntriesInSearchResults() method signature changed
1109
			 * by removing $limit and &$hasHitLimit parameters
1110
			 */
1111
			$count = $this->countEntriesInSearchResults($sr);
1112
			$counter += $count;
1113
1114
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1115
										$offset, $pagedSearchOK, $skipHandling);
1116
			$offset += $limitPerPage;
1117
			/* ++ Fixing RHDS searches with pages with zero results ++
1118
			 * Continue now depends on $hasMorePages value
1119
			 */
1120
			$continue = $pagedSearchOK && $hasMorePages;
1121
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1122
1123
		return $counter;
1124
	}
1125
1126
	/**
1127
	 * @param array $searchResults
1128
	 * @return int
1129
	 */
1130
	private function countEntriesInSearchResults($searchResults) {
1131
		$counter = 0;
1132
1133
		foreach($searchResults as $res) {
1134
			$count = intval($this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res));
1135
			$counter += $count;
1136
		}
1137
1138
		return $counter;
1139
	}
1140
1141
	/**
1142
	 * Executes an LDAP search
1143
	 * @param string $filter the LDAP filter for the search
1144
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1145
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1146
	 * @param int $limit
1147
	 * @param int $offset
1148
	 * @param bool $skipHandling
1149
	 * @return array with the search result
1150
	 */
1151
	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1152
		if($limit <= 0) {
1153
			//otherwise search will fail
1154
			$limit = null;
1155
		}
1156
1157
		/* ++ Fixing RHDS searches with pages with zero results ++
1158
		 * As we can have pages with zero results and/or pages with less
1159
		 * than $limit results but with a still valid server 'cookie',
1160
		 * loops through until we get $continue equals true and
1161
		 * $findings['count'] < $limit
1162
		 */
1163
		$findings = array();
1164
		$savedoffset = $offset;
1165
		do {
1166
			$search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
1167
			if($search === false) {
1168
				return array();
1169
			}
1170
			list($sr, $pagedSearchOK) = $search;
1171
			$cr = $this->connection->getConnectionResource();
1172
1173
			if($skipHandling) {
1174
				//i.e. result do not need to be fetched, we just need the cookie
1175
				//thus pass 1 or any other value as $iFoundItems because it is not
1176
				//used
1177
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
1178
								$offset, $pagedSearchOK,
1179
								$skipHandling);
1180
				return array();
1181
			}
1182
1183
			foreach($sr as $res) {
1184
				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1185
			}
1186
1187
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
1188
								$limit, $offset, $pagedSearchOK,
1189
										$skipHandling);
1190
			$offset += $limit;
1191
		} while ($continue && $pagedSearchOK && $findings['count'] < $limit);
1192
		// reseting offset
1193
		$offset = $savedoffset;
1194
1195
		// if we're here, probably no connection resource is returned.
1196
		// to make Nextcloud behave nicely, we simply give back an empty array.
1197
		if(is_null($findings)) {
1198
			return array();
1199
		}
1200
1201
		if(!is_null($attr)) {
1202
			$selection = array();
1203
			$i = 0;
1204
			foreach($findings as $item) {
1205
				if(!is_array($item)) {
1206
					continue;
1207
				}
1208
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1209
				foreach($attr as $key) {
1210
					$key = mb_strtolower($key, 'UTF-8');
1211
					if(isset($item[$key])) {
1212
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1213
							unset($item[$key]['count']);
1214
						}
1215
						if($key !== 'dn') {
1216
							$selection[$i][$key] = $this->resemblesDN($key) ?
1217
								$this->helper->sanitizeDN($item[$key])
1218
								: $item[$key];
1219
						} else {
1220
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1221
						}
1222
					}
1223
1224
				}
1225
				$i++;
1226
			}
1227
			$findings = $selection;
1228
		}
1229
		//we slice the findings, when
1230
		//a) paged search unsuccessful, though attempted
1231
		//b) no paged search, but limit set
1232
		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...
1233
			&& $pagedSearchOK)
1234
			|| (
1235
				!$pagedSearchOK
1236
				&& !is_null($limit)
1237
			)
1238
		) {
1239
			$findings = array_slice($findings, intval($offset), $limit);
1240
		}
1241
		return $findings;
1242
	}
1243
1244
	/**
1245
	 * @param string $name
1246
	 * @return bool|mixed|string
1247
	 */
1248
	public function sanitizeUsername($name) {
1249
		if($this->connection->ldapIgnoreNamingRules) {
0 ignored issues
show
Documentation introduced by
The property ldapIgnoreNamingRules does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1250
			return $name;
1251
		}
1252
1253
		// Transliteration
1254
		// latin characters to ASCII
1255
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1256
1257
		// Replacements
1258
		$name = str_replace(' ', '_', $name);
1259
1260
		// Every remaining disallowed characters will be removed
1261
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1262
1263
		return $name;
1264
	}
1265
1266
	/**
1267
	* escapes (user provided) parts for LDAP filter
1268
	* @param string $input, the provided value
0 ignored issues
show
Documentation introduced by
There is no parameter named $input,. Did you maybe mean $input?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
1269
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1270
	* @return string the escaped string
1271
	*/
1272
	public function escapeFilterPart($input, $allowAsterisk = false) {
1273
		$asterisk = '';
1274
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1275
			$asterisk = '*';
1276
			$input = mb_substr($input, 1, null, 'UTF-8');
1277
		}
1278
		$search  = array('*', '\\', '(', ')');
1279
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1280
		return $asterisk . str_replace($search, $replace, $input);
1281
	}
1282
1283
	/**
1284
	 * combines the input filters with AND
1285
	 * @param string[] $filters the filters to connect
1286
	 * @return string the combined filter
1287
	 */
1288
	public function combineFilterWithAnd($filters) {
1289
		return $this->combineFilter($filters, '&');
1290
	}
1291
1292
	/**
1293
	 * combines the input filters with OR
1294
	 * @param string[] $filters the filters to connect
1295
	 * @return string the combined filter
1296
	 * Combines Filter arguments with OR
1297
	 */
1298
	public function combineFilterWithOr($filters) {
1299
		return $this->combineFilter($filters, '|');
1300
	}
1301
1302
	/**
1303
	 * combines the input filters with given operator
1304
	 * @param string[] $filters the filters to connect
1305
	 * @param string $operator either & or |
1306
	 * @return string the combined filter
1307
	 */
1308
	private function combineFilter($filters, $operator) {
1309
		$combinedFilter = '('.$operator;
1310
		foreach($filters as $filter) {
1311
			if ($filter !== '' && $filter[0] !== '(') {
1312
				$filter = '('.$filter.')';
1313
			}
1314
			$combinedFilter.=$filter;
1315
		}
1316
		$combinedFilter.=')';
1317
		return $combinedFilter;
1318
	}
1319
1320
	/**
1321
	 * creates a filter part for to perform search for users
1322
	 * @param string $search the search term
1323
	 * @return string the final filter part to use in LDAP searches
1324
	 */
1325
	public function getFilterPartForUserSearch($search) {
1326
		return $this->getFilterPartForSearch($search,
1327
			$this->connection->ldapAttributesForUserSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForUserSearch does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1328
			$this->connection->ldapUserDisplayName);
1329
	}
1330
1331
	/**
1332
	 * creates a filter part for to perform search for groups
1333
	 * @param string $search the search term
1334
	 * @return string the final filter part to use in LDAP searches
1335
	 */
1336
	public function getFilterPartForGroupSearch($search) {
1337
		return $this->getFilterPartForSearch($search,
1338
			$this->connection->ldapAttributesForGroupSearch,
0 ignored issues
show
Documentation introduced by
The property ldapAttributesForGroupSearch does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __get, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

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

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1340
	}
1341
1342
	/**
1343
	 * creates a filter part for searches by splitting up the given search
1344
	 * string into single words
1345
	 * @param string $search the search term
1346
	 * @param string[] $searchAttributes needs to have at least two attributes,
1347
	 * otherwise it does not make sense :)
1348
	 * @return string the final filter part to use in LDAP searches
1349
	 * @throws \Exception
1350
	 */
1351
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1352
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1353
			throw new \Exception('searchAttributes must be an array with at least two string');
1354
		}
1355
		$searchWords = explode(' ', trim($search));
1356
		$wordFilters = array();
1357
		foreach($searchWords as $word) {
1358
			$word = $this->prepareSearchTerm($word);
1359
			//every word needs to appear at least once
1360
			$wordMatchOneAttrFilters = array();
1361
			foreach($searchAttributes as $attr) {
1362
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1363
			}
1364
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1365
		}
1366
		return $this->combineFilterWithAnd($wordFilters);
1367
	}
1368
1369
	/**
1370
	 * creates a filter part for searches
1371
	 * @param string $search the search term
1372
	 * @param string[]|null $searchAttributes
1373
	 * @param string $fallbackAttribute a fallback attribute in case the user
1374
	 * did not define search attributes. Typically the display name attribute.
1375
	 * @return string the final filter part to use in LDAP searches
1376
	 */
1377
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1378
		$filter = array();
1379
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1380
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1381
			try {
1382
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1383
			} catch(\Exception $e) {
1384
				\OCP\Util::writeLog(
1385
					'user_ldap',
1386
					'Creating advanced filter for search failed, falling back to simple method.',
1387
					\OCP\Util::INFO
1388
				);
1389
			}
1390
		}
1391
1392
		$search = $this->prepareSearchTerm($search);
1393
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1394
			if ($fallbackAttribute === '') {
1395
				return '';
1396
			}
1397
			$filter[] = $fallbackAttribute . '=' . $search;
1398
		} else {
1399
			foreach($searchAttributes as $attribute) {
1400
				$filter[] = $attribute . '=' . $search;
1401
			}
1402
		}
1403
		if(count($filter) === 1) {
1404
			return '('.$filter[0].')';
1405
		}
1406
		return $this->combineFilterWithOr($filter);
1407
	}
1408
1409
	/**
1410
	 * returns the search term depending on whether we are allowed
1411
	 * list users found by ldap with the current input appended by
1412
	 * a *
1413
	 * @return string
1414
	 */
1415
	private function prepareSearchTerm($term) {
1416
		$config = \OC::$server->getConfig();
1417
1418
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1419
1420
		$result = $term;
1421
		if ($term === '') {
1422
			$result = '*';
1423
		} else if ($allowEnum !== 'no') {
1424
			$result = $term . '*';
1425
		}
1426
		return $result;
1427
	}
1428
1429
	/**
1430
	 * returns the filter used for counting users
1431
	 * @return string
1432
	 */
1433
	public function getFilterForUserCount() {
1434
		$filter = $this->combineFilterWithAnd(array(
1435
			$this->connection->ldapUserFilter,
1436
			$this->connection->ldapUserDisplayName . '=*'
1437
		));
1438
1439
		return $filter;
1440
	}
1441
1442
	/**
1443
	 * @param string $name
1444
	 * @param string $password
1445
	 * @return bool
1446
	 */
1447
	public function areCredentialsValid($name, $password) {
1448
		$name = $this->helper->DNasBaseParameter($name);
1449
		$testConnection = clone $this->connection;
1450
		$credentials = array(
1451
			'ldapAgentName' => $name,
1452
			'ldapAgentPassword' => $password
1453
		);
1454
		if(!$testConnection->setConfiguration($credentials)) {
1455
			return false;
1456
		}
1457
		return $testConnection->bind();
1458
	}
1459
1460
	/**
1461
	 * reverse lookup of a DN given a known UUID
1462
	 *
1463
	 * @param string $uuid
1464
	 * @return string
1465
	 * @throws \Exception
1466
	 */
1467
	public function getUserDnByUuid($uuid) {
1468
		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1469
		$filter       = $this->connection->ldapUserFilter;
1470
		$base         = $this->connection->ldapBaseUsers;
1471
1472
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1473
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1474
			// existing DN to be able to reliably detect it.
1475
			$result = $this->search($filter, $base, ['dn'], 1);
1476
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1477
				throw new \Exception('Cannot determine UUID attribute');
1478
			}
1479
			$dn = $result[0]['dn'][0];
1480
			if(!$this->detectUuidAttribute($dn, true)) {
1481
				throw new \Exception('Cannot determine UUID attribute');
1482
			}
1483
		} else {
1484
			// The UUID attribute is either known or an override is given.
1485
			// By calling this method we ensure that $this->connection->$uuidAttr
1486
			// is definitely set
1487
			if(!$this->detectUuidAttribute('', true)) {
1488
				throw new \Exception('Cannot determine UUID attribute');
1489
			}
1490
		}
1491
1492
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1493
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1494
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1495
		}
1496
1497
		$filter = $uuidAttr . '=' . $uuid;
1498
		$result = $this->searchUsers($filter, ['dn'], 2);
1499
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1500
			// we put the count into account to make sure that this is
1501
			// really unique
1502
			return $result[0]['dn'][0];
1503
		}
1504
1505
		throw new \Exception('Cannot determine UUID attribute');
1506
	}
1507
1508
	/**
1509
	 * auto-detects the directory's UUID attribute
1510
	 * @param string $dn a known DN used to check against
1511
	 * @param bool $isUser
1512
	 * @param bool $force the detection should be run, even if it is not set to auto
1513
	 * @return bool true on success, false otherwise
1514
	 */
1515
	private function detectUuidAttribute($dn, $isUser = true, $force = false) {
1516 View Code Duplication
		if($isUser) {
1517
			$uuidAttr     = 'ldapUuidUserAttribute';
1518
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1519
		} else {
1520
			$uuidAttr     = 'ldapUuidGroupAttribute';
1521
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1522
		}
1523
1524
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1525
			return true;
1526
		}
1527
1528
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1529
			$this->connection->$uuidAttr = $uuidOverride;
1530
			return true;
1531
		}
1532
1533
		// for now, supported attributes are entryUUID, nsuniqueid, objectGUID, ipaUniqueID
1534
		$testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid');
1535
1536
		foreach($testAttributes as $attribute) {
1537
			$value = $this->readAttribute($dn, $attribute);
1538
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1539
				\OCP\Util::writeLog('user_ldap',
1540
									'Setting '.$attribute.' as '.$uuidAttr,
1541
									\OCP\Util::DEBUG);
1542
				$this->connection->$uuidAttr = $attribute;
1543
				return true;
1544
			}
1545
		}
1546
		\OCP\Util::writeLog('user_ldap',
1547
							'Could not autodetect the UUID attribute',
1548
							\OCP\Util::ERROR);
1549
1550
		return false;
1551
	}
1552
1553
	/**
1554
	 * @param string $dn
1555
	 * @param bool $isUser
1556
	 * @return string|bool
1557
	 */
1558
	public function getUUID($dn, $isUser = true) {
1559 View Code Duplication
		if($isUser) {
1560
			$uuidAttr     = 'ldapUuidUserAttribute';
1561
			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDUserAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1562
		} else {
1563
			$uuidAttr     = 'ldapUuidGroupAttribute';
1564
			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
0 ignored issues
show
Documentation introduced by
The property ldapExpertUUIDGroupAttr does not exist on object<OCA\User_LDAP\Connection>. Since you implemented __set, maybe consider adding a @property annotation.

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

<?php

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

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

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

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

}

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

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

See also the PhpDoc documentation for @property.

Loading history...
1565
		}
1566
1567
		$uuid = false;
1568
		if($this->detectUuidAttribute($dn, $isUser)) {
1569
			$uuid = $this->readAttribute($dn, $this->connection->$uuidAttr);
1570
			if( !is_array($uuid)
1571
				&& $uuidOverride !== ''
1572
				&& $this->detectUuidAttribute($dn, $isUser, true)) {
1573
					$uuid = $this->readAttribute($dn,
1574
												 $this->connection->$uuidAttr);
1575
			}
1576
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1577
				$uuid = $uuid[0];
1578
			}
1579
		}
1580
1581
		return $uuid;
1582
	}
1583
1584
	/**
1585
	 * converts a binary ObjectGUID into a string representation
1586
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1587
	 * @return string
1588
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1589
	 */
1590
	private function convertObjectGUID2Str($oguid) {
1591
		$hex_guid = bin2hex($oguid);
1592
		$hex_guid_to_guid_str = '';
1593 View Code Duplication
		for($k = 1; $k <= 4; ++$k) {
1594
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1595
		}
1596
		$hex_guid_to_guid_str .= '-';
1597 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1598
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1599
		}
1600
		$hex_guid_to_guid_str .= '-';
1601 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
1602
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1603
		}
1604
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1605
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1606
1607
		return strtoupper($hex_guid_to_guid_str);
1608
	}
1609
1610
	/**
1611
	 * the first three blocks of the string-converted GUID happen to be in
1612
	 * reverse order. In order to use it in a filter, this needs to be
1613
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1614
	 * to every two hax figures.
1615
	 *
1616
	 * If an invalid string is passed, it will be returned without change.
1617
	 *
1618
	 * @param string $guid
1619
	 * @return string
1620
	 */
1621
	public function formatGuid2ForFilterUser($guid) {
1622
		if(!is_string($guid)) {
1623
			throw new \InvalidArgumentException('String expected');
1624
		}
1625
		$blocks = explode('-', $guid);
1626 View Code Duplication
		if(count($blocks) !== 5) {
1627
			/*
1628
			 * Why not throw an Exception instead? This method is a utility
1629
			 * called only when trying to figure out whether a "missing" known
1630
			 * LDAP user was or was not renamed on the LDAP server. And this
1631
			 * even on the use case that a reverse lookup is needed (UUID known,
1632
			 * not DN), i.e. when finding users (search dialog, users page,
1633
			 * login, …) this will not be fired. This occurs only if shares from
1634
			 * a users are supposed to be mounted who cannot be found. Throwing
1635
			 * an exception here would kill the experience for a valid, acting
1636
			 * user. Instead we write a log message.
1637
			 */
1638
			\OC::$server->getLogger()->info(
1639
				'Passed string does not resemble a valid GUID. Known UUID ' .
1640
				'({uuid}) probably does not match UUID configuration.',
1641
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1642
			);
1643
			return $guid;
1644
		}
1645 View Code Duplication
		for($i=0; $i < 3; $i++) {
1646
			$pairs = str_split($blocks[$i], 2);
1647
			$pairs = array_reverse($pairs);
1648
			$blocks[$i] = implode('', $pairs);
1649
		}
1650 View Code Duplication
		for($i=0; $i < 5; $i++) {
1651
			$pairs = str_split($blocks[$i], 2);
1652
			$blocks[$i] = '\\' . implode('\\', $pairs);
1653
		}
1654
		return implode('', $blocks);
1655
	}
1656
1657
	/**
1658
	 * gets a SID of the domain of the given dn
1659
	 * @param string $dn
1660
	 * @return string|bool
1661
	 */
1662
	public function getSID($dn) {
1663
		$domainDN = $this->getDomainDNFromDN($dn);
1664
		$cacheKey = 'getSID-'.$domainDN;
1665
		$sid = $this->connection->getFromCache($cacheKey);
1666
		if(!is_null($sid)) {
1667
			return $sid;
1668
		}
1669
1670
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1671
		if(!is_array($objectSid) || empty($objectSid)) {
1672
			$this->connection->writeToCache($cacheKey, false);
1673
			return false;
1674
		}
1675
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1676
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1677
1678
		return $domainObjectSid;
1679
	}
1680
1681
	/**
1682
	 * converts a binary SID into a string representation
1683
	 * @param string $sid
1684
	 * @return string
1685
	 */
1686
	public function convertSID2Str($sid) {
1687
		// The format of a SID binary string is as follows:
1688
		// 1 byte for the revision level
1689
		// 1 byte for the number n of variable sub-ids
1690
		// 6 bytes for identifier authority value
1691
		// n*4 bytes for n sub-ids
1692
		//
1693
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1694
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1695
		$revision = ord($sid[0]);
1696
		$numberSubID = ord($sid[1]);
1697
1698
		$subIdStart = 8; // 1 + 1 + 6
1699
		$subIdLength = 4;
1700
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1701
			// Incorrect number of bytes present.
1702
			return '';
1703
		}
1704
1705
		// 6 bytes = 48 bits can be represented using floats without loss of
1706
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1707
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1708
1709
		$subIDs = array();
1710
		for ($i = 0; $i < $numberSubID; $i++) {
1711
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1712
			$subIDs[] = sprintf('%u', $subID[1]);
1713
		}
1714
1715
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1716
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1717
	}
1718
1719
	/**
1720
	 * checks if the given DN is part of the given base DN(s)
1721
	 * @param string $dn the DN
1722
	 * @param string[] $bases array containing the allowed base DN or DNs
1723
	 * @return bool
1724
	 */
1725
	public function isDNPartOfBase($dn, $bases) {
1726
		$belongsToBase = false;
1727
		$bases = $this->helper->sanitizeDN($bases);
1728
1729
		foreach($bases as $base) {
0 ignored issues
show
Bug introduced by
The expression $bases of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1730
			$belongsToBase = true;
1731
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1732
				$belongsToBase = false;
1733
			}
1734
			if($belongsToBase) {
1735
				break;
1736
			}
1737
		}
1738
		return $belongsToBase;
1739
	}
1740
1741
	/**
1742
	 * resets a running Paged Search operation
1743
	 */
1744
	private function abandonPagedSearch() {
1745
		if($this->connection->hasPagedResultSupport) {
1746
			$cr = $this->connection->getConnectionResource();
1747
			$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1748
			$this->getPagedSearchResultState();
1749
			$this->lastCookie = '';
1750
			$this->cookies = array();
1751
		}
1752
	}
1753
1754
	/**
1755
	 * get a cookie for the next LDAP paged search
1756
	 * @param string $base a string with the base DN for the search
1757
	 * @param string $filter the search filter to identify the correct search
1758
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1759
	 * @param int $offset the offset for the new search to identify the correct search really good
1760
	 * @return string containing the key or empty if none is cached
1761
	 */
1762
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1763
		if($offset === 0) {
1764
			return '';
1765
		}
1766
		$offset -= $limit;
1767
		//we work with cache here
1768
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1769
		$cookie = '';
1770
		if(isset($this->cookies[$cacheKey])) {
1771
			$cookie = $this->cookies[$cacheKey];
1772
			if(is_null($cookie)) {
1773
				$cookie = '';
1774
			}
1775
		}
1776
		return $cookie;
1777
	}
1778
1779
	/**
1780
	 * checks whether an LDAP paged search operation has more pages that can be
1781
	 * retrieved, typically when offset and limit are provided.
1782
	 *
1783
	 * Be very careful to use it: the last cookie value, which is inspected, can
1784
	 * be reset by other operations. Best, call it immediately after a search(),
1785
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1786
	 * well. Don't rely on it with any fetchList-method.
1787
	 * @return bool
1788
	 */
1789
	public function hasMoreResults() {
1790
		if(!$this->connection->hasPagedResultSupport) {
1791
			return false;
1792
		}
1793
1794
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1795
			// as in RFC 2696, when all results are returned, the cookie will
1796
			// be empty.
1797
			return false;
1798
		}
1799
1800
		return true;
1801
	}
1802
1803
	/**
1804
	 * set a cookie for LDAP paged search run
1805
	 * @param string $base a string with the base DN for the search
1806
	 * @param string $filter the search filter to identify the correct search
1807
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1808
	 * @param int $offset the offset for the run search to identify the correct search really good
1809
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1810
	 * @return void
1811
	 */
1812
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1813
		// allow '0' for 389ds
1814
		if(!empty($cookie) || $cookie === '0') {
1815
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1816
			$this->cookies[$cacheKey] = $cookie;
1817
			$this->lastCookie = $cookie;
1818
		}
1819
	}
1820
1821
	/**
1822
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1823
	 * @return boolean|null true on success, null or false otherwise
1824
	 */
1825
	public function getPagedSearchResultState() {
1826
		$result = $this->pagedSearchedSuccessful;
1827
		$this->pagedSearchedSuccessful = null;
1828
		return $result;
1829
	}
1830
1831
	/**
1832
	 * Prepares a paged search, if possible
1833
	 * @param string $filter the LDAP filter for the search
1834
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1835
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1836
	 * @param int $limit
1837
	 * @param int $offset
1838
	 * @return bool|true
1839
	 */
1840
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1841
		$pagedSearchOK = false;
1842
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1843
			$offset = intval($offset); //can be null
1844
			\OCP\Util::writeLog('user_ldap',
1845
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1846
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1847
				\OCP\Util::DEBUG);
1848
			//get the cookie from the search for the previous search, required by LDAP
1849
			foreach($bases as $base) {
1850
1851
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1852
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1853
					// no cookie known, although the offset is not 0. Maybe cache run out. We need
1854
					// to start all over *sigh* (btw, Dear Reader, did you know LDAP paged
1855
					// searching was designed by MSFT?)
1856
					// 		Lukas: No, but thanks to reading that source I finally know!
1857
					// '0' is valid, because 389ds
1858
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1859
					//a bit recursive, $offset of 0 is the exit
1860
					\OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
1861
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1862
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1863
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1864
					//TODO: remember this, probably does not change in the next request...
1865
					if(empty($cookie) && $cookie !== '0') {
1866
						// '0' is valid, because 389ds
1867
						$cookie = null;
1868
					}
1869
				}
1870
				if(!is_null($cookie)) {
1871
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1872
					$this->abandonPagedSearch();
1873
					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1874
						$this->connection->getConnectionResource(), $limit,
1875
						false, $cookie);
1876
					if(!$pagedSearchOK) {
1877
						return false;
1878
					}
1879
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1880
				} else {
1881
					\OCP\Util::writeLog('user_ldap',
1882
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1883
						\OCP\Util::INFO);
1884
				}
1885
1886
			}
1887
		/* ++ Fixing RHDS searches with pages with zero results ++
1888
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1889
		 * due to pages with zero results.
1890
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1891
		 * if we don't have a previous paged search.
1892
		 */
1893
		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1894
			// a search without limit was requested. However, if we do use
1895
			// Paged Search once, we always must do it. This requires us to
1896
			// initialize it with the configured page size.
1897
			$this->abandonPagedSearch();
1898
			// in case someone set it to 0 … use 500, otherwise no results will
1899
			// be returned.
1900
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1901
			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
1902
				$this->connection->getConnectionResource(),
1903
				$pageSize, false, '');
1904
		}
1905
1906
		return $pagedSearchOK;
1907
	}
1908
1909
}
1910