access.php 51.4 KB
Newer Older
1
2
<?php
/**
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
3
4
5
6
7
8
9
10
11
12
13
14
 * @author Alexander Bergolth <leo@strike.wu.ac.at>
 * @author Andreas Fischer <bantu@owncloud.com>
 * @author Arthur Schiwon <blizzz@owncloud.com>
 * @author Bart Visscher <bartv@thisnet.nl>
 * @author Benjamin Diele <benjamin@diele.be>
 * @author Christopher Schäpers <kondou@ts.unde.re>
 * @author Donald Buczek <buczek@molgen.mpg.de>
 * @author Jörn Friedrich Dreyer <jfd@butonic.de>
 * @author Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
 * @author Lukas Reschke <lukas@owncloud.com>
 * @author Lyonel Vincent <lyonel@ezix.org>
 * @author Morris Jobke <hey@morrisjobke.de>
15
 * @author Nicolas Grekas <nicolas.grekas@gmail.com>
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
16
17
 * @author Robin McCorkell <rmccorkell@karoshi.org.uk>
 * @author Scrutinizer Auto-Fixer <auto-fixer@scrutinizer-ci.com>
18
 *
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
19
20
 * @copyright Copyright (c) 2015, ownCloud, Inc.
 * @license AGPL-3.0
21
 *
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
22
23
24
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
25
 *
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
26
 * This program is distributed in the hope that it will be useful,
27
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
28
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
30
 *
Jenkins for ownCloud's avatar
Jenkins for ownCloud committed
31
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
33
34
 *
 */
35

36
namespace OCA\user_ldap\lib;
37

38
use OCA\user_ldap\lib\user\OfflineUser;
39
40
use OCA\User_LDAP\Mapping\AbstractMapping;

41
42
43
44
/**
 * Class Access
 * @package OCA\user_ldap\lib
 */
45
class Access extends LDAPUtility implements user\IUserTools {
Arthur Schiwon's avatar
Arthur Schiwon committed
46
47
48
	/**
	 * @var \OCA\user_ldap\lib\Connection
	 */
49
	public $connection;
50
	public $userManager;
51
52
53
	//never ever check this var directly, always use getPagedSearchResultState
	protected $pagedSearchedSuccessful;

54
55
56
	/**
	 * @var string[] $cookies an array of returned Paged Result cookies
	 */
Arthur Schiwon's avatar
Arthur Schiwon committed
57
58
	protected $cookies = array();

59
60
61
62
63
	/**
	 * @var string $lastCookie the last cookie returned from a Paged Results
	 * operation, defaults to an empty string
	 */
	protected $lastCookie = '';
64

65
66
67
68
69
70
71
72
73
74
	/**
	 * @var AbstractMapping $userMapper
	 */
	protected $userMapper;

	/**
	* @var AbstractMapping $userMapper
	*/
	protected $groupMapper;

75
76
	public function __construct(Connection $connection, ILDAPWrapper $ldap,
		user\Manager $userManager) {
77
		parent::__construct($ldap);
78
		$this->connection = $connection;
79
80
		$this->userManager = $userManager;
		$this->userManager->setLdapAccess($this);
81
82
	}

83
84
85
86
87
88
89
90
	/**
	 * sets the User Mapper
	 * @param AbstractMapping $mapper
	 */
	public function setUserMapper(AbstractMapping $mapper) {
		$this->userMapper = $mapper;
	}

91
92
93
94
95
96
97
98
99
100
101
102
	/**
	 * returns the User Mapper
	 * @throws \Exception
	 * @return AbstractMapping
	 */
	public function getUserMapper() {
		if(is_null($this->userMapper)) {
			throw new \Exception('UserMapper was not assigned to this Access instance.');
		}
		return $this->userMapper;
	}

103
104
105
106
107
108
109
110
	/**
	 * sets the Group Mapper
	 * @param AbstractMapping $mapper
	 */
	public function setGroupMapper(AbstractMapping $mapper) {
		$this->groupMapper = $mapper;
	}

111
112
113
114
115
116
117
118
119
120
121
122
	/**
	 * returns the Group Mapper
	 * @throws \Exception
	 * @return AbstractMapping
	 */
	public function getGroupMapper() {
		if(is_null($this->groupMapper)) {
			throw new \Exception('GroupMapper was not assigned to this Access instance.');
		}
		return $this->groupMapper;
	}

123
124
125
	/**
	 * @return bool
	 */
126
127
	private function checkConnection() {
		return ($this->connection instanceof Connection);
Arthur Schiwon's avatar
Arthur Schiwon committed
128
	}
Arthur Schiwon's avatar
Arthur Schiwon committed
129

130
	/**
131
	 * returns the Connection instance
132
133
134
135
136
137
138
	 * @return \OCA\user_ldap\lib\Connection
	 */
	public function getConnection() {
		return $this->connection;
	}

