Completed
Push — master ( c20409...a4266f )
by Lukas
07:37
created

Access::dn2groupname()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 10
loc 10
rs 9.4285
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
/**
51
 * Class Access
52
 * @package OCA\User_LDAP
53
 */
54
class Access extends LDAPUtility implements IUserTools {
55
	/**
56
	 * @var \OCA\User_LDAP\Connection
57
	 */
58
	public $connection;
59
	/** @var Manager */
60
	public $userManager;
61
	//never ever check this var directly, always use getPagedSearchResultState
62
	protected $pagedSearchedSuccessful;
63
64
	/**
65
	 * @var string[] $cookies an array of returned Paged Result cookies
66
	 */
67
	protected $cookies = array();
68
69
	/**
70
	 * @var string $lastCookie the last cookie returned from a Paged Results
71
	 * operation, defaults to an empty string
72
	 */
73
	protected $lastCookie = '';
74
75
	/**
76
	 * @var AbstractMapping $userMapper
77
	 */
78
	protected $userMapper;
79
80
	/**
81
	* @var AbstractMapping $userMapper
82
	*/
83
	protected $groupMapper;
84
	
85
	/**
86
	 * @var \OCA\User_LDAP\Helper
87
	 */
88
	private $helper;
89
90
	public function __construct(Connection $connection, ILDAPWrapper $ldap,
91
		Manager $userManager, Helper $helper) {
92
		parent::__construct($ldap);
93
		$this->connection = $connection;
94
		$this->userManager = $userManager;
95
		$this->userManager->setLdapAccess($this);
96
		$this->helper = $helper;
97
	}
98
99
	/**
100
	 * sets the User Mapper
101
	 * @param AbstractMapping $mapper
102
	 */
103
	public function setUserMapper(AbstractMapping $mapper) {
104
		$this->userMapper = $mapper;
105
	}
106
107
	/**
108
	 * returns the User Mapper
109
	 * @throws \Exception
110
	 * @return AbstractMapping
111
	 */
112
	public function getUserMapper() {
113
		if(is_null($this->userMapper)) {
114
			throw new \Exception('UserMapper was not assigned to this Access instance.');
115
		}
116
		return $this->userMapper;
117
	}
118
119
	/**
120
	 * sets the Group Mapper
121
	 * @param AbstractMapping $mapper
122
	 */
123
	public function setGroupMapper(AbstractMapping $mapper) {
124
		$this->groupMapper = $mapper;
125
	}
126
127
	/**
128
	 * returns the Group Mapper
129
	 * @throws \Exception
130
	 * @return AbstractMapping
131
	 */
132
	public function getGroupMapper() {
133
		if(is_null($this->groupMapper)) {
134
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
135
		}
136
		return $this->groupMapper;
137
	}
138
139
	/**
140
	 * @return bool
141
	 */
142
	private function checkConnection() {
143
		return ($this->connection instanceof Connection);
144
	}
145
146
	/**
147
	 * returns the Connection instance
148
	 * @return \OCA\User_LDAP\Connection
149
	 */
150
	public function getConnection() {
151
		return $this->connection;
152
	}
153
154
	/**
155
	 * reads a given attribute for an LDAP record identified by a DN
156
	 * @param string $dn the record in question
157
	 * @param string $attr the attribute that shall be retrieved
158
	 *        if empty, just check the record's existence
159
	 * @param string $filter
160
	 * @return array|false an array of values on success or an empty
161
	 *          array if $attr is empty, false otherwise
162
	 */
163
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
164
		if(!$this->checkConnection()) {
165
			\OCP\Util::writeLog('user_ldap',
166
				'No LDAP Connector assigned, access impossible for readAttribute.',
167
				\OCP\Util::WARN);
168
			return false;
169
		}
170
		$cr = $this->connection->getConnectionResource();
171
		if(!$this->ldap->isResource($cr)) {
172
			//LDAP not available
173
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
174
			return false;
175
		}
176
		//Cancel possibly running Paged Results operation, otherwise we run in
177
		//LDAP protocol errors
178
		$this->abandonPagedSearch();
179
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
180
		// but does not hurt either.
181
		$pagingSize = intval($this->connection->ldapPagingSize);
182
		// 0 won't result in replies, small numbers may leave out groups
183
		// (cf. #12306), 500 is default for paging and should work everywhere.
184
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
185
		$attr = mb_strtolower($attr, 'UTF-8');
186
		// the actual read attribute later may contain parameters on a ranged
187
		// request, e.g. member;range=99-199. Depends on server reply.
188
		$attrToRead = $attr;
189
190
		$values = [];
191
		$isRangeRequest = false;
192
		do {
193
			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
194
			if(is_bool($result)) {
195
				// when an exists request was run and it was successful, an empty
196
				// array must be returned
197
				return $result ? [] : false;
198
			}
199
200
			if (!$isRangeRequest) {
201
				$values = $this->extractAttributeValuesFromResult($result, $attr);
202
				if (!empty($values)) {
203
					return $values;
204
				}
205
			}
206
207
			$isRangeRequest = false;
208
			$result = $this->extractRangeData($result, $attr);
209
			if (!empty($result)) {
210
				$normalizedResult = $this->extractAttributeValuesFromResult(
211
					[ $attr => $result['values'] ],
212
					$attr
213
				);
214
				$values = array_merge($values, $normalizedResult);
215
216
				if($result['rangeHigh'] === '*') {
217
					// when server replies with * as high range value, there are
218
					// no more results left
219
					return $values;
220
				} else {
221
					$low  = $result['rangeHigh'] + 1;
222
					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
223
					$isRangeRequest = true;
224
				}
225
			}
226
		} while($isRangeRequest);
227
228
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
229
		return false;
230
	}
231
232
	/**
233
	 * Runs an read operation against LDAP
234
	 *
235
	 * @param resource $cr the LDAP connection
236
	 * @param string $dn
237
	 * @param string $attribute
238
	 * @param string $filter
239
	 * @param int $maxResults
240
	 * @return array|bool false if there was any error, true if an exists check
241
	 *                    was performed and the requested DN found, array with the
242
	 *                    returned data on a successful usual operation
243
	 */
244
	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
245
		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
246
		$dn = $this->helper->DNasBaseParameter($dn);
247
		$rr = @$this->ldap->read($cr, $dn, $filter, array($attribute));
0 ignored issues
show
Documentation introduced by
$dn is of type string, but the function expects a array.

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...
248
		if (!$this->ldap->isResource($rr)) {
249
			if ($attribute !== '') {
250
				//do not throw this message on userExists check, irritates
251
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, \OCP\Util::DEBUG);
252
			}
253
			//in case an error occurs , e.g. object does not exist
254
			return false;
255
		}
256
		if ($attribute === '' && ($filter === 'objectclass=*' || $this->ldap->countEntries($cr, $rr) === 1)) {
257
			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', \OCP\Util::DEBUG);
258
			return true;
259
		}
260
		$er = $this->ldap->firstEntry($cr, $rr);
261
		if (!$this->ldap->isResource($er)) {
262
			//did not match the filter, return false
263
			return false;
264
		}
265
		//LDAP attributes are not case sensitive
266
		$result = \OCP\Util::mb_array_change_key_case(
267
			$this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
268
269
		return $result;
270
	}
271
272
	/**
273
	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
274
	 * data if present.
275
	 *
276
	 * @param array $result from ILDAPWrapper::getAttributes()
277
	 * @param string $attribute the attribute name that was read
278
	 * @return string[]
279
	 */
