access.php 45.8 KB
Newer Older
1
2
3
<?php

/**
4
 * ownCloud – LDAP Access
5
6
 *
 * @author Arthur Schiwon
Arthur Schiwon's avatar
Arthur Schiwon committed
7
 * @copyright 2012, 2013 Arthur Schiwon blizzz@owncloud.com
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

24
namespace OCA\user_ldap\lib;
25

26
27
use OCA\User_LDAP\Mapping\AbstractMapping;

28
29
30
31
/**
 * Class Access
 * @package OCA\user_ldap\lib
 */
32
class Access extends LDAPUtility implements user\IUserTools {
Arthur Schiwon's avatar
Arthur Schiwon committed
33
34
35
	/**
	 * @var \OCA\user_ldap\lib\Connection
	 */
36
	public $connection;
37
	public $userManager;
38
39
40
	//never ever check this var directly, always use getPagedSearchResultState
	protected $pagedSearchedSuccessful;

41
42
43
	/**
	 * @var string[] $cookies an array of returned Paged Result cookies
	 */
Arthur Schiwon's avatar
Arthur Schiwon committed
44
45
	protected $cookies = array();

46
47
48
49
50
	/**
	 * @var string $lastCookie the last cookie returned from a Paged Results
	 * operation, defaults to an empty string
	 */
	protected $lastCookie = '';
51

52
53
54
55
56
57
58
59
60
61
	/**
	 * @var AbstractMapping $userMapper
	 */
	protected $userMapper;

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

62
63
	public function __construct(Connection $connection, ILDAPWrapper $ldap,
		user\Manager $userManager) {
64
		parent::__construct($ldap);
65
		$this->connection = $connection;
66
67
		$this->userManager = $userManager;
		$this->userManager->setLdapAccess($this);
68
69
	}

70
71
72
73
74
75
76
77
	/**
	 * sets the User Mapper
	 * @param AbstractMapping $mapper
	 */
	public function setUserMapper(AbstractMapping $mapper) {
		$this->userMapper = $mapper;
	}

78
79
80
81
82
83
84
85
86
87
88
89
	/**
	 * 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;
	}

90
91
92
93
94
95
96
97
	/**
	 * sets the Group Mapper
	 * @param AbstractMapping $mapper
	 */
	public function setGroupMapper(AbstractMapping $mapper) {
		$this->groupMapper = $mapper;
	}

98
99
100
101
102
103
104
105
106
107
108
109
	/**
	 * 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;
	}

110
111
112
	/**
	 * @return bool
	 */
113
114
	private function checkConnection() {
		return ($this->connection instanceof Connection);
Arthur Schiwon's avatar
Arthur Schiwon committed
115
	}
Arthur Schiwon's avatar
Arthur Schiwon committed
116

117
	/**
118
	 * returns the Connection instance
119
120
121
122
123
124
125
	 * @return \OCA\user_ldap\lib\Connection
	 */
	public function getConnection() {
		return $this->connection;
	}

	/**
126
	 * reads a given attribute for an LDAP record identified by a DN
Arthur Schiwon's avatar
Arthur Schiwon committed
127
128
	 * @param string $dn the record in question
	 * @param string $attr the attribute that shall be retrieved
129
	 *        if empty, just check the record's existence
130
131
	 * @param string $filter
	 * @return array|false an array of values on success or an empty
132
	 *          array if $attr is empty, false otherwise
133
	 */
134
	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
135
		if(!$this->checkConnection()) {
Bart Visscher's avatar
Bart Visscher committed
136
137
138
			\OCP\Util::writeLog('user_ldap',
				'No LDAP Connector assigned, access impossible for readAttribute.',
				\OCP\Util::WARN);
139
			return false;
Arthur Schiwon's avatar
Arthur Schiwon committed
140
		}
141
		$cr = $this->connection->getConnectionResource();
142
		if(!$this->ldap->isResource($cr)) {
143
			//LDAP not available
144
			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', \OCP\Util::DEBUG);
145
146
			return false;
		}
147
148
149
		//Cancel possibly running Paged Results operation, otherwise we run in
		//LDAP protocol errors
		$this->abandonPagedSearch();
150
151
		// 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
152
153
154
155
156
		$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.
		$maxResults = $pagingSize < 20 ? $pagingSize : 500;
		$this->initPagedSearch($filter, array($dn), array($attr), $maxResults, 0);
Arthur Schiwon's avatar
Arthur Schiwon committed
157
		$dn = $this->DNasBaseParameter($dn);
158
		$rr = @$this->ldap->read($cr, $dn, $filter, array($attr));
159
		if(!$this->ldap->isResource($rr)) {
160
161
162
163
			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);
			}
