Completed
Push — master ( 39468f...49d871 )
by Björn
14:59
created
apps/user_ldap/lib/User_LDAP.php 1 patch
Indentation   +485 added lines, -485 removed lines patch added patch discarded remove patch
@@ -44,492 +44,492 @@
 block discarded – undo
44 44
 use OCP\Util;
45 45
 
46 46
 class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserInterface, IUserLDAP {
47
-	/** @var string[] $homesToKill */
48
-	protected $homesToKill = array();
49
-
50
-	/** @var \OCP\IConfig */
51
-	protected $ocConfig;
52
-
53
-	/**
54
-	 * @param Access $access
55
-	 * @param \OCP\IConfig $ocConfig
56
-	 */
57
-	public function __construct(Access $access, IConfig $ocConfig) {
58
-		parent::__construct($access);
59
-		$this->ocConfig = $ocConfig;
60
-	}
61
-
62
-	/**
63
-	 * checks whether the user is allowed to change his avatar in Nextcloud
64
-	 * @param string $uid the Nextcloud user name
65
-	 * @return boolean either the user can or cannot
66
-	 */
67
-	public function canChangeAvatar($uid) {
68
-		$user = $this->access->userManager->get($uid);
69
-		if(!$user instanceof User) {
70
-			return false;
71
-		}
72
-		if($user->getAvatarImage() === false) {
73
-			return true;
74
-		}
75
-
76
-		return false;
77
-	}
78
-
79
-	/**
80
-	 * returns the username for the given login name, if available
81
-	 *
82
-	 * @param string $loginName
83
-	 * @return string|false
84
-	 */
85
-	public function loginName2UserName($loginName) {
86
-		$cacheKey = 'loginName2UserName-'.$loginName;
87
-		$username = $this->access->connection->getFromCache($cacheKey);
88
-		if(!is_null($username)) {
89
-			return $username;
90
-		}
91
-
92
-		try {
93
-			$ldapRecord = $this->getLDAPUserByLoginName($loginName);
94
-			$user = $this->access->userManager->get($ldapRecord['dn'][0]);
95
-			if($user instanceof OfflineUser) {
96
-				// this path is not really possible, however get() is documented
97
-				// to return User or OfflineUser so we are very defensive here.
98
-				$this->access->connection->writeToCache($cacheKey, false);
99
-				return false;
100
-			}
101
-			$username = $user->getUsername();
102
-			$this->access->connection->writeToCache($cacheKey, $username);
103
-			return $username;
104
-		} catch (NotOnLDAP $e) {
105
-			$this->access->connection->writeToCache($cacheKey, false);
106
-			return false;
107
-		}
108
-	}
47
+    /** @var string[] $homesToKill */
48
+    protected $homesToKill = array();
49
+
50
+    /** @var \OCP\IConfig */
51
+    protected $ocConfig;
52
+
53
+    /**
54
+     * @param Access $access
55
+     * @param \OCP\IConfig $ocConfig
56
+     */
57
+    public function __construct(Access $access, IConfig $ocConfig) {
58
+        parent::__construct($access);
59
+        $this->ocConfig = $ocConfig;
60
+    }
61
+
62
+    /**
63
+     * checks whether the user is allowed to change his avatar in Nextcloud
64
+     * @param string $uid the Nextcloud user name
65
+     * @return boolean either the user can or cannot
66
+     */
67
+    public function canChangeAvatar($uid) {
68
+        $user = $this->access->userManager->get($uid);
69
+        if(!$user instanceof User) {
70
+            return false;
71
+        }
72
+        if($user->getAvatarImage() === false) {
73
+            return true;
74
+        }
75
+
76
+        return false;
77
+    }
78
+
79
+    /**
80
+     * returns the username for the given login name, if available
81
+     *
82
+     * @param string $loginName
83
+     * @return string|false
84
+     */
85
+    public function loginName2UserName($loginName) {
86
+        $cacheKey = 'loginName2UserName-'.$loginName;
87
+        $username = $this->access->connection->getFromCache($cacheKey);
88
+        if(!is_null($username)) {
89
+            return $username;
90
+        }
91
+
92
+        try {
93
+            $ldapRecord = $this->getLDAPUserByLoginName($loginName);
94
+            $user = $this->access->userManager->get($ldapRecord['dn'][0]);
95
+            if($user instanceof OfflineUser) {
96
+                // this path is not really possible, however get() is documented
97
+                // to return User or OfflineUser so we are very defensive here.
98
+                $this->access->connection->writeToCache($cacheKey, false);
99
+                return false;
100
+            }
101
+            $username = $user->getUsername();
102
+            $this->access->connection->writeToCache($cacheKey, $username);
103
+            return $username;
104
+        } catch (NotOnLDAP $e) {
105
+            $this->access->connection->writeToCache($cacheKey, false);
106
+            return false;
107
+        }
108
+    }
109 109
 	
110
-	/**
111
-	 * returns the username for the given LDAP DN, if available
112
-	 *
113
-	 * @param string $dn
114
-	 * @return string|false with the username
115
-	 */
116
-	public function dn2UserName($dn) {
117
-		return $this->access->dn2username($dn);
118
-	}
119
-
120
-	/**
121
-	 * returns an LDAP record based on a given login name
122
-	 *
123
-	 * @param string $loginName
124
-	 * @return array
125
-	 * @throws NotOnLDAP
126
-	 */
127
-	public function getLDAPUserByLoginName($loginName) {
128
-		//find out dn of the user name
129
-		$attrs = $this->access->userManager->getAttributes();
130
-		$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
131
-		if(count($users) < 1) {
132
-			throw new NotOnLDAP('No user available for the given login name on ' .
133
-				$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
134
-		}
135
-		return $users[0];
136
-	}
137
-
138
-	/**
139
-	 * Check if the password is correct without logging in the user
140
-	 *
141
-	 * @param string $uid The username
142
-	 * @param string $password The password
143
-	 * @return false|string
144
-	 */
145
-	public function checkPassword($uid, $password) {
146
-		try {
147
-			$ldapRecord = $this->getLDAPUserByLoginName($uid);
148
-		} catch(NotOnLDAP $e) {
149
-			if($this->ocConfig->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
150
-				\OC::$server->getLogger()->logException($e, ['app' => 'user_ldap']);
151
-			}
152
-			return false;
153
-		}
154
-		$dn = $ldapRecord['dn'][0];
155
-		$user = $this->access->userManager->get($dn);
156
-
157
-		if(!$user instanceof User) {
158
-			Util::writeLog('user_ldap',
159
-				'LDAP Login: Could not get user object for DN ' . $dn .
160
-				'. Maybe the LDAP entry has no set display name attribute?',
161
-				Util::WARN);
162
-			return false;
163
-		}
164
-		if($user->getUsername() !== false) {
165
-			//are the credentials OK?
166
-			if(!$this->access->areCredentialsValid($dn, $password)) {
167
-				return false;
168
-			}
169
-
170
-			$this->access->cacheUserExists($user->getUsername());
171
-			$user->processAttributes($ldapRecord);
172
-			$user->markLogin();
173
-
174
-			return $user->getUsername();
175
-		}
176
-
177
-		return false;
178
-	}
179
-
180
-	/**
181
-	 * Set password
182
-	 * @param string $uid The username
183
-	 * @param string $password The new password
184
-	 * @return bool
185
-	 */
186
-	public function setPassword($uid, $password) {
187
-		$user = $this->access->userManager->get($uid);
188
-
189
-		if(!$user instanceof User) {
190
-			throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
191
-				'. Maybe the LDAP entry has no set display name attribute?');
192
-		}
193
-		if($user->getUsername() !== false) {
194
-			return $this->access->setPassword($user->getDN(), $password);
195
-		}
196
-
197
-		return false;
198
-	}
199
-
200
-	/**
201
-	 * Get a list of all users
202
-	 *
203
-	 * @param string $search
204
-	 * @param integer $limit
205
-	 * @param integer $offset
206
-	 * @return string[] an array of all uids
207
-	 */
208
-	public function getUsers($search = '', $limit = 10, $offset = 0) {
209
-		$search = $this->access->escapeFilterPart($search, true);
210
-		$cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
211
-
212
-		//check if users are cached, if so return
213
-		$ldap_users = $this->access->connection->getFromCache($cachekey);
214
-		if(!is_null($ldap_users)) {
215
-			return $ldap_users;
216
-		}
217
-
218
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
219
-		// error. With a limit of 0, we get 0 results. So we pass null.
220
-		if($limit <= 0) {
221
-			$limit = null;
222
-		}
223
-		$filter = $this->access->combineFilterWithAnd(array(
224
-			$this->access->connection->ldapUserFilter,
225
-			$this->access->connection->ldapUserDisplayName . '=*',
226
-			$this->access->getFilterPartForUserSearch($search)
227
-		));
228
-
229
-		Util::writeLog('user_ldap',
230
-			'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
231
-			Util::DEBUG);
232
-		//do the search and translate results to owncloud names
233
-		$ldap_users = $this->access->fetchListOfUsers(
234
-			$filter,
235
-			$this->access->userManager->getAttributes(true),
236
-			$limit, $offset);
237
-		$ldap_users = $this->access->ownCloudUserNames($ldap_users);
238
-		Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', Util::DEBUG);
239
-
240
-		$this->access->connection->writeToCache($cachekey, $ldap_users);
241
-		return $ldap_users;
242
-	}
243
-
244
-	/**
245
-	 * checks whether a user is still available on LDAP
246
-	 *
247
-	 * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
248
-	 * name or an instance of that user
249
-	 * @return bool
250
-	 * @throws \Exception
251
-	 * @throws \OC\ServerNotAvailableException
252
-	 */
253
-	public function userExistsOnLDAP($user) {
254
-		if(is_string($user)) {
255
-			$user = $this->access->userManager->get($user);
256
-		}
257
-		if(is_null($user)) {
258
-			return false;
259
-		}
260
-
261
-		$dn = $user->getDN();
262
-		//check if user really still exists by reading its entry
263
-		if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
264
-			$lcr = $this->access->connection->getConnectionResource();
265
-			if(is_null($lcr)) {
266
-				throw new \Exception('No LDAP Connection to server ' . $this->access->connection->ldapHost);
267
-			}
268
-
269
-			try {
270
-				$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
271
-				if(!$uuid) {
272
-					return false;
273
-				}
274
-				$newDn = $this->access->getUserDnByUuid($uuid);
275
-				//check if renamed user is still valid by reapplying the ldap filter
276
-				if(!is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
277
-					return false;
278
-				}
279
-				$this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
280
-				return true;
281
-			} catch (\Exception $e) {
282
-				return false;
283
-			}
284
-		}
285
-
286
-		if($user instanceof OfflineUser) {
287
-			$user->unmark();
288
-		}
289
-
290
-		return true;
291
-	}
292
-
293
-	/**
294
-	 * check if a user exists
295
-	 * @param string $uid the username
296
-	 * @return boolean
297
-	 * @throws \Exception when connection could not be established
298
-	 */
299
-	public function userExists($uid) {
300
-		$userExists = $this->access->connection->getFromCache('userExists'.$uid);
301
-		if(!is_null($userExists)) {
302
-			return (bool)$userExists;
303
-		}
304
-		//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
305
-		$user = $this->access->userManager->get($uid);
306
-
307
-		if(is_null($user)) {
308
-			Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
309
-				$this->access->connection->ldapHost, Util::DEBUG);
310
-			$this->access->connection->writeToCache('userExists'.$uid, false);
311
-			return false;
312
-		} else if($user instanceof OfflineUser) {
313
-			//express check for users marked as deleted. Returning true is
314
-			//necessary for cleanup
315
-			return true;
316
-		}
317
-
318
-		$result = $this->userExistsOnLDAP($user);
319
-		$this->access->connection->writeToCache('userExists'.$uid, $result);
320
-		if($result === true) {
321
-			$user->update();
322
-		}
323
-		return $result;
324
-	}
325
-
326
-	/**
327
-	* returns whether a user was deleted in LDAP
328
-	*
329
-	* @param string $uid The username of the user to delete
330
-	* @return bool
331
-	*/
332
-	public function deleteUser($uid) {
333
-		$marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
334
-		if(intval($marked) === 0) {
335
-			\OC::$server->getLogger()->notice(
336
-				'User '.$uid . ' is not marked as deleted, not cleaning up.',
337
-				array('app' => 'user_ldap'));
338
-			return false;
339
-		}
340
-		\OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
341
-			array('app' => 'user_ldap'));
342
-
343
-		//Get Home Directory out of user preferences so we can return it later,
344
-		//necessary for removing directories as done by OC_User.
345
-		$home = $this->ocConfig->getUserValue($uid, 'user_ldap', 'homePath', '');
346
-		$this->homesToKill[$uid] = $home;
347
-		$this->access->getUserMapper()->unmap($uid);
348
-
349
-		return true;
350
-	}
351
-
352
-	/**
353
-	 * get the user's home directory
354
-	 *
355
-	 * @param string $uid the username
356
-	 * @return bool|string
357
-	 * @throws NoUserException
358
-	 * @throws \Exception
359
-	 */
360
-	public function getHome($uid) {
361
-		if(isset($this->homesToKill[$uid]) && !empty($this->homesToKill[$uid])) {
362
-			//a deleted user who needs some clean up
363
-			return $this->homesToKill[$uid];
364
-		}
365
-
366
-		// user Exists check required as it is not done in user proxy!
367
-		if(!$this->userExists($uid)) {
368
-			return false;
369
-		}
370
-
371
-		$cacheKey = 'getHome'.$uid;
372
-		$path = $this->access->connection->getFromCache($cacheKey);
373
-		if(!is_null($path)) {
374
-			return $path;
375
-		}
376
-
377
-		$user = $this->access->userManager->get($uid);
378
-		if(is_null($user) || ($user instanceof OfflineUser && !$this->userExistsOnLDAP($user->getOCName()))) {
379
-			throw new NoUserException($uid . ' is not a valid user anymore');
380
-		}
381
-		if($user instanceof OfflineUser) {
382
-			// apparently this user survived the userExistsOnLDAP check,
383
-			// we request the user instance again in order to retrieve a User
384
-			// instance instead
385
-			$user = $this->access->userManager->get($uid);
386
-		}
387
-		$path = $user->getHomePath();
388
-		$this->access->cacheUserHome($uid, $path);
389
-
390
-		return $path;
391
-	}
392
-
393
-	/**
394
-	 * get display name of the user
395
-	 * @param string $uid user ID of the user
396
-	 * @return string|false display name
397
-	 */
398
-	public function getDisplayName($uid) {
399
-		if(!$this->userExists($uid)) {
400
-			return false;
401
-		}
402
-
403
-		$cacheKey = 'getDisplayName'.$uid;
404
-		if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
405
-			return $displayName;
406
-		}
407
-
408
-		//Check whether the display name is configured to have a 2nd feature
409
-		$additionalAttribute = $this->access->connection->ldapUserDisplayName2;
410
-		$displayName2 = '';
411
-		if ($additionalAttribute !== '') {
412
-			$displayName2 = $this->access->readAttribute(
413
-				$this->access->username2dn($uid),
414
-				$additionalAttribute);
415
-		}
416
-
417
-		$displayName = $this->access->readAttribute(
418
-			$this->access->username2dn($uid),
419
-			$this->access->connection->ldapUserDisplayName);
420
-
421
-		if($displayName && (count($displayName) > 0)) {
422
-			$displayName = $displayName[0];
423
-
424
-			if (is_array($displayName2)){
425
-				$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
426
-			}
427
-
428
-			$user = $this->access->userManager->get($uid);
429
-			if ($user instanceof User) {
430
-				$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
431
-				$this->access->connection->writeToCache($cacheKey, $displayName);
432
-			}
433
-			if ($user instanceof OfflineUser) {
434
-				/** @var OfflineUser $user*/
435
-				$displayName = $user->getDisplayName();
436
-			}
437
-			return $displayName;
438
-		}
439
-
440
-		return null;
441
-	}
442
-
443
-	/**
444
-	 * Get a list of all display names
445
-	 *
446
-	 * @param string $search
447
-	 * @param string|null $limit
448
-	 * @param string|null $offset
449
-	 * @return array an array of all displayNames (value) and the corresponding uids (key)
450
-	 */
451
-	public function getDisplayNames($search = '', $limit = null, $offset = null) {
452
-		$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
453
-		if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
454
-			return $displayNames;
455
-		}
456
-
457
-		$displayNames = array();
458
-		$users = $this->getUsers($search, $limit, $offset);
459
-		foreach ($users as $user) {
460
-			$displayNames[$user] = $this->getDisplayName($user);
461
-		}
462
-		$this->access->connection->writeToCache($cacheKey, $displayNames);
463
-		return $displayNames;
464
-	}
465
-
466
-	/**
467
-	* Check if backend implements actions
468
-	* @param int $actions bitwise-or'ed actions
469
-	* @return boolean
470
-	*
471
-	* Returns the supported actions as int to be
472
-	* compared with OC_USER_BACKEND_CREATE_USER etc.
473
-	*/
474
-	public function implementsActions($actions) {
475
-		return (bool)((Backend::CHECK_PASSWORD
476
-			| Backend::GET_HOME
477
-			| Backend::GET_DISPLAYNAME
478
-			| Backend::PROVIDE_AVATAR
479
-			| Backend::COUNT_USERS
480
-			| ((intval($this->access->connection->turnOnPasswordChange) === 1)?(Backend::SET_PASSWORD):0))
481
-			& $actions);
482
-	}
483
-
484
-	/**
485
-	 * @return bool
486
-	 */
487
-	public function hasUserListings() {
488
-		return true;
489
-	}
490
-
491
-	/**
492
-	 * counts the users in LDAP
493
-	 *
494
-	 * @return int|bool
495
-	 */
496
-	public function countUsers() {
497
-		$filter = $this->access->getFilterForUserCount();
498
-		$cacheKey = 'countUsers-'.$filter;
499
-		if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
500
-			return $entries;
501
-		}
502
-		$entries = $this->access->countUsers($filter);
503
-		$this->access->connection->writeToCache($cacheKey, $entries);
504
-		return $entries;
505
-	}
506
-
507
-	/**
508
-	 * Backend name to be shown in user management
509
-	 * @return string the name of the backend to be shown
510
-	 */
511
-	public function getBackendName(){
512
-		return 'LDAP';
513
-	}
110
+    /**
111
+     * returns the username for the given LDAP DN, if available
112
+     *
113
+     * @param string $dn
114
+     * @return string|false with the username
115
+     */
116
+    public function dn2UserName($dn) {
117
+        return $this->access->dn2username($dn);
118
+    }
119
+
120
+    /**
121
+     * returns an LDAP record based on a given login name
122
+     *
123
+     * @param string $loginName
124
+     * @return array
125
+     * @throws NotOnLDAP
126
+     */
127
+    public function getLDAPUserByLoginName($loginName) {
128
+        //find out dn of the user name
129
+        $attrs = $this->access->userManager->getAttributes();
130
+        $users = $this->access->fetchUsersByLoginName($loginName, $attrs);
131
+        if(count($users) < 1) {
132
+            throw new NotOnLDAP('No user available for the given login name on ' .
133
+                $this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
134
+        }
135
+        return $users[0];
136
+    }
137
+
138
+    /**
139
+     * Check if the password is correct without logging in the user
140
+     *
141
+     * @param string $uid The username
142
+     * @param string $password The password
143
+     * @return false|string
144
+     */
145
+    public function checkPassword($uid, $password) {
146
+        try {
147
+            $ldapRecord = $this->getLDAPUserByLoginName($uid);
148
+        } catch(NotOnLDAP $e) {
149
+            if($this->ocConfig->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
150
+                \OC::$server->getLogger()->logException($e, ['app' => 'user_ldap']);
151
+            }
152
+            return false;
153
+        }
154
+        $dn = $ldapRecord['dn'][0];
155
+        $user = $this->access->userManager->get($dn);
156
+
157
+        if(!$user instanceof User) {
158
+            Util::writeLog('user_ldap',
159
+                'LDAP Login: Could not get user object for DN ' . $dn .
160
+                '. Maybe the LDAP entry has no set display name attribute?',
161
+                Util::WARN);
162
+            return false;
163
+        }
164
+        if($user->getUsername() !== false) {
165
+            //are the credentials OK?
166
+            if(!$this->access->areCredentialsValid($dn, $password)) {
167
+                return false;
168
+            }
169
+
170
+            $this->access->cacheUserExists($user->getUsername());
171
+            $user->processAttributes($ldapRecord);
172
+            $user->markLogin();
173
+
174
+            return $user->getUsername();
175
+        }
176
+
177
+        return false;
178
+    }
179
+
180
+    /**
181
+     * Set password
182
+     * @param string $uid The username
183
+     * @param string $password The new password
184
+     * @return bool
185
+     */
186
+    public function setPassword($uid, $password) {
187
+        $user = $this->access->userManager->get($uid);
188
+
189
+        if(!$user instanceof User) {
190
+            throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
191
+                '. Maybe the LDAP entry has no set display name attribute?');
192
+        }
193
+        if($user->getUsername() !== false) {
194
+            return $this->access->setPassword($user->getDN(), $password);
195
+        }
196
+
197
+        return false;
198
+    }
199
+
200
+    /**
201
+     * Get a list of all users
202
+     *
203
+     * @param string $search
204
+     * @param integer $limit
205
+     * @param integer $offset
206
+     * @return string[] an array of all uids
207
+     */
208
+    public function getUsers($search = '', $limit = 10, $offset = 0) {
209
+        $search = $this->access->escapeFilterPart($search, true);
210
+        $cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
211
+
212
+        //check if users are cached, if so return
213
+        $ldap_users = $this->access->connection->getFromCache($cachekey);
214
+        if(!is_null($ldap_users)) {
215
+            return $ldap_users;
216
+        }
217
+
218
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
219
+        // error. With a limit of 0, we get 0 results. So we pass null.
220
+        if($limit <= 0) {
221
+            $limit = null;
222
+        }
223
+        $filter = $this->access->combineFilterWithAnd(array(
224
+            $this->access->connection->ldapUserFilter,
225
+            $this->access->connection->ldapUserDisplayName . '=*',
226
+            $this->access->getFilterPartForUserSearch($search)
227
+        ));
228
+
229
+        Util::writeLog('user_ldap',
230
+            'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
231
+            Util::DEBUG);
232
+        //do the search and translate results to owncloud names
233
+        $ldap_users = $this->access->fetchListOfUsers(
234
+            $filter,
235
+            $this->access->userManager->getAttributes(true),
236
+            $limit, $offset);
237
+        $ldap_users = $this->access->ownCloudUserNames($ldap_users);
238
+        Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', Util::DEBUG);
239
+
240
+        $this->access->connection->writeToCache($cachekey, $ldap_users);
241
+        return $ldap_users;
242
+    }
243
+
244
+    /**
245
+     * checks whether a user is still available on LDAP
246
+     *
247
+     * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
248
+     * name or an instance of that user
249
+     * @return bool
250
+     * @throws \Exception
251
+     * @throws \OC\ServerNotAvailableException
252
+     */
253
+    public function userExistsOnLDAP($user) {
254
+        if(is_string($user)) {
255
+            $user = $this->access->userManager->get($user);
256
+        }
257
+        if(is_null($user)) {
258
+            return false;
259
+        }
260
+
261
+        $dn = $user->getDN();
262
+        //check if user really still exists by reading its entry
263
+        if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
264
+            $lcr = $this->access->connection->getConnectionResource();
265
+            if(is_null($lcr)) {
266
+                throw new \Exception('No LDAP Connection to server ' . $this->access->connection->ldapHost);
267
+            }
268
+
269
+            try {
270
+                $uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
271
+                if(!$uuid) {
272
+                    return false;
273
+                }
274
+                $newDn = $this->access->getUserDnByUuid($uuid);
275
+                //check if renamed user is still valid by reapplying the ldap filter
276
+                if(!is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
277
+                    return false;
278
+                }
279
+                $this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
280
+                return true;
281
+            } catch (\Exception $e) {
282
+                return false;
283
+            }
284
+        }
285
+
286
+        if($user instanceof OfflineUser) {
287
+            $user->unmark();
288
+        }
289
+
290
+        return true;
291
+    }
292
+
293
+    /**
294
+     * check if a user exists
295
+     * @param string $uid the username
296
+     * @return boolean
297
+     * @throws \Exception when connection could not be established
298
+     */
299
+    public function userExists($uid) {
300
+        $userExists = $this->access->connection->getFromCache('userExists'.$uid);
301
+        if(!is_null($userExists)) {
302
+            return (bool)$userExists;
303
+        }
304
+        //getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
305
+        $user = $this->access->userManager->get($uid);
306
+
307
+        if(is_null($user)) {
308
+            Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
309
+                $this->access->connection->ldapHost, Util::DEBUG);
310
+            $this->access->connection->writeToCache('userExists'.$uid, false);
311
+            return false;
312
+        } else if($user instanceof OfflineUser) {
313
+            //express check for users marked as deleted. Returning true is
314
+            //necessary for cleanup
315
+            return true;
316
+        }
317
+
318
+        $result = $this->userExistsOnLDAP($user);
319
+        $this->access->connection->writeToCache('userExists'.$uid, $result);
320
+        if($result === true) {
321
+            $user->update();
322
+        }
323
+        return $result;
324
+    }
325
+
326
+    /**
327
+     * returns whether a user was deleted in LDAP
328
+     *
329
+     * @param string $uid The username of the user to delete
330
+     * @return bool
331
+     */
332
+    public function deleteUser($uid) {
333
+        $marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
334
+        if(intval($marked) === 0) {
335
+            \OC::$server->getLogger()->notice(
336
+                'User '.$uid . ' is not marked as deleted, not cleaning up.',
337
+                array('app' => 'user_ldap'));
338
+            return false;
339
+        }
340
+        \OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
341
+            array('app' => 'user_ldap'));
342
+
343
+        //Get Home Directory out of user preferences so we can return it later,
344
+        //necessary for removing directories as done by OC_User.
345
+        $home = $this->ocConfig->getUserValue($uid, 'user_ldap', 'homePath', '');
346
+        $this->homesToKill[$uid] = $home;
347
+        $this->access->getUserMapper()->unmap($uid);
348
+
349
+        return true;
350
+    }
351
+
352
+    /**
353
+     * get the user's home directory
354
+     *
355
+     * @param string $uid the username
356
+     * @return bool|string
357
+     * @throws NoUserException
358
+     * @throws \Exception
359
+     */
360
+    public function getHome($uid) {
361
+        if(isset($this->homesToKill[$uid]) && !empty($this->homesToKill[$uid])) {
362
+            //a deleted user who needs some clean up
363
+            return $this->homesToKill[$uid];
364
+        }
365
+
366
+        // user Exists check required as it is not done in user proxy!
367
+        if(!$this->userExists($uid)) {
368
+            return false;
369
+        }
370
+
371
+        $cacheKey = 'getHome'.$uid;
372
+        $path = $this->access->connection->getFromCache($cacheKey);
373
+        if(!is_null($path)) {
374
+            return $path;
375
+        }
376
+
377
+        $user = $this->access->userManager->get($uid);
378
+        if(is_null($user) || ($user instanceof OfflineUser && !$this->userExistsOnLDAP($user->getOCName()))) {
379
+            throw new NoUserException($uid . ' is not a valid user anymore');
380
+        }
381
+        if($user instanceof OfflineUser) {
382
+            // apparently this user survived the userExistsOnLDAP check,
383
+            // we request the user instance again in order to retrieve a User
384
+            // instance instead
385
+            $user = $this->access->userManager->get($uid);
386
+        }
387
+        $path = $user->getHomePath();
388
+        $this->access->cacheUserHome($uid, $path);
389
+
390
+        return $path;
391
+    }
392
+
393
+    /**
394
+     * get display name of the user
395
+     * @param string $uid user ID of the user
396
+     * @return string|false display name
397
+     */
398
+    public function getDisplayName($uid) {
399
+        if(!$this->userExists($uid)) {
400
+            return false;
401
+        }
402
+
403
+        $cacheKey = 'getDisplayName'.$uid;
404
+        if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
405
+            return $displayName;
406
+        }
407
+
408
+        //Check whether the display name is configured to have a 2nd feature
409
+        $additionalAttribute = $this->access->connection->ldapUserDisplayName2;
410
+        $displayName2 = '';
411
+        if ($additionalAttribute !== '') {
412
+            $displayName2 = $this->access->readAttribute(
413
+                $this->access->username2dn($uid),
414
+                $additionalAttribute);
415
+        }
416
+
417
+        $displayName = $this->access->readAttribute(
418
+            $this->access->username2dn($uid),
419
+            $this->access->connection->ldapUserDisplayName);
420
+
421
+        if($displayName && (count($displayName) > 0)) {
422
+            $displayName = $displayName[0];
423
+
424
+            if (is_array($displayName2)){
425
+                $displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
426
+            }
427
+
428
+            $user = $this->access->userManager->get($uid);
429
+            if ($user instanceof User) {
430
+                $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
431
+                $this->access->connection->writeToCache($cacheKey, $displayName);
432
+            }
433
+            if ($user instanceof OfflineUser) {
434
+                /** @var OfflineUser $user*/
435
+                $displayName = $user->getDisplayName();
436
+            }
437
+            return $displayName;
438
+        }
439
+
440
+        return null;
441
+    }
442
+
443
+    /**
444
+     * Get a list of all display names
445
+     *
446
+     * @param string $search
447
+     * @param string|null $limit
448
+     * @param string|null $offset
449
+     * @return array an array of all displayNames (value) and the corresponding uids (key)
450
+     */
451
+    public function getDisplayNames($search = '', $limit = null, $offset = null) {
452
+        $cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
453
+        if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
454
+            return $displayNames;
455
+        }
456
+
457
+        $displayNames = array();
458
+        $users = $this->getUsers($search, $limit, $offset);
459
+        foreach ($users as $user) {
460
+            $displayNames[$user] = $this->getDisplayName($user);
461
+        }
462
+        $this->access->connection->writeToCache($cacheKey, $displayNames);
463
+        return $displayNames;
464
+    }
465
+
466
+    /**
467
+     * Check if backend implements actions
468
+     * @param int $actions bitwise-or'ed actions
469
+     * @return boolean
470
+     *
471
+     * Returns the supported actions as int to be
472
+     * compared with OC_USER_BACKEND_CREATE_USER etc.
473
+     */
474
+    public function implementsActions($actions) {
475
+        return (bool)((Backend::CHECK_PASSWORD
476
+            | Backend::GET_HOME
477
+            | Backend::GET_DISPLAYNAME
478
+            | Backend::PROVIDE_AVATAR
479
+            | Backend::COUNT_USERS
480
+            | ((intval($this->access->connection->turnOnPasswordChange) === 1)?(Backend::SET_PASSWORD):0))
481
+            & $actions);
482
+    }
483
+
484
+    /**
485
+     * @return bool
486
+     */
487
+    public function hasUserListings() {
488
+        return true;
489
+    }
490
+
491
+    /**
492
+     * counts the users in LDAP
493
+     *
494
+     * @return int|bool
495
+     */
496
+    public function countUsers() {
497
+        $filter = $this->access->getFilterForUserCount();
498
+        $cacheKey = 'countUsers-'.$filter;
499
+        if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
500
+            return $entries;
501
+        }
502
+        $entries = $this->access->countUsers($filter);
503
+        $this->access->connection->writeToCache($cacheKey, $entries);
504
+        return $entries;
505
+    }
506
+
507
+    /**
508
+     * Backend name to be shown in user management
509
+     * @return string the name of the backend to be shown
510
+     */
511
+    public function getBackendName(){
512
+        return 'LDAP';
513
+    }
514 514
 	