280
	public function extractAttributeValuesFromResult($result, $attribute) {
281
		$values = [];
282
		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
283
			$lowercaseAttribute = strtolower($attribute);
284
			for($i=0;$i<$result[$attribute]['count'];$i++) {
285
				if($this->resemblesDN($attribute)) {
286
					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
287
				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
288
					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
289
				} else {
290
					$values[] = $result[$attribute][$i];
291
				}
292
			}
293
		}
294
		return $values;
295
	}
296
297
	/**
298
	 * Attempts to find ranged data in a getAttribute results and extracts the
299
	 * returned values as well as information on the range and full attribute
300
	 * name for further processing.
301
	 *
302
	 * @param array $result from ILDAPWrapper::getAttributes()
303
	 * @param string $attribute the attribute name that was read. Without ";range=…"
304
	 * @return array If a range was detected with keys 'values', 'attributeName',
305
	 *               'attributeFull' and 'rangeHigh', otherwise empty.
306
	 */
307
	public function extractRangeData($result, $attribute) {
308
		$keys = array_keys($result);
309
		foreach($keys as $key) {
310
			if($key !== $attribute && strpos($key, $attribute) === 0) {
311
				$queryData = explode(';', $key);
312
				if(strpos($queryData[1], 'range=') === 0) {
313
					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
314
					$data = [
315
						'values' => $result[$key],
316
						'attributeName' => $queryData[0],
317
						'attributeFull' => $key,
318
						'rangeHigh' => $high,
319
					];
320
					return $data;
321
				}
322
			}
323
		}
324
		return [];
325
	}
326
	
327
	/**
328
	 * Set password for an LDAP user identified by a DN
329
	 *
330
	 * @param string $userDN the user in question
331
	 * @param string $password the new password
332
	 * @return bool
333
	 * @throws HintException
334
	 * @throws \Exception
335
	 */
336
	public function setPassword($userDN, $password) {
337
		if(intval($this->connection->turnOnPasswordChange) !== 1) {
338
			throw new \Exception('LDAP password changes are disabled.');
339
		}
340
		$cr = $this->connection->getConnectionResource();
341
		if(!$this->ldap->isResource($cr)) {
342
			//LDAP not available
343
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
344
			return false;
345
		}
346
		
347
		try {
348
			return $this->ldap->modReplace($cr, $userDN, $password);
349
		} catch(ConstraintViolationException $e) {
350
			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
351
		}
352
	}
353
354
	/**
355
	 * checks whether the given attributes value is probably a DN
356
	 * @param string $attr the attribute in question
357
	 * @return boolean if so true, otherwise false
358
	 */
359
	private function resemblesDN($attr) {
360
		$resemblingAttributes = array(
361
			'dn',
362
			'uniquemember',
363
			'member',
364
			// memberOf is an "operational" attribute, without a definition in any RFC
365
			'memberof'
366
		);
367
		return in_array($attr, $resemblingAttributes);
368
	}
369
370
	/**
371
	 * checks whether the given string is probably a DN
372
	 * @param string $string
373
	 * @return boolean
374
	 */
375
	public function stringResemblesDN($string) {
376
		$r = $this->ldap->explodeDN($string, 0);
377
		// if exploding a DN succeeds and does not end up in
378
		// an empty array except for $r[count] being 0.
379
		return (is_array($r) && count($r) > 1);
380
	}
381
382
	/**
383
	 * returns a DN-string that is cleaned from not domain parts, e.g.
384
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
385
	 * becomes dc=foobar,dc=server,dc=org
386
	 * @param string $dn
387
	 * @return string
388
	 */
389
	public function getDomainDNFromDN($dn) {
390
		$allParts = $this->ldap->explodeDN($dn, 0);
391
		if($allParts === false) {
392
			//not a valid DN
393
			return '';
394
		}
395
		$domainParts = array();
396
		$dcFound = false;
397
		foreach($allParts as $part) {
398
			if(!$dcFound && strpos($part, 'dc=') === 0) {
399
				$dcFound = true;
400
			}
401
			if($dcFound) {
402
				$domainParts[] = $part;
403
			}
404
		}
405
		$domainDN = implode(',', $domainParts);
406
		return $domainDN;
407
	}
408
409
	/**
410
	 * returns the LDAP DN for the given internal ownCloud name of the group
411
	 * @param string $name the ownCloud name in question
412
	 * @return string|false LDAP DN on success, otherwise false
413
	 */
414
	public function groupname2dn($name) {
415
		return $this->groupMapper->getDNByName($name);
416
	}
417
418
	/**
419
	 * returns the LDAP DN for the given internal ownCloud name of the user
420
	 * @param string $name the ownCloud name in question
421
	 * @return string|false with the LDAP DN on success, otherwise false
422
	 */
423
	public function username2dn($name) {
424
		$fdn = $this->userMapper->getDNByName($name);
425
426
		//Check whether the DN belongs to the Base, to avoid issues on multi-
427
		//server setups
428
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
429
			return $fdn;
430
		}
431
432
		return false;
433
	}
434
435
	/**
436
	 * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
437
	 * @param string $fdn the dn of the group object
438
	 * @param string $ldapName optional, the display name of the object
439
	 * @return string|false with the name to use in ownCloud, false on DN outside of search DN
440
	 */
441 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...
442
		//To avoid bypassing the base DN settings under certain circumstances
443
		//with the group support, check whether the provided DN matches one of
444
		//the given Bases
445
		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...
446
			return false;
447
		}
448
449
		return $this->dn2ocname($fdn, $ldapName, false);
450
	}
451
452
	/**
453
	 * accepts an array of group DNs and tests whether they match the user
454
	 * filter by doing read operations against the group entries. Returns an
455
	 * array of DNs that match the filter.
456
	 *
457
	 * @param string[] $groupDNs
458
	 * @return string[]
459
	 */
460
	public function groupsMatchFilter($groupDNs) {
461
		$validGroupDNs = [];
462
		foreach($groupDNs as $dn) {
463
			$cacheKey = 'groupsMatchFilter-'.$dn;
464
			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
465
			if(!is_null($groupMatchFilter)) {
466
				if($groupMatchFilter) {
467
					$validGroupDNs[] = $dn;
468
				}
469
				continue;
470
			}
471
472
			// Check the base DN first. If this is not met already, we don't
473
			// need to ask the server at all.
474
			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...
475
				$this->connection->writeToCache($cacheKey, false);
476
				continue;
477
			}
478
479
			$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...
480
			if(is_array($result)) {
481
				$this->connection->writeToCache($cacheKey, true);
482
				$validGroupDNs[] = $dn;
483
			} else {
484
				$this->connection->writeToCache($cacheKey, false);
485
			}
486
487
		}
488
		return $validGroupDNs;
489
	}
490
491
	/**
492
	 * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
493
	 * @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...
494
	 * @param string $ldapName optional, the display name of the object
495
	 * @return string|false with with the name to use in ownCloud
496
	 */
497 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...
498
		//To avoid bypassing the base DN settings under certain circumstances
499
		//with the group support, check whether the provided DN matches one of
500
		//the given Bases
501
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
502
			return false;
503
		}
504
505
		return $this->dn2ocname($fdn, $ldapName, true);
506
	}