164
165
166
			//in case an error occurs , e.g. object does not exist
			return false;
		}
167
168
		if (empty($attr)) {
			\OCP\Util::writeLog('user_ldap', 'readAttribute: '.$dn.' found', \OCP\Util::DEBUG);
169
			return array();
170
		}
171
		$er = $this->ldap->firstEntry($cr, $rr);
172
		if(!$this->ldap->isResource($er)) {
173
174
175
			//did not match the filter, return false
			return false;
		}
176
		//LDAP attributes are not case sensitive
Arthur Schiwon's avatar
Arthur Schiwon committed
177
178
		$result = \OCP\Util::mb_array_change_key_case(
				$this->ldap->getAttributes($cr, $er), MB_CASE_LOWER, 'UTF-8');
179
		$attr = mb_strtolower($attr, 'UTF-8');
180

Arthur Schiwon's avatar
Arthur Schiwon committed
181
		if(isset($result[$attr]) && $result[$attr]['count'] > 0) {
182
183
			$values = array();
			for($i=0;$i<$result[$attr]['count'];$i++) {
184
185
				if($this->resemblesDN($attr)) {
					$values[] = $this->sanitizeDN($result[$attr][$i]);
186
				} elseif(strtolower($attr) === 'objectguid' || strtolower($attr) === 'guid') {
187
188
189
190
					$values[] = $this->convertObjectGUID2Str($result[$attr][$i]);
				} else {
					$values[] = $result[$attr][$i];
				}
191
192
193
			}
			return $values;
		}
194
		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, \OCP\Util::DEBUG);
195
		return false;
Arthur Schiwon's avatar
Arthur Schiwon committed
196
197
	}

198
	/**
199
	 * checks whether the given attributes value is probably a DN
200
201
	 * @param string $attr the attribute in question
	 * @return boolean if so true, otherwise false
202
	 */
203
204
205
206
207
	private function resemblesDN($attr) {
		$resemblingAttributes = array(
			'dn',
			'uniquemember',
			'member'
Arthur Schiwon's avatar
Arthur Schiwon committed
208
		);
209
210
		return in_array($attr, $resemblingAttributes);
	}
211