	/**
139
	 * reads a given attribute for an LDAP record identified by a DN
Arthur Schiwon's avatar
Arthur Schiwon committed
140
141
	 * @param string $dn the record in question
	 * @param string $attr the attribute that shall be retrieved
142
	 *        if empty, just check the record's existence
143
144
	 * @param string $filter
	 * @return array|false an array of values on success or an empty
145
	 *          array if $attr is empty, false otherwise
146
	 */
147
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
148
		if(!$this->checkConnection()) {
Bart Visscher's avatar
Bart Visscher committed
149
150
151
			\OCP\Util::writeLog('user_ldap',
				'No LDAP Connector assigned, access impossible for readAttribute.',
				\OCP\Util::WARN);
152
			return false;
Arthur Schiwon's avatar
Arthur Schiwon committed
153
		}
154
		$cr = $this->connection->getConnectionResource();
155
		if(!$this->ldap->isResource($cr)) {
156
			//LDAP not available
157
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
158
159
			return false;
		}
160
161
162
		//Cancel possibly running Paged Results operation, otherwise we run in
		//LDAP protocol errors
		$this->abandonPagedSearch();
163
164
		// openLDAP requires that we init a new Paged Search. Not needed by AD,
		// but does not hurt either.
Arthur Schiwon's avatar
Arthur Schiwon committed
165
166
167
		$pagingSize = intval($this->connection->ldapPagingSize);
		// 0 won't result in replies, small numbers may leave out groups
		// (cf. #12306), 500 is default for paging and should work everywhere.
168
		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
Arthur Schiwon's avatar
Arthur Schiwon committed
169
		$this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0);
Arthur Schiwon's avatar
Arthur Schiwon committed
170
		$dn = $this->DNasBaseParameter($dn);
171
		$rr = @$this->ldap->read($cr, $dn, $filter, array($attr));
172
		if(!$this->ldap->isResource($rr)) {
173
174
175
176
			if(!empty($attr)) {
				//do not throw this message on userExists check, irritates
				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN '.$dn, \OCP\Util::DEBUG);
			}
177
178
179
			//in case an error occurs , e.g. object does not exist
			return false;
		}
180
181
		if (empty($attr)) {
			\OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
182
			return array();
183
		}
184
		$er = $this->ldap->firstEntry($cr, $rr);
185
		if(!$this->ldap->isResource($er)) {
186
187
188
			//did not match the filter, return false
			return false;
		}
189
		//LDAP attributes are not case sensitive
Arthur Schiwon's avatar
Arthur Schiwon committed
190
191
		$result = \OCP\Util::mb_array_change_key_case(
				$this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
192
		$attr = mb_strtolower($attr, 'UTF-8');
193

Arthur Schiwon's avatar
Arthur Schiwon committed
194
		if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
195
196
			$values = array();
			for($i=0;$i<$result[$attr]['count'];$i++) {
197
198
				if($this->resemblesDN($attr)) {
					$values[] = $this->sanitizeDN($result[$attr][$i]);
199
				} elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
200
201
202
203
					$values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
				} else {
					$values[] = $result[$attr][$i];
				}
204
205
206
			}
			return $values;
		}
207
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
208
		return false;
Arthur Schiwon's avatar
Arthur Schiwon committed
209
210
	}