507
508
	/**
509
	 * returns an internal ownCloud name for the given LDAP DN, false on DN outside of search DN
510
	 * @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...
511
	 * @param string $ldapName optional, the display name of the object
512
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
513
	 * @return string|false with with the name to use in ownCloud
514
	 */
515
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
516
		if($isUser) {
517
			$mapper = $this->getUserMapper();
518
			$nameAttribute = $this->connection->ldapUserDisplayName;
519
		} else {
520
			$mapper = $this->getGroupMapper();
521
			$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...
522
		}
523
524
		//let's try to retrieve the ownCloud name from the mappings table
525
		$ocName = $mapper->getNameByDN($fdn);
526
		if(is_string($ocName)) {
527
			return $ocName;
528
		}
529
530
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
531
		$uuid = $this->getUUID($fdn, $isUser);
532
		if(is_string($uuid)) {
533
			$ocName = $mapper->getNameByUUID($uuid);
534
			if(is_string($ocName)) {
535
				$mapper->setDNbyUUID($fdn, $uuid);
536
				return $ocName;
537
			}
538
		} else {
539
			//If the UUID can't be detected something is foul.
540
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
541
			return false;
542
		}
543
544
		if(is_null($ldapName)) {
545
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
546
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
547
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
548
				return false;
549
			}
550
			$ldapName = $ldapName[0];
551
		}
552
553
		if($isUser) {
554
			$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...
555
			if ($usernameAttribute !== '') {
556
				$username = $this->readAttribute($fdn, $usernameAttribute);
557
				$username = $username[0];
558
			} else {
559
				$username = $uuid;
560
			}
561
			$intName = $this->sanitizeUsername($username);
562
		} else {
563
			$intName = $ldapName;
564
		}
565
566
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
567
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
568
		//NOTE: mind, disabling cache affects only this instance! Using it
569
		// outside of core user management will still cache the user as non-existing.
570
		$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...
571
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
572
		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...
573
			|| (!$isUser && !\OC_Group::groupExists($intName))) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_Group::groupExists() has been deprecated with message: Use \OC::$server->getGroupManager->groupExists($gid)

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
			if($mapper->map($fdn, $intName, $uuid)) {
575
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
576
				return $intName;
577
			}
578
		}
579
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
580
581
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
582
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
583
			return $altName;
584
		}
585
586
		//if everything else did not help..
587
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
588
		return false;
589
	}
590
591
	/**
592
	 * gives back the user names as they are used ownClod internally
593
	 * @param array $ldapUsers as returned by fetchList()
594
	 * @return array an array with the user names to use in ownCloud
595
	 *
596
	 * gives back the user names as they are used ownClod internally
597
	 */
598
	public function ownCloudUserNames($ldapUsers) {
599
		return $this->ldap2ownCloudNames($ldapUsers, true);
600
	}
601
602
	/**
603
	 * gives back the group names as they are used ownClod internally
604
	 * @param array $ldapGroups as returned by fetchList()
605
	 * @return array an array with the group names to use in ownCloud
606
	 *
607
	 * gives back the group names as they are used ownClod internally
608
	 */
609
	public function ownCloudGroupNames($ldapGroups) {
610
		return $this->ldap2ownCloudNames($ldapGroups, false);
611
	}
612
613
	/**
614
	 * @param array $ldapObjects as returned by fetchList()
615
	 * @param bool $isUsers
616
	 * @return array
617
	 */
618
	private function ldap2ownCloudNames($ldapObjects, $isUsers) {
619
		if($isUsers) {
620
			$nameAttribute = $this->connection->ldapUserDisplayName;
621
			$sndAttribute  = $this->connection->ldapUserDisplayName2;
622
		} else {
623
			$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...
624
		}
625
		$ownCloudNames = array();
626
627
		foreach($ldapObjects as $ldapObject) {
628
			$nameByLDAP = null;
629
			if(    isset($ldapObject[$nameAttribute])
630
				&& is_array($ldapObject[$nameAttribute])
631
				&& isset($ldapObject[$nameAttribute][0])
632
			) {
633
				// might be set, but not necessarily. if so, we use it.
634
				$nameByLDAP = $ldapObject[$nameAttribute][0];
635
			}
636
637
			$ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
638
			if($ocName) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ocName 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...
639
				$ownCloudNames[] = $ocName;
640
				if($isUsers) {
641
					//cache the user names so it does not need to be retrieved
642
					//again later (e.g. sharing dialogue).
643
					if(is_null($nameByLDAP)) {
644
						continue;
645
					}
646
					$sndName = isset($ldapObject[$sndAttribute][0])
647
						? $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...
648
					$this->cacheUserDisplayName($ocName, $nameByLDAP, $sndName);
649
				}
650
			}
651
		}
652
		return $ownCloudNames;
653
	}
654
655
	/**
656
	 * caches the user display name
657
	 * @param string $ocName the internal ownCloud username
658
	 * @param string|false $home the home directory path
659
	 */
660
	public function cacheUserHome($ocName, $home) {
661
		$cacheKey = 'getHome'.$ocName;
662
		$this->connection->writeToCache($cacheKey, $home);
663
	}
664
665
	/**
666
	 * caches a user as existing
667
	 * @param string $ocName the internal ownCloud username
668
	 */
669
	public function cacheUserExists($ocName) {
670
		$this->connection->writeToCache('userExists'.$ocName, true);
671
	}
672
673
	/**
674
	 * caches the user display name
675
	 * @param string $ocName the internal ownCloud username
676
	 * @param string $displayName the display name
677
	 * @param string $displayName2 the second display name
678
	 */
679
	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
680
		$user = $this->userManager->get($ocName);
681
		if($user === null) {
682
			return;
683
		}
684
		$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...
685
		$cacheKeyTrunk = 'getDisplayName';
686
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
687
	}
688
689
	/**
690
	 * creates a unique name for internal ownCloud use for users. Don't call it directly.
691
	 * @param string $name the display name of the object
692
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
693
	 *
694
	 * Instead of using this method directly, call
695
	 * createAltInternalOwnCloudName($name, true)
696
	 */
697
	private function _createAltInternalOwnCloudNameForUsers($name) {
698
		$attempts = 0;
699
		//while loop is just a precaution. If a name is not generated within
700
		//20 attempts, something else is very wrong. Avoids infinite loop.
701
		while($attempts < 20){
702
			$altName = $name . '_' . rand(1000,9999);
703
			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...
704
				return $altName;
705
			}
706
			$attempts++;
707
		}
708
		return false;
709
	}
710
711
	/**
712
	 * creates a unique name for internal ownCloud use for groups. Don't call it directly.
713
	 * @param string $name the display name of the object
714
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful.
715
	 *
716
	 * Instead of using this method directly, call
717
	 * createAltInternalOwnCloudName($name, false)
718
	 *
719
	 * Group names are also used as display names, so we do a sequential
720
	 * numbering, e.g. Developers_42 when there are 41 other groups called
721
	 * "Developers"
722
	 */
723
	private function _createAltInternalOwnCloudNameForGroups($name) {
724
		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
725
		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...
726
			$lastNo = 1; //will become name_2
727
		} else {
728
			natsort($usedNames);
729
			$lastName = array_pop($usedNames);
730
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
731
		}
732
		$altName = $name.'_'.strval($lastNo+1);
733
		unset($usedNames);
734
735
		$attempts = 1;