212
213
214
215
216
217
218
219
220
221
222
223
	/**
	 * 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);
	}

224
	/**
225
	 * sanitizes a DN received from the LDAP server
226
227
	 * @param array $dn the DN in question
	 * @return array the sanitized DN
228
229
	 */
	private function sanitizeDN($dn) {
Arthur Schiwon's avatar
Arthur Schiwon committed
230
231
232
233
		//treating multiple base DNs
		if(is_array($dn)) {
			$result = array();
			foreach($dn as $singleDN) {
Robin McCorkell's avatar
Robin McCorkell committed
234
				$result[] = $this->sanitizeDN($singleDN);
Arthur Schiwon's avatar
Arthur Schiwon committed
235
236
237
238
			}
			return $result;
		}

Bart Visscher's avatar
Bart Visscher committed
239
240
		//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
241
		$dn = preg_replace('/([^\\\]),(\s+)/u', '\1,', $dn);
242

243
244
245
		//make comparisons and everything work
		$dn = mb_strtolower($dn, 'UTF-8');

Arthur Schiwon's avatar
Arthur Schiwon committed
246
247
248
		//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()
249
250
251
252
253
254
255
256
257
		$replacements = array(
			'\,' => '\5c2C',
			'\=' => '\5c3D',
			'\+' => '\5c2B',
			'\<' => '\5c3C',
			'\>' => '\5c3E',
			'\;' => '\5c3B',
			'\"' => '\5c22',
			'\#' => '\5c23',
258
259
260
			'('  => '\28',
			')'  => '\29',
			'*'  => '\2A',
261
		);
262
		$dn = str_replace(array_keys($replacements), array_values($replacements), $dn);
263

264
		return $dn;
265
266
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
	/**
	 * 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;
	}

294
	/**
295
	 * returns the LDAP DN for the given internal ownCloud name of the group
296
	 * @param string $name the ownCloud name in question
297
	 * @return string|false LDAP DN on success, otherwise false
298
	 */
299
	public function groupname2dn($name) {
300
		return $this->groupMapper->getDNbyName($name);
301
302
303
	}

	/**
304
	 * returns the LDAP DN for the given internal ownCloud name of the user
305
306
	 * @param string $name the ownCloud name in question
	 * @return string with the LDAP DN on success, otherwise false
307
	 */
308
	public function username2dn($name) {
309
310
		$fdn = $this->userMapper->getDNbyName($name);

311
312
		//Check whether the DN belongs to the Base, to avoid issues on multi-
		//server setups
313
314
		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
			return $fdn;
315
316
317
318
319
320
		}

		return false;
	}

	/**
321
	public function ocname2dn($name, $isUser) {
322
	 * returns the internal ownCloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
323
	 * @param string $fdn the dn of the group object
324
325
	 * @param string $ldapName optional, the display name of the object
	 * @return string with the name to use in ownCloud, false on DN outside of search DN
326
	 */
327
	public function dn2groupname($fdn, $ldapName = null) {
Arthur Schiwon's avatar
Arthur Schiwon committed
328
329
330
		//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
331
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
332
333
			return false;
		}
Arthur Schiwon's avatar
Arthur Schiwon committed
334

335
		return $this->dn2ocname($fdn, $ldapName, false);
336
337
338
	}

	/**
339
	 * returns the internal ownCloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
340
341
342
	 * @param string $dn the dn of the user object
	 * @param string $ldapName optional, the display name of the object
	 * @return string with with the name to use in ownCloud
343
	 */
344
	public function dn2username($fdn, $ldapName = null) {
Arthur Schiwon's avatar
Arthur Schiwon committed
345
346
347
		//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
348
		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
349
350
			return false;
		}
Arthur Schiwon's avatar
Arthur Schiwon committed
351

352
		return $this->dn2ocname($fdn, $ldapName, true);
353
354
	}

355
	/**
356
	 * returns an internal ownCloud name for the given LDAP DN, false on DN outside of search DN
357
358
359
360
	 * @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)
	 * @return string with with the name to use in ownCloud
361
	 */
362
	public function dn2ocname($fdn, $ldapName = null, $isUser = true) {
363
		if($isUser) {
364
			$mapper = $this->getUserMapper();
365
			$nameAttribute = $this->connection->ldapUserDisplayName;
366
		} else {
367
			$mapper = $this->getGroupMapper();
368
			$nameAttribute = $this->connection->ldapGroupDisplayName;
369
370
		}

371
		//let's try to retrieve the ownCloud name from the mappings table
372
373
		$ocName = $mapper->getNameByDN($fdn);
		if(is_string($ocName)) {
374
			return $ocName;
375
376
		}

377
		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
378
379
380
381
382
383
		$uuid = $this->getUUID($fdn, $isUser);
		if(is_string($uuid)) {
			$ocName = $mapper->getNameByUUID($uuid);
			if(is_string($ocName)) {
				$mapper->setDNbyUUID($fdn, $uuid);
				return $ocName;
384
			}
385
386
		} else {
			//If the UUID can't be detected something is foul.
387
			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', \OCP\Util::INFO);
388
			return false;
389
390
		}

391
		if(is_null($ldapName)) {
392
			$ldapName = $this->readAttribute($fdn, $nameAttribute);
393
			if(!isset($ldapName[0]) && empty($ldapName[0])) {
394
				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.'.', \OCP\Util::INFO);
395
396
				return false;
			}
397
			$ldapName = $ldapName[0];
398
		}