515
-	/**
516
-	 * Return access for LDAP interaction.
517
-	 * @param string $uid
518
-	 * @return Access instance of Access for LDAP interaction
519
-	 */
520
-	public function getLDAPAccess($uid) {
521
-		return $this->access;
522
-	}
515
+    /**
516
+     * Return access for LDAP interaction.
517
+     * @param string $uid
518
+     * @return Access instance of Access for LDAP interaction
519
+     */
520
+    public function getLDAPAccess($uid) {
521
+        return $this->access;
522
+    }
523 523
 	
524
-	/**
525
-	 * Return LDAP connection resource from a cloned connection.
526
-	 * The cloned connection needs to be closed manually.
527
-	 * of the current access.
528
-	 * @param string $uid
529
-	 * @return resource of the LDAP connection
530
-	 */
531
-	public function getNewLDAPConnection($uid) {
532
-		$connection = clone $this->access->getConnection();
533
-		return $connection->getConnectionResource();
534
-	}
524
+    /**
525
+     * Return LDAP connection resource from a cloned connection.
526
+     * The cloned connection needs to be closed manually.
527
+     * of the current access.
528
+     * @param string $uid
529
+     * @return resource of the LDAP connection
530
+     */
531
+    public function getNewLDAPConnection($uid) {
532
+        $connection = clone $this->access->getConnection();
533
+        return $connection->getConnectionResource();
534
+    }
535 535
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 1 patch
Indentation   +869 added lines, -869 removed lines patch added patch discarded remove patch
@@ -40,876 +40,876 @@
 block discarded – undo