211
	/**
212
	 * checks whether the given attributes value is probably a DN
213
214
	 * @param string $attr the attribute in question
	 * @return boolean if so true, otherwise false
215
	 */
216
217
218
219
	private function resemblesDN($attr) {
		$resemblingAttributes = array(
			'dn',
			'uniquemember',
220
221
222
			'member',
			// memberOf is an "operational" attribute, without a definition in any RFC
			'memberof'
Arthur Schiwon's avatar
Arthur Schiwon committed
223
		);
224
225
		return in_array($attr, $resemblingAttributes);
	}
226

227
228
229
230
231
232
233
234
235
236
237
238
	/**
	 * checks whether the given string is probably a DN
	 * @param string $string
	 * @return boolean
	 */
	public function stringResemblesDN($string) {
		$r = $this->ldap->explodeDN($string, 0);
		// if exploding a DN succeeds and does not end up in
		// an empty array except for $r[count] being 0.
		return (is_array($r) && count($r) > 1);
	}

239
	/**
240
	 * sanitizes a DN received from the LDAP server
241
242
	 * @param array $dn the DN in question
	 * @return array the sanitized DN
243
244
	 */
	private function sanitizeDN($dn) {
Arthur Schiwon's avatar
Arthur Schiwon committed
245
246
247
248
		//treating multiple base DNs
		if(is_array($dn)) {
			$result = array();
			foreach($dn as $singleDN) {
Robin McCorkell's avatar
Robin McCorkell committed
249
				$result[] = $this->sanitizeDN($singleDN);
Arthur Schiwon's avatar
Arthur Schiwon committed
250
251
252
253
			}
			return $result;
		}

Bart Visscher's avatar
Bart Visscher committed
254
255
		//OID sometimes gives back DNs with whitespace after the comma
		// a la "uid=foo, cn=bar, dn=..." We need to tackle this!
Arthur Schiwon's avatar
Arthur Schiwon committed
256
		$dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
257

258
259
260
		//make comparisons and everything work
		$dn = mb_strtolower($dn, 'UTF-8');

Arthur Schiwon's avatar
Arthur Schiwon committed
261
262
263
		//escape DN values according to RFC 2253 – this is already done by ldap_explode_dn
		//to use the DN in search filters, \ needs to be escaped to \5c additionally
		//to use them in bases, we convert them back to simple backslashes in readAttribute()
264
265
266
267
268
269
270
271
272
		$replacements = array(
			'\,' => '\5c2C',
			'\=' => '\5c3D',
			'\+' => '\5c2B',
			'\<' => '\5c3C',
			'\>' => '\5c3E',
			'\;' => '\5c3B',
			'\"' => '\5c22',
			'\#' => '\5c23',
273
274
275
			'('  => '\28',
			')'  => '\29',
			'*'  => '\2A',
276
		);
277
		$dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
278

279
		return $dn;
280
281
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
	/**
	 * returns a DN-string that is cleaned from not domain parts, e.g.
	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
	 * becomes dc=foobar,dc=server,dc=org
	 * @param string $dn
	 * @return string
	 */
	public function getDomainDNFromDN($dn) {
		$allParts = $this->ldap->explodeDN($dn, 0);
		if($allParts === false) {
			//not a valid DN
			return '';
		}
		$domainParts = array();
		$dcFound = false;
		foreach($allParts as $part) {
			if(!$dcFound && strpos($part, 'dc=') === 0) {
				$dcFound = true;
			}
			if($dcFound) {
				$domainParts[] = $part;
			}
		}
		$domainDN = implode(',', $domainParts);
		return $domainDN;
	}

309
	/**
310
	 * returns the LDAP DN for the given internal ownCloud name of the group
311
	 * @param string $name the ownCloud name in question
312
	 * @return string|false LDAP DN on success, otherwise false
313
	 */
314
	public function groupname2dn($name) {
315
		return $this->groupMapper->getDNbyName($name);
316
317
318
	}

	/**
319
	 * returns the LDAP DN for the given internal ownCloud name of the user
320
	 * @param string $name the ownCloud name in question
321
	 * @return string|false with the LDAP DN on success, otherwise false
322
	 */
323
	public function username2dn($name) {
324
325
		$fdn = $this->userMapper->getDNbyName($name);

326
327
		//Check whether the DN belongs to the Base, to avoid issues on multi-
		//server setups
328
329
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
			return $fdn;
330
331
332
333
334
335
		}

		return false;
	}

	/**
336
	public function ocname2dn($name, $isUser) {
337
	 * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
338
	 * @param string $fdn the dn of the group object
339
	 * @param string $ldapName optional, the display name of the object
340
	 * @return string|false with the name to use in ownCloud, false on DN outside of search DN
341
	 */