399
400
401

		if($isUser) {
			$usernameAttribute = $this->connection->ldapExpertUsernameAttr;
402
403
			if(!empty($usernameAttribute)) {
				$username = $this->readAttribute($fdn, $usernameAttribute);
404
405
406
407
				$username = $username[0];
			} else {
				$username = $uuid;
			}
408
			$intName = $this->sanitizeUsername($username);
409
		} else {
410
			$intName = $ldapName;
411
		}
412

413
		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
414
		//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
415
416
		//NOTE: mind, disabling cache affects only this instance! Using it
		// outside of core user management will still cache the user as non-existing.
417
418
		$originalTTL = $this->connection->ldapCacheTTL;
		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
419
420
		if(($isUser && !\OCP\User::userExists($intName))
			|| (!$isUser && !\OC_Group::groupExists($intName))) {
421
			if($mapper->map($fdn, $intName, $uuid)) {
422
				$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
423
				return $intName;
424
			}
425
		}
426
		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
427

428
		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
429
		if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) {
430
			return $altName;
431
432
		}

433
		//if everything else did not help..
434
		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', \OCP\Util::INFO);
435
		return false;
436
437
438
	}

	/**
439
	 * gives back the user names as they are used ownClod internally
440
441
	 * @param array $ldapUsers an array with the ldap Users result in style of array ( array ('dn' => foo, 'uid' => bar), ... )
	 * @return array an array with the user names to use in ownCloud
442
443
444
	 *
	 * gives back the user names as they are used ownClod internally
	 */
445
446
	public function ownCloudUserNames($ldapUsers) {
		return $this->ldap2ownCloudNames($ldapUsers, true);
447
448
449
	}

	/**
450
	 * gives back the group names as they are used ownClod internally
451
452
	 * @param array $ldapGroups an array with the ldap Groups result in style of array ( array ('dn' => foo, 'cn' => bar), ... )
	 * @return array an array with the group names to use in ownCloud
453
454
455
	 *
	 * gives back the group names as they are used ownClod internally
	 */
456
457
	public function ownCloudGroupNames($ldapGroups) {
		return $this->ldap2ownCloudNames($ldapGroups, false);
458
459
	}

460
	/**
Lukas Reschke's avatar
Lukas Reschke committed
461
	 * @param array $ldapObjects
462
463
	 * @param bool $isUsers
	 * @return array
464
	 */
465
	private function ldap2ownCloudNames($ldapObjects, $isUsers) {
466
		if($isUsers) {
467
			$nameAttribute = $this->connection->ldapUserDisplayName;
468
		} else {
469
			$nameAttribute = $this->connection->ldapGroupDisplayName;
470
471
472
473
		}
		$ownCloudNames = array();

		foreach($ldapObjects as $ldapObject) {
474
			$nameByLDAP = isset($ldapObject[$nameAttribute]) ? $ldapObject[$nameAttribute] : null;
475
476
477
			$ocName = $this->dn2ocname($ldapObject['dn'], $nameByLDAP, $isUsers);
			if($ocName) {
				$ownCloudNames[] = $ocName;
478
479
480
				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
481
					$this->cacheUserExists($ocName);
482
					$this->cacheUserDisplayName($ocName, $nameByLDAP);
483
				}
484
			}
485
			continue;
486
487
488
489
		}
		return $ownCloudNames;
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
490
491
492
493
494
495
496
497
	/**
	 * caches a user as existing
	 * @param string $ocName the internal ownCloud username
	 */
	public function cacheUserExists($ocName) {
		$this->connection->writeToCache('userExists'.$ocName, true);
	}

498
	/**
499
	 * caches the user display name
500
501
	 * @param string $ocName the internal ownCloud username
	 * @param string $displayName the display name
502
	 */
503
	public function cacheUserDisplayName($ocName, $displayName) {
504
		$cacheKeyTrunk = 'getDisplayName';
505
		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
506
507
	}