736
		while($attempts < 21){
737
			// Check to be really sure it is unique
738
			// while loop is just a precaution. If a name is not generated within
739
			// 20 attempts, something else is very wrong. Avoids infinite loop.
740
			if(!\OC_Group::groupExists($altName)) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_Group::groupExists() has been deprecated with message: Use \OC::$server->getGroupManager->groupExists($gid)

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...
741
				return $altName;
742
			}
743
			$altName = $name . '_' . ($lastNo + $attempts);
744
			$attempts++;
745
		}
746
		return false;
747
	}
748
749
	/**
750
	 * creates a unique name for internal ownCloud use.
751
	 * @param string $name the display name of the object
752
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
753
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
754
	 */
755
	private function createAltInternalOwnCloudName($name, $isUser) {
756
		$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...
757
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
758
		if($isUser) {
759
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
760
		} else {
761
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
762
		}
763
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
764
765
		return $altName;
766
	}
767
768
	/**
769
	 * fetches a list of users according to a provided loginName and utilizing
770
	 * the login filter.
771
	 *
772
	 * @param string $loginName
773
	 * @param array $attributes optional, list of attributes to read
774
	 * @return array
775
	 */
776 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...
777
		$loginName = $this->escapeFilterPart($loginName);
778
		$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...
779
		$users = $this->fetchListOfUsers($filter, $attributes);
780
		return $users;
781
	}
782
783
	/**
784
	 * counts the number of users according to a provided loginName and
785
	 * utilizing the login filter.
786
	 *
787
	 * @param string $loginName
788
	 * @return array
789
	 */
790 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...
791
		$loginName = $this->escapeFilterPart($loginName);
792
		$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...
793
		$users = $this->countUsers($filter);
794
		return $users;
795
	}
796
797
	/**
798
	 * @param string $filter
799
	 * @param string|string[] $attr
800
	 * @param int $limit
801
	 * @param int $offset
802
	 * @return array
803
	 */
804
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
805
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
806
		$this->batchApplyUserAttributes($ldapRecords);
807
		return $this->fetchList($ldapRecords, (count($attr) > 1));
808
	}
809
810
	/**
811
	 * provided with an array of LDAP user records the method will fetch the
812
	 * user object and requests it to process the freshly fetched attributes and
813
	 * and their values
814
	 * @param array $ldapRecords
815
	 */
816
	public function batchApplyUserAttributes(array $ldapRecords){
817
		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
818
		foreach($ldapRecords as $userRecord) {
819
			if(!isset($userRecord[$displayNameAttribute])) {
820
				// displayName is obligatory
821
				continue;
822
			}
823
			$ocName  = $this->dn2ocname($userRecord['dn'][0]);
824
			if($ocName === false) {
825
				continue;
826
			}
827
			$this->cacheUserExists($ocName);
828
			$user = $this->userManager->get($ocName);
829
			if($user instanceof OfflineUser) {
830
				$user->unmark();
831
				$user = $this->userManager->get($ocName);
832
			}
833
			if ($user !== null) {
834
				$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...
835
			} else {
836
				\OC::$server->getLogger()->debug(
837
					"The ldap user manager returned null for $ocName",
838
					['app'=>'user_ldap']
839
				);
840
			}
841
		}
842
	}
843
844
	/**
845
	 * @param string $filter
846
	 * @param string|string[] $attr
847
	 * @param int $limit
848
	 * @param int $offset
849
	 * @return array
850
	 */
851
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
852
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
853
	}
854
855
	/**
856
	 * @param array $list
857
	 * @param bool $manyAttributes
858
	 * @return array
859
	 */
860
	private function fetchList($list, $manyAttributes) {
861
		if(is_array($list)) {
862
			if($manyAttributes) {
863
				return $list;
864
			} else {
865
				$list = array_reduce($list, function($carry, $item) {
866
					$attribute = array_keys($item)[0];
867
					$carry[] = $item[$attribute][0];
868
					return $carry;
869
				}, array());
870
				return array_unique($list, SORT_LOCALE_STRING);
871
			}
872
		}
873
874
		//error cause actually, maybe throw an exception in future.
875
		return array();
876
	}
877
878
	/**
879
	 * executes an LDAP search, optimized for Users
880
	 * @param string $filter the LDAP filter for the search
881
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
882
	 * @param integer $limit
883
	 * @param integer $offset
884
	 * @return array with the search result
885
	 *
886
	 * Executes an LDAP search
887
	 */
888
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
889
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
890
	}
891
892
	/**
893
	 * @param string $filter
894
	 * @param string|string[] $attr
895
	 * @param int $limit
896
	 * @param int $offset
897
	 * @return false|int
898
	 */
899
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
900
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
901
	}
902
903
	/**
904
	 * executes an LDAP search, optimized for Groups
905
	 * @param string $filter the LDAP filter for the search
906
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
907
	 * @param integer $limit
908
	 * @param integer $offset
909
	 * @return array with the search result
910
	 *
911
	 * Executes an LDAP search
912
	 */
913
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
914
		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...
915
	}
916
917
	/**
918
	 * returns the number of available groups
919
	 * @param string $filter the LDAP search filter
920
	 * @param string[] $attr optional
921
	 * @param int|null $limit
922
	 * @param int|null $offset
923
	 * @return int|bool
924
	 */
925
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
926
		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...
927
	}
928
929
	/**
930
	 * returns the number of available objects on the base DN
931
	 *
932
	 * @param int|null $limit
933
	 * @param int|null $offset
934
	 * @return int|bool
935
	 */
936
	public function countObjects($limit = null, $offset = null) {
937
		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...
938
	}
939
940
	/**
941
	 * retrieved. Results will according to the order in the array.
942
	 * @param int $limit optional, maximum results to be counted
943
	 * @param int $offset optional, a starting point
944
	 * @return array|false array with the search result as first value and pagedSearchOK as
945
	 * second | false if not successful
946
	 */
947
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
948
		if(!is_null($attr) && !is_array($attr)) {
949
			$attr = array(mb_strtolower($attr, 'UTF-8'));
950
		}
951
952
		// See if we have a resource, in case not cancel with message
953
		$cr = $this->connection->getConnectionResource();
954
		if(!$this->ldap->isResource($cr)) {
955
			// Seems like we didn't find any resource.
956
			// Return an empty array just like before.
957
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
958
			return false;
959
		}
960
961
		//check whether paged search should be attempted
962
		$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...
963
964
		$linkResources = array_pad(array(), count($base), $cr);
965
		$sr = $this->ldap->search($linkResources, $base, $filter, $attr);
0 ignored issues
show
Documentation introduced by
$linkResources is of type array, but the function expects a resource.

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...
Bug introduced by
It seems like $attr can also be of type null; however, OCA\User_LDAP\ILDAPWrapper::search() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
966
		$error = $this->ldap->errno($cr);
967
		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...
968
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
969
			return false;
970
		}
971
972
		return array($sr, $pagedSearchOK);
973
	}
974
975
	/**
976
	 * processes an LDAP paged search operation
977
	 * @param array $sr the array containing the LDAP search resources
978
	 * @param string $filter the LDAP filter for the search
979
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
980
	 * @param int $iFoundItems number of results in the search operation
981
	 * @param int $limit maximum results to be counted
982
	 * @param int $offset a starting point
983
	 * @param bool $pagedSearchOK whether a paged search has been executed
984
	 * @param bool $skipHandling required for paged search when cookies to
985
	 * prior results need to be gained
986
	 * @return bool cookie validity, true if we have more pages, false otherwise.
987
	 */
988
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
989
		$cookie = null;