40 40
 use OC\Cache\CappedMemoryCache;
41 41
 
42 42
 class Group_LDAP extends BackendUtility implements \OCP\GroupInterface {
43
-	protected $enabled = false;
44
-
45
-	/**
46
-	 * @var string[] $cachedGroupMembers array of users with gid as key
47
-	 */
48
-	protected $cachedGroupMembers;
49
-
50
-	/**
51
-	 * @var string[] $cachedGroupsByMember array of groups with uid as key
52
-	 */
53
-	protected $cachedGroupsByMember;
54
-
55
-	public function __construct(Access $access) {
56
-		parent::__construct($access);
57
-		$filter = $this->access->connection->ldapGroupFilter;
58
-		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
59
-		if(!empty($filter) && !empty($gassoc)) {
60
-			$this->enabled = true;
61
-		}
62
-
63
-		$this->cachedGroupMembers = new CappedMemoryCache();
64
-		$this->cachedGroupsByMember = new CappedMemoryCache();
65
-	}
66
-
67
-	/**
68
-	 * is user in group?
69
-	 * @param string $uid uid of the user
70
-	 * @param string $gid gid of the group
71
-	 * @return bool
72
-	 *
73
-	 * Checks whether the user is member of a group or not.
74
-	 */
75
-	public function inGroup($uid, $gid) {
76
-		if(!$this->enabled) {
77
-			return false;
78
-		}
79
-		$cacheKey = 'inGroup'.$uid.':'.$gid;
80
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
81
-		if(!is_null($inGroup)) {
82
-			return (bool)$inGroup;
83
-		}
84
-
85
-		$userDN = $this->access->username2dn($uid);
86
-
87
-		if(isset($this->cachedGroupMembers[$gid])) {
88
-			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
89
-			return $isInGroup;
90
-		}
91
-
92
-		$cacheKeyMembers = 'inGroup-members:'.$gid;
93
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
94
-		if(!is_null($members)) {
95
-			$this->cachedGroupMembers[$gid] = $members;
96
-			$isInGroup = in_array($userDN, $members);
97
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
98
-			return $isInGroup;
99
-		}
100
-
101
-		$groupDN = $this->access->groupname2dn($gid);
102
-		// just in case
103
-		if(!$groupDN || !$userDN) {
104
-			$this->access->connection->writeToCache($cacheKey, false);
105
-			return false;
106
-		}
107
-
108
-		//check primary group first
109
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
110
-			$this->access->connection->writeToCache($cacheKey, true);
111
-			return true;
112
-		}
113
-
114
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
115
-		$members = $this->_groupMembers($groupDN);
116
-		$members = array_keys($members); // uids are returned as keys
117
-		if(!is_array($members) || count($members) === 0) {
118
-			$this->access->connection->writeToCache($cacheKey, false);
119
-			return false;
120
-		}
121
-
122
-		//extra work if we don't get back user DNs
123
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
124
-			$dns = array();
125
-			$filterParts = array();
126
-			$bytes = 0;
127
-			foreach($members as $mid) {
128
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
129
-				$filterParts[] = $filter;
130
-				$bytes += strlen($filter);
131
-				if($bytes >= 9000000) {
132
-					// AD has a default input buffer of 10 MB, we do not want
133
-					// to take even the chance to exceed it
134
-					$filter = $this->access->combineFilterWithOr($filterParts);
135
-					$bytes = 0;
136
-					$filterParts = array();
137
-					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
138
-					$dns = array_merge($dns, $users);
139
-				}
140
-			}
141
-			if(count($filterParts) > 0) {
142
-				$filter = $this->access->combineFilterWithOr($filterParts);
143
-				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
144
-				$dns = array_merge($dns, $users);
145
-			}
146
-			$members = $dns;
147
-		}
148
-
149
-		$isInGroup = in_array($userDN, $members);
150
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
151
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
152
-		$this->cachedGroupMembers[$gid] = $members;
153
-
154
-		return $isInGroup;
155
-	}
156
-
157
-	/**
158
-	 * @param string $dnGroup
159
-	 * @return array
160
-	 *
161
-	 * For a group that has user membership defined by an LDAP search url attribute returns the users
162
-	 * that match the search url otherwise returns an empty array.
163
-	 */
164
-	public function getDynamicGroupMembers($dnGroup) {
165
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
166
-
167
-		if (empty($dynamicGroupMemberURL)) {
168
-			return array();
169
-		}
170
-
171
-		$dynamicMembers = array();
172
-		$memberURLs = $this->access->readAttribute(
173
-			$dnGroup,
174
-			$dynamicGroupMemberURL,
175
-			$this->access->connection->ldapGroupFilter
176
-		);
177
-		if ($memberURLs !== false) {
178
-			// this group has the 'memberURL' attribute so this is a dynamic group
179
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
180
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
181
-			$pos = strpos($memberURLs[0], '(');
182
-			if ($pos !== false) {
183
-				$memberUrlFilter = substr($memberURLs[0], $pos);
184
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
185
-				$dynamicMembers = array();
186
-				foreach($foundMembers as $value) {
187
-					$dynamicMembers[$value['dn'][0]] = 1;
188
-				}
189
-			} else {
190
-				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
191
-					'of group ' . $dnGroup, \OCP\Util::DEBUG);
192
-			}
193
-		}
194
-		return $dynamicMembers;
195
-	}
196
-
197
-	/**
198
-	 * @param string $dnGroup
199
-	 * @param array|null &$seen
200
-	 * @return array|mixed|null
201
-	 */
202
-	private function _groupMembers($dnGroup, &$seen = null) {
203
-		if ($seen === null) {
204
-			$seen = array();
205
-		}
206
-		$allMembers = array();
207
-		if (array_key_exists($dnGroup, $seen)) {
208
-			// avoid loops
209
-			return array();
210
-		}
211
-		// used extensively in cron job, caching makes sense for nested groups
212
-		$cacheKey = '_groupMembers'.$dnGroup;
213
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
214
-		if(!is_null($groupMembers)) {
215
-			return $groupMembers;
216
-		}
217
-		$seen[$dnGroup] = 1;
218
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
219
-												$this->access->connection->ldapGroupFilter);
220
-		if (is_array($members)) {
221
-			foreach ($members as $memberDN) {
222
-				$allMembers[$memberDN] = 1;
223
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
224
-				if (!empty($nestedGroups)) {
225
-					$subMembers = $this->_groupMembers($memberDN, $seen);
226
-					if ($subMembers) {
227
-						$allMembers = array_merge($allMembers, $subMembers);
228
-					}
229
-				}
230
-			}
231
-		}
43
+    protected $enabled = false;
44
+
45
+    /**
46
+     * @var string[] $cachedGroupMembers array of users with gid as key
47
+     */
48
+    protected $cachedGroupMembers;
49
+
50
+    /**
51
+     * @var string[] $cachedGroupsByMember array of groups with uid as key
52
+     */
53
+    protected $cachedGroupsByMember;
54
+
55
+    public function __construct(Access $access) {
56
+        parent::__construct($access);
57
+        $filter = $this->access->connection->ldapGroupFilter;
58
+        $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
59
+        if(!empty($filter) && !empty($gassoc)) {
60
+            $this->enabled = true;
61
+        }
62
+
63
+        $this->cachedGroupMembers = new CappedMemoryCache();
64
+        $this->cachedGroupsByMember = new CappedMemoryCache();
65
+    }
66
+
67
+    /**
68
+     * is user in group?
69
+     * @param string $uid uid of the user
70
+     * @param string $gid gid of the group
71
+     * @return bool
72
+     *
73
+     * Checks whether the user is member of a group or not.
74
+     */
75
+    public function inGroup($uid, $gid) {
76
+        if(!$this->enabled) {
77
+            return false;
78
+        }
79
+        $cacheKey = 'inGroup'.$uid.':'.$gid;
80
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
81
+        if(!is_null($inGroup)) {
82
+            return (bool)$inGroup;
83
+        }
84
+
85
+        $userDN = $this->access->username2dn($uid);
86
+
87
+        if(isset($this->cachedGroupMembers[$gid])) {
88
+            $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
89
+            return $isInGroup;
90
+        }
91
+
92
+        $cacheKeyMembers = 'inGroup-members:'.$gid;
93
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
94
+        if(!is_null($members)) {
95
+            $this->cachedGroupMembers[$gid] = $members;
96
+            $isInGroup = in_array($userDN, $members);
97
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
98
+            return $isInGroup;
99
+        }
100
+
101
+        $groupDN = $this->access->groupname2dn($gid);
102
+        // just in case
103
+        if(!$groupDN || !$userDN) {
104
+            $this->access->connection->writeToCache($cacheKey, false);
105
+            return false;
106
+        }
107
+
108
+        //check primary group first
109
+        if($gid === $this->getUserPrimaryGroup($userDN)) {
110
+            $this->access->connection->writeToCache($cacheKey, true);
111
+            return true;
112
+        }
113
+
114
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
115
+        $members = $this->_groupMembers($groupDN);
116
+        $members = array_keys($members); // uids are returned as keys
117
+        if(!is_array($members) || count($members) === 0) {
118
+            $this->access->connection->writeToCache($cacheKey, false);
119
+            return false;
120
+        }
121
+
122
+        //extra work if we don't get back user DNs
123
+        if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
124
+            $dns = array();
125
+            $filterParts = array();
126
+            $bytes = 0;
127
+            foreach($members as $mid) {
128
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
129
+                $filterParts[] = $filter;
130
+                $bytes += strlen($filter);
131
+                if($bytes >= 9000000) {
132
+                    // AD has a default input buffer of 10 MB, we do not want
133
+                    // to take even the chance to exceed it
134
+                    $filter = $this->access->combineFilterWithOr($filterParts);
135
+                    $bytes = 0;
136
+                    $filterParts = array();
137
+                    $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
138
+                    $dns = array_merge($dns, $users);
139
+                }
140
+            }
141
+            if(count($filterParts) > 0) {
142
+                $filter = $this->access->combineFilterWithOr($filterParts);
143
+                $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
144
+                $dns = array_merge($dns, $users);
145
+            }
146
+            $members = $dns;
147
+        }
148
+
149
+        $isInGroup = in_array($userDN, $members);
150
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
151
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
152
+        $this->cachedGroupMembers[$gid] = $members;
153
+
154
+        return $isInGroup;
155
+    }
156
+
157
+    /**
158
+     * @param string $dnGroup
159
+     * @return array
160
+     *
161
+     * For a group that has user membership defined by an LDAP search url attribute returns the users
162
+     * that match the search url otherwise returns an empty array.
163
+     */
164
+    public function getDynamicGroupMembers($dnGroup) {
165
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
166
+
167
+        if (empty($dynamicGroupMemberURL)) {
168
+            return array();
169
+        }
170
+
171
+        $dynamicMembers = array();
172
+        $memberURLs = $this->access->readAttribute(
173
+            $dnGroup,
174
+            $dynamicGroupMemberURL,
175
+            $this->access->connection->ldapGroupFilter
176
+        );
177
+        if ($memberURLs !== false) {
178
+            // this group has the 'memberURL' attribute so this is a dynamic group
179
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
180
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
181
+            $pos = strpos($memberURLs[0], '(');
182
+            if ($pos !== false) {
183
+                $memberUrlFilter = substr($memberURLs[0], $pos);
184
+                $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
185
+                $dynamicMembers = array();
186
+                foreach($foundMembers as $value) {
187
+                    $dynamicMembers[$value['dn'][0]] = 1;
188
+                }
189
+            } else {
190
+                \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
191
+                    'of group ' . $dnGroup, \OCP\Util::DEBUG);
192
+            }
193
+        }
194
+        return $dynamicMembers;
195
+    }
196
+
197
+    /**
198
+     * @param string $dnGroup
199
+     * @param array|null &$seen
200
+     * @return array|mixed|null
201
+     */
202
+    private function _groupMembers($dnGroup, &$seen = null) {
203
+        if ($seen === null) {
204
+            $seen = array();
205
+        }
206
+        $allMembers = array();
207
+        if (array_key_exists($dnGroup, $seen)) {
208
+            // avoid loops
209
+            return array();
210
+        }
211
+        // used extensively in cron job, caching makes sense for nested groups
212
+        $cacheKey = '_groupMembers'.$dnGroup;
213
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
214
+        if(!is_null($groupMembers)) {
215
+            return $groupMembers;
216
+        }
217
+        $seen[$dnGroup] = 1;
218
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
219
+                                                $this->access->connection->ldapGroupFilter);
220
+        if (is_array($members)) {
221
+            foreach ($members as $memberDN) {
222
+                $allMembers[$memberDN] = 1;
223
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
224
+                if (!empty($nestedGroups)) {
225
+                    $subMembers = $this->_groupMembers($memberDN, $seen);
226
+                    if ($subMembers) {
227
+                        $allMembers = array_merge($allMembers, $subMembers);
228
+                    }
229
+                }
230
+            }
231
+        }
232 232
 		
233
-		$allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
233
+        $allMembers = array_merge($allMembers, $this->getDynamicGroupMembers($dnGroup));
234 234
 		
235
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
236
-		return $allMembers;
237
-	}
238
-
239
-	/**
240
-	 * @param string $DN
241
-	 * @param array|null &$seen
242
-	 * @return array
243
-	 */
244
-	private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
245
-		if ($seen === null) {
246
-			$seen = array();
247
-		}
248
-		if (array_key_exists($DN, $seen)) {
249
-			// avoid loops
250
-			return array();
251
-		}
252
-		$seen[$DN] = 1;
253
-		$groups = $this->access->readAttribute($DN, 'memberOf');
254
-		if (!is_array($groups)) {
255
-			return array();
256
-		}
257
-		$groups = $this->access->groupsMatchFilter($groups);
258
-		$allGroups =  $groups;
259
-		$nestedGroups = $this->access->connection->ldapNestedGroups;
260
-		if (intval($nestedGroups) === 1) {
261
-			foreach ($groups as $group) {
262
-				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
263
-				$allGroups = array_merge($allGroups, $subGroups);
264
-			}
265
-		}
266
-		return $allGroups;	
267
-	}
268
-
269
-	/**
270
-	 * translates a primary group ID into an Nextcloud internal name
271
-	 * @param string $gid as given by primaryGroupID on AD
272
-	 * @param string $dn a DN that belongs to the same domain as the group
273
-	 * @return string|bool
274
-	 */
275
-	public function primaryGroupID2Name($gid, $dn) {
276
-		$cacheKey = 'primaryGroupIDtoName';
277
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
278
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
279
-			return $groupNames[$gid];
280
-		}
281
-
282
-		$domainObjectSid = $this->access->getSID($dn);
283
-		if($domainObjectSid === false) {
284
-			return false;
285
-		}
286
-
287
-		//we need to get the DN from LDAP
288
-		$filter = $this->access->combineFilterWithAnd(array(
289
-			$this->access->connection->ldapGroupFilter,
290
-			'objectsid=' . $domainObjectSid . '-' . $gid
291
-		));
292
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
293
-		if(empty($result)) {
294
-			return false;
295
-		}
296
-		$dn = $result[0]['dn'][0];
297
-
298
-		//and now the group name
299
-		//NOTE once we have separate Nextcloud group IDs and group names we can
300
-		//directly read the display name attribute instead of the DN
301
-		$name = $this->access->dn2groupname($dn);
302
-
303
-		$this->access->connection->writeToCache($cacheKey, $name);
304
-
305
-		return $name;
306
-	}
307
-
308
-	/**
309
-	 * returns the entry's primary group ID
310
-	 * @param string $dn
311
-	 * @param string $attribute
312
-	 * @return string|bool
313
-	 */
314
-	private function getEntryGroupID($dn, $attribute) {
315
-		$value = $this->access->readAttribute($dn, $attribute);
316
-		if(is_array($value) && !empty($value)) {
317
-			return $value[0];
318
-		}
319
-		return false;
320
-	}
321
-
322
-	/**
323
-	 * returns the group's primary ID
324
-	 * @param string $dn
325
-	 * @return string|bool
326
-	 */
327
-	public function getGroupPrimaryGroupID($dn) {
328
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
329
-	}
330
-
331
-	/**
332
-	 * returns the user's primary group ID
333
-	 * @param string $dn
334
-	 * @return string|bool
335
-	 */
336
-	public function getUserPrimaryGroupIDs($dn) {
337
-		$primaryGroupID = false;
338
-		if($this->access->connection->hasPrimaryGroups) {
339
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
340
-			if($primaryGroupID === false) {
341
-				$this->access->connection->hasPrimaryGroups = false;
342
-			}
343
-		}
344
-		return $primaryGroupID;
345
-	}
346
-
347
-	/**
348
-	 * returns a filter for a "users in primary group" search or count operation
349
-	 *
350
-	 * @param string $groupDN
351
-	 * @param string $search
352
-	 * @return string
353
-	 * @throws \Exception
354
-	 */
355
-	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
356
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
357
-		if($groupID === false) {
358
-			throw new \Exception('Not a valid group');
359
-		}
360
-
361
-		$filterParts = [];
362
-		$filterParts[] = $this->access->getFilterForUserCount();
363
-		if ($search !== '') {
364
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
365
-		}
366
-		$filterParts[] = 'primaryGroupID=' . $groupID;
367
-
368
-		$filter = $this->access->combineFilterWithAnd($filterParts);
369
-
370
-		return $filter;
371
-	}
372
-
373
-	/**
374
-	 * returns a list of users that have the given group as primary group
375
-	 *
376
-	 * @param string $groupDN
377
-	 * @param string $search
378
-	 * @param int $limit
379
-	 * @param int $offset
380
-	 * @return string[]
381
-	 */
382
-	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
383
-		try {
384
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
385
-			$users = $this->access->fetchListOfUsers(
386
-				$filter,
387
-				array($this->access->connection->ldapUserDisplayName, 'dn'),
388
-				$limit,
389
-				$offset
390
-			);
391
-			return $this->access->ownCloudUserNames($users);
392
-		} catch (\Exception $e) {
393
-			return array();
394
-		}
395
-	}
396
-
397
-	/**
398
-	 * returns the number of users that have the given group as primary group
399
-	 *
400
-	 * @param string $groupDN
401
-	 * @param string $search
402
-	 * @param int $limit
403
-	 * @param int $offset
404
-	 * @return int
405
-	 */
406
-	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
407
-		try {
408
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
409
-			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
410
-			return (int)$users;
411
-		} catch (\Exception $e) {
412
-			return 0;
413
-		}
414
-	}
415
-
416
-	/**
417
-	 * gets the primary group of a user
418
-	 * @param string $dn
419
-	 * @return string
420
-	 */
421
-	public function getUserPrimaryGroup($dn) {
422
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
423
-		if($groupID !== false) {
424
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
425
-			if($groupName !== false) {
426
-				return $groupName;
427
-			}
428
-		}
429
-
430
-		return false;
431
-	}
432
-
433
-	/**
434
-	 * Get all groups a user belongs to
435
-	 * @param string $uid Name of the user
436
-	 * @return array with group names
437
-	 *
438
-	 * This function fetches all groups a user belongs to. It does not check
439
-	 * if the user exists at all.
440
-	 *
441
-	 * This function includes groups based on dynamic group membership.
442
-	 */
443
-	public function getUserGroups($uid) {
444
-		if(!$this->enabled) {
445
-			return array();
446
-		}
447
-		$cacheKey = 'getUserGroups'.$uid;
448
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
449
-		if(!is_null($userGroups)) {
450
-			return $userGroups;
451
-		}
452
-		$userDN = $this->access->username2dn($uid);
453
-		if(!$userDN) {
454
-			$this->access->connection->writeToCache($cacheKey, array());
455
-			return array();
456
-		}
457
-
458
-		$groups = [];
459
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
460
-
461
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
462
-
463
-		if (!empty($dynamicGroupMemberURL)) {
464
-			// look through dynamic groups to add them to the result array if needed
465
-			$groupsToMatch = $this->access->fetchListOfGroups(
466
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
467
-			foreach($groupsToMatch as $dynamicGroup) {
468
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
469
-					continue;
470
-				}
471
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
472
-				if ($pos !== false) {
473
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
474
-					// apply filter via ldap search to see if this user is in this
475
-					// dynamic group
476
-					$userMatch = $this->access->readAttribute(
477
-						$userDN,
478
-						$this->access->connection->ldapUserDisplayName,
479
-						$memberUrlFilter
480
-					);
481
-					if ($userMatch !== false) {
482
-						// match found so this user is in this group
483
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
484
-						if(is_string($groupName)) {
485
-							// be sure to never return false if the dn could not be
486
-							// resolved to a name, for whatever reason.
487
-							$groups[] = $groupName;
488
-						}
489
-					}
490
-				} else {
491
-					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
492
-						'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
493
-				}
494
-			}
495
-		}
496
-
497
-		// if possible, read out membership via memberOf. It's far faster than
498
-		// performing a search, which still is a fallback later.
499
-		// memberof doesn't support memberuid, so skip it here.
500
-		if(intval($this->access->connection->hasMemberOfFilterSupport) === 1
501
-			&& intval($this->access->connection->useMemberOfToDetectMembership) === 1
502
-		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
503
-		    ) {
504
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
505
-			if (is_array($groupDNs)) {
506
-				foreach ($groupDNs as $dn) {
507
-					$groupName = $this->access->dn2groupname($dn);
508
-					if(is_string($groupName)) {
509
-						// be sure to never return false if the dn could not be
510
-						// resolved to a name, for whatever reason.
511
-						$groups[] = $groupName;
512
-					}
513
-				}
514
-			}
235
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
236
+        return $allMembers;
237
+    }
238
+
239
+    /**
240
+     * @param string $DN
241
+     * @param array|null &$seen
242
+     * @return array
243
+     */
244
+    private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
245
+        if ($seen === null) {
246
+            $seen = array();
247
+        }
248
+        if (array_key_exists($DN, $seen)) {
249
+            // avoid loops
250
+            return array();
251
+        }
252
+        $seen[$DN] = 1;
253
+        $groups = $this->access->readAttribute($DN, 'memberOf');
254
+        if (!is_array($groups)) {
255
+            return array();
256
+        }
257
+        $groups = $this->access->groupsMatchFilter($groups);
258
+        $allGroups =  $groups;
259
+        $nestedGroups = $this->access->connection->ldapNestedGroups;
260
+        if (intval($nestedGroups) === 1) {
261
+            foreach ($groups as $group) {
262
+                $subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
263
+                $allGroups = array_merge($allGroups, $subGroups);
264
+            }
265
+        }
266
+        return $allGroups;	
267
+    }
268
+
269
+    /**
270
+     * translates a primary group ID into an Nextcloud internal name
271
+     * @param string $gid as given by primaryGroupID on AD
272
+     * @param string $dn a DN that belongs to the same domain as the group
273
+     * @return string|bool
274
+     */
275
+    public function primaryGroupID2Name($gid, $dn) {
276
+        $cacheKey = 'primaryGroupIDtoName';
277
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
278
+        if(!is_null($groupNames) && isset($groupNames[$gid])) {
279
+            return $groupNames[$gid];
280
+        }
281
+
282
+        $domainObjectSid = $this->access->getSID($dn);
283
+        if($domainObjectSid === false) {
284
+            return false;
285
+        }
286
+
287
+        //we need to get the DN from LDAP
288
+        $filter = $this->access->combineFilterWithAnd(array(
289
+            $this->access->connection->ldapGroupFilter,
290
+            'objectsid=' . $domainObjectSid . '-' . $gid
291
+        ));
292
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
293
+        if(empty($result)) {
294
+            return false;
295
+        }
296
+        $dn = $result[0]['dn'][0];
297
+
298
+        //and now the group name
299
+        //NOTE once we have separate Nextcloud group IDs and group names we can
300
+        //directly read the display name attribute instead of the DN
301
+        $name = $this->access->dn2groupname($dn);
302
+
303
+        $this->access->connection->writeToCache($cacheKey, $name);
304
+
305
+        return $name;
306
+    }
307
+
308
+    /**
309
+     * returns the entry's primary group ID
310
+     * @param string $dn
311
+     * @param string $attribute
312
+     * @return string|bool
313
+     */
314
+    private function getEntryGroupID($dn, $attribute) {
315
+        $value = $this->access->readAttribute($dn, $attribute);
316
+        if(is_array($value) && !empty($value)) {
317
+            return $value[0];
318
+        }
319
+        return false;
320
+    }
321
+
322
+    /**
323
+     * returns the group's primary ID
324
+     * @param string $dn
325
+     * @return string|bool
326
+     */
327
+    public function getGroupPrimaryGroupID($dn) {
328
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
329
+    }
330
+
331
+    /**
332
+     * returns the user's primary group ID
333
+     * @param string $dn
334
+     * @return string|bool
335
+     */
336
+    public function getUserPrimaryGroupIDs($dn) {
337
+        $primaryGroupID = false;
338
+        if($this->access->connection->hasPrimaryGroups) {
339
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
340
+            if($primaryGroupID === false) {
341
+                $this->access->connection->hasPrimaryGroups = false;
342
+            }
343
+        }
344
+        return $primaryGroupID;
345
+    }
346
+
347
+    /**
348
+     * returns a filter for a "users in primary group" search or count operation
349
+     *
350
+     * @param string $groupDN
351
+     * @param string $search
352
+     * @return string
353
+     * @throws \Exception
354
+     */
355
+    private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
356
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
357
+        if($groupID === false) {
358
+            throw new \Exception('Not a valid group');
359
+        }
360
+
361
+        $filterParts = [];
362
+        $filterParts[] = $this->access->getFilterForUserCount();
363
+        if ($search !== '') {
364
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
365
+        }
366
+        $filterParts[] = 'primaryGroupID=' . $groupID;
367
+
368
+        $filter = $this->access->combineFilterWithAnd($filterParts);
369
+
370
+        return $filter;
371
+    }
372
+
373
+    /**
374
+     * returns a list of users that have the given group as primary group
375
+     *
376
+     * @param string $groupDN
377
+     * @param string $search
378
+     * @param int $limit
379
+     * @param int $offset
380
+     * @return string[]
381
+     */
382
+    public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
383
+        try {
384
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
385
+            $users = $this->access->fetchListOfUsers(
386
+                $filter,
387
+                array($this->access->connection->ldapUserDisplayName, 'dn'),
388
+                $limit,
389
+                $offset
390
+            );
391
+            return $this->access->ownCloudUserNames($users);
392
+        } catch (\Exception $e) {
393
+            return array();
394
+        }
395
+    }
396
+
397
+    /**
398
+     * returns the number of users that have the given group as primary group
399
+     *
400
+     * @param string $groupDN
401
+     * @param string $search
402
+     * @param int $limit
403
+     * @param int $offset
404
+     * @return int
405
+     */
406
+    public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
407
+        try {
408
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
409
+            $users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
410
+            return (int)$users;
411
+        } catch (\Exception $e) {
412
+            return 0;
413
+        }
414
+    }
415
+
416
+    /**
417
+     * gets the primary group of a user
418
+     * @param string $dn
419
+     * @return string
420
+     */
421
+    public function getUserPrimaryGroup($dn) {
422
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
423
+        if($groupID !== false) {
424
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
425
+            if($groupName !== false) {
426
+                return $groupName;
427
+            }
428
+        }
429
+
430
+        return false;
431
+    }
432
+
433
+    /**
434
+     * Get all groups a user belongs to
435
+     * @param string $uid Name of the user
436
+     * @return array with group names
437
+     *
438
+     * This function fetches all groups a user belongs to. It does not check
439
+     * if the user exists at all.
440
+     *
441
+     * This function includes groups based on dynamic group membership.
442
+     */
443
+    public function getUserGroups($uid) {
444
+        if(!$this->enabled) {
445
+            return array();
446
+        }
447
+        $cacheKey = 'getUserGroups'.$uid;
448
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
449
+        if(!is_null($userGroups)) {
450
+            return $userGroups;
451
+        }
452
+        $userDN = $this->access->username2dn($uid);
453
+        if(!$userDN) {
454
+            $this->access->connection->writeToCache($cacheKey, array());
455
+            return array();
456
+        }
457
+
458
+        $groups = [];
459
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
460
+
461
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
462
+
463
+        if (!empty($dynamicGroupMemberURL)) {
464
+            // look through dynamic groups to add them to the result array if needed
465
+            $groupsToMatch = $this->access->fetchListOfGroups(
466
+                $this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
467
+            foreach($groupsToMatch as $dynamicGroup) {
468
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
469
+                    continue;
470
+                }
471
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
472
+                if ($pos !== false) {
473
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
474
+                    // apply filter via ldap search to see if this user is in this
475
+                    // dynamic group
476
+                    $userMatch = $this->access->readAttribute(
477
+                        $userDN,
478
+                        $this->access->connection->ldapUserDisplayName,
479
+                        $memberUrlFilter
480
+                    );
481
+                    if ($userMatch !== false) {
482
+                        // match found so this user is in this group
483
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
484
+                        if(is_string($groupName)) {
485
+                            // be sure to never return false if the dn could not be
486
+                            // resolved to a name, for whatever reason.
487
+                            $groups[] = $groupName;
488
+                        }
489
+                    }
490
+                } else {
491
+                    \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
492
+                        'of group ' . print_r($dynamicGroup, true), \OCP\Util::DEBUG);
493
+                }
494
+            }
495
+        }
496
+
497
+        // if possible, read out membership via memberOf. It's far faster than
498
+        // performing a search, which still is a fallback later.
499
+        // memberof doesn't support memberuid, so skip it here.
500
+        if(intval($this->access->connection->hasMemberOfFilterSupport) === 1
501
+            && intval($this->access->connection->useMemberOfToDetectMembership) === 1
502
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
503
+            ) {
504
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
505
+            if (is_array($groupDNs)) {
506
+                foreach ($groupDNs as $dn) {
507
+                    $groupName = $this->access->dn2groupname($dn);
508
+                    if(is_string($groupName)) {
509
+                        // be sure to never return false if the dn could not be
510
+                        // resolved to a name, for whatever reason.
511
+                        $groups[] = $groupName;
512
+                    }
513
+                }
514
+            }
515 515
 			
516
-			if($primaryGroup !== false) {
517
-				$groups[] = $primaryGroup;
518
-			}
519
-			$this->access->connection->writeToCache($cacheKey, $groups);
520
-			return $groups;
521
-		}
522
-
523
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
524
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
525
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
526
-		) {
527
-			$uid = $userDN;
528
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
529
-			$result = $this->access->readAttribute($userDN, 'uid');
530
-			if ($result === false) {
531
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
532
-					$this->access->connection->ldapHost, \OCP\Util::DEBUG);
533
-			}
534
-			$uid = $result[0];
535
-		} else {
536
-			// just in case
537
-			$uid = $userDN;
538
-		}
539
-
540
-		if(isset($this->cachedGroupsByMember[$uid])) {
541
-			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
542
-		} else {
543
-			$groupsByMember = array_values($this->getGroupsByMember($uid));
544
-			$groupsByMember = $this->access->ownCloudGroupNames($groupsByMember);
545
-			$this->cachedGroupsByMember[$uid] = $groupsByMember;
546
-			$groups = array_merge($groups, $groupsByMember);
547
-		}
548
-
549
-		if($primaryGroup !== false) {
550
-			$groups[] = $primaryGroup;
551
-		}
552
-
553
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
554
-		$this->access->connection->writeToCache($cacheKey, $groups);
555
-
556
-		return $groups;
557
-	}
558
-
559
-	/**
560
-	 * @param string $dn
561
-	 * @param array|null &$seen
562
-	 * @return array
563
-	 */
564
-	private function getGroupsByMember($dn, &$seen = null) {
565
-		if ($seen === null) {
566
-			$seen = array();
567
-		}
568
-		$allGroups = array();
569
-		if (array_key_exists($dn, $seen)) {
570
-			// avoid loops
571
-			return array();
572
-		}
573
-		$seen[$dn] = true;
574
-		$filter = $this->access->combineFilterWithAnd(array(
575
-			$this->access->connection->ldapGroupFilter,
576
-			$this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
577
-		));
578
-		$groups = $this->access->fetchListOfGroups($filter,
579
-			array($this->access->connection->ldapGroupDisplayName, 'dn'));
580
-		if (is_array($groups)) {
581
-			foreach ($groups as $groupobj) {
582
-				$groupDN = $groupobj['dn'][0];
583
-				$allGroups[$groupDN] = $groupobj;
584
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
585
-				if (!empty($nestedGroups)) {
586
-					$supergroups = $this->getGroupsByMember($groupDN, $seen);
587
-					if (is_array($supergroups) && (count($supergroups)>0)) {
588
-						$allGroups = array_merge($allGroups, $supergroups);
589
-					}
590
-				}
591
-			}
592
-		}
593
-		return $allGroups;
594
-	}
595
-
596
-	/**
597
-	 * get a list of all users in a group
598
-	 *
599
-	 * @param string $gid
600
-	 * @param string $search
601
-	 * @param int $limit
602
-	 * @param int $offset
603
-	 * @return array with user ids
604
-	 */
605
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
606
-		if(!$this->enabled) {
607
-			return array();
608
-		}
609
-		if(!$this->groupExists($gid)) {
610
-			return array();
611
-		}
612
-		$search = $this->access->escapeFilterPart($search, true);
613
-		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
614
-		// check for cache of the exact query
615
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
616
-		if(!is_null($groupUsers)) {
617
-			return $groupUsers;
618
-		}
619
-
620
-		// check for cache of the query without limit and offset
621
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
622
-		if(!is_null($groupUsers)) {
623
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
624
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
625
-			return $groupUsers;
626
-		}
627
-
628
-		if($limit === -1) {
629
-			$limit = null;
630
-		}
631
-		$groupDN = $this->access->groupname2dn($gid);
632
-		if(!$groupDN) {
633
-			// group couldn't be found, return empty resultset
634
-			$this->access->connection->writeToCache($cacheKey, array());
635
-			return array();
636
-		}
637
-
638
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
639
-		$members = array_keys($this->_groupMembers($groupDN));
640
-		if(!$members && empty($primaryUsers)) {
641
-			//in case users could not be retrieved, return empty result set
642
-			$this->access->connection->writeToCache($cacheKey, array());
643
-			return array();
644
-		}
645
-
646
-		$groupUsers = array();
647
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
648
-		$attrs = $this->access->userManager->getAttributes(true);
649
-		foreach($members as $member) {
650
-			if($isMemberUid) {
651
-				//we got uids, need to get their DNs to 'translate' them to user names
652
-				$filter = $this->access->combineFilterWithAnd(array(
653
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
654
-					$this->access->getFilterPartForUserSearch($search)
655
-				));
656
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
657
-				if(count($ldap_users) < 1) {
658
-					continue;
659
-				}
660
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
661
-			} else {
662
-				//we got DNs, check if we need to filter by search or we can give back all of them
663
-				if ($search !== '') {
664
-					if(!$this->access->readAttribute($member,
665
-						$this->access->connection->ldapUserDisplayName,
666
-						$this->access->getFilterPartForUserSearch($search))) {
667
-						continue;
668
-					}
669
-				}
670
-				// dn2username will also check if the users belong to the allowed base
671
-				if($ocname = $this->access->dn2username($member)) {
672
-					$groupUsers[] = $ocname;
673
-				}
674
-			}
675
-		}
676
-
677
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers));
678
-		natsort($groupUsers);
679
-		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
680
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
681
-
682
-
683
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
684
-
685
-		return $groupUsers;
686
-	}
687
-
688
-	/**
689
-	 * returns the number of users in a group, who match the search term
690
-	 * @param string $gid the internal group name
691
-	 * @param string $search optional, a search string
692
-	 * @return int|bool
693
-	 */
694
-	public function countUsersInGroup($gid, $search = '') {
695
-		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
696
-		if(!$this->enabled || !$this->groupExists($gid)) {
697
-			return false;
698
-		}
699
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
700
-		if(!is_null($groupUsers)) {
701
-			return $groupUsers;
702
-		}
703
-
704
-		$groupDN = $this->access->groupname2dn($gid);
705
-		if(!$groupDN) {
706
-			// group couldn't be found, return empty result set
707
-			$this->access->connection->writeToCache($cacheKey, false);
708
-			return false;
709
-		}
710
-
711
-		$members = array_keys($this->_groupMembers($groupDN));
712
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
713
-		if(!$members && $primaryUserCount === 0) {
714
-			//in case users could not be retrieved, return empty result set
715
-			$this->access->connection->writeToCache($cacheKey, false);
716
-			return false;
717
-		}
718
-
719
-		if ($search === '') {
720
-			$groupUsers = count($members) + $primaryUserCount;
721
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
722
-			return $groupUsers;
723
-		}
724
-		$search = $this->access->escapeFilterPart($search, true);
725
-		$isMemberUid =
726
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
727
-			=== 'memberuid');
728
-
729
-		//we need to apply the search filter
730
-		//alternatives that need to be checked:
731
-		//a) get all users by search filter and array_intersect them
732
-		//b) a, but only when less than 1k 10k ?k users like it is
733
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
734
-		//   and let it count.
735
-		//For now this is not important, because the only use of this method
736
-		//does not supply a search string
737
-		$groupUsers = array();
738
-		foreach($members as $member) {
739
-			if($isMemberUid) {
740
-				//we got uids, need to get their DNs to 'translate' them to user names
741
-				$filter = $this->access->combineFilterWithAnd(array(
742
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
743
-					$this->access->getFilterPartForUserSearch($search)
744
-				));
745
-				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
746
-				if(count($ldap_users) < 1) {
747
-					continue;
748
-				}
749
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
750
-			} else {
751
-				//we need to apply the search filter now
752
-				if(!$this->access->readAttribute($member,
753
-					$this->access->connection->ldapUserDisplayName,
754
-					$this->access->getFilterPartForUserSearch($search))) {
755
-					continue;
756
-				}
757
-				// dn2username will also check if the users belong to the allowed base
758
-				if($ocname = $this->access->dn2username($member)) {
759
-					$groupUsers[] = $ocname;
760
-				}
761
-			}
762
-		}
763
-
764
-		//and get users that have the group as primary
765
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
766
-
767
-		return count($groupUsers) + $primaryUsers;
768
-	}
769
-
770
-	/**
771
-	 * get a list of all groups
772
-	 *
773
-	 * @param string $search
774
-	 * @param $limit
775
-	 * @param int $offset
776
-	 * @return array with group names
777
-	 *
778
-	 * Returns a list with all groups (used by getGroups)
779
-	 */
780
-	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
781
-		if(!$this->enabled) {
782
-			return array();
783
-		}
784
-		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
785
-
786
-		//Check cache before driving unnecessary searches
787
-		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
788
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
789
-		if(!is_null($ldap_groups)) {
790
-			return $ldap_groups;
791
-		}
792
-
793
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
794
-		// error. With a limit of 0, we get 0 results. So we pass null.
795
-		if($limit <= 0) {
796
-			$limit = null;
797
-		}
798
-		$filter = $this->access->combineFilterWithAnd(array(
799
-			$this->access->connection->ldapGroupFilter,
800
-			$this->access->getFilterPartForGroupSearch($search)
801
-		));
802
-		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG);
803
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
804
-				array($this->access->connection->ldapGroupDisplayName, 'dn'),
805
-				$limit,
806
-				$offset);
807
-		$ldap_groups = $this->access->ownCloudGroupNames($ldap_groups);
808
-
809
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
810
-		return $ldap_groups;
811
-	}
812
-
813
-	/**
814
-	 * get a list of all groups using a paged search
815
-	 *
816
-	 * @param string $search
817
-	 * @param int $limit
818
-	 * @param int $offset
819
-	 * @return array with group names
820
-	 *
821
-	 * Returns a list with all groups
822
-	 * Uses a paged search if available to override a
823
-	 * server side search limit.
824
-	 * (active directory has a limit of 1000 by default)
825
-	 */
826
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
827
-		if(!$this->enabled) {
828
-			return array();
829
-		}
830
-		$search = $this->access->escapeFilterPart($search, true);
831
-		$pagingSize = intval($this->access->connection->ldapPagingSize);
832
-		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
833
-			return $this->getGroupsChunk($search, $limit, $offset);
834
-		}
835
-		$maxGroups = 100000; // limit max results (just for safety reasons)
836
-		if ($limit > -1) {
837
-		   $overallLimit = min($limit + $offset, $maxGroups);
838
-		} else {
839
-		   $overallLimit = $maxGroups;
840
-		}
841
-		$chunkOffset = $offset;
842
-		$allGroups = array();
843
-		while ($chunkOffset < $overallLimit) {
844
-			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
845
-			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
846
-			$nread = count($ldapGroups);
847
-			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', \OCP\Util::DEBUG);
848
-			if ($nread) {
849
-				$allGroups = array_merge($allGroups, $ldapGroups);
850
-				$chunkOffset += $nread;
851
-			}
852
-			if ($nread < $chunkLimit) {
853
-				break;
854
-			}
855
-		}
856
-		return $allGroups;
857
-	}
858
-
859
-	/**
860
-	 * @param string $group
861
-	 * @return bool
862
-	 */
863
-	public function groupMatchesFilter($group) {
864
-		return (strripos($group, $this->groupSearch) !== false);
865
-	}
866
-
867
-	/**
868
-	 * check if a group exists
869
-	 * @param string $gid
870
-	 * @return bool
871
-	 */
872
-	public function groupExists($gid) {
873
-		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
874
-		if(!is_null($groupExists)) {
875
-			return (bool)$groupExists;
876
-		}
877
-
878
-		//getting dn, if false the group does not exist. If dn, it may be mapped
879
-		//only, requires more checking.
880
-		$dn = $this->access->groupname2dn($gid);
881
-		if(!$dn) {
882
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
883
-			return false;
884
-		}
885
-
886
-		//if group really still exists, we will be able to read its objectclass
887
-		if(!is_array($this->access->readAttribute($dn, ''))) {
888
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
889
-			return false;
890
-		}
891
-
892
-		$this->access->connection->writeToCache('groupExists'.$gid, true);
893
-		return true;
894
-	}
895
-
896
-	/**
897
-	* Check if backend implements actions
898
-	* @param int $actions bitwise-or'ed actions
899
-	* @return boolean
900
-	*
901
-	* Returns the supported actions as int to be
902
-	* compared with OC_USER_BACKEND_CREATE_USER etc.
903
-	*/
904
-	public function implementsActions($actions) {
905
-		return (bool)(\OC\Group\Backend::COUNT_USERS & $actions);
906
-	}
907
-
908
-	/**
909
-	 * Return access for LDAP interaction.
910
-	 * @return Access instance of Access for LDAP interaction
911
-	 */
912
-	public function getLDAPAccess() {
913
-		return $this->access;
914
-	}
516
+            if($primaryGroup !== false) {
517
+                $groups[] = $primaryGroup;
518
+            }
519
+            $this->access->connection->writeToCache($cacheKey, $groups);
520
+            return $groups;
521
+        }
522
+
523
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
524
+        if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
525
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
526
+        ) {
527
+            $uid = $userDN;
528
+        } else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
529
+            $result = $this->access->readAttribute($userDN, 'uid');
530
+            if ($result === false) {
531
+                \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
532
+                    $this->access->connection->ldapHost, \OCP\Util::DEBUG);
533
+            }
534
+            $uid = $result[0];
535
+        } else {
536
+            // just in case
537
+            $uid = $userDN;
538
+        }
539
+
540
+        if(isset($this->cachedGroupsByMember[$uid])) {
541
+            $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
542
+        } else {
543
+            $groupsByMember = array_values($this->getGroupsByMember($uid));
544
+            $groupsByMember = $this->access->ownCloudGroupNames($groupsByMember);
545
+            $this->cachedGroupsByMember[$uid] = $groupsByMember;
546
+            $groups = array_merge($groups, $groupsByMember);
547
+        }
548
+
549
+        if($primaryGroup !== false) {
550
+            $groups[] = $primaryGroup;
551
+        }
552
+
553
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
554
+        $this->access->connection->writeToCache($cacheKey, $groups);
555
+
556
+        return $groups;
557
+    }
558
+
559
+    /**
560
+     * @param string $dn
561
+     * @param array|null &$seen
562
+     * @return array
563
+     */
564
+    private function getGroupsByMember($dn, &$seen = null) {
565
+        if ($seen === null) {
566
+            $seen = array();
567
+        }
568
+        $allGroups = array();
569
+        if (array_key_exists($dn, $seen)) {
570
+            // avoid loops
571
+            return array();
572
+        }
573
+        $seen[$dn] = true;
574
+        $filter = $this->access->combineFilterWithAnd(array(
575
+            $this->access->connection->ldapGroupFilter,
576
+            $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
577
+        ));
578
+        $groups = $this->access->fetchListOfGroups($filter,
579
+            array($this->access->connection->ldapGroupDisplayName, 'dn'));
580
+        if (is_array($groups)) {
581
+            foreach ($groups as $groupobj) {
582
+                $groupDN = $groupobj['dn'][0];
583
+                $allGroups[$groupDN] = $groupobj;
584
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
585
+                if (!empty($nestedGroups)) {
586
+                    $supergroups = $this->getGroupsByMember($groupDN, $seen);
587
+                    if (is_array($supergroups) && (count($supergroups)>0)) {
588
+                        $allGroups = array_merge($allGroups, $supergroups);
589
+                    }
590
+                }
591
+            }
592
+        }
593
+        return $allGroups;
594
+    }
595
+
596
+    /**
597
+     * get a list of all users in a group
598
+     *
599
+     * @param string $gid
600
+     * @param string $search
601
+     * @param int $limit
602
+     * @param int $offset
603
+     * @return array with user ids
604
+     */
605
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
606
+        if(!$this->enabled) {
607
+            return array();
608
+        }
609
+        if(!$this->groupExists($gid)) {
610
+            return array();
611
+        }
612
+        $search = $this->access->escapeFilterPart($search, true);
613
+        $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
614
+        // check for cache of the exact query
615
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
616
+        if(!is_null($groupUsers)) {
617
+            return $groupUsers;
618
+        }
619
+
620
+        // check for cache of the query without limit and offset
621
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
622
+        if(!is_null($groupUsers)) {
623
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
624
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
625
+            return $groupUsers;
626
+        }
627
+
628
+        if($limit === -1) {
629
+            $limit = null;
630
+        }
631
+        $groupDN = $this->access->groupname2dn($gid);
632
+        if(!$groupDN) {
633
+            // group couldn't be found, return empty resultset
634
+            $this->access->connection->writeToCache($cacheKey, array());
635
+            return array();
636
+        }
637
+
638
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
639
+        $members = array_keys($this->_groupMembers($groupDN));
640
+        if(!$members && empty($primaryUsers)) {
641
+            //in case users could not be retrieved, return empty result set
642
+            $this->access->connection->writeToCache($cacheKey, array());
643
+            return array();
644
+        }
645
+
646
+        $groupUsers = array();
647
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
648
+        $attrs = $this->access->userManager->getAttributes(true);
649
+        foreach($members as $member) {
650
+            if($isMemberUid) {
651
+                //we got uids, need to get their DNs to 'translate' them to user names
652
+                $filter = $this->access->combineFilterWithAnd(array(
653
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
654
+                    $this->access->getFilterPartForUserSearch($search)
655
+                ));
656
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
657
+                if(count($ldap_users) < 1) {
658
+                    continue;
659
+                }
660
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
661
+            } else {
662
+                //we got DNs, check if we need to filter by search or we can give back all of them
663
+                if ($search !== '') {
664
+                    if(!$this->access->readAttribute($member,
665
+                        $this->access->connection->ldapUserDisplayName,
666
+                        $this->access->getFilterPartForUserSearch($search))) {
667
+                        continue;
668
+                    }
669
+                }
670
+                // dn2username will also check if the users belong to the allowed base
671
+                if($ocname = $this->access->dn2username($member)) {
672
+                    $groupUsers[] = $ocname;
673
+                }
674
+            }
675
+        }
676
+
677
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers));
678
+        natsort($groupUsers);
679
+        $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
680
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
681
+
682
+
683
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
684
+
685
+        return $groupUsers;
686
+    }
687
+
688
+    /**
689
+     * returns the number of users in a group, who match the search term
690
+     * @param string $gid the internal group name
691
+     * @param string $search optional, a search string
692
+     * @return int|bool
693
+     */
694
+    public function countUsersInGroup($gid, $search = '') {
695
+        $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
696
+        if(!$this->enabled || !$this->groupExists($gid)) {
697
+            return false;
698
+        }
699
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
700
+        if(!is_null($groupUsers)) {
701
+            return $groupUsers;
702
+        }
703
+
704
+        $groupDN = $this->access->groupname2dn($gid);
705
+        if(!$groupDN) {
706
+            // group couldn't be found, return empty result set
707
+            $this->access->connection->writeToCache($cacheKey, false);
708
+            return false;
709
+        }
710
+
711
+        $members = array_keys($this->_groupMembers($groupDN));
712
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
713
+        if(!$members && $primaryUserCount === 0) {
714
+            //in case users could not be retrieved, return empty result set
715
+            $this->access->connection->writeToCache($cacheKey, false);
716
+            return false;
717
+        }
718
+
719
+        if ($search === '') {
720
+            $groupUsers = count($members) + $primaryUserCount;
721
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
722
+            return $groupUsers;
723
+        }
724
+        $search = $this->access->escapeFilterPart($search, true);
725
+        $isMemberUid =
726
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
727
+            === 'memberuid');
728
+
729
+        //we need to apply the search filter
730
+        //alternatives that need to be checked:
731
+        //a) get all users by search filter and array_intersect them
732
+        //b) a, but only when less than 1k 10k ?k users like it is
733
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
734
+        //   and let it count.
735
+        //For now this is not important, because the only use of this method
736
+        //does not supply a search string
737
+        $groupUsers = array();
738
+        foreach($members as $member) {
739
+            if($isMemberUid) {
740
+                //we got uids, need to get their DNs to 'translate' them to user names
741
+                $filter = $this->access->combineFilterWithAnd(array(
742
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
743
+                    $this->access->getFilterPartForUserSearch($search)
744
+                ));
745
+                $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
746
+                if(count($ldap_users) < 1) {
747
+                    continue;
748
+                }
749
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
750
+            } else {
751
+                //we need to apply the search filter now
752
+                if(!$this->access->readAttribute($member,
753
+                    $this->access->connection->ldapUserDisplayName,
754
+                    $this->access->getFilterPartForUserSearch($search))) {
755
+                    continue;
756
+                }
757
+                // dn2username will also check if the users belong to the allowed base
758
+                if($ocname = $this->access->dn2username($member)) {
759
+                    $groupUsers[] = $ocname;
760
+                }
761
+            }
762
+        }
763
+
764
+        //and get users that have the group as primary
765
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
766
+
767
+        return count($groupUsers) + $primaryUsers;
768
+    }
769
+
770
+    /**
771
+     * get a list of all groups
772
+     *
773
+     * @param string $search
774
+     * @param $limit
775
+     * @param int $offset
776
+     * @return array with group names
777
+     *
778
+     * Returns a list with all groups (used by getGroups)
779
+     */
780
+    protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
781
+        if(!$this->enabled) {
782
+            return array();
783
+        }
784
+        $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
785
+
786
+        //Check cache before driving unnecessary searches
787
+        \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, \OCP\Util::DEBUG);
788
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
789
+        if(!is_null($ldap_groups)) {
790
+            return $ldap_groups;
791
+        }
792
+
793
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
794
+        // error. With a limit of 0, we get 0 results. So we pass null.
795
+        if($limit <= 0) {
796
+            $limit = null;
797
+        }
798
+        $filter = $this->access->combineFilterWithAnd(array(
799
+            $this->access->connection->ldapGroupFilter,
800
+            $this->access->getFilterPartForGroupSearch($search)
801
+        ));
802
+        \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, \OCP\Util::DEBUG);
803
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
804
+                array($this->access->connection->ldapGroupDisplayName, 'dn'),
805
+                $limit,
806
+                $offset);
807
+        $ldap_groups = $this->access->ownCloudGroupNames($ldap_groups);
808
+
809
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
810
+        return $ldap_groups;
811
+    }
812
+
813
+    /**
814
+     * get a list of all groups using a paged search
815
+     *
816
+     * @param string $search
817
+     * @param int $limit
818
+     * @param int $offset
819
+     * @return array with group names
820
+     *
821
+     * Returns a list with all groups
822
+     * Uses a paged search if available to override a
823
+     * server side search limit.
824
+     * (active directory has a limit of 1000 by default)
825
+     */
826
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
827
+        if(!$this->enabled) {
828
+            return array();
829
+        }
830
+        $search = $this->access->escapeFilterPart($search, true);
831
+        $pagingSize = intval($this->access->connection->ldapPagingSize);
832
+        if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
833
+            return $this->getGroupsChunk($search, $limit, $offset);
834
+        }
835
+        $maxGroups = 100000; // limit max results (just for safety reasons)
836
+        if ($limit > -1) {
837
+            $overallLimit = min($limit + $offset, $maxGroups);
838
+        } else {
839
+            $overallLimit = $maxGroups;
840
+        }
841
+        $chunkOffset = $offset;
842
+        $allGroups = array();
843
+        while ($chunkOffset < $overallLimit) {
844
+            $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
845
+            $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
846
+            $nread = count($ldapGroups);
847
+            \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', \OCP\Util::DEBUG);
848
+            if ($nread) {
849
+                $allGroups = array_merge($allGroups, $ldapGroups);
850
+                $chunkOffset += $nread;
851
+            }
852
+            if ($nread < $chunkLimit) {
853
+                break;
854
+            }
855
+        }
856
+        return $allGroups;
857
+    }
858
+
859
+    /**
860
+     * @param string $group
861
+     * @return bool
862
+     */
863
+    public function groupMatchesFilter($group) {
864
+        return (strripos($group, $this->groupSearch) !== false);
865
+    }
866
+
867
+    /**
868
+     * check if a group exists
869
+     * @param string $gid
870
+     * @return bool
871
+     */
872
+    public function groupExists($gid) {
873
+        $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
874
+        if(!is_null($groupExists)) {
875
+            return (bool)$groupExists;
876
+        }
877
+
878
+        //getting dn, if false the group does not exist. If dn, it may be mapped
879
+        //only, requires more checking.
880
+        $dn = $this->access->groupname2dn($gid);
881
+        if(!$dn) {
882
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
883
+            return false;
884
+        }
885
+
886
+        //if group really still exists, we will be able to read its objectclass
887
+        if(!is_array($this->access->readAttribute($dn, ''))) {
888
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
889
+            return false;
890
+        }
891
+
892
+        $this->access->connection->writeToCache('groupExists'.$gid, true);
893
+        return true;
894
+    }
895
+
896
+    /**
897
+     * Check if backend implements actions
898
+     * @param int $actions bitwise-or'ed actions
899
+     * @return boolean
900
+     *
901
+     * Returns the supported actions as int to be
902
+     * compared with OC_USER_BACKEND_CREATE_USER etc.
903
+     */
904
+    public function implementsActions($actions) {
905
+        return (bool)(\OC\Group\Backend::COUNT_USERS & $actions);
906
+    }
907
+
908
+    /**
909
+     * Return access for LDAP interaction.
910
+     * @return Access instance of Access for LDAP interaction
911
+     */
912
+    public function getLDAPAccess() {
913
+        return $this->access;
914
+    }
915 915
 }
Please login to merge, or discard this patch.
apps/files_sharing/lib/External/Storage.php 1 patch
Indentation   +324 added lines, -324 removed lines patch added patch discarded remove patch
@@ -41,330 +41,330 @@
 block discarded – undo
41 41
 use OCP\Files\StorageNotAvailableException;
42 42
 
43 43
 class Storage extends DAV implements ISharedStorage {
44
-	/** @var ICloudId */
45
-	private $cloudId;
46
-	/** @var string */
47
-	private $mountPoint;
48
-	/** @var string */
49
-	private $token;
50
-	/** @var \OCP\ICacheFactory */
51
-	private $memcacheFactory;
52
-	/** @var \OCP\Http\Client\IClientService */
53
-	private $httpClient;
54
-	/** @var \OCP\ICertificateManager */
55
-	private $certificateManager;
56
-	/** @var bool */
57
-	private $updateChecked = false;
58
-
59
-	/**
60
-	 * @var \OCA\Files_Sharing\External\Manager
61
-	 */
62
-	private $manager;
63
-
64
-	public function __construct($options) {
65
-		$this->memcacheFactory = \OC::$server->getMemCacheFactory();
66
-		$this->httpClient = $options['HttpClientService'];
67
-		$discoveryManager = new DiscoveryManager(
68
-			$this->memcacheFactory,
69
-			$this->httpClient
70
-		);
71
-
72
-		$this->manager = $options['manager'];
73
-		$this->certificateManager = $options['certificateManager'];
74
-		$this->cloudId = $options['cloudId'];
75
-		list($protocol, $remote) = explode('://', $this->cloudId->getRemote());
76
-		if (strpos($remote, '/')) {
77
-			list($host, $root) = explode('/', $remote, 2);
78
-		} else {
79
-			$host = $remote;
80
-			$root = '';
81
-		}
82
-		$secure = $protocol === 'https';
83
-		$root = rtrim($root, '/') . $discoveryManager->getWebDavEndpoint($this->cloudId->getRemote());
84
-		$this->mountPoint = $options['mountpoint'];
85
-		$this->token = $options['token'];
86
-		parent::__construct(array(
87
-			'secure' => $secure,
88
-			'host' => $host,
89
-			'root' => $root,
90
-			'user' => $options['token'],
91
-			'password' => (string)$options['password']
92
-		));
93
-	}
94
-
95
-	public function getWatcher($path = '', $storage = null) {
96
-		if (!$storage) {
97
-			$storage = $this;
98
-		}
99
-		if (!isset($this->watcher)) {
100
-			$this->watcher = new Watcher($storage);
101
-			$this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
102
-		}
103
-		return $this->watcher;
104
-	}
105
-
106
-	public function getRemoteUser() {
107
-		return $this->cloudId->getUser();
108
-	}
109
-
110
-	public function getRemote() {
111
-		return $this->cloudId->getRemote();
112
-	}
113
-
114
-	public function getMountPoint() {
115
-		return $this->mountPoint;
116
-	}
117
-
118
-	public function getToken() {
119
-		return $this->token;
120
-	}
121
-
122
-	public function getPassword() {
123
-		return $this->password;
124
-	}
125
-
126
-	/**
127
-	 * @brief get id of the mount point
128
-	 * @return string
129
-	 */
130
-	public function getId() {
131
-		return 'shared::' . md5($this->token . '@' . $this->getRemote());
132
-	}
133
-
134
-	public function getCache($path = '', $storage = null) {
135
-		if (is_null($this->cache)) {
136
-			$this->cache = new Cache($this, $this->cloudId);
137
-		}
138
-		return $this->cache;
139
-	}
140
-
141
-	/**
142
-	 * @param string $path
143
-	 * @param \OC\Files\Storage\Storage $storage
144
-	 * @return \OCA\Files_Sharing\External\Scanner
145
-	 */
146
-	public function getScanner($path = '', $storage = null) {
147
-		if (!$storage) {
148
-			$storage = $this;
149
-		}
150
-		if (!isset($this->scanner)) {
151
-			$this->scanner = new Scanner($storage);
152
-		}
153
-		return $this->scanner;
154
-	}
155
-
156
-	/**
157
-	 * check if a file or folder has been updated since $time
158
-	 *
159
-	 * @param string $path
160
-	 * @param int $time
161
-	 * @throws \OCP\Files\StorageNotAvailableException
162
-	 * @throws \OCP\Files\StorageInvalidException
163
-	 * @return bool
164
-	 */
165
-	public function hasUpdated($path, $time) {
166
-		// since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
167
-		// because of that we only do one check for the entire storage per request
168
-		if ($this->updateChecked) {
169
-			return false;
170
-		}
171
-		$this->updateChecked = true;
172
-		try {
173
-			return parent::hasUpdated('', $time);
174
-		} catch (StorageInvalidException $e) {
175
-			// check if it needs to be removed
176
-			$this->checkStorageAvailability();
177
-			throw $e;
178
-		} catch (StorageNotAvailableException $e) {
179
-			// check if it needs to be removed or just temp unavailable
180
-			$this->checkStorageAvailability();
181
-			throw $e;
182
-		}
183
-	}
184
-
185
-	public function test() {
186
-		try {
187
-			return parent::test();
188
-		} catch (StorageInvalidException $e) {
189
-			// check if it needs to be removed
190
-			$this->checkStorageAvailability();
191
-			throw $e;
192
-		} catch (StorageNotAvailableException $e) {
193
-			// check if it needs to be removed or just temp unavailable
194
-			$this->checkStorageAvailability();
195
-			throw $e;
196
-		}
197
-	}
198
-
199
-	/**
200
-	 * Check whether this storage is permanently or temporarily
201
-	 * unavailable
202
-	 *
203
-	 * @throws \OCP\Files\StorageNotAvailableException
204
-	 * @throws \OCP\Files\StorageInvalidException
205
-	 */
206
-	public function checkStorageAvailability() {
207
-		// see if we can find out why the share is unavailable
208
-		try {
209
-			$this->getShareInfo();
210
-		} catch (NotFoundException $e) {
211
-			// a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
212
-			if ($this->testRemote()) {
213
-				// valid Nextcloud instance means that the public share no longer exists
214
-				// since this is permanent (re-sharing the file will create a new token)
215
-				// we remove the invalid storage
216
-				$this->manager->removeShare($this->mountPoint);
217
-				$this->manager->getMountManager()->removeMount($this->mountPoint);
218
-				throw new StorageInvalidException();
219
-			} else {
220
-				// Nextcloud instance is gone, likely to be a temporary server configuration error
221
-				throw new StorageNotAvailableException();
222
-			}
223
-		} catch (ForbiddenException $e) {
224
-			// auth error, remove share for now (provide a dialog in the future)
225
-			$this->manager->removeShare($this->mountPoint);
226
-			$this->manager->getMountManager()->removeMount($this->mountPoint);
227
-			throw new StorageInvalidException();
228
-		} catch (\GuzzleHttp\Exception\ConnectException $e) {
229
-			throw new StorageNotAvailableException();
230
-		} catch (\GuzzleHttp\Exception\RequestException $e) {
231
-			throw new StorageNotAvailableException();
232
-		} catch (\Exception $e) {
233
-			throw $e;
234
-		}
235
-	}
236
-
237
-	public function file_exists($path) {
238
-		if ($path === '') {
239
-			return true;
240
-		} else {
241
-			return parent::file_exists($path);
242
-		}
243
-	}
244
-
245
-	/**
246
-	 * check if the configured remote is a valid federated share provider
247
-	 *
248
-	 * @return bool
249
-	 */
250
-	protected function testRemote() {
251
-		try {
252
-			return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
253
-				|| $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
254
-				|| $this->testRemoteUrl($this->getRemote() . '/status.php');
255
-		} catch (\Exception $e) {
256
-			return false;
257
-		}
258
-	}
259
-
260
-	/**
261
-	 * @param string $url
262
-	 * @return bool
263
-	 */
264
-	private function testRemoteUrl($url) {
265
-		$cache = $this->memcacheFactory->create('files_sharing_remote_url');
266
-		if($cache->hasKey($url)) {
267
-			return (bool)$cache->get($url);
268
-		}
269
-
270
-		$client = $this->httpClient->newClient();
271
-		try {
272
-			$result = $client->get($url, [
273
-				'timeout' => 10,
274
-				'connect_timeout' => 10,
275
-			])->getBody();
276
-			$data = json_decode($result);
277
-			$returnValue = (is_object($data) && !empty($data->version));
278
-		} catch (ConnectException $e) {
279
-			$returnValue = false;
280
-		} catch (ClientException $e) {
281
-			$returnValue = false;
282
-		}
283
-
284
-		$cache->set($url, $returnValue);
285
-		return $returnValue;
286
-	}
287
-
288
-	/**
289
-	 * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
290
-	 * standardized. Let's use this to detect whether to use it.
291
-	 *
292
-	 * @return bool
293
-	 */
294
-	public function remoteIsOwnCloud() {
295
-		if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
296
-			return false;
297
-		}
298
-		return true;
299
-	}
300
-
301
-	/**
302
-	 * @return mixed
303
-	 * @throws ForbiddenException
304
-	 * @throws NotFoundException
305
-	 * @throws \Exception
306
-	 */
307
-	public function getShareInfo() {
308
-		$remote = $this->getRemote();
309
-		$token = $this->getToken();
310
-		$password = $this->getPassword();
311
-
312
-		// If remote is not an ownCloud do not try to get any share info
313
-		if(!$this->remoteIsOwnCloud()) {
314
-			return ['status' => 'unsupported'];
315
-		}
316
-
317
-		$url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
318
-
319
-		// TODO: DI
320
-		$client = \OC::$server->getHTTPClientService()->newClient();
321
-		try {
322
-			$response = $client->post($url, [
323
-				'body' => ['password' => $password],
324
-				'timeout' => 10,
325
-				'connect_timeout' => 10,
326
-			]);
327
-		} catch (\GuzzleHttp\Exception\RequestException $e) {
328
-			if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
329
-				throw new ForbiddenException();
330
-			}
331
-			if ($e->getCode() === Http::STATUS_NOT_FOUND) {
332
-				throw new NotFoundException();
333
-			}
334
-			// throw this to be on the safe side: the share will still be visible
335
-			// in the UI in case the failure is intermittent, and the user will
336
-			// be able to decide whether to remove it if it's really gone
337
-			throw new StorageNotAvailableException();
338
-		}
339
-
340
-		return json_decode($response->getBody(), true);
341
-	}
342
-
343
-	public function getOwner($path) {
344
-		return $this->cloudId->getDisplayId();
345
-	}
346
-
347
-	public function isSharable($path) {
348
-		if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
349
-			return false;
350
-		}
351
-		return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE);
352
-	}
44
+    /** @var ICloudId */
45
+    private $cloudId;
46
+    /** @var string */
47
+    private $mountPoint;
48
+    /** @var string */
49
+    private $token;
50
+    /** @var \OCP\ICacheFactory */
51
+    private $memcacheFactory;
52
+    /** @var \OCP\Http\Client\IClientService */
53
+    private $httpClient;
54
+    /** @var \OCP\ICertificateManager */
55
+    private $certificateManager;
56
+    /** @var bool */
57
+    private $updateChecked = false;
58
+
59
+    /**
60
+     * @var \OCA\Files_Sharing\External\Manager
61
+     */
62
+    private $manager;
63
+
64
+    public function __construct($options) {
65
+        $this->memcacheFactory = \OC::$server->getMemCacheFactory();
66
+        $this->httpClient = $options['HttpClientService'];
67
+        $discoveryManager = new DiscoveryManager(
68
+            $this->memcacheFactory,
69
+            $this->httpClient
70
+        );
71
+
72
+        $this->manager = $options['manager'];
73
+        $this->certificateManager = $options['certificateManager'];
74
+        $this->cloudId = $options['cloudId'];
75
+        list($protocol, $remote) = explode('://', $this->cloudId->getRemote());
76
+        if (strpos($remote, '/')) {
77
+            list($host, $root) = explode('/', $remote, 2);
78
+        } else {
79
+            $host = $remote;
80
+            $root = '';
81
+        }
82
+        $secure = $protocol === 'https';
83
+        $root = rtrim($root, '/') . $discoveryManager->getWebDavEndpoint($this->cloudId->getRemote());
84
+        $this->mountPoint = $options['mountpoint'];
85
+        $this->token = $options['token'];
86
+        parent::__construct(array(
87
+            'secure' => $secure,
88
+            'host' => $host,
89
+            'root' => $root,
90
+            'user' => $options['token'],
91
+            'password' => (string)$options['password']
92
+        ));
93
+    }
94
+
95
+    public function getWatcher($path = '', $storage = null) {
96
+        if (!$storage) {
97
+            $storage = $this;
98
+        }
99
+        if (!isset($this->watcher)) {
100
+            $this->watcher = new Watcher($storage);
101
+            $this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
102
+        }
103
+        return $this->watcher;
104
+    }
105
+
106
+    public function getRemoteUser() {
107
+        return $this->cloudId->getUser();
108
+    }
109
+
110
+    public function getRemote() {
111
+        return $this->cloudId->getRemote();
112
+    }
113
+
114
+    public function getMountPoint() {
115
+        return $this->mountPoint;
116
+    }
117
+
118
+    public function getToken() {
119
+        return $this->token;
120
+    }
121
+
122
+    public function getPassword() {
123
+        return $this->password;
124
+    }
125
+
126
+    /**
127
+     * @brief get id of the mount point
128
+     * @return string
129
+     */
130
+    public function getId() {
131
+        return 'shared::' . md5($this->token . '@' . $this->getRemote());
132
+    }
133
+
134
+    public function getCache($path = '', $storage = null) {
135
+        if (is_null($this->cache)) {
136
+            $this->cache = new Cache($this, $this->cloudId);
137
+        }
138
+        return $this->cache;
139
+    }
140
+
141
+    /**
142
+     * @param string $path
143
+     * @param \OC\Files\Storage\Storage $storage
144
+     * @return \OCA\Files_Sharing\External\Scanner
145
+     */
146
+    public function getScanner($path = '', $storage = null) {
147
+        if (!$storage) {
148
+            $storage = $this;
149
+        }
150
+        if (!isset($this->scanner)) {
151
+            $this->scanner = new Scanner($storage);
152
+        }
153
+        return $this->scanner;
154
+    }
155
+
156
+    /**
157
+     * check if a file or folder has been updated since $time
158
+     *
159
+     * @param string $path
160
+     * @param int $time
161
+     * @throws \OCP\Files\StorageNotAvailableException
162
+     * @throws \OCP\Files\StorageInvalidException
163
+     * @return bool
164
+     */
165
+    public function hasUpdated($path, $time) {
166
+        // since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
167
+        // because of that we only do one check for the entire storage per request
168
+        if ($this->updateChecked) {
169
+            return false;
170
+        }
171
+        $this->updateChecked = true;
172
+        try {
173
+            return parent::hasUpdated('', $time);
174
+        } catch (StorageInvalidException $e) {
175
+            // check if it needs to be removed
176
+            $this->checkStorageAvailability();
177
+            throw $e;
178
+        } catch (StorageNotAvailableException $e) {
179
+            // check if it needs to be removed or just temp unavailable
180
+            $this->checkStorageAvailability();
181
+            throw $e;
182
+        }
183
+    }
184
+
185
+    public function test() {
186
+        try {
187
+            return parent::test();
188
+        } catch (StorageInvalidException $e) {
189
+            // check if it needs to be removed
190
+            $this->checkStorageAvailability();
191
+            throw $e;
192
+        } catch (StorageNotAvailableException $e) {
193
+            // check if it needs to be removed or just temp unavailable
194
+            $this->checkStorageAvailability();
195
+            throw $e;
196
+        }
197
+    }
198
+
199
+    /**
200
+     * Check whether this storage is permanently or temporarily
201
+     * unavailable
202
+     *
203
+     * @throws \OCP\Files\StorageNotAvailableException
204
+     * @throws \OCP\Files\StorageInvalidException
205
+     */
206
+    public function checkStorageAvailability() {
207
+        // see if we can find out why the share is unavailable
208
+        try {
209
+            $this->getShareInfo();
210
+        } catch (NotFoundException $e) {
211
+            // a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
212
+            if ($this->testRemote()) {
213
+                // valid Nextcloud instance means that the public share no longer exists
214
+                // since this is permanent (re-sharing the file will create a new token)
215
+                // we remove the invalid storage
216
+                $this->manager->removeShare($this->mountPoint);
217
+                $this->manager->getMountManager()->removeMount($this->mountPoint);
218
+                throw new StorageInvalidException();
219
+            } else {
220
+                // Nextcloud instance is gone, likely to be a temporary server configuration error
221
+                throw new StorageNotAvailableException();
222
+            }
223
+        } catch (ForbiddenException $e) {
224
+            // auth error, remove share for now (provide a dialog in the future)
225
+            $this->manager->removeShare($this->mountPoint);
226
+            $this->manager->getMountManager()->removeMount($this->mountPoint);
227
+            throw new StorageInvalidException();
228
+        } catch (\GuzzleHttp\Exception\ConnectException $e) {
229
+            throw new StorageNotAvailableException();
230
+        } catch (\GuzzleHttp\Exception\RequestException $e) {
231
+            throw new StorageNotAvailableException();
232
+        } catch (\Exception $e) {
233
+            throw $e;
234
+        }
235
+    }
236
+
237
+    public function file_exists($path) {
238
+        if ($path === '') {
239
+            return true;
240
+        } else {
241
+            return parent::file_exists($path);
242
+        }
243
+    }
244
+
245
+    /**
246
+     * check if the configured remote is a valid federated share provider
247
+     *
248
+     * @return bool
249
+     */
250
+    protected function testRemote() {
251
+        try {
252
+            return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
253
+                || $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
254
+                || $this->testRemoteUrl($this->getRemote() . '/status.php');
255
+        } catch (\Exception $e) {
256
+            return false;
257
+        }
258
+    }
259
+
260
+    /**
261
+     * @param string $url
262
+     * @return bool
263
+     */
264
+    private function testRemoteUrl($url) {
265
+        $cache = $this->memcacheFactory->create('files_sharing_remote_url');
266
+        if($cache->hasKey($url)) {
267
+            return (bool)$cache->get($url);
268
+        }
269
+
270
+        $client = $this->httpClient->newClient();
271
+        try {
272
+            $result = $client->get($url, [
273
+                'timeout' => 10,
274
+                'connect_timeout' => 10,
275
+            ])->getBody();
276
+            $data = json_decode($result);
277
+            $returnValue = (is_object($data) && !empty($data->version));
278
+        } catch (ConnectException $e) {
279
+            $returnValue = false;
280
+        } catch (ClientException $e) {
281
+            $returnValue = false;
282
+        }
283
+
284
+        $cache->set($url, $returnValue);
285
+        return $returnValue;
286
+    }
287
+
288
+    /**
289
+     * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
290
+     * standardized. Let's use this to detect whether to use it.
291
+     *
292
+     * @return bool
293
+     */
294
+    public function remoteIsOwnCloud() {
295
+        if(defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
296
+            return false;
297
+        }
298
+        return true;
299
+    }
300
+
301
+    /**
302
+     * @return mixed
303
+     * @throws ForbiddenException
304
+     * @throws NotFoundException
305
+     * @throws \Exception
306
+     */
307
+    public function getShareInfo() {
308
+        $remote = $this->getRemote();
309
+        $token = $this->getToken();
310
+        $password = $this->getPassword();
311
+
312
+        // If remote is not an ownCloud do not try to get any share info
313
+        if(!$this->remoteIsOwnCloud()) {
314
+            return ['status' => 'unsupported'];
315
+        }
316
+
317
+        $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
318
+
319
+        // TODO: DI
320
+        $client = \OC::$server->getHTTPClientService()->newClient();
321
+        try {
322
+            $response = $client->post($url, [
323
+                'body' => ['password' => $password],
324
+                'timeout' => 10,
325
+                'connect_timeout' => 10,
326
+            ]);
327
+        } catch (\GuzzleHttp\Exception\RequestException $e) {
328
+            if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
329
+                throw new ForbiddenException();
330
+            }
331
+            if ($e->getCode() === Http::STATUS_NOT_FOUND) {
332
+                throw new NotFoundException();
333
+            }
334
+            // throw this to be on the safe side: the share will still be visible
335
+            // in the UI in case the failure is intermittent, and the user will
336
+            // be able to decide whether to remove it if it's really gone
337
+            throw new StorageNotAvailableException();
338
+        }
339
+
340
+        return json_decode($response->getBody(), true);
341
+    }
342
+
343
+    public function getOwner($path) {
344
+        return $this->cloudId->getDisplayId();
345
+    }
346
+
347
+    public function isSharable($path) {
348
+        if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
349
+            return false;
350
+        }
351
+        return ($this->getPermissions($path) & \OCP\Constants::PERMISSION_SHARE);
352
+    }
353 353
 	
354
-	public function getPermissions($path) {
355
-		$response = $this->propfind($path);
356
-		if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
357
-			$permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
358
-		} else {
359
-			// use default permission if remote server doesn't provide the share permissions
360
-			if ($this->is_dir($path)) {
361
-				$permissions = \OCP\Constants::PERMISSION_ALL;
362
-			} else {
363
-				$permissions = \OCP\Constants::PERMISSION_ALL & ~\OCP\Constants::PERMISSION_CREATE;
364
-			}
365
-		}
366
-
367
-		return $permissions;
368
-	}
354
+    public function getPermissions($path) {
355
+        $response = $this->propfind($path);
356
+        if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
357
+            $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
358
+        } else {
359
+            // use default permission if remote server doesn't provide the share permissions
360
+            if ($this->is_dir($path)) {
361
+                $permissions = \OCP\Constants::PERMISSION_ALL;
362
+            } else {
363
+                $permissions = \OCP\Constants::PERMISSION_ALL & ~\OCP\Constants::PERMISSION_CREATE;
364
+            }
365
+        }
366
+
367
+        return $permissions;
368
+    }
369 369
 
370 370
 }
Please login to merge, or discard this patch.
apps/federation/lib/TrustedServers.php 1 patch
Indentation   +237 added lines, -237 removed lines patch added patch discarded remove patch
@@ -38,241 +38,241 @@
 block discarded – undo
38 38
 
39 39
 class TrustedServers {
40 40
 
41
-	/** after a user list was exchanged at least once successfully */
42
-	const STATUS_OK = 1;
43
-	/** waiting for shared secret or initial user list exchange */
44
-	const STATUS_PENDING = 2;
45
-	/** something went wrong, misconfigured server, software bug,... user interaction needed */
46
-	const STATUS_FAILURE = 3;
47
-	/** remote server revoked access */
48
-	const STATUS_ACCESS_REVOKED = 4;
49
-
50
-	/** @var  dbHandler */
51
-	private $dbHandler;
52
-
53
-	/** @var  IClientService */
54
-	private $httpClientService;
55
-
56
-	/** @var ILogger */
57
-	private $logger;
58
-
59
-	/** @var IJobList */
60
-	private $jobList;
61
-
62
-	/** @var ISecureRandom */
63
-	private $secureRandom;
64
-
65
-	/** @var IConfig */
66
-	private $config;
67
-
68
-	/** @var EventDispatcherInterface */
69
-	private $dispatcher;
70
-
71
-	/**
72
-	 * @param DbHandler $dbHandler
73
-	 * @param IClientService $httpClientService
74
-	 * @param ILogger $logger
75
-	 * @param IJobList $jobList
76
-	 * @param ISecureRandom $secureRandom
77
-	 * @param IConfig $config
78
-	 * @param EventDispatcherInterface $dispatcher
79
-	 */
80
-	public function __construct(
81
-		DbHandler $dbHandler,
82
-		IClientService $httpClientService,
83
-		ILogger $logger,
84
-		IJobList $jobList,
85
-		ISecureRandom $secureRandom,
86
-		IConfig $config,
87
-		EventDispatcherInterface $dispatcher
88
-	) {
89
-		$this->dbHandler = $dbHandler;
90
-		$this->httpClientService = $httpClientService;
91
-		$this->logger = $logger;
92
-		$this->jobList = $jobList;
93
-		$this->secureRandom = $secureRandom;
94
-		$this->config = $config;
95
-		$this->dispatcher = $dispatcher;
96
-	}
97
-
98
-	/**
99
-	 * add server to the list of trusted servers
100
-	 *
101
-	 * @param $url
102
-	 * @return int server id
103
-	 */
104
-	public function addServer($url) {
105
-		$url = $this->updateProtocol($url);
106
-		$result = $this->dbHandler->addServer($url);
107
-		if ($result) {
108
-			$token = $this->secureRandom->generate(16);
109
-			$this->dbHandler->addToken($url, $token);
110
-			$this->jobList->add(
111
-				'OCA\Federation\BackgroundJob\RequestSharedSecret',
112
-				[
113
-					'url' => $url,
114
-					'token' => $token
115
-				]
116
-			);
117
-		}
118
-
119
-		return $result;
120
-	}
121
-
122
-	/**
123
-	 * enable/disable to automatically add servers to the list of trusted servers
124
-	 * once a federated share was created and accepted successfully
125
-	 *
126
-	 * @param bool $status
127
-	 */
128
-	public function setAutoAddServers($status) {
129
-		$value = $status ? '1' : '0';
130
-		$this->config->setAppValue('federation', 'autoAddServers', $value);
131
-	}
132
-
133
-	/**
134
-	 * return if we automatically add servers to the list of trusted servers
135
-	 * once a federated share was created and accepted successfully
136
-	 *
137
-	 * @return bool
138
-	 */
139
-	public function getAutoAddServers() {
140
-		$value = $this->config->getAppValue('federation', 'autoAddServers', '0');
141
-		return $value === '1';
142
-	}
143
-
144
-	/**
145
-	 * get shared secret for the given server
146
-	 *
147
-	 * @param string $url
148
-	 * @return string
149
-	 */
150
-	public function getSharedSecret($url) {
151
-		return $this->dbHandler->getSharedSecret($url);
152
-	}
153
-
154
-	/**
155
-	 * add shared secret for the given server
156
-	 *
157
-	 * @param string $url
158
-	 * @param $sharedSecret
159
-	 */
160
-	public function addSharedSecret($url, $sharedSecret) {
161
-		$this->dbHandler->addSharedSecret($url, $sharedSecret);
162
-	}
163
-
164
-	/**
165
-	 * remove server from the list of trusted servers
166
-	 *
167
-	 * @param int $id
168
-	 */
169
-	public function removeServer($id) {
170
-		$server = $this->dbHandler->getServerById($id);
171
-		$this->dbHandler->removeServer($id);
172
-		$event = new GenericEvent($server['url_hash']);
173
-		$this->dispatcher->dispatch('OCP\Federation\TrustedServerEvent::remove', $event);
174
-	}
175
-
176
-	/**
177
-	 * get all trusted servers
178
-	 *
179
-	 * @return array
180
-	 */
181
-	public function getServers() {
182
-		return $this->dbHandler->getAllServer();
183
-	}
184
-
185
-	/**
186
-	 * check if given server is a trusted Nextcloud server
187
-	 *
188
-	 * @param string $url
189
-	 * @return bool
190
-	 */
191
-	public function isTrustedServer($url) {
192
-		return $this->dbHandler->serverExists($url);
193
-	}
194
-
195
-	/**
196
-	 * set server status
197
-	 *
198
-	 * @param string $url
199
-	 * @param int $status
200
-	 */
201
-	public function setServerStatus($url, $status) {
202
-		$this->dbHandler->setServerStatus($url, $status);
203
-	}
204
-
205
-	/**
206
-	 * @param string $url
207
-	 * @return int
208
-	 */
209
-	public function getServerStatus($url) {
210
-		return $this->dbHandler->getServerStatus($url);
211
-	}
212
-
213
-	/**
214
-	 * check if URL point to a ownCloud/Nextcloud server
215
-	 *
216
-	 * @param string $url
217
-	 * @return bool
218
-	 */
219
-	public function isOwnCloudServer($url) {
220
-		$isValidOwnCloud = false;
221
-		$client = $this->httpClientService->newClient();
222
-		try {
223
-			$result = $client->get(
224
-				$url . '/status.php',
225
-				[
226
-					'timeout' => 3,
227
-					'connect_timeout' => 3,
228
-				]
229
-			);
230
-			if ($result->getStatusCode() === Http::STATUS_OK) {
231
-				$isValidOwnCloud = $this->checkOwnCloudVersion($result->getBody());
232
-
233
-			}
234
-		} catch (\Exception $e) {
235
-			$this->logger->debug('No Nextcloud server: ' . $e->getMessage());
236
-			return false;
237
-		}
238
-
239
-		return $isValidOwnCloud;
240
-	}
241
-
242
-	/**
243
-	 * check if ownCloud version is >= 9.0
244
-	 *
245
-	 * @param $status
246
-	 * @return bool
247
-	 * @throws HintException
248
-	 */
249
-	protected function checkOwnCloudVersion($status) {
250
-		$decoded = json_decode($status, true);
251
-		if (!empty($decoded) && isset($decoded['version'])) {
252
-			if (!version_compare($decoded['version'], '9.0.0', '>=')) {
253
-				throw new HintException('Remote server version is too low. 9.0 is required.');
254
-			}
255
-			return true;
256
-		}
257
-		return false;
258
-	}
259
-
260
-	/**
261
-	 * check if the URL contain a protocol, if not add https
262
-	 *
263
-	 * @param string $url
264
-	 * @return string
265
-	 */
266
-	protected function updateProtocol($url) {
267
-		if (
268
-			strpos($url, 'https://') === 0
269
-			|| strpos($url, 'http://') === 0
270
-		) {
271
-
272
-			return $url;
273
-
274
-		}
275
-
276
-		return 'https://' . $url;
277
-	}
41
+    /** after a user list was exchanged at least once successfully */
42
+    const STATUS_OK = 1;
43
+    /** waiting for shared secret or initial user list exchange */
44
+    const STATUS_PENDING = 2;
45
+    /** something went wrong, misconfigured server, software bug,... user interaction needed */
46
+    const STATUS_FAILURE = 3;
47
+    /** remote server revoked access */
48
+    const STATUS_ACCESS_REVOKED = 4;
49
+
50
+    /** @var  dbHandler */
51
+    private $dbHandler;
52
+
53
+    /** @var  IClientService */
54
+    private $httpClientService;
55
+
56
+    /** @var ILogger */
57
+    private $logger;
58
+
59
+    /** @var IJobList */
60
+    private $jobList;
61
+
62
+    /** @var ISecureRandom */
63
+    private $secureRandom;
64
+
65
+    /** @var IConfig */
66
+    private $config;
67
+
68
+    /** @var EventDispatcherInterface */
69
+    private $dispatcher;
70
+
71
+    /**
72
+     * @param DbHandler $dbHandler
73
+     * @param IClientService $httpClientService
74
+     * @param ILogger $logger
75
+     * @param IJobList $jobList
76
+     * @param ISecureRandom $secureRandom
77
+     * @param IConfig $config
78
+     * @param EventDispatcherInterface $dispatcher
79
+     */
80
+    public function __construct(
81
+        DbHandler $dbHandler,
82
+        IClientService $httpClientService,
83
+        ILogger $logger,
84
+        IJobList $jobList,
85
+        ISecureRandom $secureRandom,
86
+        IConfig $config,
87
+        EventDispatcherInterface $dispatcher
88
+    ) {
89
+        $this->dbHandler = $dbHandler;
90
+        $this->httpClientService = $httpClientService;
91
+        $this->logger = $logger;
92
+        $this->jobList = $jobList;
93
+        $this->secureRandom = $secureRandom;
94
+        $this->config = $config;
95
+        $this->dispatcher = $dispatcher;
96
+    }
97
+
98
+    /**
99
+     * add server to the list of trusted servers
100
+     *
101
+     * @param $url
102
+     * @return int server id
103
+     */
104
+    public function addServer($url) {
105
+        $url = $this->updateProtocol($url);
106
+        $result = $this->dbHandler->addServer($url);
107
+        if ($result) {
108
+            $token = $this->secureRandom->generate(16);
109
+            $this->dbHandler->addToken($url, $token);
110
+            $this->jobList->add(
111
+                'OCA\Federation\BackgroundJob\RequestSharedSecret',
112
+                [
113
+                    'url' => $url,
114
+                    'token' => $token
115
+                ]
116
+            );
117
+        }
118
+
119
+        return $result;
120
+    }
121
+
122
+    /**
123
+     * enable/disable to automatically add servers to the list of trusted servers
124
+     * once a federated share was created and accepted successfully
125
+     *
126
+     * @param bool $status
127
+     */
128
+    public function setAutoAddServers($status) {
129
+        $value = $status ? '1' : '0';
130
+        $this->config->setAppValue('federation', 'autoAddServers', $value);
131
+    }
132
+
133
+    /**
134
+     * return if we automatically add servers to the list of trusted servers
135
+     * once a federated share was created and accepted successfully
136
+     *
137
+     * @return bool
138
+     */
139
+    public function getAutoAddServers() {
140
+        $value = $this->config->getAppValue('federation', 'autoAddServers', '0');
141
+        return $value === '1';
142
+    }
143
+
144
+    /**
145
+     * get shared secret for the given server
146
+     *
147
+     * @param string $url
148
+     * @return string
149
+     */
150
+    public function getSharedSecret($url) {
151
+        return $this->dbHandler->getSharedSecret($url);
152
+    }
153
+
154
+    /**
155
+     * add shared secret for the given server
156
+     *
157
+     * @param string $url
158
+     * @param $sharedSecret
159
+     */
160
+    public function addSharedSecret($url, $sharedSecret) {
161
+        $this->dbHandler->addSharedSecret($url, $sharedSecret);
162
+    }
163
+
164
+    /**
165
+     * remove server from the list of trusted servers
166
+     *
167
+     * @param int $id
168
+     */
169
+    public function removeServer($id) {
170
+        $server = $this->dbHandler->getServerById($id);
171
+        $this->dbHandler->removeServer($id);
172
+        $event = new GenericEvent($server['url_hash']);
173
+        $this->dispatcher->dispatch('OCP\Federation\TrustedServerEvent::remove', $event);
174
+    }
175
+
176
+    /**
177
+     * get all trusted servers
178
+     *
179
+     * @return array
180
+     */
181
+    public function getServers() {
182
+        return $this->dbHandler->getAllServer();
183
+    }
184
+
185
+    /**
186
+     * check if given server is a trusted Nextcloud server
187
+     *
188
+     * @param string $url
189
+     * @return bool
190
+     */
191
+    public function isTrustedServer($url) {
192
+        return $this->dbHandler->serverExists($url);
193
+    }
194
+
195
+    /**
196
+     * set server status
197
+     *
198
+     * @param string $url
199
+     * @param int $status
200
+     */
201
+    public function setServerStatus($url, $status) {
202
+        $this->dbHandler->setServerStatus($url, $status);
203
+    }
204
+
205
+    /**
206
+     * @param string $url
207
+     * @return int
208
+     */
209
+    public function getServerStatus($url) {
210
+        return $this->dbHandler->getServerStatus($url);
211
+    }
212
+
213
+    /**
214
+     * check if URL point to a ownCloud/Nextcloud server
215
+     *
216
+     * @param string $url
217
+     * @return bool
218
+     */
219
+    public function isOwnCloudServer($url) {
220
+        $isValidOwnCloud = false;
221
+        $client = $this->httpClientService->newClient();
222
+        try {
223
+            $result = $client->get(
224
+                $url . '/status.php',
225
+                [
226
+                    'timeout' => 3,
227
+                    'connect_timeout' => 3,
228
+                ]
229
+            );
230
+            if ($result->getStatusCode() === Http::STATUS_OK) {
231
+                $isValidOwnCloud = $this->checkOwnCloudVersion($result->getBody());
232
+
233
+            }
234
+        } catch (\Exception $e) {
235
+            $this->logger->debug('No Nextcloud server: ' . $e->getMessage());
236
+            return false;
237
+        }
238
+
239
+        return $isValidOwnCloud;
240
+    }
241
+
242
+    /**
243
+     * check if ownCloud version is >= 9.0
244
+     *
245
+     * @param $status
246
+     * @return bool
247
+     * @throws HintException
248
+     */
249
+    protected function checkOwnCloudVersion($status) {
250
+        $decoded = json_decode($status, true);
251
+        if (!empty($decoded) && isset($decoded['version'])) {
252
+            if (!version_compare($decoded['version'], '9.0.0', '>=')) {
253
+                throw new HintException('Remote server version is too low. 9.0 is required.');
254
+            }
255
+            return true;
256
+        }
257
+        return false;
258
+    }
259
+
260
+    /**
261
+     * check if the URL contain a protocol, if not add https
262
+     *
263
+     * @param string $url
264
+     * @return string
265
+     */
266
+    protected function updateProtocol($url) {
267
+        if (
268
+            strpos($url, 'https://') === 0
269
+            || strpos($url, 'http://') === 0
270
+        ) {
271
+
272
+            return $url;
273
+
274
+        }
275
+
276
+        return 'https://' . $url;
277
+    }
278 278
 }
Please login to merge, or discard this patch.