508
	/**
509
	 * creates a unique name for internal ownCloud use for users. Don't call it directly.
510
511
	 * @param string $name the display name of the object
	 * @return string with with the name to use in ownCloud or false if unsuccessful
512
513
514
515
516
517
518
519
520
	 *
	 * 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){
521
522
			$altName = $name . '_' . rand(1000,9999);
			if(!\OCP\User::userExists($altName)) {
523
524
525
526
527
528
529
530
				return $altName;
			}
			$attempts++;
		}
		return false;
	}

	/**
531
	 * creates a unique name for internal ownCloud use for groups. Don't call it directly.
532
533
	 * @param string $name the display name of the object
	 * @return string with with the name to use in ownCloud or false if unsuccessful.
534
535
536
	 *
	 * Instead of using this method directly, call
	 * createAltInternalOwnCloudName($name, false)
537
	 *
538
539
540
541
542
	 * 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) {
543
		$usedNames = $this->groupMapper->getNamesBySearch($name.'_%');
544
		if(!($usedNames) || count($usedNames) === 0) {
545
546
547
			$lastNo = 1; //will become name_2
		} else {
			natsort($usedNames);
548
549
			$lastName = array_pop($usedNames);
			$lastNo = intval(substr($lastName, strrpos($lastName, '_') + 1));
550
551
552
553
554
555
		}
		$altName = $name.'_'.strval($lastNo+1);
		unset($usedNames);

		$attempts = 1;
		while($attempts < 21){
556
557
558
			// 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.
559
560
561
			if(!\OC_Group::groupExists($altName)) {
				return $altName;
			}
Arthur Schiwon's avatar
Arthur Schiwon committed
562
			$altName = $name . '_' . ($lastNo + $attempts);
563
564
565
566
567
568
			$attempts++;
		}
		return false;
	}

	/**
569
	 * creates a unique name for internal ownCloud use.
570
	 * @param string $name the display name of the object
571
	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
572
	 * @return string with with the name to use in ownCloud or false if unsuccessful
573
	 */
574
575
576
577
578
579
580
581
582
583
584
	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;
585
586
	}

587
	/**
Robin McCorkell's avatar
Robin McCorkell committed
588
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
589
	 * @param string|string[] $attr
590
591
592
	 * @param int $limit
	 * @param int $offset
	 * @return array
593
	 */
594
595
	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null) {
		return $this->fetchList($this->searchUsers($filter, $attr, $limit, $offset), (count($attr) > 1));
596
597
	}

598
	/**
Robin McCorkell's avatar
Robin McCorkell committed
599
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
600
	 * @param string|string[] $attr
601
602
603
	 * @param int $limit
	 * @param int $offset
	 * @return array
604
	 */
605
606
	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), (count($attr) > 1));
607
608
	}

609
	/**
Robin McCorkell's avatar
Robin McCorkell committed
610
	 * @param array $list
611
612
	 * @param bool $manyAttributes
	 * @return array
613
	 */
614
	private function fetchList($list, $manyAttributes) {
615
616
617
618
619
620
621
622
623
624
625
626
		if(is_array($list)) {
			if($manyAttributes) {
				return $list;
			} else {
				return array_unique($list, SORT_LOCALE_STRING);
			}
		}

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

627
	/**
628
	 * executes an LDAP search, optimized for Users
629
	 * @param string $filter the LDAP filter for the search
Robin McCorkell's avatar
Robin McCorkell committed
630
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
631
632
	 * @param integer $limit
	 * @param integer $offset
633
	 * @return array with the search result
634
635
636
	 *
	 * Executes an LDAP search
	 */
637
638
	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
		return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
639
640
	}

641
642
	/**
	 * @param string $filter
Robin McCorkell's avatar
Robin McCorkell committed
643
	 * @param string|string[] $attr
644
645
646
	 * @param int $limit
	 * @param int $offset
	 * @return false|int
647
	 */
648
	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
649
		return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset);
650
651
	}