990
		if($pagedSearchOK) {
991
			$cr = $this->connection->getConnectionResource();
992
			foreach($sr as $key => $res) {
993
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
994
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
995
				}
996
			}
997
998
			//browsing through prior pages to get the cookie for the new one
999
			if($skipHandling) {
1000
				return false;
1001
			}
1002
			// if count is bigger, then the server does not support
1003
			// paged search. Instead, he did a normal search. We set a
1004
			// flag here, so the callee knows how to deal with it.
1005
			if($iFoundItems <= $limit) {
1006
				$this->pagedSearchedSuccessful = true;
1007
			}
1008
		} else {
1009
			if(!is_null($limit)) {
1010
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
1011
			}
1012
		}
1013
		/* ++ Fixing RHDS searches with pages with zero results ++
1014
		 * Return cookie status. If we don't have more pages, with RHDS
1015
		 * cookie is null, with openldap cookie is an empty string and
1016
		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1017
		 */
1018
		return !empty($cookie) || $cookie === '0';
1019
	}
1020
1021
	/**
1022
	 * executes an LDAP search, but counts the results only
1023
	 * @param string $filter the LDAP filter for the search
1024
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1025
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1026
	 * retrieved. Results will according to the order in the array.
1027
	 * @param int $limit optional, maximum results to be counted
1028
	 * @param int $offset optional, a starting point
1029
	 * @param bool $skipHandling indicates whether the pages search operation is
1030
	 * completed
1031
	 * @return int|false Integer or false if the search could not be initialized
1032
	 *
1033
	 */
1034
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1035
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
1036
1037
		$limitPerPage = intval($this->connection->ldapPagingSize);
1038
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1039
			$limitPerPage = $limit;
1040
		}
1041
1042
		$counter = 0;
1043
		$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...
1044
		$this->connection->getConnectionResource();
1045
1046
		do {
1047
			$search = $this->executeSearch($filter, $base, $attr,
1048
										   $limitPerPage, $offset);
1049
			if($search === false) {
1050
				return $counter > 0 ? $counter : false;
1051
			}
1052
			list($sr, $pagedSearchOK) = $search;
1053
1054
			/* ++ Fixing RHDS searches with pages with zero results ++
1055
			 * countEntriesInSearchResults() method signature changed
1056
			 * by removing $limit and &$hasHitLimit parameters
1057
			 */
1058
			$count = $this->countEntriesInSearchResults($sr);
1059
			$counter += $count;
1060
1061
			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1062
										$offset, $pagedSearchOK, $skipHandling);
1063
			$offset += $limitPerPage;
1064
			/* ++ Fixing RHDS searches with pages with zero results ++
1065
			 * Continue now depends on $hasMorePages value
1066
			 */
1067
			$continue = $pagedSearchOK && $hasMorePages;
1068
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1069
1070
		return $counter;
1071
	}
1072
1073
	/**
1074
	 * @param array $searchResults
1075
	 * @return int
1076
	 */
1077
	private function countEntriesInSearchResults($searchResults) {
1078
		$cr = $this->connection->getConnectionResource();
1079
		$counter = 0;
1080
1081
		foreach($searchResults as $res) {
1082
			$count = intval($this->ldap->countEntries($cr, $res));
1083
			$counter += $count;
1084
		}
1085
1086
		return $counter;
1087
	}
1088
1089
	/**
1090
	 * Executes an LDAP search
1091
	 * @param string $filter the LDAP filter for the search
1092
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1093
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1094
	 * @param int $limit
1095
	 * @param int $offset
1096
	 * @param bool $skipHandling
1097
	 * @return array with the search result
1098
	 */
1099
	private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1100
		if($limit <= 0) {
1101
			//otherwise search will fail
1102
			$limit = null;
1103
		}
1104
1105
		/* ++ Fixing RHDS searches with pages with zero results ++
1106
		 * As we can have pages with zero results and/or pages with less
1107
		 * than $limit results but with a still valid server 'cookie',
1108
		 * loops through until we get $continue equals true and
1109
		 * $findings['count'] < $limit
1110
		 */
1111
		$findings = array();
1112
		$savedoffset = $offset;
1113
		do {
1114
			$search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
1115
			if($search === false) {
1116
				return array();
1117
			}
1118
			list($sr, $pagedSearchOK) = $search;
1119
			$cr = $this->connection->getConnectionResource();
1120
1121
			if($skipHandling) {
1122
				//i.e. result do not need to be fetched, we just need the cookie
1123
				//thus pass 1 or any other value as $iFoundItems because it is not
1124
				//used
1125
				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
1126
								$offset, $pagedSearchOK,
1127
								$skipHandling);
1128
				return array();
1129
			}
1130
1131
			foreach($sr as $res) {
1132
				$findings = array_merge($findings, $this->ldap->getEntries($cr	, $res ));
1133
			}
1134
1135
			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
1136
								$limit, $offset, $pagedSearchOK,
1137
										$skipHandling);
1138
			$offset += $limit;
1139
		} while ($continue && $pagedSearchOK && $findings['count'] < $limit);
1140
		// reseting offset
1141
		$offset = $savedoffset;
1142
1143
		// if we're here, probably no connection resource is returned.
1144
		// to make ownCloud behave nicely, we simply give back an empty array.
1145
		if(is_null($findings)) {
1146
			return array();
1147
		}
1148
1149
		if(!is_null($attr)) {
1150
			$selection = array();
1151
			$i = 0;
1152
			foreach($findings as $item) {
1153
				if(!is_array($item)) {
1154
					continue;
1155
				}
1156
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1157
				foreach($attr as $key) {
1158
					$key = mb_strtolower($key, 'UTF-8');
1159
					if(isset($item[$key])) {
1160
						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1161
							unset($item[$key]['count']);
1162
						}
1163
						if($key !== 'dn') {
1164
							$selection[$i][$key] = $this->resemblesDN($key) ?
1165
								$this->helper->sanitizeDN($item[$key])
1166
								: $item[$key];
1167
						} else {
1168
							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1169
						}
1170
					}
1171
1172
				}
1173
				$i++;
1174
			}
1175
			$findings = $selection;
1176
		}
1177
		//we slice the findings, when
1178
		//a) paged search unsuccessful, though attempted
1179
		//b) no paged search, but limit set
1180
		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...
1181
			&& $pagedSearchOK)
1182
			|| (
1183
				!$pagedSearchOK
1184
				&& !is_null($limit)
1185
			)
1186
		) {
1187
			$findings = array_slice($findings, intval($offset), $limit);
1188
		}
1189
		return $findings;
1190
	}
1191
1192
	/**
1193
	 * @param string $name
1194
	 * @return bool|mixed|string
1195
	 */
1196
	public function sanitizeUsername($name) {
1197
		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...
1198
			return $name;
1199
		}
1200
1201
		// Transliteration
1202
		// latin characters to ASCII
1203
		$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1204
1205
		// Replacements
1206
		$name = str_replace(' ', '_', $name);
1207
1208
		// Every remaining disallowed characters will be removed
1209
		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1210
1211
		return $name;
1212
	}
1213
1214
	/**
1215
	* escapes (user provided) parts for LDAP filter
1216
	* @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...
1217
	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1218
	* @return string the escaped string
1219
	*/
1220
	public function escapeFilterPart($input, $allowAsterisk = false) {
1221
		$asterisk = '';
1222
		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1223
			$asterisk = '*';
1224
			$input = mb_substr($input, 1, null, 'UTF-8');
1225
		}
1226
		$search  = array('*', '\\', '(', ')');
1227
		$replace = array('\\*', '\\\\', '\\(', '\\)');
1228
		return $asterisk . str_replace($search, $replace, $input);
1229
	}
1230
1231
	/**
1232
	 * combines the input filters with AND
1233
	 * @param string[] $filters the filters to connect
1234
	 * @return string the combined filter
1235
	 */
1236
	public function combineFilterWithAnd($filters) {
1237
		return $this->combineFilter($filters, '&');
1238
	}
1239
1240
	/**
1241
	 * combines the input filters with OR
1242
	 * @param string[] $filters the filters to connect
1243
	 * @return string the combined filter
1244
	 * Combines Filter arguments with OR
1245
	 */
1246
	public function combineFilterWithOr($filters) {
1247
		return $this->combineFilter($filters, '|');
1248
	}
1249
1250
	/**
1251
	 * combines the input filters with given operator
1252
	 * @param string[] $filters the filters to connect
1253
	 * @param string $operator either & or |
1254
	 * @return string the combined filter
1255
	 */
1256
	private function combineFilter($filters, $operator) {
1257
		$combinedFilter = '('.$operator;
1258
		foreach($filters as $filter) {
1259
			if ($filter !== '' && $filter[0] !== '(') {
1260
				$filter = '('.$filter.')';
1261
			}
1262
			$combinedFilter.=$filter;
1263
		}
1264
		$combinedFilter.=')';
1265
		return $combinedFilter;
1266
	}
1267
1268
	/**
1269
	 * creates a filter part for to perform search for users
1270
	 * @param string $search the search term
1271
	 * @return string the final filter part to use in LDAP searches
1272
	 */
1273
	public function getFilterPartForUserSearch($search) {
1274
		return $this->getFilterPartForSearch($search,
1275
			$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...
1276
			$this->connection->ldapUserDisplayName);
1277
	}
1278
1279
	/**
1280
	 * creates a filter part for to perform search for groups
1281
	 * @param string $search the search term
1282
	 * @return string the final filter part to use in LDAP searches
1283
	 */
1284
	public function getFilterPartForGroupSearch($search) {
1285
		return $this->getFilterPartForSearch($search,
1286
			$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...
1287
			$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...
1288
	}
1289
1290
	/**
1291
	 * creates a filter part for searches by splitting up the given search
1292
	 * string into single words
1293
	 * @param string $search the search term
1294
	 * @param string[] $searchAttributes needs to have at least two attributes,
1295
	 * otherwise it does not make sense :)
1296
	 * @return string the final filter part to use in LDAP searches
1297
	 * @throws \Exception
1298
	 */
1299
	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1300
		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1301
			throw new \Exception('searchAttributes must be an array with at least two string');
1302
		}
1303
		$searchWords = explode(' ', trim($search));
1304
		$wordFilters = array();
1305
		foreach($searchWords as $word) {
1306
			$word = $this->prepareSearchTerm($word);
1307
			//every word needs to appear at least once
1308
			$wordMatchOneAttrFilters = array();
1309
			foreach($searchAttributes as $attr) {
1310
				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1311
			}
1312
			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1313
		}
1314
		return $this->combineFilterWithAnd($wordFilters);
1315
	}
1316
1317
	/**
1318
	 * creates a filter part for searches
1319
	 * @param string $search the search term
1320
	 * @param string[]|null $searchAttributes
1321
	 * @param string $fallbackAttribute a fallback attribute in case the user
1322
	 * did not define search attributes. Typically the display name attribute.
1323
	 * @return string the final filter part to use in LDAP searches
1324
	 */
1325
	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1326
		$filter = array();
1327
		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1328
		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1329
			try {
1330
				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1331
			} catch(\Exception $e) {
1332
				\OCP\Util::writeLog(
1333
					'user_ldap',
1334
					'Creating advanced filter for search failed, falling back to simple method.',
1335
					\OCP\Util::INFO
1336
				);
1337
			}
1338
		}
1339
1340
		$search = $this->prepareSearchTerm($search);
1341
		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1342
			if ($fallbackAttribute === '') {
1343
				return '';
1344
			}
1345
			$filter[] = $fallbackAttribute . '=' . $search;
1346
		} else {
1347
			foreach($searchAttributes as $attribute) {
1348
				$filter[] = $attribute . '=' . $search;
1349
			}
1350
		}
1351
		if(count($filter) === 1) {
1352
			return '('.$filter[0].')';
1353
		}
1354
		return $this->combineFilterWithOr($filter);
1355
	}
1356
1357
	/**
1358
	 * returns the search term depending on whether we are allowed
1359
	 * list users found by ldap with the current input appended by
1360
	 * a *
1361
	 * @return string
1362
	 */
1363
	private function prepareSearchTerm($term) {
1364
		$config = \OC::$server->getConfig();
1365
1366
		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1367
1368
		$result = $term;
1369
		if ($term === '') {
1370
			$result = '*';
1371
		} else if ($allowEnum !== 'no') {
1372
			$result = $term . '*';
1373
		}
1374
		return $result;
1375
	}
1376
1377
	/**
1378
	 * returns the filter used for counting users
1379
	 * @return string
1380
	 */
1381
	public function getFilterForUserCount() {
1382
		$filter = $this->combineFilterWithAnd(array(
1383
			$this->connection->ldapUserFilter,
1384
			$this->connection->ldapUserDisplayName . '=*'
1385
		));
1386
1387
		return $filter;
1388
	}
1389
1390
	/**
1391
	 * @param string $name
1392
	 * @param string $password
1393
	 * @return bool
1394
	 */
1395
	public function areCredentialsValid($name, $password) {
1396
		$name = $this->helper->DNasBaseParameter($name);
1397
		$testConnection = clone $this->connection;
1398
		$credentials = array(
1399
			'ldapAgentName' => $name,
1400
			'ldapAgentPassword' => $password
1401
		);
1402
		if(!$testConnection->setConfiguration($credentials)) {
1403
			return false;
1404
		}
1405
		return $testConnection->bind();
1406
	}
1407
1408
	/**
1409
	 * reverse lookup of a DN given a known UUID
1410
	 *
1411
	 * @param string $uuid
1412
	 * @return string
1413
	 * @throws \Exception
1414
	 */
1415
	public function getUserDnByUuid($uuid) {
1416
		$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...
1417
		$filter       = $this->connection->ldapUserFilter;
1418
		$base         = $this->connection->ldapBaseUsers;
1419
1420
		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1421
			// Sacrebleu! The UUID attribute is unknown :( We need first an
1422
			// existing DN to be able to reliably detect it.
1423
			$result = $this->search($filter, $base, ['dn'], 1);
1424
			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1425
				throw new \Exception('Cannot determine UUID attribute');
1426
			}
1427
			$dn = $result[0]['dn'][0];
1428
			if(!$this->detectUuidAttribute($dn, true)) {
1429
				throw new \Exception('Cannot determine UUID attribute');
1430
			}
1431
		} else {
1432
			// The UUID attribute is either known or an override is given.
1433
			// By calling this method we ensure that $this->connection->$uuidAttr
1434
			// is definitely set
1435
			if(!$this->detectUuidAttribute('', true)) {
1436
				throw new \Exception('Cannot determine UUID attribute');
1437
			}
1438
		}
1439
1440
		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1441
		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1442
			$uuid = $this->formatGuid2ForFilterUser($uuid);
1443
		}
1444
1445
		$filter = $uuidAttr . '=' . $uuid;
1446
		$result = $this->searchUsers($filter, ['dn'], 2);
1447
		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1448
			// we put the count into account to make sure that this is
1449
			// really unique
1450
			return $result[0]['dn'][0];
1451
		}
1452
1453
		throw new \Exception('Cannot determine UUID attribute');
1454
	}
1455
1456
	/**
1457
	 * auto-detects the directory's UUID attribute
1458
	 * @param string $dn a known DN used to check against
1459
	 * @param bool $isUser
1460
	 * @param bool $force the detection should be run, even if it is not set to auto
1461
	 * @return bool true on success, false otherwise
1462
	 */
1463
	private function detectUuidAttribute($dn, $isUser = true, $force = false) {
1464 View Code Duplication
		if($isUser) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1465
			$uuidAttr     = 'ldapUuidUserAttribute';
1466
			$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...
1467
		} else {
1468
			$uuidAttr     = 'ldapUuidGroupAttribute';
1469
			$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...
1470
		}
1471
1472
		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1473
			return true;
1474
		}
1475
1476
		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1477
			$this->connection->$uuidAttr = $uuidOverride;
1478
			return true;
1479
		}
1480
1481
		// for now, supported attributes are entryUUID, nsuniqueid, objectGUID, ipaUniqueID
1482
		$testAttributes = array('entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid');
1483
1484
		foreach($testAttributes as $attribute) {
1485
			$value = $this->readAttribute($dn, $attribute);
1486
			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1487
				\OCP\Util::writeLog('user_ldap',
1488
									'Setting '.$attribute.' as '.$uuidAttr,
1489
									\OCP\Util::DEBUG);
1490
				$this->connection->$uuidAttr = $attribute;
1491
				return true;
1492
			}
1493
		}
1494
		\OCP\Util::writeLog('user_ldap',
1495
							'Could not autodetect the UUID attribute',
1496
							\OCP\Util::ERROR);
1497
1498
		return false;
1499
	}
1500
1501
	/**
1502
	 * @param string $dn
1503
	 * @param bool $isUser
1504
	 * @return string|bool
1505
	 */
1506
	public function getUUID($dn, $isUser = true) {
1507 View Code Duplication
		if($isUser) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1508
			$uuidAttr     = 'ldapUuidUserAttribute';
1509
			$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...
1510
		} else {
1511
			$uuidAttr     = 'ldapUuidGroupAttribute';
1512
			$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...
1513
		}
1514
1515
		$uuid = false;
1516
		if($this->detectUuidAttribute($dn, $isUser)) {
1517
			$uuid = $this->readAttribute($dn, $this->connection->$uuidAttr);
1518
			if( !is_array($uuid)
1519
				&& $uuidOverride !== ''
1520
				&& $this->detectUuidAttribute($dn, $isUser, true)) {
1521
					$uuid = $this->readAttribute($dn,
1522
												 $this->connection->$uuidAttr);
1523
			}
1524
			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1525
				$uuid = $uuid[0];
1526
			}
1527
		}
1528
1529
		return $uuid;
1530
	}
1531
1532
	/**
1533
	 * converts a binary ObjectGUID into a string representation
1534
	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1535
	 * @return string
1536
	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1537
	 */
1538
	private function convertObjectGUID2Str($oguid) {
1539
		$hex_guid = bin2hex($oguid);
1540
		$hex_guid_to_guid_str = '';
1541 View Code Duplication
		for($k = 1; $k <= 4; ++$k) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1542
			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1543
		}
1544
		$hex_guid_to_guid_str .= '-';
1545 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1546
			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1547
		}
1548
		$hex_guid_to_guid_str .= '-';
1549 View Code Duplication
		for($k = 1; $k <= 2; ++$k) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1550
			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1551
		}
1552
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1553
		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1554
1555
		return strtoupper($hex_guid_to_guid_str);
1556
	}
1557
1558
	/**
1559
	 * the first three blocks of the string-converted GUID happen to be in
1560
	 * reverse order. In order to use it in a filter, this needs to be
1561
	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1562
	 * to every two hax figures.
1563
	 *
1564
	 * If an invalid string is passed, it will be returned without change.
1565
	 *
1566
	 * @param string $guid
1567
	 * @return string
1568
	 */
1569
	public function formatGuid2ForFilterUser($guid) {
1570
		if(!is_string($guid)) {
1571
			throw new \InvalidArgumentException('String expected');
1572
		}
1573
		$blocks = explode('-', $guid);
1574 View Code Duplication
		if(count($blocks) !== 5) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1575
			/*
1576
			 * Why not throw an Exception instead? This method is a utility
1577
			 * called only when trying to figure out whether a "missing" known
1578
			 * LDAP user was or was not renamed on the LDAP server. And this
1579
			 * even on the use case that a reverse lookup is needed (UUID known,
1580
			 * not DN), i.e. when finding users (search dialog, users page,
1581
			 * login, …) this will not be fired. This occurs only if shares from
1582
			 * a users are supposed to be mounted who cannot be found. Throwing
1583
			 * an exception here would kill the experience for a valid, acting
1584
			 * user. Instead we write a log message.
1585
			 */
1586
			\OC::$server->getLogger()->info(
1587
				'Passed string does not resemble a valid GUID. Known UUID ' .
1588
				'({uuid}) probably does not match UUID configuration.',
1589
				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1590
			);
1591
			return $guid;
1592
		}
1593 View Code Duplication
		for($i=0; $i < 3; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1594
			$pairs = str_split($blocks[$i], 2);
1595
			$pairs = array_reverse($pairs);
1596
			$blocks[$i] = implode('', $pairs);
1597
		}
1598 View Code Duplication
		for($i=0; $i < 5; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1599
			$pairs = str_split($blocks[$i], 2);
1600
			$blocks[$i] = '\\' . implode('\\', $pairs);
1601
		}
1602
		return implode('', $blocks);
1603
	}
1604
1605
	/**
1606
	 * gets a SID of the domain of the given dn
1607
	 * @param string $dn
1608
	 * @return string|bool
1609
	 */
1610
	public function getSID($dn) {
1611
		$domainDN = $this->getDomainDNFromDN($dn);
1612
		$cacheKey = 'getSID-'.$domainDN;
1613
		$sid = $this->connection->getFromCache($cacheKey);
1614
		if(!is_null($sid)) {
1615
			return $sid;
1616
		}
1617
1618
		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1619
		if(!is_array($objectSid) || empty($objectSid)) {
1620
			$this->connection->writeToCache($cacheKey, false);
1621
			return false;
1622
		}
1623
		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1624
		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1625
1626
		return $domainObjectSid;
1627
	}
1628
1629
	/**
1630
	 * converts a binary SID into a string representation
1631
	 * @param string $sid
1632
	 * @return string
1633
	 */
1634
	public function convertSID2Str($sid) {
1635
		// The format of a SID binary string is as follows:
1636
		// 1 byte for the revision level
1637
		// 1 byte for the number n of variable sub-ids
1638
		// 6 bytes for identifier authority value
1639
		// n*4 bytes for n sub-ids
1640
		//
1641
		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1642
		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1643
		$revision = ord($sid[0]);
1644
		$numberSubID = ord($sid[1]);
1645
1646
		$subIdStart = 8; // 1 + 1 + 6
1647
		$subIdLength = 4;
1648
		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1649
			// Incorrect number of bytes present.
1650
			return '';
1651
		}