342
	public function dn2groupname($fdn, $ldapName = null) {
Arthur Schiwon's avatar
Arthur Schiwon committed
343
344
345
		//To avoid bypassing the base DN settings under certain circumstances
		//with the group support, check whether the provided DN matches one of
		//the given Bases
346
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
347
348
			return false;
		}
Arthur Schiwon's avatar
Arthur Schiwon committed
349

350
		return $this->dn2ocname($fdn, $ldapName, false);
351
352
	}

353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
	/**
	 * accepts an array of group DNs and tests whether they match the user
	 * filter by doing read operations against the group entries. Returns an
	 * array of DNs that match the filter.
	 *
	 * @param string[] $groupDNs
	 * @return string[]
	 */
	public function groupsMatchFilter($groupDNs) {
		$validGroupDNs = [];
		foreach($groupDNs as $dn) {
			$cacheKey = 'groupsMatchFilter-'.$dn;
			if($this->connection->isCached($cacheKey)) {
				if($this->connection->getFromCache($cacheKey)) {
					$validGroupDNs[] = $dn;
				}
				continue;
			}

372
373
374
375
376
377
378
			// Check the base DN first. If this is not met already, we don't
			// need to ask the server at all.
			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
				$this->connection->writeToCache($cacheKey, false);
				continue;
			}

379
380
			$result = $this->readAttribute($dn, 'cn', $this->connection->ldapGroupFilter);
			if(is_array($result)) {
381
				$this->connection->writeToCache($cacheKey, true);
382
				$validGroupDNs[] = $dn;
383
384
			} else {
				$this->connection->writeToCache($cacheKey, false);
385
			}
386

387
388
389
390
		}
		return $validGroupDNs;
	}

391
	/**
392
	 * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
393
394
	 * @param string $dn the dn of the user object
	 * @param string $ldapName optional, the display name of the object
395
	 * @return string|false with with the name to use in ownCloud
396
	 */
397
	public function dn2username($fdn, $ldapName = null) {
Arthur Schiwon's avatar
Arthur Schiwon committed
398
399
400
		//To avoid bypassing the base DN settings under certain circumstances
		//with the group support, check whether the provided DN matches one of
		//the given Bases
401
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
402
403
			return false;
		}
Arthur Schiwon's avatar
Arthur Schiwon committed
404

405
		return $this->dn2ocname($fdn, $ldapName, true);
406
407
	}

408
	/**
409
	 * returns an internal ownCloud name for the given LDAP DN, false on DN outside of search DN
410
411
412
	 * @param string $dn the dn of the user object
	 * @param string $ldapName optional, the display name of the object
	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
413
	 * @return string|false with with the name to use in ownCloud
414
	 */
415
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
416
		if($isUser) {
417
			$mapper = $this->getUserMapper();
418
			$nameAttribute = $this->connection->ldapUserDisplayName;
419
		} else {
420
			$mapper = $this->getGroupMapper();
421
			$nameAttribute = $this->connection->ldapGroupDisplayName;
422
423
		}

424
		//let's try to retrieve the ownCloud name from the mappings table
425
426
		$ocName = $mapper->getNameByDN($fdn);
		if(is_string($ocName)) {
427
			return $ocName;
428
429
		}

430
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
431
432
433
434
435
436
		$uuid = $this->getUUID($fdn, $isUser);
		if(is_string($uuid)) {
			$ocName = $mapper->getNameByUUID($uuid);
			if(is_string($ocName)) {
				$mapper->setDNbyUUID($fdn, $uuid);
				return $ocName;
437
			}
438
439
		} else {
			//If the UUID can't be detected something is foul.
440
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
441
			return false;
442
443
		}

444
		if(is_null($ldapName)) {
445
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
446
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
447
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
448
449
				return false;
			}
450
			$ldapName = $ldapName[0];
451
		}
452
453
454

		if($isUser) {
			$usernameAttribute = $this->connection->ldapExpertUsernameAttr;
455
456
			if(!empty($usernameAttribute)) {
				$username = $this->readAttribute($fdn, $usernameAttribute);
457
458
459
460
				$username = $username[0];
			} else {
				$username = $uuid;
			}
461
			$intName = $this->sanitizeUsername($username);
462
		} else {
463
			$intName = $ldapName;
464
		}
465

466
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
467
		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
Arthur Schiwon's avatar
Arthur Schiwon committed
468
469
		//NOTE: mind, disabling cache affects only this instance! Using it
		// outside of core user management will still cache the user as non-existing.
470
471
		$originalTTL = $this->connection->ldapCacheTTL;
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
472
473
		if(($isUser && !\OCP\User::userExists($intName))
			|| (!$isUser && !\OC_Group::groupExists($intName))) {
474
			if($mapper->map($fdn, $intName, $uuid)) {
475
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
476
				return $intName;
477
			}
478
		}
479
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
480

481
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
482
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
483
			return $altName;
484
485
		}

486
		//if everything else did not help..
487
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
488
		return false;
489
490
491
	}

	/**
492
	 * gives back the user names as they are used ownClod internally
493
	 * @param array $ldapUsers as returned by fetchList()
494
	 * @return array an array with the user names to use in ownCloud
495
496
497
	 *
	 * gives back the user names as they are used ownClod internally
	 */
498
499
	public function ownCloudUserNames($ldapUsers) {
		return $this->ldap2ownCloudNames($ldapUsers, true);
500
501
502
	}

	/**
503
	 * gives back the group names as they are used ownClod internally
504
	 * @param array $ldapGroups as returned by fetchList()
505
	 * @return array an array with the group names to use in ownCloud
506
507
508
	 *
	 * gives back the group names as they are used ownClod internally
	 */
509
510
	public function ownCloudGroupNames($ldapGroups) {
		return $this->ldap2ownCloudNames($ldapGroups, false);
511
512
	}

513
	/**
514
	 * @param array $ldapObjects as returned by fetchList()
515
516
	 * @param bool $isUsers
	 * @return array
517
	 */
518
	private function ldap2ownCloudNames($ldapObjects, $isUsers) {
519
		if($isUsers) {
520
			$nameAttribute = $this->connection->ldapUserDisplayName;
521
		} else {
522
			$nameAttribute = $this->connection->ldapGroupDisplayName;
523
524
525
526
		}
		$ownCloudNames = array();

		foreach($ldapObjects as $ldapObject) {
527
528
529
530
531
532
533
534
535
			$nameByLDAP = null;
			if(    isset($ldapObject[$nameAttribute])
				&& is_array($ldapObject[$nameAttribute])
				&& isset($ldapObject[$nameAttribute][0])
			) {
				// might be set, but not necessarily. if so, we use it.
				$nameByLDAP = $ldapObject[$nameAttribute][0];
			}

536
			$ocName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
537
538
			if($ocName) {
				$ownCloudNames[] = $ocName;
539
540
541
				if($isUsers) {
					//cache the user names so it does not need to be retrieved
					//again later (e.g. sharing dialogue).
Arthur Schiwon's avatar
Arthur Schiwon committed
542
					$this->cacheUserExists($ocName);
543
544
545
					if(!is_null($nameByLDAP)) {
						$this->cacheUserDisplayName($ocName, $nameByLDAP);
					}
546
				}
547
			}
548
			continue;
549
550
551
552
		}
		return $ownCloudNames;
	}

553
554
555
556
557
558
559
560
561
562
	/**
	 * caches the user display name
	 * @param string $ocName the internal ownCloud username
	 * @param string|false $home the home directory path
	 */
	public function cacheUserHome($ocName, $home) {
		$cacheKey = 'getHome'.$ocName;
		$this->connection->writeToCache($cacheKey, $home);
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
563
564
565
566
567
568
569
570
	/**
	 * caches a user as existing
	 * @param string $ocName the internal ownCloud username
	 */
	public function cacheUserExists($ocName) {
		$this->connection->writeToCache('userExists'.$ocName, true);
	}

571
	/**
572
	 * caches the user display name
573
574
	 * @param string $ocName the internal ownCloud username
	 * @param string $displayName the display name
575
	 */
576
	public function cacheUserDisplayName($ocName, $displayName) {
577
		$cacheKeyTrunk = 'getDisplayName';
578
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
579
580
	}

581
	/**
582
	 * creates a unique name for internal ownCloud use for users. Don't call it directly.
583
	 * @param string $name the display name of the object
584
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
585
586
587
588
589
590
591
592
593
	 *
	 * Instead of using this method directly, call
	 * createAltInternalOwnCloudName($name, true)
	 */
	private function _createAltInternalOwnCloudNameForUsers($name) {
		$attempts = 0;
		//while loop is just a precaution. If a name is not generated within
		//20 attempts, something else is very wrong. Avoids infinite loop.
		while($attempts < 20){
594
595
			$altName = $name . '_' . rand(1000,9999);
			if(!\OCP\User::userExists($altName)) {
596
597
598
599
600
601
602
603
				return $altName;
			}
			$attempts++;
		}
		return false;
	}

	/**
604
	 * creates a unique name for internal ownCloud use for groups. Don't call it directly.
605
	 * @param string $name the display name of the object
606
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful.
607
608
609
	 *
	 * Instead of using this method directly, call
	 * createAltInternalOwnCloudName($name, false)
610
	 *
611
612
613
614
615
	 * Group names are also used as display names, so we do a sequential
	 * numbering, e.g. Developers_42 when there are 41 other groups called
	 * "Developers"
	 */
	private function _createAltInternalOwnCloudNameForGroups($name) {
616
		$usedNames = $this->groupMapper->getNamesBySearch($name.'_%');
617
		if(!($usedNames) || count($usedNames) === 0) {
618
619
620
			$lastNo = 1; //will become name_2
		} else {
			natsort($usedNames);
621
622
			$lastName = array_pop($usedNames);
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
623
624
625
626
627
628
		}
		$altName = $name.'_'.strval($lastNo+1);
		unset($usedNames);

		$attempts = 1;
		while($attempts < 21){
629
630
631
			// Check to be really sure it is unique
			// while loop is just a precaution. If a name is not generated within
			// 20 attempts, something else is very wrong. Avoids infinite loop.
632
633
634
			if(!\OC_Group::groupExists($altName)) {
				return $altName;
			}
Arthur Schiwon's avatar
Arthur Schiwon committed
635
			$altName = $name . '_' . ($lastNo + $attempts);
636
637
638
639
640
641
			$attempts++;
		}
		return false;
	}

	/**
642
	 * creates a unique name for internal ownCloud use.
643
	 * @param string $name the display name of the object
644
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
645
	 * @return string|false with with the name to use in ownCloud or false if unsuccessful
646
	 */
647
648
649
650
651
652
653
654
655
656
657
	private function createAltInternalOwnCloudName($name, $isUser) {
		$originalTTL = $this->connection->ldapCacheTTL;
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
		if($isUser) {
			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
		} else {
			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
		}
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));

		return $altName;
658
659
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
660
661
662
663
664
665
666
667
668
669
	/**
	 * fetches a list of users according to a provided loginName and utilizing
	 * the login filter.
	 *
	 * @param string $loginName
	 * @param array $attributes optional, list of attributes to read
	 * @return array
	 */
	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
		$loginName = $this->escapeFilterPart($loginName);
670
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
Arthur Schiwon's avatar
Arthur Schiwon committed
671
672
673
674
		$users = $this->fetchListOfUsers($filter, $attributes);
		return $users;
	}

675
676
677
678
679
680
681
682
683
684
685
686
687
688
	/**
	 * counts the number of users according to a provided loginName and
	 * utilizing the login filter.
	 *
	 * @param string $loginName
	 * @return array
	 */
	public function countUsersByLoginName($loginName) {
		$loginName = $this->escapeFilterPart($loginName);
		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
		$users = $this->countUsers($filter);
		return $users;
	}

689
	/**
Robin McCorkell's avatar
Robin McCorkell committed
690
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
691
	 * @param string|string[] $attr
692
693
694
	 * @param int $limit
	 * @param int $offset
	 * @return array
695
	 */
696
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
697
698
699
700
701
702
703
704
705
706
707
708
709
		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
		$this->batchApplyUserAttributes($ldapRecords);
		return $this->fetchList($ldapRecords, (count($attr) > 1));
	}

	/**
	 * provided with an array of LDAP user records the method will fetch the
	 * user object and requests it to process the freshly fetched attributes and
	 * and their values
	 * @param array $ldapRecords
	 */
	public function batchApplyUserAttributes(array $ldapRecords){
		foreach($ldapRecords as $userRecord) {
710
			$ocName  = $this->dn2ocname($userRecord['dn'][0]);
711
712
			$this->cacheUserExists($ocName);
			$user = $this->userManager->get($ocName);
713
714
715
716
			if($user instanceof OfflineUser) {
				$user->unmark();
				$user = $this->userManager->get($ocName);
			}
717
718
			$user->processAttributes($userRecord);
		}
719
720
	}

721
	/**
Robin McCorkell's avatar
Robin McCorkell committed
722
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
723
	 * @param string|string[] $attr
724
725
726
	 * @param int $limit
	 * @param int $offset
	 * @return array
727
	 */
728
729
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
730
731
	}

732
	/**
Robin McCorkell's avatar
Robin McCorkell committed
733
	 * @param array $list
734
735
	 * @param bool $manyAttributes
	 * @return array
736
	 */
737
	private function fetchList($list, $manyAttributes) {
738
739
740
741
		if(is_array($list)) {
			if($manyAttributes) {
				return $list;
			} else {
742
				$list = array_reduce($list, function($carry, $item) {
743
744
745
					$attribute = array_keys($item)[0];
					$carry[] = $item[$attribute][0];
					return $carry;
746
				}, array());
747
748
749
750
751
752
753
754
				return array_unique($list, SORT_LOCALE_STRING);
			}
		}

		//error cause actually, maybe throw an exception in future.
		return array();
	}

755
	/**
756
	 * executes an LDAP search, optimized for Users
757
	 * @param string $filter the LDAP filter for the search
Robin McCorkell's avatar
Robin McCorkell committed
758
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
759
760
	 * @param integer $limit
	 * @param integer $offset
761
	 * @return array with the search result
762
763
764
	 *
	 * Executes an LDAP search
	 */
765
766
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
767
768
	}

769
770
	/**
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
771
	 * @param string|string[] $attr
772
773
774
	 * @param int $limit
	 * @param int $offset
	 * @return false|int
775
	 */
776
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
777
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
778
779
	}

780
	/**
781
	 * executes an LDAP search, optimized for Groups
782
	 * @param string $filter the LDAP filter for the search
Robin McCorkell's avatar
Robin McCorkell committed
783
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
784
785
	 * @param integer $limit
	 * @param integer $offset
786
	 * @return array with the search result
787
788
789
	 *
	 * Executes an LDAP search
	 */
790
791
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
792
793
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
794
	/**
795
796
797
798
799
800
801
802
803
804
805
	 * returns the number of available groups
	 * @param string $filter the LDAP search filter
	 * @param string[] $attr optional
	 * @param int|null $limit
	 * @param int|null $offset
	 * @return int|bool
	 */
	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
		return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