652
	/**
653
	 * executes an LDAP search, optimized for Groups
654
	 * @param string $filter the LDAP filter for the search
Robin McCorkell's avatar
Robin McCorkell committed
655
	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
656
657
	 * @param integer $limit
	 * @param integer $offset
658
	 * @return array with the search result
659
660
661
	 *
	 * Executes an LDAP search
	 */
662
663
	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
		return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset);
664
665
	}

Arthur Schiwon's avatar
Arthur Schiwon committed
666
	/**
667
668
669
670
671
672
673
674
675
676
677
678
	 * 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);
	}

	/**
679
	 * retrieved. Results will according to the order in the array.
680
681
682
	 * @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
683
	 * second | false if not successful
Arthur Schiwon's avatar
Arthur Schiwon committed
684
	 */
685
	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
686
		if(!is_null($attr) && !is_array($attr)) {
Arthur Schiwon's avatar
Arthur Schiwon committed
687
			$attr = array(mb_strtolower($attr, 'UTF-8'));
688
		}
689

690
		// See if we have a resource, in case not cancel with message
691
692
		$cr = $this->connection->getConnectionResource();
		if(!$this->ldap->isResource($cr)) {
693
694
			// Seems like we didn't find any resource.
			// Return an empty array just like before.
695
			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', \OCP\Util::DEBUG);
696
			return false;
697
		}
698

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

702
		$linkResources = array_pad(array(), count($base), $cr);
703
		$sr = $this->ldap->search($linkResources, $base, $filter, $attr);
704
		$error = $this->ldap->errno($cr);
705
		if(!is_array($sr) || $error !== 0) {
Bart Visscher's avatar
Bart Visscher committed
706
			\OCP\Util::writeLog('user_ldap',
707
708
				'Error when searching: '.$this->ldap->error($cr).
					' code '.$this->ldap->errno($cr),
Bart Visscher's avatar
Bart Visscher committed
709
				\OCP\Util::ERROR);
710
			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), \OCP\Util::ERROR);
711
			return false;
712
		}
713

714
715
		return array($sr, $pagedSearchOK);
	}
716

717
	/**
718
	 * processes an LDAP paged search operation
719
720
721
722
723
724
725
726
	 * @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
727
	 * prior results need to be gained
728
	 * @return array|false array with the search result as first value and pagedSearchOK as
729
730
731
	 * second | false if not successful
	 */
	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
732
		if($pagedSearchOK) {
733
			$cr = $this->connection->getConnectionResource();
734
735
			foreach($sr as $key => $res) {
				$cookie = null;
736
				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
737
738
739
740
					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
				}
			}

741
742
743
744
			//browsing through prior pages to get the cookie for the new one
			if($skipHandling) {
				return;
			}
Bart Visscher's avatar
Bart Visscher committed
745
746
747
			// 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.
748
			if($iFoundItems <= $limit) {
749
750
751
				$this->pagedSearchedSuccessful = true;
			}
		} else {
752
			if(!is_null($limit)) {
753
				\OCP\Util::writeLog('user_ldap', 'Paged search was not available', \OCP\Util::INFO);
754
			}
755
		}
756
757
758
	}

	/**
759
	 * executes an LDAP search, but counts the results only
760
	 * @param string $filter the LDAP filter for the search
761
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
Robin McCorkell's avatar
Robin McCorkell committed
762
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
763
	 * retrieved. Results will according to the order in the array.
764
765
766
	 * @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
767
	 * completed
768
	 * @return int|false Integer or false if the search could not be initialized
769
770
771
772
	 *
	 */
	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);
773

774
775
776
		$limitPerPage = intval($this->connection->ldapPagingSize);
		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
			$limitPerPage = $limit;
777
		}
778

779
		$counter = 0;
Arthur Schiwon's avatar
Arthur Schiwon committed
780
		$count = null;
781
		$this->connection->getConnectionResource();