1652
1653
		// 6 bytes = 48 bits can be represented using floats without loss of
1654
		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1655
		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1656
1657
		$subIDs = array();
1658
		for ($i = 0; $i < $numberSubID; $i++) {
1659
			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1660
			$subIDs[] = sprintf('%u', $subID[1]);
1661
		}
1662
1663
		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1664
		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1665
	}
1666
1667
	/**
1668
	 * checks if the given DN is part of the given base DN(s)
1669
	 * @param string $dn the DN
1670
	 * @param string[] $bases array containing the allowed base DN or DNs
1671
	 * @return bool
1672
	 */
1673
	public function isDNPartOfBase($dn, $bases) {
1674
		$belongsToBase = false;
1675
		$bases = $this->helper->sanitizeDN($bases);
1676
1677
		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...
1678
			$belongsToBase = true;
1679
			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1680
				$belongsToBase = false;
1681
			}
1682
			if($belongsToBase) {
1683
				break;
1684
			}
1685
		}
1686
		return $belongsToBase;
1687
	}
1688
1689
	/**
1690
	 * resets a running Paged Search operation
1691
	 */
1692
	private function abandonPagedSearch() {
1693
		if($this->connection->hasPagedResultSupport) {
1694
			$cr = $this->connection->getConnectionResource();
1695
			$this->ldap->controlPagedResult($cr, 0, false, $this->lastCookie);
1696
			$this->getPagedSearchResultState();
1697
			$this->lastCookie = '';
1698
			$this->cookies = array();
1699
		}
1700
	}
1701
1702
	/**
1703
	 * get a cookie for the next LDAP paged search
1704
	 * @param string $base a string with the base DN for the search
1705
	 * @param string $filter the search filter to identify the correct search
1706
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1707
	 * @param int $offset the offset for the new search to identify the correct search really good
1708
	 * @return string containing the key or empty if none is cached
1709
	 */
1710
	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1711
		if($offset === 0) {
1712
			return '';
1713
		}
1714
		$offset -= $limit;
1715
		//we work with cache here
1716
		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . intval($limit) . '-' . intval($offset);
1717
		$cookie = '';
1718
		if(isset($this->cookies[$cacheKey])) {
1719
			$cookie = $this->cookies[$cacheKey];
1720
			if(is_null($cookie)) {
1721
				$cookie = '';
1722
			}
1723
		}
1724
		return $cookie;
1725
	}
1726
1727
	/**
1728
	 * checks whether an LDAP paged search operation has more pages that can be
1729
	 * retrieved, typically when offset and limit are provided.
1730
	 *
1731
	 * Be very careful to use it: the last cookie value, which is inspected, can
1732
	 * be reset by other operations. Best, call it immediately after a search(),
1733
	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1734
	 * well. Don't rely on it with any fetchList-method.
1735
	 * @return bool
1736
	 */
1737
	public function hasMoreResults() {
1738
		if(!$this->connection->hasPagedResultSupport) {
1739
			return false;
1740
		}
1741
1742
		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1743
			// as in RFC 2696, when all results are returned, the cookie will
1744
			// be empty.
1745
			return false;
1746
		}
1747
1748
		return true;
1749
	}
1750
1751
	/**
1752
	 * set a cookie for LDAP paged search run
1753
	 * @param string $base a string with the base DN for the search
1754
	 * @param string $filter the search filter to identify the correct search
1755
	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1756
	 * @param int $offset the offset for the run search to identify the correct search really good
1757
	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1758
	 * @return void
1759
	 */
1760
	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1761
		// allow '0' for 389ds
1762
		if(!empty($cookie) || $cookie === '0') {
1763
			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' .intval($limit) . '-' . intval($offset);
1764
			$this->cookies[$cacheKey] = $cookie;
1765
			$this->lastCookie = $cookie;
1766
		}
1767
	}
1768
1769
	/**
1770
	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1771
	 * @return boolean|null true on success, null or false otherwise
1772
	 */
1773
	public function getPagedSearchResultState() {
1774
		$result = $this->pagedSearchedSuccessful;
1775
		$this->pagedSearchedSuccessful = null;
1776
		return $result;
1777
	}
1778
1779
	/**
1780
	 * Prepares a paged search, if possible
1781
	 * @param string $filter the LDAP filter for the search
1782
	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1783
	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1784
	 * @param int $limit
1785
	 * @param int $offset
1786
	 * @return bool|true
1787
	 */
1788
	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1789
		$pagedSearchOK = false;
1790
		if($this->connection->hasPagedResultSupport && ($limit !== 0)) {
1791
			$offset = intval($offset); //can be null
1792
			\OCP\Util::writeLog('user_ldap',
1793
				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
1794
				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
1795
				\OCP\Util::DEBUG);
1796
			//get the cookie from the search for the previous search, required by LDAP
1797
			foreach($bases as $base) {
1798
1799
				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1800
				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
1801
					// no cookie known, although the offset is not 0. Maybe cache run out. We need
1802
					// to start all over *sigh* (btw, Dear Reader, did you know LDAP paged
1803
					// searching was designed by MSFT?)
1804
					// 		Lukas: No, but thanks to reading that source I finally know!
1805
					// '0' is valid, because 389ds
1806
					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
1807
					//a bit recursive, $offset of 0 is the exit
1808
					\OCP\Util::writeLog('user_ldap', 'Looking for cookie L/O '.$limit.'/'.$reOffset, \OCP\Util::INFO);
1809
					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
1810
					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
1811
					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
1812
					//TODO: remember this, probably does not change in the next request...
1813
					if(empty($cookie) && $cookie !== '0') {
1814
						// '0' is valid, because 389ds
1815
						$cookie = null;
1816
					}
1817
				}
1818
				if(!is_null($cookie)) {
1819
					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
1820
					$this->abandonPagedSearch();
1821
					$pagedSearchOK = $this->ldap->controlPagedResult(
1822
						$this->connection->getConnectionResource(), $limit,
1823
						false, $cookie);
1824
					if(!$pagedSearchOK) {
1825
						return false;
1826
					}
1827
					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', \OCP\Util::DEBUG);
1828
				} else {
1829
					\OCP\Util::writeLog('user_ldap',
1830
						'No paged search for us, Cpt., Limit '.$limit.' Offset '.$offset,
1831
						\OCP\Util::INFO);
1832
				}
1833
1834
			}
1835
		/* ++ Fixing RHDS searches with pages with zero results ++
1836
		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
1837
		 * due to pages with zero results.
1838
		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
1839
		 * if we don't have a previous paged search.
1840
		 */
1841
		} else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) {
1842
			// a search without limit was requested. However, if we do use
1843
			// Paged Search once, we always must do it. This requires us to
1844
			// initialize it with the configured page size.
1845
			$this->abandonPagedSearch();
1846
			// in case someone set it to 0 … use 500, otherwise no results will
1847
			// be returned.
1848
			$pageSize = intval($this->connection->ldapPagingSize) > 0 ? intval($this->connection->ldapPagingSize) : 500;
1849
			$pagedSearchOK = $this->ldap->controlPagedResult(
1850
				$this->connection->getConnectionResource(), $pageSize, false, ''
1851
			);
1852
		}
1853
1854
		return $pagedSearchOK;
1855
	}
1856
1857
}
1858