806
807
808
809
810
811
812
813
814
815
816
	/**
	 * returns the number of available objects on the base DN
	 *
	 * @param int|null $limit
	 * @param int|null $offset
	 * @return int|bool
	 */
	public function countObjects($limit = null, $offset = null) {
		return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset);
	}

817
	/**
818
	 * retrieved. Results will according to the order in the array.
819
820
821
	 * @param int $limit optional, maximum results to be counted
	 * @param int $offset optional, a starting point
	 * @return array|false array with the search result as first value and pagedSearchOK as
822
	 * second | false if not successful
Arthur Schiwon's avatar
Arthur Schiwon committed
823
	 */
824
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
825
		if(!is_null($attr) && !is_array($attr)) {
Arthur Schiwon's avatar
Arthur Schiwon committed
826
			$attr = array(mb_strtolower($attr, 'UTF-8'));
827
		}
828

829
		// See if we have a resource, in case not cancel with message
830
831
		$cr = $this->connection->getConnectionResource();
		if(!$this->ldap->isResource($cr)) {
832
833
			// Seems like we didn't find any resource.
			// Return an empty array just like before.
834
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
835
			return false;
836
		}
837

838
		//check whether paged search should be attempted
839
		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, intval($limit), $offset);
840

841
		$linkResources = array_pad(array(), count($base), $cr);
842
		$sr = $this->ldap->search($linkResources, $base, $filter, $attr);
843
		$error = $this->ldap->errno($cr);
844
		if(!is_array($sr) || $error !== 0) {
Bart Visscher's avatar
Bart Visscher committed
845
			\OCP\Util::writeLog('user_ldap',
846
847
				'Error when searching: '.$this->ldap->error($cr).
					' code '.$this->ldap->errno($cr),
Bart Visscher's avatar
Bart Visscher committed
848
				\OCP\Util::ERROR);
849
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
850
			return false;
851
		}
852

853
854
		return array($sr, $pagedSearchOK);
	}
855

856
	/**
857
	 * processes an LDAP paged search operation
858
859
860
861
862
863
864
865
	 * @param array $sr the array containing the LDAP search resources
	 * @param string $filter the LDAP filter for the search
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
	 * @param int $iFoundItems number of results in the search operation
	 * @param int $limit maximum results to be counted
	 * @param int $offset a starting point
	 * @param bool $pagedSearchOK whether a paged search has been executed
	 * @param bool $skipHandling required for paged search when cookies to
866
	 * prior results need to be gained
867
	 * @return array|false array with the search result as first value and pagedSearchOK as
868
869
870
	 * second | false if not successful
	 */
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
871
		if($pagedSearchOK) {
872
			$cr = $this->connection->getConnectionResource();
873
874
			foreach($sr as $key => $res) {
				$cookie = null;
875
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
876
877
878
879
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
				}
			}

880
881
882
883
			//browsing through prior pages to get the cookie for the new one
			if($skipHandling) {
				return;
			}
Bart Visscher's avatar
Bart Visscher committed
884
885
886
			// if count is bigger, then the server does not support
			// paged search. Instead, he did a normal search. We set a
			// flag here, so the callee knows how to deal with it.
887
			if($iFoundItems <= $limit) {
888
889
890
				$this->pagedSearchedSuccessful = true;
			}
		} else {
891
			if(!is_null($limit)) {
892
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
893
			}
894
		}
895
896
897
	}

	/**
898
	 * executes an LDAP search, but counts the results only
899
	 * @param string $filter the LDAP filter for the search
900
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
Robin McCorkell's avatar
Robin McCorkell committed
901
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
902
	 * retrieved. Results will according to the order in the array.
903
904
905
	 * @param int $limit optional, maximum results to be counted
	 * @param int $offset optional, a starting point
	 * @param bool $skipHandling indicates whether the pages search operation is
906
	 * completed
907
	 * @return int|false Integer or false if the search could not be initialized
908
909
910
911
	 *
	 */
	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), \OCP\Util::DEBUG);
912

913
914
915
		$limitPerPage = intval($this->connection->ldapPagingSize);
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
			$limitPerPage = $limit;
916
		}
Arthur Schiwon's avatar