782
783

		do {
784
			$continue = false;
785
			$search = $this->executeSearch($filter, $base, $attr,
786
										   $limitPerPage, $offset);
787
788
789
790
791
			if($search === false) {
				return $counter > 0 ? $counter : false;
			}
			list($sr, $pagedSearchOK) = $search;

792
			$count = $this->countEntriesInSearchResults($sr, $limitPerPage, $continue);
793
			$counter += $count;
794

795
			$this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
796
										$offset, $pagedSearchOK, $skipHandling);
797
798
			$offset += $limitPerPage;
		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
799
800
801
802

		return $counter;
	}

803
	/**
Robin McCorkell's avatar
Robin McCorkell committed
804
805
806
	 * @param array $searchResults
	 * @param int $limit
	 * @param bool $hasHitLimit
807
808
809
	 * @return int
	 */
	private function countEntriesInSearchResults($searchResults, $limit, &$hasHitLimit) {
810
		$cr = $this->connection->getConnectionResource();
811
		$counter = 0;
812
813
814
815

		foreach($searchResults as $res) {
			$count = intval($this->ldap->countEntries($cr, $res));
			$counter += $count;
816
			if($count > 0 && $count === $limit) {
817
818
819
820
821
822
823
				$hasHitLimit = true;
			}
		}

		return $counter;
	}

824
	/**
825
	 * Executes an LDAP search
826
827
	 * @param string $filter the LDAP filter for the search
	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
Robin McCorkell's avatar
Robin McCorkell committed
828
	 * @param string|string[] $attr optional, array, one or more attributes that shall be
829
830
831
832
	 * @param int $limit
	 * @param int $offset
	 * @param bool $skipHandling
	 * @return array with the search result
833
834
	 */
	private function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
Arthur Schiwon's avatar
Arthur Schiwon committed
835
836
837
838
		if($limit <= 0) {
			//otherwise search will fail
			$limit = null;
		}
839
840
841
842
843
844
845
846
847
848
849
850
851
852
		$search = $this->executeSearch($filter, $base, $attr, $limit, $offset);
		if($search === false) {
			return array();
		}
		list($sr, $pagedSearchOK) = $search;
		$cr = $this->connection->getConnectionResource();

		if($skipHandling) {
			//i.e. result do not need to be fetched, we just need the cookie
			//thus pass 1 or any other value as $iFoundItems because it is not
			//used
			$this->processPagedSearchStatus($sr, $filter, $base, 1, $limit,
											$offset, $pagedSearchOK,
											$skipHandling);
Arthur Schiwon's avatar
Arthur Schiwon committed
853
			return array();
854
855
856
857
858
859
860
861
862
863
		}

		// Do the server-side sorting
		foreach(array_reverse($attr) as $sortAttr){
			foreach($sr as $searchResource) {
				$this->ldap->sort($cr, $searchResource, $sortAttr);
			}
		}

		$findings = array();
864
		foreach($sr as $res) {
Robin McCorkell's avatar
Robin McCorkell committed
865
			$findings = array_merge($findings, $this->ldap->getEntries($cr	, $res ));
866
867
868
869
870
		}

		$this->processPagedSearchStatus($sr, $filter, $base, $findings['count'],
										$limit, $offset, $pagedSearchOK,
										$skipHandling);
871
872
873
874
875
876
877

		// if we're here, probably no connection resource is returned.
		// to make ownCloud behave nicely, we simply give back an empty array.
		if(is_null($findings)) {
			return array();
		}

878
879
		if(!is_null($attr)) {
			$selection = array();
880
			$multiArray = false;
881
			if(count($attr) > 1) {
882
				$multiArray = true;
883
884
				$i = 0;
			}
885
			foreach($findings as $item) {
886
887
888
				if(!is_array($item)) {
					continue;
				}
889
				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
890

891
				if($multiArray) {
892
					foreach($attr as $key) {
Arthur Schiwon's avatar
Arthur Schiwon committed
893
						$key = mb_strtolower($key, 'UTF-8');
894
						if(isset($item[$key])) {
895
							if($key !== 'dn') {
Bart Visscher's avatar
Bart Visscher committed
896
897
898
								$selection[$i][$key] = $this->resemblesDN($key) ?
									$this->sanitizeDN($item[$key][0])
									: