Passed
Push — master ( 087343...c1eff7 )
by Blizzz
13:03 queued 10s
created
settings/Controller/UsersController.php 1 patch
Indentation   +432 added lines, -432 removed lines patch added patch discarded remove patch
@@ -66,436 +66,436 @@
 block discarded – undo
66 66
  * @package OC\Settings\Controller
67 67
  */
68 68
 class UsersController extends Controller {
69
-	/** @var IUserManager */
70
-	private $userManager;
71
-	/** @var IGroupManager */
72
-	private $groupManager;
73
-	/** @var IUserSession */
74
-	private $userSession;
75
-	/** @var IConfig */
76
-	private $config;
77
-	/** @var bool */
78
-	private $isAdmin;
79
-	/** @var IL10N */
80
-	private $l10n;
81
-	/** @var IMailer */
82
-	private $mailer;
83
-	/** @var IFactory */
84
-	private $l10nFactory;
85
-	/** @var IAppManager */
86
-	private $appManager;
87
-	/** @var AccountManager */
88
-	private $accountManager;
89
-	/** @var Manager */
90
-	private $keyManager;
91
-	/** @var IJobList */
92
-	private $jobList;
93
-	/** @var IManager */
94
-	private $encryptionManager;
95
-
96
-
97
-	public function __construct(string $appName,
98
-								IRequest $request,
99
-								IUserManager $userManager,
100
-								IGroupManager $groupManager,
101
-								IUserSession $userSession,
102
-								IConfig $config,
103
-								bool $isAdmin,
104
-								IL10N $l10n,
105
-								IMailer $mailer,
106
-								IFactory $l10nFactory,
107
-								IAppManager $appManager,
108
-								AccountManager $accountManager,
109
-								Manager $keyManager,
110
-								IJobList $jobList,
111
-								IManager $encryptionManager) {
112
-		parent::__construct($appName, $request);
113
-		$this->userManager = $userManager;
114
-		$this->groupManager = $groupManager;
115
-		$this->userSession = $userSession;
116
-		$this->config = $config;
117
-		$this->isAdmin = $isAdmin;
118
-		$this->l10n = $l10n;
119
-		$this->mailer = $mailer;
120
-		$this->l10nFactory = $l10nFactory;
121
-		$this->appManager = $appManager;
122
-		$this->accountManager = $accountManager;
123
-		$this->keyManager = $keyManager;
124
-		$this->jobList = $jobList;
125
-		$this->encryptionManager = $encryptionManager;
126
-	}
127
-
128
-
129
-	/**
130
-	 * @NoCSRFRequired
131
-	 * @NoAdminRequired
132
-	 *
133
-	 * Display users list template
134
-	 *
135
-	 * @return TemplateResponse
136
-	 */
137
-	public function usersListByGroup() {
138
-		return $this->usersList();
139
-	}
140
-
141
-	/**
142
-	 * @NoCSRFRequired
143
-	 * @NoAdminRequired
144
-	 *
145
-	 * Display users list template
146
-	 *
147
-	 * @return TemplateResponse
148
-	 */
149
-	public function usersList() {
150
-		$user = $this->userSession->getUser();
151
-		$uid = $user->getUID();
152
-
153
-		\OC::$server->getNavigationManager()->setActiveEntry('core_users');
154
-
155
-		/* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
156
-		$sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT;
157
-		$isLDAPUsed = false;
158
-		if ($this->config->getSystemValue('sort_groups_by_name', false)) {
159
-			$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
160
-		} else {
161
-			if ($this->appManager->isEnabledForUser('user_ldap')) {
162
-				$isLDAPUsed =
163
-					$this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
164
-				if ($isLDAPUsed) {
165
-					// LDAP user count can be slow, so we sort by group name here
166
-					$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
167
-				}
168
-			}
169
-		}
170
-
171
-		$canChangePassword = $this->canAdminChangeUserPasswords();
172
-
173
-		/* GROUPS */
174
-		$groupsInfo = new \OC\Group\MetaData(
175
-			$uid,
176
-			$this->isAdmin,
177
-			$this->groupManager,
178
-			$this->userSession
179
-		);
180
-
181
-		$groupsInfo->setSorting($sortGroupsBy);
182
-		list($adminGroup, $groups) = $groupsInfo->get();
183
-
184
-		if(!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
185
-			$isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
186
-				return $ldapFound || $backend instanceof User_Proxy;
187
-			});
188
-		}
189
-
190
-		if ($this->isAdmin) {
191
-			$disabledUsers = $isLDAPUsed ? -1 : $this->userManager->countDisabledUsers();
192
-			$userCount = $isLDAPUsed ? 0 : array_reduce($this->userManager->countUsers(), function($v, $w) {
193
-				return $v + (int)$w;
194
-			}, 0);
195
-		} else {
196
-			// User is subadmin !
197
-			// Map group list to names to retrieve the countDisabledUsersOfGroups
198
-			$userGroups = $this->groupManager->getUserGroups($user);
199
-			$groupsNames = [];
200
-			$userCount = 0;
201
-
202
-			foreach($groups as $key => $group) {
203
-				// $userCount += (int)$group['usercount'];
204
-				array_push($groupsNames, $group['name']);
205
-				// we prevent subadmins from looking up themselves
206
-				// so we lower the count of the groups he belongs to
207
-				if (array_key_exists($group['id'], $userGroups)) {
208
-					$groups[$key]['usercount']--;
209
-					$userCount = -1; // we also lower from one the total count
210
-				}
211
-			};
212
-			$userCount += $isLDAPUsed ? 0 : $this->userManager->countUsersOfGroups($groupsInfo->getGroups());
213
-			$disabledUsers = $isLDAPUsed ? -1 : $this->userManager->countDisabledUsersOfGroups($groupsNames);
214
-		}
215
-		$disabledUsersGroup = [
216
-			'id' => 'disabled',
217
-			'name' => 'Disabled users',
218
-			'usercount' => $disabledUsers
219
-		];
220
-
221
-		/* QUOTAS PRESETS */
222
-		$quotaPreset = $this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB');
223
-		$quotaPreset = explode(',', $quotaPreset);
224
-		foreach ($quotaPreset as &$preset) {
225
-			$preset = trim($preset);
226
-		}
227
-		$quotaPreset = array_diff($quotaPreset, array('default', 'none'));
228
-		$defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
229
-
230
-		\OC::$server->getEventDispatcher()->dispatch('OC\Settings\Users::loadAdditionalScripts');
231
-
232
-		/* LANGUAGES */
233
-		$languages = $this->l10nFactory->getLanguages();
234
-
235
-		/* FINAL DATA */
236
-		$serverData = array();
237
-		// groups
238
-		$serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups);
239
-		// Various data
240
-		$serverData['isAdmin'] = $this->isAdmin;
241
-		$serverData['sortGroups'] = $sortGroupsBy;
242
-		$serverData['quotaPreset'] = $quotaPreset;
243
-		$serverData['userCount'] = $userCount - $disabledUsers;
244
-		$serverData['languages'] = $languages;
245
-		$serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
246
-		// Settings
247
-		$serverData['defaultQuota'] = $defaultQuota;
248
-		$serverData['canChangePassword'] = $canChangePassword;
249
-		$serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
250
-		$serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
251
-
252
-		return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]);
253
-	}
254
-
255
-	/**
256
-	 * check if the admin can change the users password
257
-	 *
258
-	 * The admin can change the passwords if:
259
-	 *
260
-	 *   - no encryption module is loaded and encryption is disabled
261
-	 *   - encryption module is loaded but it doesn't require per user keys
262
-	 *
263
-	 * The admin can not change the passwords if:
264
-	 *
265
-	 *   - an encryption module is loaded and it uses per-user keys
266
-	 *   - encryption is enabled but no encryption modules are loaded
267
-	 *
268
-	 * @return bool
269
-	 */
270
-	protected function canAdminChangeUserPasswords() {
271
-		$isEncryptionEnabled = $this->encryptionManager->isEnabled();
272
-		try {
273
-			$noUserSpecificEncryptionKeys =!$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
274
-			$isEncryptionModuleLoaded = true;
275
-		} catch (ModuleDoesNotExistsException $e) {
276
-			$noUserSpecificEncryptionKeys = true;
277
-			$isEncryptionModuleLoaded = false;
278
-		}
279
-
280
-		$canChangePassword = ($isEncryptionEnabled && $isEncryptionModuleLoaded  && $noUserSpecificEncryptionKeys)
281
-			|| (!$isEncryptionEnabled && !$isEncryptionModuleLoaded)
282
-			|| (!$isEncryptionEnabled && $isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys);
283
-
284
-		return $canChangePassword;
285
-	}
286
-
287
-	/**
288
-	 * @NoAdminRequired
289
-	 * @NoSubadminRequired
290
-	 * @PasswordConfirmationRequired
291
-	 *
292
-	 * @param string $avatarScope
293
-	 * @param string $displayname
294
-	 * @param string $displaynameScope
295
-	 * @param string $phone
296
-	 * @param string $phoneScope
297
-	 * @param string $email
298
-	 * @param string $emailScope
299
-	 * @param string $website
300
-	 * @param string $websiteScope
301
-	 * @param string $address
302
-	 * @param string $addressScope
303
-	 * @param string $twitter
304
-	 * @param string $twitterScope
305
-	 * @return DataResponse
306
-	 */
307
-	public function setUserSettings($avatarScope,
308
-									$displayname,
309
-									$displaynameScope,
310
-									$phone,
311
-									$phoneScope,
312
-									$email,
313
-									$emailScope,
314
-									$website,
315
-									$websiteScope,
316
-									$address,
317
-									$addressScope,
318
-									$twitter,
319
-									$twitterScope
320
-	) {
321
-		if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
322
-			return new DataResponse(
323
-				[
324
-					'status' => 'error',
325
-					'data' => [
326
-						'message' => $this->l10n->t('Invalid mail address')
327
-					]
328
-				],
329
-				Http::STATUS_UNPROCESSABLE_ENTITY
330
-			);
331
-		}
332
-		$user = $this->userSession->getUser();
333
-		$data = $this->accountManager->getUser($user);
334
-		$data[AccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope];
335
-		if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
336
-			$data[AccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope];
337
-			$data[AccountManager::PROPERTY_EMAIL] = ['value' => $email, 'scope' => $emailScope];
338
-		}
339
-		if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
340
-			$federatedFileSharing = new \OCA\FederatedFileSharing\AppInfo\Application();
341
-			$shareProvider = $federatedFileSharing->getFederatedShareProvider();
342
-			if ($shareProvider->isLookupServerUploadEnabled()) {
343
-				$data[AccountManager::PROPERTY_WEBSITE] = ['value' => $website, 'scope' => $websiteScope];
344
-				$data[AccountManager::PROPERTY_ADDRESS] = ['value' => $address, 'scope' => $addressScope];
345
-				$data[AccountManager::PROPERTY_PHONE] = ['value' => $phone, 'scope' => $phoneScope];
346
-				$data[AccountManager::PROPERTY_TWITTER] = ['value' => $twitter, 'scope' => $twitterScope];
347
-			}
348
-		}
349
-		try {
350
-			$this->saveUserSettings($user, $data);
351
-			return new DataResponse(
352
-				[
353
-					'status' => 'success',
354
-					'data' => [
355
-						'userId' => $user->getUID(),
356
-						'avatarScope' => $data[AccountManager::PROPERTY_AVATAR]['scope'],
357
-						'displayname' => $data[AccountManager::PROPERTY_DISPLAYNAME]['value'],
358
-						'displaynameScope' => $data[AccountManager::PROPERTY_DISPLAYNAME]['scope'],
359
-						'email' => $data[AccountManager::PROPERTY_EMAIL]['value'],
360
-						'emailScope' => $data[AccountManager::PROPERTY_EMAIL]['scope'],
361
-						'website' => $data[AccountManager::PROPERTY_WEBSITE]['value'],
362
-						'websiteScope' => $data[AccountManager::PROPERTY_WEBSITE]['scope'],
363
-						'address' => $data[AccountManager::PROPERTY_ADDRESS]['value'],
364
-						'addressScope' => $data[AccountManager::PROPERTY_ADDRESS]['scope'],
365
-						'message' => $this->l10n->t('Settings saved')
366
-					]
367
-				],
368
-				Http::STATUS_OK
369
-			);
370
-		} catch (ForbiddenException $e) {
371
-			return new DataResponse([
372
-				'status' => 'error',
373
-				'data' => [
374
-					'message' => $e->getMessage()
375
-				],
376
-			]);
377
-		}
378
-	}
379
-	/**
380
-	 * update account manager with new user data
381
-	 *
382
-	 * @param IUser $user
383
-	 * @param array $data
384
-	 * @throws ForbiddenException
385
-	 */
386
-	protected function saveUserSettings(IUser $user, array $data) {
387
-		// keep the user back-end up-to-date with the latest display name and email
388
-		// address
389
-		$oldDisplayName = $user->getDisplayName();
390
-		$oldDisplayName = is_null($oldDisplayName) ? '' : $oldDisplayName;
391
-		if (isset($data[AccountManager::PROPERTY_DISPLAYNAME]['value'])
392
-			&& $oldDisplayName !== $data[AccountManager::PROPERTY_DISPLAYNAME]['value']
393
-		) {
394
-			$result = $user->setDisplayName($data[AccountManager::PROPERTY_DISPLAYNAME]['value']);
395
-			if ($result === false) {
396
-				throw new ForbiddenException($this->l10n->t('Unable to change full name'));
397
-			}
398
-		}
399
-		$oldEmailAddress = $user->getEMailAddress();
400
-		$oldEmailAddress = is_null($oldEmailAddress) ? '' : $oldEmailAddress;
401
-		if (isset($data[AccountManager::PROPERTY_EMAIL]['value'])
402
-			&& $oldEmailAddress !== $data[AccountManager::PROPERTY_EMAIL]['value']
403
-		) {
404
-			// this is the only permission a backend provides and is also used
405
-			// for the permission of setting a email address
406
-			if (!$user->canChangeDisplayName()) {
407
-				throw new ForbiddenException($this->l10n->t('Unable to change email address'));
408
-			}
409
-			$user->setEMailAddress($data[AccountManager::PROPERTY_EMAIL]['value']);
410
-		}
411
-		$this->accountManager->updateUser($user, $data);
412
-	}
413
-
414
-	/**
415
-	 * Set the mail address of a user
416
-	 *
417
-	 * @NoAdminRequired
418
-	 * @NoSubadminRequired
419
-	 * @PasswordConfirmationRequired
420
-	 *
421
-	 * @param string $account
422
-	 * @param bool $onlyVerificationCode only return verification code without updating the data
423
-	 * @return DataResponse
424
-	 */
425
-	public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
426
-
427
-		$user = $this->userSession->getUser();
428
-
429
-		if ($user === null) {
430
-			return new DataResponse([], Http::STATUS_BAD_REQUEST);
431
-		}
432
-
433
-		$accountData = $this->accountManager->getUser($user);
434
-		$cloudId = $user->getCloudId();
435
-		$message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
436
-		$signature = $this->signMessage($user, $message);
437
-
438
-		$code = $message . ' ' . $signature;
439
-		$codeMd5 = $message . ' ' . md5($signature);
440
-
441
-		switch ($account) {
442
-			case 'verify-twitter':
443
-				$accountData[AccountManager::PROPERTY_TWITTER]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS;
444
-				$msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
445
-				$code = $codeMd5;
446
-				$type = AccountManager::PROPERTY_TWITTER;
447
-				$data = $accountData[AccountManager::PROPERTY_TWITTER]['value'];
448
-				$accountData[AccountManager::PROPERTY_TWITTER]['signature'] = $signature;
449
-				break;
450
-			case 'verify-website':
451
-				$accountData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS;
452
-				$msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
453
-				$type = AccountManager::PROPERTY_WEBSITE;
454
-				$data = $accountData[AccountManager::PROPERTY_WEBSITE]['value'];
455
-				$accountData[AccountManager::PROPERTY_WEBSITE]['signature'] = $signature;
456
-				break;
457
-			default:
458
-				return new DataResponse([], Http::STATUS_BAD_REQUEST);
459
-		}
460
-
461
-		if ($onlyVerificationCode === false) {
462
-			$this->accountManager->updateUser($user, $accountData);
463
-
464
-			$this->jobList->add(VerifyUserData::class,
465
-				[
466
-					'verificationCode' => $code,
467
-					'data' => $data,
468
-					'type' => $type,
469
-					'uid' => $user->getUID(),
470
-					'try' => 0,
471
-					'lastRun' => $this->getCurrentTime()
472
-				]
473
-			);
474
-		}
475
-
476
-		return new DataResponse(['msg' => $msg, 'code' => $code]);
477
-	}
478
-
479
-	/**
480
-	 * get current timestamp
481
-	 *
482
-	 * @return int
483
-	 */
484
-	protected function getCurrentTime(): int {
485
-		return time();
486
-	}
487
-
488
-	/**
489
-	 * sign message with users private key
490
-	 *
491
-	 * @param IUser $user
492
-	 * @param string $message
493
-	 *
494
-	 * @return string base64 encoded signature
495
-	 */
496
-	protected function signMessage(IUser $user, string $message): string {
497
-		$privateKey = $this->keyManager->getKey($user)->getPrivate();
498
-		openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
499
-		return base64_encode($signature);
500
-	}
69
+    /** @var IUserManager */
70
+    private $userManager;
71
+    /** @var IGroupManager */
72
+    private $groupManager;
73
+    /** @var IUserSession */
74
+    private $userSession;
75
+    /** @var IConfig */
76
+    private $config;
77
+    /** @var bool */
78
+    private $isAdmin;
79
+    /** @var IL10N */
80
+    private $l10n;
81
+    /** @var IMailer */
82
+    private $mailer;
83
+    /** @var IFactory */
84
+    private $l10nFactory;
85
+    /** @var IAppManager */
86
+    private $appManager;
87
+    /** @var AccountManager */
88
+    private $accountManager;
89
+    /** @var Manager */
90
+    private $keyManager;
91
+    /** @var IJobList */
92
+    private $jobList;
93
+    /** @var IManager */
94
+    private $encryptionManager;
95
+
96
+
97
+    public function __construct(string $appName,
98
+                                IRequest $request,
99
+                                IUserManager $userManager,
100
+                                IGroupManager $groupManager,
101
+                                IUserSession $userSession,
102
+                                IConfig $config,
103
+                                bool $isAdmin,
104
+                                IL10N $l10n,
105
+                                IMailer $mailer,
106
+                                IFactory $l10nFactory,
107
+                                IAppManager $appManager,
108
+                                AccountManager $accountManager,
109
+                                Manager $keyManager,
110
+                                IJobList $jobList,
111
+                                IManager $encryptionManager) {
112
+        parent::__construct($appName, $request);
113
+        $this->userManager = $userManager;
114
+        $this->groupManager = $groupManager;
115
+        $this->userSession = $userSession;
116
+        $this->config = $config;
117
+        $this->isAdmin = $isAdmin;
118
+        $this->l10n = $l10n;
119
+        $this->mailer = $mailer;
120
+        $this->l10nFactory = $l10nFactory;
121
+        $this->appManager = $appManager;
122
+        $this->accountManager = $accountManager;
123
+        $this->keyManager = $keyManager;
124
+        $this->jobList = $jobList;
125
+        $this->encryptionManager = $encryptionManager;
126
+    }
127
+
128
+
129
+    /**
130
+     * @NoCSRFRequired
131
+     * @NoAdminRequired
132
+     *
133
+     * Display users list template
134
+     *
135
+     * @return TemplateResponse
136
+     */
137
+    public function usersListByGroup() {
138
+        return $this->usersList();
139
+    }
140
+
141
+    /**
142
+     * @NoCSRFRequired
143
+     * @NoAdminRequired
144
+     *
145
+     * Display users list template
146
+     *
147
+     * @return TemplateResponse
148
+     */
149
+    public function usersList() {
150
+        $user = $this->userSession->getUser();
151
+        $uid = $user->getUID();
152
+
153
+        \OC::$server->getNavigationManager()->setActiveEntry('core_users');
154
+
155
+        /* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */
156
+        $sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT;
157
+        $isLDAPUsed = false;
158
+        if ($this->config->getSystemValue('sort_groups_by_name', false)) {
159
+            $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
160
+        } else {
161
+            if ($this->appManager->isEnabledForUser('user_ldap')) {
162
+                $isLDAPUsed =
163
+                    $this->groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
164
+                if ($isLDAPUsed) {
165
+                    // LDAP user count can be slow, so we sort by group name here
166
+                    $sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
167
+                }
168
+            }
169
+        }
170
+
171
+        $canChangePassword = $this->canAdminChangeUserPasswords();
172
+
173
+        /* GROUPS */
174
+        $groupsInfo = new \OC\Group\MetaData(
175
+            $uid,
176
+            $this->isAdmin,
177
+            $this->groupManager,
178
+            $this->userSession
179
+        );
180
+
181
+        $groupsInfo->setSorting($sortGroupsBy);
182
+        list($adminGroup, $groups) = $groupsInfo->get();
183
+
184
+        if(!$isLDAPUsed && $this->appManager->isEnabledForUser('user_ldap')) {
185
+            $isLDAPUsed = (bool)array_reduce($this->userManager->getBackends(), function ($ldapFound, $backend) {
186
+                return $ldapFound || $backend instanceof User_Proxy;
187
+            });
188
+        }
189
+
190
+        if ($this->isAdmin) {
191
+            $disabledUsers = $isLDAPUsed ? -1 : $this->userManager->countDisabledUsers();
192
+            $userCount = $isLDAPUsed ? 0 : array_reduce($this->userManager->countUsers(), function($v, $w) {
193
+                return $v + (int)$w;
194
+            }, 0);
195
+        } else {
196
+            // User is subadmin !
197
+            // Map group list to names to retrieve the countDisabledUsersOfGroups
198
+            $userGroups = $this->groupManager->getUserGroups($user);
199
+            $groupsNames = [];
200
+            $userCount = 0;
201
+
202
+            foreach($groups as $key => $group) {
203
+                // $userCount += (int)$group['usercount'];
204
+                array_push($groupsNames, $group['name']);
205
+                // we prevent subadmins from looking up themselves
206
+                // so we lower the count of the groups he belongs to
207
+                if (array_key_exists($group['id'], $userGroups)) {
208
+                    $groups[$key]['usercount']--;
209
+                    $userCount = -1; // we also lower from one the total count
210
+                }
211
+            };
212
+            $userCount += $isLDAPUsed ? 0 : $this->userManager->countUsersOfGroups($groupsInfo->getGroups());
213
+            $disabledUsers = $isLDAPUsed ? -1 : $this->userManager->countDisabledUsersOfGroups($groupsNames);
214
+        }
215
+        $disabledUsersGroup = [
216
+            'id' => 'disabled',
217
+            'name' => 'Disabled users',
218
+            'usercount' => $disabledUsers
219
+        ];
220
+
221
+        /* QUOTAS PRESETS */
222
+        $quotaPreset = $this->config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB');
223
+        $quotaPreset = explode(',', $quotaPreset);
224
+        foreach ($quotaPreset as &$preset) {
225
+            $preset = trim($preset);
226
+        }
227
+        $quotaPreset = array_diff($quotaPreset, array('default', 'none'));
228
+        $defaultQuota = $this->config->getAppValue('files', 'default_quota', 'none');
229
+
230
+        \OC::$server->getEventDispatcher()->dispatch('OC\Settings\Users::loadAdditionalScripts');
231
+
232
+        /* LANGUAGES */
233
+        $languages = $this->l10nFactory->getLanguages();
234
+
235
+        /* FINAL DATA */
236
+        $serverData = array();
237
+        // groups
238
+        $serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups);
239
+        // Various data
240
+        $serverData['isAdmin'] = $this->isAdmin;
241
+        $serverData['sortGroups'] = $sortGroupsBy;
242
+        $serverData['quotaPreset'] = $quotaPreset;
243
+        $serverData['userCount'] = $userCount - $disabledUsers;
244
+        $serverData['languages'] = $languages;
245
+        $serverData['defaultLanguage'] = $this->config->getSystemValue('default_language', 'en');
246
+        // Settings
247
+        $serverData['defaultQuota'] = $defaultQuota;
248
+        $serverData['canChangePassword'] = $canChangePassword;
249
+        $serverData['newUserGenerateUserID'] = $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes';
250
+        $serverData['newUserRequireEmail'] = $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes';
251
+
252
+        return new TemplateResponse('settings', 'settings-vue', ['serverData' => $serverData]);
253
+    }
254
+
255
+    /**
256
+     * check if the admin can change the users password
257
+     *
258
+     * The admin can change the passwords if:
259
+     *
260
+     *   - no encryption module is loaded and encryption is disabled
261
+     *   - encryption module is loaded but it doesn't require per user keys
262
+     *
263
+     * The admin can not change the passwords if:
264
+     *
265
+     *   - an encryption module is loaded and it uses per-user keys
266
+     *   - encryption is enabled but no encryption modules are loaded
267
+     *
268
+     * @return bool
269
+     */
270
+    protected function canAdminChangeUserPasswords() {
271
+        $isEncryptionEnabled = $this->encryptionManager->isEnabled();
272
+        try {
273
+            $noUserSpecificEncryptionKeys =!$this->encryptionManager->getEncryptionModule()->needDetailedAccessList();
274
+            $isEncryptionModuleLoaded = true;
275
+        } catch (ModuleDoesNotExistsException $e) {
276
+            $noUserSpecificEncryptionKeys = true;
277
+            $isEncryptionModuleLoaded = false;
278
+        }
279
+
280
+        $canChangePassword = ($isEncryptionEnabled && $isEncryptionModuleLoaded  && $noUserSpecificEncryptionKeys)
281
+            || (!$isEncryptionEnabled && !$isEncryptionModuleLoaded)
282
+            || (!$isEncryptionEnabled && $isEncryptionModuleLoaded && $noUserSpecificEncryptionKeys);
283
+
284
+        return $canChangePassword;
285
+    }
286
+
287
+    /**
288
+     * @NoAdminRequired
289
+     * @NoSubadminRequired
290
+     * @PasswordConfirmationRequired
291
+     *
292
+     * @param string $avatarScope
293
+     * @param string $displayname
294
+     * @param string $displaynameScope
295
+     * @param string $phone
296
+     * @param string $phoneScope
297
+     * @param string $email
298
+     * @param string $emailScope
299
+     * @param string $website
300
+     * @param string $websiteScope
301
+     * @param string $address
302
+     * @param string $addressScope
303
+     * @param string $twitter
304
+     * @param string $twitterScope
305
+     * @return DataResponse
306
+     */
307
+    public function setUserSettings($avatarScope,
308
+                                    $displayname,
309
+                                    $displaynameScope,
310
+                                    $phone,
311
+                                    $phoneScope,
312
+                                    $email,
313
+                                    $emailScope,
314
+                                    $website,
315
+                                    $websiteScope,
316
+                                    $address,
317
+                                    $addressScope,
318
+                                    $twitter,
319
+                                    $twitterScope
320
+    ) {
321
+        if (!empty($email) && !$this->mailer->validateMailAddress($email)) {
322
+            return new DataResponse(
323
+                [
324
+                    'status' => 'error',
325
+                    'data' => [
326
+                        'message' => $this->l10n->t('Invalid mail address')
327
+                    ]
328
+                ],
329
+                Http::STATUS_UNPROCESSABLE_ENTITY
330
+            );
331
+        }
332
+        $user = $this->userSession->getUser();
333
+        $data = $this->accountManager->getUser($user);
334
+        $data[AccountManager::PROPERTY_AVATAR] = ['scope' => $avatarScope];
335
+        if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
336
+            $data[AccountManager::PROPERTY_DISPLAYNAME] = ['value' => $displayname, 'scope' => $displaynameScope];
337
+            $data[AccountManager::PROPERTY_EMAIL] = ['value' => $email, 'scope' => $emailScope];
338
+        }
339
+        if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
340
+            $federatedFileSharing = new \OCA\FederatedFileSharing\AppInfo\Application();
341
+            $shareProvider = $federatedFileSharing->getFederatedShareProvider();
342
+            if ($shareProvider->isLookupServerUploadEnabled()) {
343
+                $data[AccountManager::PROPERTY_WEBSITE] = ['value' => $website, 'scope' => $websiteScope];
344
+                $data[AccountManager::PROPERTY_ADDRESS] = ['value' => $address, 'scope' => $addressScope];
345
+                $data[AccountManager::PROPERTY_PHONE] = ['value' => $phone, 'scope' => $phoneScope];
346
+                $data[AccountManager::PROPERTY_TWITTER] = ['value' => $twitter, 'scope' => $twitterScope];
347
+            }
348
+        }
349
+        try {
350
+            $this->saveUserSettings($user, $data);
351
+            return new DataResponse(
352
+                [
353
+                    'status' => 'success',
354
+                    'data' => [
355
+                        'userId' => $user->getUID(),
356
+                        'avatarScope' => $data[AccountManager::PROPERTY_AVATAR]['scope'],
357
+                        'displayname' => $data[AccountManager::PROPERTY_DISPLAYNAME]['value'],
358
+                        'displaynameScope' => $data[AccountManager::PROPERTY_DISPLAYNAME]['scope'],
359
+                        'email' => $data[AccountManager::PROPERTY_EMAIL]['value'],
360
+                        'emailScope' => $data[AccountManager::PROPERTY_EMAIL]['scope'],
361
+                        'website' => $data[AccountManager::PROPERTY_WEBSITE]['value'],
362
+                        'websiteScope' => $data[AccountManager::PROPERTY_WEBSITE]['scope'],
363
+                        'address' => $data[AccountManager::PROPERTY_ADDRESS]['value'],
364
+                        'addressScope' => $data[AccountManager::PROPERTY_ADDRESS]['scope'],
365
+                        'message' => $this->l10n->t('Settings saved')
366
+                    ]
367
+                ],
368
+                Http::STATUS_OK
369
+            );
370
+        } catch (ForbiddenException $e) {
371
+            return new DataResponse([
372
+                'status' => 'error',
373
+                'data' => [
374
+                    'message' => $e->getMessage()
375
+                ],
376
+            ]);
377
+        }
378
+    }
379
+    /**
380
+     * update account manager with new user data
381
+     *
382
+     * @param IUser $user
383
+     * @param array $data
384
+     * @throws ForbiddenException
385
+     */
386
+    protected function saveUserSettings(IUser $user, array $data) {
387
+        // keep the user back-end up-to-date with the latest display name and email
388
+        // address
389
+        $oldDisplayName = $user->getDisplayName();
390
+        $oldDisplayName = is_null($oldDisplayName) ? '' : $oldDisplayName;
391
+        if (isset($data[AccountManager::PROPERTY_DISPLAYNAME]['value'])
392
+            && $oldDisplayName !== $data[AccountManager::PROPERTY_DISPLAYNAME]['value']
393
+        ) {
394
+            $result = $user->setDisplayName($data[AccountManager::PROPERTY_DISPLAYNAME]['value']);
395
+            if ($result === false) {
396
+                throw new ForbiddenException($this->l10n->t('Unable to change full name'));
397
+            }
398
+        }
399
+        $oldEmailAddress = $user->getEMailAddress();
400
+        $oldEmailAddress = is_null($oldEmailAddress) ? '' : $oldEmailAddress;
401
+        if (isset($data[AccountManager::PROPERTY_EMAIL]['value'])
402
+            && $oldEmailAddress !== $data[AccountManager::PROPERTY_EMAIL]['value']
403
+        ) {
404
+            // this is the only permission a backend provides and is also used
405
+            // for the permission of setting a email address
406
+            if (!$user->canChangeDisplayName()) {
407
+                throw new ForbiddenException($this->l10n->t('Unable to change email address'));
408
+            }
409
+            $user->setEMailAddress($data[AccountManager::PROPERTY_EMAIL]['value']);
410
+        }
411
+        $this->accountManager->updateUser($user, $data);
412
+    }
413
+
414
+    /**
415
+     * Set the mail address of a user
416
+     *
417
+     * @NoAdminRequired
418
+     * @NoSubadminRequired
419
+     * @PasswordConfirmationRequired
420
+     *
421
+     * @param string $account
422
+     * @param bool $onlyVerificationCode only return verification code without updating the data
423
+     * @return DataResponse
424
+     */
425
+    public function getVerificationCode(string $account, bool $onlyVerificationCode): DataResponse {
426
+
427
+        $user = $this->userSession->getUser();
428
+
429
+        if ($user === null) {
430
+            return new DataResponse([], Http::STATUS_BAD_REQUEST);
431
+        }
432
+
433
+        $accountData = $this->accountManager->getUser($user);
434
+        $cloudId = $user->getCloudId();
435
+        $message = 'Use my Federated Cloud ID to share with me: ' . $cloudId;
436
+        $signature = $this->signMessage($user, $message);
437
+
438
+        $code = $message . ' ' . $signature;
439
+        $codeMd5 = $message . ' ' . md5($signature);
440
+
441
+        switch ($account) {
442
+            case 'verify-twitter':
443
+                $accountData[AccountManager::PROPERTY_TWITTER]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS;
444
+                $msg = $this->l10n->t('In order to verify your Twitter account, post the following tweet on Twitter (please make sure to post it without any line breaks):');
445
+                $code = $codeMd5;
446
+                $type = AccountManager::PROPERTY_TWITTER;
447
+                $data = $accountData[AccountManager::PROPERTY_TWITTER]['value'];
448
+                $accountData[AccountManager::PROPERTY_TWITTER]['signature'] = $signature;
449
+                break;
450
+            case 'verify-website':
451
+                $accountData[AccountManager::PROPERTY_WEBSITE]['verified'] = AccountManager::VERIFICATION_IN_PROGRESS;
452
+                $msg = $this->l10n->t('In order to verify your Website, store the following content in your web-root at \'.well-known/CloudIdVerificationCode.txt\' (please make sure that the complete text is in one line):');
453
+                $type = AccountManager::PROPERTY_WEBSITE;
454
+                $data = $accountData[AccountManager::PROPERTY_WEBSITE]['value'];
455
+                $accountData[AccountManager::PROPERTY_WEBSITE]['signature'] = $signature;
456
+                break;
457
+            default:
458
+                return new DataResponse([], Http::STATUS_BAD_REQUEST);
459
+        }
460
+
461
+        if ($onlyVerificationCode === false) {
462
+            $this->accountManager->updateUser($user, $accountData);
463
+
464
+            $this->jobList->add(VerifyUserData::class,
465
+                [
466
+                    'verificationCode' => $code,
467
+                    'data' => $data,
468
+                    'type' => $type,
469
+                    'uid' => $user->getUID(),
470
+                    'try' => 0,
471
+                    'lastRun' => $this->getCurrentTime()
472
+                ]
473
+            );
474
+        }
475
+
476
+        return new DataResponse(['msg' => $msg, 'code' => $code]);
477
+    }
478
+
479
+    /**
480
+     * get current timestamp
481
+     *
482
+     * @return int
483
+     */
484
+    protected function getCurrentTime(): int {
485
+        return time();
486
+    }
487
+
488
+    /**
489
+     * sign message with users private key
490
+     *
491
+     * @param IUser $user
492
+     * @param string $message
493
+     *
494
+     * @return string base64 encoded signature
495
+     */
496
+    protected function signMessage(IUser $user, string $message): string {
497
+        $privateKey = $this->keyManager->getKey($user)->getPrivate();
498
+        openssl_sign(json_encode($message), $signature, $privateKey, OPENSSL_ALGO_SHA512);
499
+        return base64_encode($signature);
500
+    }
501 501
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Access.php 1 patch
Indentation   +1976 added lines, -1976 removed lines patch added patch discarded remove patch
@@ -59,1732 +59,1732 @@  discard block
 block discarded – undo
59 59
  * @package OCA\User_LDAP
60 60
  */
61 61
 class Access extends LDAPUtility {
62
-	const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
63
-
64
-	/** @var \OCA\User_LDAP\Connection */
65
-	public $connection;
66
-	/** @var Manager */
67
-	public $userManager;
68
-	//never ever check this var directly, always use getPagedSearchResultState
69
-	protected $pagedSearchedSuccessful;
70
-
71
-	/**
72
-	 * @var string[] $cookies an array of returned Paged Result cookies
73
-	 */
74
-	protected $cookies = array();
75
-
76
-	/**
77
-	 * @var string $lastCookie the last cookie returned from a Paged Results
78
-	 * operation, defaults to an empty string
79
-	 */
80
-	protected $lastCookie = '';
81
-
82
-	/**
83
-	 * @var AbstractMapping $userMapper
84
-	 */
85
-	protected $userMapper;
86
-
87
-	/**
88
-	* @var AbstractMapping $userMapper
89
-	*/
90
-	protected $groupMapper;
91
-
92
-	/**
93
-	 * @var \OCA\User_LDAP\Helper
94
-	 */
95
-	private $helper;
96
-	/** @var IConfig */
97
-	private $config;
98
-	/** @var IUserManager */
99
-	private $ncUserManager;
100
-
101
-	public function __construct(
102
-		Connection $connection,
103
-		ILDAPWrapper $ldap,
104
-		Manager $userManager,
105
-		Helper $helper,
106
-		IConfig $config,
107
-		IUserManager $ncUserManager
108
-	) {
109
-		parent::__construct($ldap);
110
-		$this->connection = $connection;
111
-		$this->userManager = $userManager;
112
-		$this->userManager->setLdapAccess($this);
113
-		$this->helper = $helper;
114
-		$this->config = $config;
115
-		$this->ncUserManager = $ncUserManager;
116
-	}
117
-
118
-	/**
119
-	 * sets the User Mapper
120
-	 * @param AbstractMapping $mapper
121
-	 */
122
-	public function setUserMapper(AbstractMapping $mapper) {
123
-		$this->userMapper = $mapper;
124
-	}
125
-
126
-	/**
127
-	 * returns the User Mapper
128
-	 * @throws \Exception
129
-	 * @return AbstractMapping
130
-	 */
131
-	public function getUserMapper() {
132
-		if(is_null($this->userMapper)) {
133
-			throw new \Exception('UserMapper was not assigned to this Access instance.');
134
-		}
135
-		return $this->userMapper;
136
-	}
137
-
138
-	/**
139
-	 * sets the Group Mapper
140
-	 * @param AbstractMapping $mapper
141
-	 */
142
-	public function setGroupMapper(AbstractMapping $mapper) {
143
-		$this->groupMapper = $mapper;
144
-	}
145
-
146
-	/**
147
-	 * returns the Group Mapper
148
-	 * @throws \Exception
149
-	 * @return AbstractMapping
150
-	 */
151
-	public function getGroupMapper() {
152
-		if(is_null($this->groupMapper)) {
153
-			throw new \Exception('GroupMapper was not assigned to this Access instance.');
154
-		}
155
-		return $this->groupMapper;
156
-	}
157
-
158
-	/**
159
-	 * @return bool
160
-	 */
161
-	private function checkConnection() {
162
-		return ($this->connection instanceof Connection);
163
-	}
164
-
165
-	/**
166
-	 * returns the Connection instance
167
-	 * @return \OCA\User_LDAP\Connection
168
-	 */
169
-	public function getConnection() {
170
-		return $this->connection;
171
-	}
172
-
173
-	/**
174
-	 * reads a given attribute for an LDAP record identified by a DN
175
-	 *
176
-	 * @param string $dn the record in question
177
-	 * @param string $attr the attribute that shall be retrieved
178
-	 *        if empty, just check the record's existence
179
-	 * @param string $filter
180
-	 * @return array|false an array of values on success or an empty
181
-	 *          array if $attr is empty, false otherwise
182
-	 * @throws ServerNotAvailableException
183
-	 */
184
-	public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
185
-		if(!$this->checkConnection()) {
186
-			\OCP\Util::writeLog('user_ldap',
187
-				'No LDAP Connector assigned, access impossible for readAttribute.',
188
-				ILogger::WARN);
189
-			return false;
190
-		}
191
-		$cr = $this->connection->getConnectionResource();
192
-		if(!$this->ldap->isResource($cr)) {
193
-			//LDAP not available
194
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
195
-			return false;
196
-		}
197
-		//Cancel possibly running Paged Results operation, otherwise we run in
198
-		//LDAP protocol errors
199
-		$this->abandonPagedSearch();
200
-		// openLDAP requires that we init a new Paged Search. Not needed by AD,
201
-		// but does not hurt either.
202
-		$pagingSize = (int)$this->connection->ldapPagingSize;
203
-		// 0 won't result in replies, small numbers may leave out groups
204
-		// (cf. #12306), 500 is default for paging and should work everywhere.
205
-		$maxResults = $pagingSize > 20 ? $pagingSize : 500;
206
-		$attr = mb_strtolower($attr, 'UTF-8');
207
-		// the actual read attribute later may contain parameters on a ranged
208
-		// request, e.g. member;range=99-199. Depends on server reply.
209
-		$attrToRead = $attr;
210
-
211
-		$values = [];
212
-		$isRangeRequest = false;
213
-		do {
214
-			$result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
215
-			if(is_bool($result)) {
216
-				// when an exists request was run and it was successful, an empty
217
-				// array must be returned
218
-				return $result ? [] : false;
219
-			}
220
-
221
-			if (!$isRangeRequest) {
222
-				$values = $this->extractAttributeValuesFromResult($result, $attr);
223
-				if (!empty($values)) {
224
-					return $values;
225
-				}
226
-			}
227
-
228
-			$isRangeRequest = false;
229
-			$result = $this->extractRangeData($result, $attr);
230
-			if (!empty($result)) {
231
-				$normalizedResult = $this->extractAttributeValuesFromResult(
232
-					[ $attr => $result['values'] ],
233
-					$attr
234
-				);
235
-				$values = array_merge($values, $normalizedResult);
236
-
237
-				if($result['rangeHigh'] === '*') {
238
-					// when server replies with * as high range value, there are
239
-					// no more results left
240
-					return $values;
241
-				} else {
242
-					$low  = $result['rangeHigh'] + 1;
243
-					$attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
244
-					$isRangeRequest = true;
245
-				}
246
-			}
247
-		} while($isRangeRequest);
248
-
249
-		\OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);
250
-		return false;
251
-	}
252
-
253
-	/**
254
-	 * Runs an read operation against LDAP
255
-	 *
256
-	 * @param resource $cr the LDAP connection
257
-	 * @param string $dn
258
-	 * @param string $attribute
259
-	 * @param string $filter
260
-	 * @param int $maxResults
261
-	 * @return array|bool false if there was any error, true if an exists check
262
-	 *                    was performed and the requested DN found, array with the
263
-	 *                    returned data on a successful usual operation
264
-	 * @throws ServerNotAvailableException
265
-	 */
266
-	public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
267
-		$this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
268
-		$dn = $this->helper->DNasBaseParameter($dn);
269
-		$rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
270
-		if (!$this->ldap->isResource($rr)) {
271
-			if ($attribute !== '') {
272
-				//do not throw this message on userExists check, irritates
273
-				\OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
274
-			}
275
-			//in case an error occurs , e.g. object does not exist
276
-			return false;
277
-		}
278
-		if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
279
-			\OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
280
-			return true;
281
-		}
282
-		$er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
283
-		if (!$this->ldap->isResource($er)) {
284
-			//did not match the filter, return false
285
-			return false;
286
-		}
287
-		//LDAP attributes are not case sensitive
288
-		$result = \OCP\Util::mb_array_change_key_case(
289
-			$this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
290
-
291
-		return $result;
292
-	}
293
-
294
-	/**
295
-	 * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
296
-	 * data if present.
297
-	 *
298
-	 * @param array $result from ILDAPWrapper::getAttributes()
299
-	 * @param string $attribute the attribute name that was read
300
-	 * @return string[]
301
-	 */
302
-	public function extractAttributeValuesFromResult($result, $attribute) {
303
-		$values = [];
304
-		if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
305
-			$lowercaseAttribute = strtolower($attribute);
306
-			for($i=0;$i<$result[$attribute]['count'];$i++) {
307
-				if($this->resemblesDN($attribute)) {
308
-					$values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
309
-				} elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
310
-					$values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
311
-				} else {
312
-					$values[] = $result[$attribute][$i];
313
-				}
314
-			}
315
-		}
316
-		return $values;
317
-	}
318
-
319
-	/**
320
-	 * Attempts to find ranged data in a getAttribute results and extracts the
321
-	 * returned values as well as information on the range and full attribute
322
-	 * name for further processing.
323
-	 *
324
-	 * @param array $result from ILDAPWrapper::getAttributes()
325
-	 * @param string $attribute the attribute name that was read. Without ";range=…"
326
-	 * @return array If a range was detected with keys 'values', 'attributeName',
327
-	 *               'attributeFull' and 'rangeHigh', otherwise empty.
328
-	 */
329
-	public function extractRangeData($result, $attribute) {
330
-		$keys = array_keys($result);
331
-		foreach($keys as $key) {
332
-			if($key !== $attribute && strpos($key, $attribute) === 0) {
333
-				$queryData = explode(';', $key);
334
-				if(strpos($queryData[1], 'range=') === 0) {
335
-					$high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
336
-					$data = [
337
-						'values' => $result[$key],
338
-						'attributeName' => $queryData[0],
339
-						'attributeFull' => $key,
340
-						'rangeHigh' => $high,
341
-					];
342
-					return $data;
343
-				}
344
-			}
345
-		}
346
-		return [];
347
-	}
62
+    const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid'];
63
+
64
+    /** @var \OCA\User_LDAP\Connection */
65
+    public $connection;
66
+    /** @var Manager */
67
+    public $userManager;
68
+    //never ever check this var directly, always use getPagedSearchResultState
69
+    protected $pagedSearchedSuccessful;
70
+
71
+    /**
72
+     * @var string[] $cookies an array of returned Paged Result cookies
73
+     */
74
+    protected $cookies = array();
75
+
76
+    /**
77
+     * @var string $lastCookie the last cookie returned from a Paged Results
78
+     * operation, defaults to an empty string
79
+     */
80
+    protected $lastCookie = '';
81
+
82
+    /**
83
+     * @var AbstractMapping $userMapper
84
+     */
85
+    protected $userMapper;
86
+
87
+    /**
88
+     * @var AbstractMapping $userMapper
89
+     */
90
+    protected $groupMapper;
91
+
92
+    /**
93
+     * @var \OCA\User_LDAP\Helper
94
+     */
95
+    private $helper;
96
+    /** @var IConfig */
97
+    private $config;
98
+    /** @var IUserManager */
99
+    private $ncUserManager;
100
+
101
+    public function __construct(
102
+        Connection $connection,
103
+        ILDAPWrapper $ldap,
104
+        Manager $userManager,
105
+        Helper $helper,
106
+        IConfig $config,
107
+        IUserManager $ncUserManager
108
+    ) {
109
+        parent::__construct($ldap);
110
+        $this->connection = $connection;
111
+        $this->userManager = $userManager;
112
+        $this->userManager->setLdapAccess($this);
113
+        $this->helper = $helper;
114
+        $this->config = $config;
115
+        $this->ncUserManager = $ncUserManager;
116
+    }
117
+
118
+    /**
119
+     * sets the User Mapper
120
+     * @param AbstractMapping $mapper
121
+     */
122
+    public function setUserMapper(AbstractMapping $mapper) {
123
+        $this->userMapper = $mapper;
124
+    }
125
+
126
+    /**
127
+     * returns the User Mapper
128
+     * @throws \Exception
129
+     * @return AbstractMapping
130
+     */
131
+    public function getUserMapper() {
132
+        if(is_null($this->userMapper)) {
133
+            throw new \Exception('UserMapper was not assigned to this Access instance.');
134
+        }
135
+        return $this->userMapper;
136
+    }
137
+
138
+    /**
139
+     * sets the Group Mapper
140
+     * @param AbstractMapping $mapper
141
+     */
142
+    public function setGroupMapper(AbstractMapping $mapper) {
143
+        $this->groupMapper = $mapper;
144
+    }
145
+
146
+    /**
147
+     * returns the Group Mapper
148
+     * @throws \Exception
149
+     * @return AbstractMapping
150
+     */
151
+    public function getGroupMapper() {
152
+        if(is_null($this->groupMapper)) {
153
+            throw new \Exception('GroupMapper was not assigned to this Access instance.');
154
+        }
155
+        return $this->groupMapper;
156
+    }
157
+
158
+    /**
159
+     * @return bool
160
+     */
161
+    private function checkConnection() {
162
+        return ($this->connection instanceof Connection);
163
+    }
164
+
165
+    /**
166
+     * returns the Connection instance
167
+     * @return \OCA\User_LDAP\Connection
168
+     */
169
+    public function getConnection() {
170
+        return $this->connection;
171
+    }
172
+
173
+    /**
174
+     * reads a given attribute for an LDAP record identified by a DN
175
+     *
176
+     * @param string $dn the record in question
177
+     * @param string $attr the attribute that shall be retrieved
178
+     *        if empty, just check the record's existence
179
+     * @param string $filter
180
+     * @return array|false an array of values on success or an empty
181
+     *          array if $attr is empty, false otherwise
182
+     * @throws ServerNotAvailableException
183
+     */
184
+    public function readAttribute($dn, $attr, $filter = 'objectClass=*') {
185
+        if(!$this->checkConnection()) {
186
+            \OCP\Util::writeLog('user_ldap',
187
+                'No LDAP Connector assigned, access impossible for readAttribute.',
188
+                ILogger::WARN);
189
+            return false;
190
+        }
191
+        $cr = $this->connection->getConnectionResource();
192
+        if(!$this->ldap->isResource($cr)) {
193
+            //LDAP not available
194
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
195
+            return false;
196
+        }
197
+        //Cancel possibly running Paged Results operation, otherwise we run in
198
+        //LDAP protocol errors
199
+        $this->abandonPagedSearch();
200
+        // openLDAP requires that we init a new Paged Search. Not needed by AD,
201
+        // but does not hurt either.
202
+        $pagingSize = (int)$this->connection->ldapPagingSize;
203
+        // 0 won't result in replies, small numbers may leave out groups
204
+        // (cf. #12306), 500 is default for paging and should work everywhere.
205
+        $maxResults = $pagingSize > 20 ? $pagingSize : 500;
206
+        $attr = mb_strtolower($attr, 'UTF-8');
207
+        // the actual read attribute later may contain parameters on a ranged
208
+        // request, e.g. member;range=99-199. Depends on server reply.
209
+        $attrToRead = $attr;
210
+
211
+        $values = [];
212
+        $isRangeRequest = false;
213
+        do {
214
+            $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults);
215
+            if(is_bool($result)) {
216
+                // when an exists request was run and it was successful, an empty
217
+                // array must be returned
218
+                return $result ? [] : false;
219
+            }
220
+
221
+            if (!$isRangeRequest) {
222
+                $values = $this->extractAttributeValuesFromResult($result, $attr);
223
+                if (!empty($values)) {
224
+                    return $values;
225
+                }
226
+            }
227
+
228
+            $isRangeRequest = false;
229
+            $result = $this->extractRangeData($result, $attr);
230
+            if (!empty($result)) {
231
+                $normalizedResult = $this->extractAttributeValuesFromResult(
232
+                    [ $attr => $result['values'] ],
233
+                    $attr
234
+                );
235
+                $values = array_merge($values, $normalizedResult);
236
+
237
+                if($result['rangeHigh'] === '*') {
238
+                    // when server replies with * as high range value, there are
239
+                    // no more results left
240
+                    return $values;
241
+                } else {
242
+                    $low  = $result['rangeHigh'] + 1;
243
+                    $attrToRead = $result['attributeName'] . ';range=' . $low . '-*';
244
+                    $isRangeRequest = true;
245
+                }
246
+            }
247
+        } while($isRangeRequest);
248
+
249
+        \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG);
250
+        return false;
251
+    }
252
+
253
+    /**
254
+     * Runs an read operation against LDAP
255
+     *
256
+     * @param resource $cr the LDAP connection
257
+     * @param string $dn
258
+     * @param string $attribute
259
+     * @param string $filter
260
+     * @param int $maxResults
261
+     * @return array|bool false if there was any error, true if an exists check
262
+     *                    was performed and the requested DN found, array with the
263
+     *                    returned data on a successful usual operation
264
+     * @throws ServerNotAvailableException
265
+     */
266
+    public function executeRead($cr, $dn, $attribute, $filter, $maxResults) {
267
+        $this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0);
268
+        $dn = $this->helper->DNasBaseParameter($dn);
269
+        $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute));
270
+        if (!$this->ldap->isResource($rr)) {
271
+            if ($attribute !== '') {
272
+                //do not throw this message on userExists check, irritates
273
+                \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG);
274
+            }
275
+            //in case an error occurs , e.g. object does not exist
276
+            return false;
277
+        }
278
+        if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) {
279
+            \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG);
280
+            return true;
281
+        }
282
+        $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr);
283
+        if (!$this->ldap->isResource($er)) {
284
+            //did not match the filter, return false
285
+            return false;
286
+        }
287
+        //LDAP attributes are not case sensitive
288
+        $result = \OCP\Util::mb_array_change_key_case(
289
+            $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8');
290
+
291
+        return $result;
292
+    }
293
+
294
+    /**
295
+     * Normalizes a result grom getAttributes(), i.e. handles DNs and binary
296
+     * data if present.
297
+     *
298
+     * @param array $result from ILDAPWrapper::getAttributes()
299
+     * @param string $attribute the attribute name that was read
300
+     * @return string[]
301
+     */
302
+    public function extractAttributeValuesFromResult($result, $attribute) {
303
+        $values = [];
304
+        if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) {
305
+            $lowercaseAttribute = strtolower($attribute);
306
+            for($i=0;$i<$result[$attribute]['count'];$i++) {
307
+                if($this->resemblesDN($attribute)) {
308
+                    $values[] = $this->helper->sanitizeDN($result[$attribute][$i]);
309
+                } elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') {
310
+                    $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]);
311
+                } else {
312
+                    $values[] = $result[$attribute][$i];
313
+                }
314
+            }
315
+        }
316
+        return $values;
317
+    }
318
+
319
+    /**
320
+     * Attempts to find ranged data in a getAttribute results and extracts the
321
+     * returned values as well as information on the range and full attribute
322
+     * name for further processing.
323
+     *
324
+     * @param array $result from ILDAPWrapper::getAttributes()
325
+     * @param string $attribute the attribute name that was read. Without ";range=…"
326
+     * @return array If a range was detected with keys 'values', 'attributeName',
327
+     *               'attributeFull' and 'rangeHigh', otherwise empty.
328
+     */
329
+    public function extractRangeData($result, $attribute) {
330
+        $keys = array_keys($result);
331
+        foreach($keys as $key) {
332
+            if($key !== $attribute && strpos($key, $attribute) === 0) {
333
+                $queryData = explode(';', $key);
334
+                if(strpos($queryData[1], 'range=') === 0) {
335
+                    $high = substr($queryData[1], 1 + strpos($queryData[1], '-'));
336
+                    $data = [
337
+                        'values' => $result[$key],
338
+                        'attributeName' => $queryData[0],
339
+                        'attributeFull' => $key,
340
+                        'rangeHigh' => $high,
341
+                    ];
342
+                    return $data;
343
+                }
344
+            }
345
+        }
346
+        return [];
347
+    }
348 348
 	
349
-	/**
350
-	 * Set password for an LDAP user identified by a DN
351
-	 *
352
-	 * @param string $userDN the user in question
353
-	 * @param string $password the new password
354
-	 * @return bool
355
-	 * @throws HintException
356
-	 * @throws \Exception
357
-	 */
358
-	public function setPassword($userDN, $password) {
359
-		if((int)$this->connection->turnOnPasswordChange !== 1) {
360
-			throw new \Exception('LDAP password changes are disabled.');
361
-		}
362
-		$cr = $this->connection->getConnectionResource();
363
-		if(!$this->ldap->isResource($cr)) {
364
-			//LDAP not available
365
-			\OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
366
-			return false;
367
-		}
368
-		try {
369
-			return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
370
-		} catch(ConstraintViolationException $e) {
371
-			throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
372
-		}
373
-	}
374
-
375
-	/**
376
-	 * checks whether the given attributes value is probably a DN
377
-	 * @param string $attr the attribute in question
378
-	 * @return boolean if so true, otherwise false
379
-	 */
380
-	private function resemblesDN($attr) {
381
-		$resemblingAttributes = array(
382
-			'dn',
383
-			'uniquemember',
384
-			'member',
385
-			// memberOf is an "operational" attribute, without a definition in any RFC
386
-			'memberof'
387
-		);
388
-		return in_array($attr, $resemblingAttributes);
389
-	}
390
-
391
-	/**
392
-	 * checks whether the given string is probably a DN
393
-	 * @param string $string
394
-	 * @return boolean
395
-	 */
396
-	public function stringResemblesDN($string) {
397
-		$r = $this->ldap->explodeDN($string, 0);
398
-		// if exploding a DN succeeds and does not end up in
399
-		// an empty array except for $r[count] being 0.
400
-		return (is_array($r) && count($r) > 1);
401
-	}
402
-
403
-	/**
404
-	 * returns a DN-string that is cleaned from not domain parts, e.g.
405
-	 * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
406
-	 * becomes dc=foobar,dc=server,dc=org
407
-	 * @param string $dn
408
-	 * @return string
409
-	 */
410
-	public function getDomainDNFromDN($dn) {
411
-		$allParts = $this->ldap->explodeDN($dn, 0);
412
-		if($allParts === false) {
413
-			//not a valid DN
414
-			return '';
415
-		}
416
-		$domainParts = array();
417
-		$dcFound = false;
418
-		foreach($allParts as $part) {
419
-			if(!$dcFound && strpos($part, 'dc=') === 0) {
420
-				$dcFound = true;
421
-			}
422
-			if($dcFound) {
423
-				$domainParts[] = $part;
424
-			}
425
-		}
426
-		return implode(',', $domainParts);
427
-	}
428
-
429
-	/**
430
-	 * returns the LDAP DN for the given internal Nextcloud name of the group
431
-	 * @param string $name the Nextcloud name in question
432
-	 * @return string|false LDAP DN on success, otherwise false
433
-	 */
434
-	public function groupname2dn($name) {
435
-		return $this->groupMapper->getDNByName($name);
436
-	}
437
-
438
-	/**
439
-	 * returns the LDAP DN for the given internal Nextcloud name of the user
440
-	 * @param string $name the Nextcloud name in question
441
-	 * @return string|false with the LDAP DN on success, otherwise false
442
-	 */
443
-	public function username2dn($name) {
444
-		$fdn = $this->userMapper->getDNByName($name);
445
-
446
-		//Check whether the DN belongs to the Base, to avoid issues on multi-
447
-		//server setups
448
-		if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
449
-			return $fdn;
450
-		}
451
-
452
-		return false;
453
-	}
454
-
455
-	/**
456
-	 * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
457
-	 * @param string $fdn the dn of the group object
458
-	 * @param string $ldapName optional, the display name of the object
459
-	 * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
460
-	 */
461
-	public function dn2groupname($fdn, $ldapName = null) {
462
-		//To avoid bypassing the base DN settings under certain circumstances
463
-		//with the group support, check whether the provided DN matches one of
464
-		//the given Bases
465
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
466
-			return false;
467
-		}
468
-
469
-		return $this->dn2ocname($fdn, $ldapName, false);
470
-	}
471
-
472
-	/**
473
-	 * accepts an array of group DNs and tests whether they match the user
474
-	 * filter by doing read operations against the group entries. Returns an
475
-	 * array of DNs that match the filter.
476
-	 *
477
-	 * @param string[] $groupDNs
478
-	 * @return string[]
479
-	 * @throws ServerNotAvailableException
480
-	 */
481
-	public function groupsMatchFilter($groupDNs) {
482
-		$validGroupDNs = [];
483
-		foreach($groupDNs as $dn) {
484
-			$cacheKey = 'groupsMatchFilter-'.$dn;
485
-			$groupMatchFilter = $this->connection->getFromCache($cacheKey);
486
-			if(!is_null($groupMatchFilter)) {
487
-				if($groupMatchFilter) {
488
-					$validGroupDNs[] = $dn;
489
-				}
490
-				continue;
491
-			}
492
-
493
-			// Check the base DN first. If this is not met already, we don't
494
-			// need to ask the server at all.
495
-			if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
496
-				$this->connection->writeToCache($cacheKey, false);
497
-				continue;
498
-			}
499
-
500
-			$result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
501
-			if(is_array($result)) {
502
-				$this->connection->writeToCache($cacheKey, true);
503
-				$validGroupDNs[] = $dn;
504
-			} else {
505
-				$this->connection->writeToCache($cacheKey, false);
506
-			}
507
-
508
-		}
509
-		return $validGroupDNs;
510
-	}
511
-
512
-	/**
513
-	 * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
514
-	 * @param string $dn the dn of the user object
515
-	 * @param string $ldapName optional, the display name of the object
516
-	 * @return string|false with with the name to use in Nextcloud
517
-	 */
518
-	public function dn2username($fdn, $ldapName = null) {
519
-		//To avoid bypassing the base DN settings under certain circumstances
520
-		//with the group support, check whether the provided DN matches one of
521
-		//the given Bases
522
-		if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
523
-			return false;
524
-		}
525
-
526
-		return $this->dn2ocname($fdn, $ldapName, true);
527
-	}
528
-
529
-	/**
530
-	 * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
531
-	 *
532
-	 * @param string $fdn the dn of the user object
533
-	 * @param string|null $ldapName optional, the display name of the object
534
-	 * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
535
-	 * @param bool|null $newlyMapped
536
-	 * @param array|null $record
537
-	 * @return false|string with with the name to use in Nextcloud
538
-	 * @throws \Exception
539
-	 */
540
-	public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
541
-		$newlyMapped = false;
542
-		if($isUser) {
543
-			$mapper = $this->getUserMapper();
544
-			$nameAttribute = $this->connection->ldapUserDisplayName;
545
-			$filter = $this->connection->ldapUserFilter;
546
-		} else {
547
-			$mapper = $this->getGroupMapper();
548
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
549
-			$filter = $this->connection->ldapGroupFilter;
550
-		}
551
-
552
-		//let's try to retrieve the Nextcloud name from the mappings table
553
-		$ncName = $mapper->getNameByDN($fdn);
554
-		if(is_string($ncName)) {
555
-			return $ncName;
556
-		}
557
-
558
-		//second try: get the UUID and check if it is known. Then, update the DN and return the name.
559
-		$uuid = $this->getUUID($fdn, $isUser, $record);
560
-		if(is_string($uuid)) {
561
-			$ncName = $mapper->getNameByUUID($uuid);
562
-			if(is_string($ncName)) {
563
-				$mapper->setDNbyUUID($fdn, $uuid);
564
-				return $ncName;
565
-			}
566
-		} else {
567
-			//If the UUID can't be detected something is foul.
568
-			\OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);
569
-			return false;
570
-		}
571
-
572
-		if(is_null($ldapName)) {
573
-			$ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
574
-			if(!isset($ldapName[0]) && empty($ldapName[0])) {
575
-				\OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);
576
-				return false;
577
-			}
578
-			$ldapName = $ldapName[0];
579
-		}
580
-
581
-		if($isUser) {
582
-			$usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
583
-			if ($usernameAttribute !== '') {
584
-				$username = $this->readAttribute($fdn, $usernameAttribute);
585
-				$username = $username[0];
586
-			} else {
587
-				$username = $uuid;
588
-			}
589
-			try {
590
-				$intName = $this->sanitizeUsername($username);
591
-			} catch (\InvalidArgumentException $e) {
592
-				\OC::$server->getLogger()->logException($e, [
593
-					'app' => 'user_ldap',
594
-					'level' => ILogger::WARN,
595
-				]);
596
-				// we don't attempt to set a username here. We can go for
597
-				// for an alternative 4 digit random number as we would append
598
-				// otherwise, however it's likely not enough space in bigger
599
-				// setups, and most importantly: this is not intended.
600
-				return false;
601
-			}
602
-		} else {
603
-			$intName = $ldapName;
604
-		}
605
-
606
-		//a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
607
-		//disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
608
-		//NOTE: mind, disabling cache affects only this instance! Using it
609
-		// outside of core user management will still cache the user as non-existing.
610
-		$originalTTL = $this->connection->ldapCacheTTL;
611
-		$this->connection->setConfiguration(['ldapCacheTTL' => 0]);
612
-		if( $intName !== ''
613
-			&& (($isUser && !$this->ncUserManager->userExists($intName))
614
-				|| (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
615
-			)
616
-		) {
617
-			$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
618
-			$newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
619
-			if($newlyMapped) {
620
-				return $intName;
621
-			}
622
-		}
623
-
624
-		$this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
625
-		$altName = $this->createAltInternalOwnCloudName($intName, $isUser);
626
-		if (is_string($altName)) {
627
-			if($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
628
-				$newlyMapped = true;
629
-				return $altName;
630
-			}
631
-		}
632
-
633
-		//if everything else did not help..
634
-		\OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
635
-		return false;
636
-	}
637
-
638
-	public function mapAndAnnounceIfApplicable(
639
-		AbstractMapping $mapper,
640
-		string $fdn,
641
-		string $name,
642
-		string $uuid,
643
-		bool $isUser
644
-	) :bool {
645
-		if($mapper->map($fdn, $name, $uuid)) {
646
-			if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
647
-				$this->cacheUserExists($name);
648
-				$this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
649
-			}
650
-			return true;
651
-		}
652
-		return false;
653
-	}
654
-
655
-	/**
656
-	 * gives back the user names as they are used ownClod internally
657
-	 * @param array $ldapUsers as returned by fetchList()
658
-	 * @return array an array with the user names to use in Nextcloud
659
-	 *
660
-	 * gives back the user names as they are used ownClod internally
661
-	 */
662
-	public function nextcloudUserNames($ldapUsers) {
663
-		return $this->ldap2NextcloudNames($ldapUsers, true);
664
-	}
665
-
666
-	/**
667
-	 * gives back the group names as they are used ownClod internally
668
-	 * @param array $ldapGroups as returned by fetchList()
669
-	 * @return array an array with the group names to use in Nextcloud
670
-	 *
671
-	 * gives back the group names as they are used ownClod internally
672
-	 */
673
-	public function nextcloudGroupNames($ldapGroups) {
674
-		return $this->ldap2NextcloudNames($ldapGroups, false);
675
-	}
676
-
677
-	/**
678
-	 * @param array $ldapObjects as returned by fetchList()
679
-	 * @param bool $isUsers
680
-	 * @return array
681
-	 * @throws \Exception
682
-	 */
683
-	private function ldap2NextcloudNames($ldapObjects, $isUsers) {
684
-		if($isUsers) {
685
-			$nameAttribute = $this->connection->ldapUserDisplayName;
686
-			$sndAttribute  = $this->connection->ldapUserDisplayName2;
687
-		} else {
688
-			$nameAttribute = $this->connection->ldapGroupDisplayName;
689
-		}
690
-		$nextcloudNames = [];
691
-
692
-		foreach($ldapObjects as $ldapObject) {
693
-			$nameByLDAP = null;
694
-			if(    isset($ldapObject[$nameAttribute])
695
-				&& is_array($ldapObject[$nameAttribute])
696
-				&& isset($ldapObject[$nameAttribute][0])
697
-			) {
698
-				// might be set, but not necessarily. if so, we use it.
699
-				$nameByLDAP = $ldapObject[$nameAttribute][0];
700
-			}
701
-
702
-			$ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
703
-			if($ncName) {
704
-				$nextcloudNames[] = $ncName;
705
-				if($isUsers) {
706
-					$this->updateUserState($ncName);
707
-					//cache the user names so it does not need to be retrieved
708
-					//again later (e.g. sharing dialogue).
709
-					if(is_null($nameByLDAP)) {
710
-						continue;
711
-					}
712
-					$sndName = isset($ldapObject[$sndAttribute][0])
713
-						? $ldapObject[$sndAttribute][0] : '';
714
-					$this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
715
-				} else if($nameByLDAP !== null) {
716
-					$this->cacheGroupDisplayName($ncName, $nameByLDAP);
717
-				}
718
-			}
719
-		}
720
-		return $nextcloudNames;
721
-	}
722
-
723
-	/**
724
-	 * removes the deleted-flag of a user if it was set
725
-	 *
726
-	 * @param string $ncname
727
-	 * @throws \Exception
728
-	 */
729
-	public function updateUserState($ncname) {
730
-		$user = $this->userManager->get($ncname);
731
-		if($user instanceof OfflineUser) {
732
-			$user->unmark();
733
-		}
734
-	}
735
-
736
-	/**
737
-	 * caches the user display name
738
-	 * @param string $ocName the internal Nextcloud username
739
-	 * @param string|false $home the home directory path
740
-	 */
741
-	public function cacheUserHome($ocName, $home) {
742
-		$cacheKey = 'getHome'.$ocName;
743
-		$this->connection->writeToCache($cacheKey, $home);
744
-	}
745
-
746
-	/**
747
-	 * caches a user as existing
748
-	 * @param string $ocName the internal Nextcloud username
749
-	 */
750
-	public function cacheUserExists($ocName) {
751
-		$this->connection->writeToCache('userExists'.$ocName, true);
752
-	}
753
-
754
-	/**
755
-	 * caches the user display name
756
-	 * @param string $ocName the internal Nextcloud username
757
-	 * @param string $displayName the display name
758
-	 * @param string $displayName2 the second display name
759
-	 */
760
-	public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
761
-		$user = $this->userManager->get($ocName);
762
-		if($user === null) {
763
-			return;
764
-		}
765
-		$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
766
-		$cacheKeyTrunk = 'getDisplayName';
767
-		$this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
768
-	}
769
-
770
-	public function cacheGroupDisplayName(string $ncName, string $displayName): void {
771
-		$cacheKey = 'group_getDisplayName' . $ncName;
772
-		$this->connection->writeToCache($cacheKey, $displayName);
773
-	}
774
-
775
-	/**
776
-	 * creates a unique name for internal Nextcloud use for users. Don't call it directly.
777
-	 * @param string $name the display name of the object
778
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
779
-	 *
780
-	 * Instead of using this method directly, call
781
-	 * createAltInternalOwnCloudName($name, true)
782
-	 */
783
-	private function _createAltInternalOwnCloudNameForUsers($name) {
784
-		$attempts = 0;
785
-		//while loop is just a precaution. If a name is not generated within
786
-		//20 attempts, something else is very wrong. Avoids infinite loop.
787
-		while($attempts < 20){
788
-			$altName = $name . '_' . rand(1000,9999);
789
-			if(!$this->ncUserManager->userExists($altName)) {
790
-				return $altName;
791
-			}
792
-			$attempts++;
793
-		}
794
-		return false;
795
-	}
796
-
797
-	/**
798
-	 * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
799
-	 * @param string $name the display name of the object
800
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
801
-	 *
802
-	 * Instead of using this method directly, call
803
-	 * createAltInternalOwnCloudName($name, false)
804
-	 *
805
-	 * Group names are also used as display names, so we do a sequential
806
-	 * numbering, e.g. Developers_42 when there are 41 other groups called
807
-	 * "Developers"
808
-	 */
809
-	private function _createAltInternalOwnCloudNameForGroups($name) {
810
-		$usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
811
-		if(!$usedNames || count($usedNames) === 0) {
812
-			$lastNo = 1; //will become name_2
813
-		} else {
814
-			natsort($usedNames);
815
-			$lastName = array_pop($usedNames);
816
-			$lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
817
-		}
818
-		$altName = $name.'_'. (string)($lastNo+1);
819
-		unset($usedNames);
820
-
821
-		$attempts = 1;
822
-		while($attempts < 21){
823
-			// Check to be really sure it is unique
824
-			// while loop is just a precaution. If a name is not generated within
825
-			// 20 attempts, something else is very wrong. Avoids infinite loop.
826
-			if(!\OC::$server->getGroupManager()->groupExists($altName)) {
827
-				return $altName;
828
-			}
829
-			$altName = $name . '_' . ($lastNo + $attempts);
830
-			$attempts++;
831
-		}
832
-		return false;
833
-	}
834
-
835
-	/**
836
-	 * creates a unique name for internal Nextcloud use.
837
-	 * @param string $name the display name of the object
838
-	 * @param boolean $isUser whether name should be created for a user (true) or a group (false)
839
-	 * @return string|false with with the name to use in Nextcloud or false if unsuccessful
840
-	 */
841
-	private function createAltInternalOwnCloudName($name, $isUser) {
842
-		$originalTTL = $this->connection->ldapCacheTTL;
843
-		$this->connection->setConfiguration(array('ldapCacheTTL' => 0));
844
-		if($isUser) {
845
-			$altName = $this->_createAltInternalOwnCloudNameForUsers($name);
846
-		} else {
847
-			$altName = $this->_createAltInternalOwnCloudNameForGroups($name);
848
-		}
849
-		$this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
850
-
851
-		return $altName;
852
-	}
853
-
854
-	/**
855
-	 * fetches a list of users according to a provided loginName and utilizing
856
-	 * the login filter.
857
-	 *
858
-	 * @param string $loginName
859
-	 * @param array $attributes optional, list of attributes to read
860
-	 * @return array
861
-	 */
862
-	public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
863
-		$loginName = $this->escapeFilterPart($loginName);
864
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
865
-		return $this->fetchListOfUsers($filter, $attributes);
866
-	}
867
-
868
-	/**
869
-	 * counts the number of users according to a provided loginName and
870
-	 * utilizing the login filter.
871
-	 *
872
-	 * @param string $loginName
873
-	 * @return int
874
-	 */
875
-	public function countUsersByLoginName($loginName) {
876
-		$loginName = $this->escapeFilterPart($loginName);
877
-		$filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
878
-		return $this->countUsers($filter);
879
-	}
880
-
881
-	/**
882
-	 * @param string $filter
883
-	 * @param string|string[] $attr
884
-	 * @param int $limit
885
-	 * @param int $offset
886
-	 * @param bool $forceApplyAttributes
887
-	 * @return array
888
-	 */
889
-	public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
890
-		$ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
891
-		$recordsToUpdate = $ldapRecords;
892
-		if(!$forceApplyAttributes) {
893
-			$isBackgroundJobModeAjax = $this->config
894
-					->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
895
-			$recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
896
-				$newlyMapped = false;
897
-				$uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
898
-				if(is_string($uid)) {
899
-					$this->cacheUserExists($uid);
900
-				}
901
-				return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
902
-			});
903
-		}
904
-		$this->batchApplyUserAttributes($recordsToUpdate);
905
-		return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
906
-	}
907
-
908
-	/**
909
-	 * provided with an array of LDAP user records the method will fetch the
910
-	 * user object and requests it to process the freshly fetched attributes and
911
-	 * and their values
912
-	 *
913
-	 * @param array $ldapRecords
914
-	 * @throws \Exception
915
-	 */
916
-	public function batchApplyUserAttributes(array $ldapRecords){
917
-		$displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
918
-		foreach($ldapRecords as $userRecord) {
919
-			if(!isset($userRecord[$displayNameAttribute])) {
920
-				// displayName is obligatory
921
-				continue;
922
-			}
923
-			$ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
924
-			if($ocName === false) {
925
-				continue;
926
-			}
927
-			$this->updateUserState($ocName);
928
-			$user = $this->userManager->get($ocName);
929
-			if ($user !== null) {
930
-				$user->processAttributes($userRecord);
931
-			} else {
932
-				\OC::$server->getLogger()->debug(
933
-					"The ldap user manager returned null for $ocName",
934
-					['app'=>'user_ldap']
935
-				);
936
-			}
937
-		}
938
-	}
939
-
940
-	/**
941
-	 * @param string $filter
942
-	 * @param string|string[] $attr
943
-	 * @param int $limit
944
-	 * @param int $offset
945
-	 * @return array
946
-	 */
947
-	public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
948
-		return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), $this->manyAttributes($attr));
949
-	}
950
-
951
-	/**
952
-	 * @param array $list
953
-	 * @param bool $manyAttributes
954
-	 * @return array
955
-	 */
956
-	private function fetchList($list, $manyAttributes) {
957
-		if(is_array($list)) {
958
-			if($manyAttributes) {
959
-				return $list;
960
-			} else {
961
-				$list = array_reduce($list, function($carry, $item) {
962
-					$attribute = array_keys($item)[0];
963
-					$carry[] = $item[$attribute][0];
964
-					return $carry;
965
-				}, array());
966
-				return array_unique($list, SORT_LOCALE_STRING);
967
-			}
968
-		}
969
-
970
-		//error cause actually, maybe throw an exception in future.
971
-		return array();
972
-	}
973
-
974
-	/**
975
-	 * executes an LDAP search, optimized for Users
976
-	 * @param string $filter the LDAP filter for the search
977
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
978
-	 * @param integer $limit
979
-	 * @param integer $offset
980
-	 * @return array with the search result
981
-	 *
982
-	 * Executes an LDAP search
983
-	 */
984
-	public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
985
-		$result = [];
986
-		foreach($this->connection->ldapBaseUsers as $base) {
987
-			$result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
988
-		}
989
-		return $result;
990
-	}
991
-
992
-	/**
993
-	 * @param string $filter
994
-	 * @param string|string[] $attr
995
-	 * @param int $limit
996
-	 * @param int $offset
997
-	 * @return false|int
998
-	 */
999
-	public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
1000
-		$result = false;
1001
-		foreach($this->connection->ldapBaseUsers as $base) {
1002
-			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1003
-			$result = is_int($count) ? (int)$result + $count : $result;
1004
-		}
1005
-		return $result;
1006
-	}
1007
-
1008
-	/**
1009
-	 * executes an LDAP search, optimized for Groups
1010
-	 * @param string $filter the LDAP filter for the search
1011
-	 * @param string|string[] $attr optional, when a certain attribute shall be filtered out
1012
-	 * @param integer $limit
1013
-	 * @param integer $offset
1014
-	 * @return array with the search result
1015
-	 *
1016
-	 * Executes an LDAP search
1017
-	 */
1018
-	public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1019
-		$result = [];
1020
-		foreach($this->connection->ldapBaseGroups as $base) {
1021
-			$result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
1022
-		}
1023
-		return $result;
1024
-	}
1025
-
1026
-	/**
1027
-	 * returns the number of available groups
1028
-	 * @param string $filter the LDAP search filter
1029
-	 * @param string[] $attr optional
1030
-	 * @param int|null $limit
1031
-	 * @param int|null $offset
1032
-	 * @return int|bool
1033
-	 */
1034
-	public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
1035
-		$result = false;
1036
-		foreach($this->connection->ldapBaseGroups as $base) {
1037
-			$count = $this->count($filter, [$base], $attr, $limit, $offset);
1038
-			$result = is_int($count) ? (int)$result + $count : $result;
1039
-		}
1040
-		return $result;
1041
-	}
1042
-
1043
-	/**
1044
-	 * returns the number of available objects on the base DN
1045
-	 *
1046
-	 * @param int|null $limit
1047
-	 * @param int|null $offset
1048
-	 * @return int|bool
1049
-	 */
1050
-	public function countObjects($limit = null, $offset = null) {
1051
-		$result = false;
1052
-		foreach($this->connection->ldapBase as $base) {
1053
-			$count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
1054
-			$result = is_int($count) ? (int)$result + $count : $result;
1055
-		}
1056
-		return $result;
1057
-	}
1058
-
1059
-	/**
1060
-	 * Returns the LDAP handler
1061
-	 * @throws \OC\ServerNotAvailableException
1062
-	 */
1063
-
1064
-	/**
1065
-	 * @return mixed
1066
-	 * @throws \OC\ServerNotAvailableException
1067
-	 */
1068
-	private function invokeLDAPMethod() {
1069
-		$arguments = func_get_args();
1070
-		$command = array_shift($arguments);
1071
-		$cr = array_shift($arguments);
1072
-		if (!method_exists($this->ldap, $command)) {
1073
-			return null;
1074
-		}
1075
-		array_unshift($arguments, $cr);
1076
-		// php no longer supports call-time pass-by-reference
1077
-		// thus cannot support controlPagedResultResponse as the third argument
1078
-		// is a reference
1079
-		$doMethod = function () use ($command, &$arguments) {
1080
-			if ($command == 'controlPagedResultResponse') {
1081
-				throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1082
-			} else {
1083
-				return call_user_func_array(array($this->ldap, $command), $arguments);
1084
-			}
1085
-		};
1086
-		try {
1087
-			$ret = $doMethod();
1088
-		} catch (ServerNotAvailableException $e) {
1089
-			/* Server connection lost, attempt to reestablish it
349
+    /**
350
+     * Set password for an LDAP user identified by a DN
351
+     *
352
+     * @param string $userDN the user in question
353
+     * @param string $password the new password
354
+     * @return bool
355
+     * @throws HintException
356
+     * @throws \Exception
357
+     */
358
+    public function setPassword($userDN, $password) {
359
+        if((int)$this->connection->turnOnPasswordChange !== 1) {
360
+            throw new \Exception('LDAP password changes are disabled.');
361
+        }
362
+        $cr = $this->connection->getConnectionResource();
363
+        if(!$this->ldap->isResource($cr)) {
364
+            //LDAP not available
365
+            \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG);
366
+            return false;
367
+        }
368
+        try {
369
+            return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password);
370
+        } catch(ConstraintViolationException $e) {
371
+            throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode());
372
+        }
373
+    }
374
+
375
+    /**
376
+     * checks whether the given attributes value is probably a DN
377
+     * @param string $attr the attribute in question
378
+     * @return boolean if so true, otherwise false
379
+     */
380
+    private function resemblesDN($attr) {
381
+        $resemblingAttributes = array(
382
+            'dn',
383
+            'uniquemember',
384
+            'member',
385
+            // memberOf is an "operational" attribute, without a definition in any RFC
386
+            'memberof'
387
+        );
388
+        return in_array($attr, $resemblingAttributes);
389
+    }
390
+
391
+    /**
392
+     * checks whether the given string is probably a DN
393
+     * @param string $string
394
+     * @return boolean
395
+     */
396
+    public function stringResemblesDN($string) {
397
+        $r = $this->ldap->explodeDN($string, 0);
398
+        // if exploding a DN succeeds and does not end up in
399
+        // an empty array except for $r[count] being 0.
400
+        return (is_array($r) && count($r) > 1);
401
+    }
402
+
403
+    /**
404
+     * returns a DN-string that is cleaned from not domain parts, e.g.
405
+     * cn=foo,cn=bar,dc=foobar,dc=server,dc=org
406
+     * becomes dc=foobar,dc=server,dc=org
407
+     * @param string $dn
408
+     * @return string
409
+     */
410
+    public function getDomainDNFromDN($dn) {
411
+        $allParts = $this->ldap->explodeDN($dn, 0);
412
+        if($allParts === false) {
413
+            //not a valid DN
414
+            return '';
415
+        }
416
+        $domainParts = array();
417
+        $dcFound = false;
418
+        foreach($allParts as $part) {
419
+            if(!$dcFound && strpos($part, 'dc=') === 0) {
420
+                $dcFound = true;
421
+            }
422
+            if($dcFound) {
423
+                $domainParts[] = $part;
424
+            }
425
+        }
426
+        return implode(',', $domainParts);
427
+    }
428
+
429
+    /**
430
+     * returns the LDAP DN for the given internal Nextcloud name of the group
431
+     * @param string $name the Nextcloud name in question
432
+     * @return string|false LDAP DN on success, otherwise false
433
+     */
434
+    public function groupname2dn($name) {
435
+        return $this->groupMapper->getDNByName($name);
436
+    }
437
+
438
+    /**
439
+     * returns the LDAP DN for the given internal Nextcloud name of the user
440
+     * @param string $name the Nextcloud name in question
441
+     * @return string|false with the LDAP DN on success, otherwise false
442
+     */
443
+    public function username2dn($name) {
444
+        $fdn = $this->userMapper->getDNByName($name);
445
+
446
+        //Check whether the DN belongs to the Base, to avoid issues on multi-
447
+        //server setups
448
+        if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
449
+            return $fdn;
450
+        }
451
+
452
+        return false;
453
+    }
454
+
455
+    /**
456
+     * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure
457
+     * @param string $fdn the dn of the group object
458
+     * @param string $ldapName optional, the display name of the object
459
+     * @return string|false with the name to use in Nextcloud, false on DN outside of search DN
460
+     */
461
+    public function dn2groupname($fdn, $ldapName = null) {
462
+        //To avoid bypassing the base DN settings under certain circumstances
463
+        //with the group support, check whether the provided DN matches one of
464
+        //the given Bases
465
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) {
466
+            return false;
467
+        }
468
+
469
+        return $this->dn2ocname($fdn, $ldapName, false);
470
+    }
471
+
472
+    /**
473
+     * accepts an array of group DNs and tests whether they match the user
474
+     * filter by doing read operations against the group entries. Returns an
475
+     * array of DNs that match the filter.
476
+     *
477
+     * @param string[] $groupDNs
478
+     * @return string[]
479
+     * @throws ServerNotAvailableException
480
+     */
481
+    public function groupsMatchFilter($groupDNs) {
482
+        $validGroupDNs = [];
483
+        foreach($groupDNs as $dn) {
484
+            $cacheKey = 'groupsMatchFilter-'.$dn;
485
+            $groupMatchFilter = $this->connection->getFromCache($cacheKey);
486
+            if(!is_null($groupMatchFilter)) {
487
+                if($groupMatchFilter) {
488
+                    $validGroupDNs[] = $dn;
489
+                }
490
+                continue;
491
+            }
492
+
493
+            // Check the base DN first. If this is not met already, we don't
494
+            // need to ask the server at all.
495
+            if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) {
496
+                $this->connection->writeToCache($cacheKey, false);
497
+                continue;
498
+            }
499
+
500
+            $result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter);
501
+            if(is_array($result)) {
502
+                $this->connection->writeToCache($cacheKey, true);
503
+                $validGroupDNs[] = $dn;
504
+            } else {
505
+                $this->connection->writeToCache($cacheKey, false);
506
+            }
507
+
508
+        }
509
+        return $validGroupDNs;
510
+    }
511
+
512
+    /**
513
+     * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure
514
+     * @param string $dn the dn of the user object
515
+     * @param string $ldapName optional, the display name of the object
516
+     * @return string|false with with the name to use in Nextcloud
517
+     */
518
+    public function dn2username($fdn, $ldapName = null) {
519
+        //To avoid bypassing the base DN settings under certain circumstances
520
+        //with the group support, check whether the provided DN matches one of
521
+        //the given Bases
522
+        if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) {
523
+            return false;
524
+        }
525
+
526
+        return $this->dn2ocname($fdn, $ldapName, true);
527
+    }
528
+
529
+    /**
530
+     * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN
531
+     *
532
+     * @param string $fdn the dn of the user object
533
+     * @param string|null $ldapName optional, the display name of the object
534
+     * @param bool $isUser optional, whether it is a user object (otherwise group assumed)
535
+     * @param bool|null $newlyMapped
536
+     * @param array|null $record
537
+     * @return false|string with with the name to use in Nextcloud
538
+     * @throws \Exception
539
+     */
540
+    public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) {
541
+        $newlyMapped = false;
542
+        if($isUser) {
543
+            $mapper = $this->getUserMapper();
544
+            $nameAttribute = $this->connection->ldapUserDisplayName;
545
+            $filter = $this->connection->ldapUserFilter;
546
+        } else {
547
+            $mapper = $this->getGroupMapper();
548
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
549
+            $filter = $this->connection->ldapGroupFilter;
550
+        }
551
+
552
+        //let's try to retrieve the Nextcloud name from the mappings table
553
+        $ncName = $mapper->getNameByDN($fdn);
554
+        if(is_string($ncName)) {
555
+            return $ncName;
556
+        }
557
+
558
+        //second try: get the UUID and check if it is known. Then, update the DN and return the name.
559
+        $uuid = $this->getUUID($fdn, $isUser, $record);
560
+        if(is_string($uuid)) {
561
+            $ncName = $mapper->getNameByUUID($uuid);
562
+            if(is_string($ncName)) {
563
+                $mapper->setDNbyUUID($fdn, $uuid);
564
+                return $ncName;
565
+            }
566
+        } else {
567
+            //If the UUID can't be detected something is foul.
568
+            \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO);
569
+            return false;
570
+        }
571
+
572
+        if(is_null($ldapName)) {
573
+            $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter);
574
+            if(!isset($ldapName[0]) && empty($ldapName[0])) {
575
+                \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO);
576
+                return false;
577
+            }
578
+            $ldapName = $ldapName[0];
579
+        }
580
+
581
+        if($isUser) {
582
+            $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr;
583
+            if ($usernameAttribute !== '') {
584
+                $username = $this->readAttribute($fdn, $usernameAttribute);
585
+                $username = $username[0];
586
+            } else {
587
+                $username = $uuid;
588
+            }
589
+            try {
590
+                $intName = $this->sanitizeUsername($username);
591
+            } catch (\InvalidArgumentException $e) {
592
+                \OC::$server->getLogger()->logException($e, [
593
+                    'app' => 'user_ldap',
594
+                    'level' => ILogger::WARN,
595
+                ]);
596
+                // we don't attempt to set a username here. We can go for
597
+                // for an alternative 4 digit random number as we would append
598
+                // otherwise, however it's likely not enough space in bigger
599
+                // setups, and most importantly: this is not intended.
600
+                return false;
601
+            }
602
+        } else {
603
+            $intName = $ldapName;
604
+        }
605
+
606
+        //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups
607
+        //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check
608
+        //NOTE: mind, disabling cache affects only this instance! Using it
609
+        // outside of core user management will still cache the user as non-existing.
610
+        $originalTTL = $this->connection->ldapCacheTTL;
611
+        $this->connection->setConfiguration(['ldapCacheTTL' => 0]);
612
+        if( $intName !== ''
613
+            && (($isUser && !$this->ncUserManager->userExists($intName))
614
+                || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))
615
+            )
616
+        ) {
617
+            $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
618
+            $newlyMapped = $this->mapAndAnnounceIfApplicable($mapper, $fdn, $intName, $uuid, $isUser);
619
+            if($newlyMapped) {
620
+                return $intName;
621
+            }
622
+        }
623
+
624
+        $this->connection->setConfiguration(['ldapCacheTTL' => $originalTTL]);
625
+        $altName = $this->createAltInternalOwnCloudName($intName, $isUser);
626
+        if (is_string($altName)) {
627
+            if($this->mapAndAnnounceIfApplicable($mapper, $fdn, $altName, $uuid, $isUser)) {
628
+                $newlyMapped = true;
629
+                return $altName;
630
+            }
631
+        }
632
+
633
+        //if everything else did not help..
634
+        \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO);
635
+        return false;
636
+    }
637
+
638
+    public function mapAndAnnounceIfApplicable(
639
+        AbstractMapping $mapper,
640
+        string $fdn,
641
+        string $name,
642
+        string $uuid,
643
+        bool $isUser
644
+    ) :bool {
645
+        if($mapper->map($fdn, $name, $uuid)) {
646
+            if ($this->ncUserManager instanceof PublicEmitter && $isUser) {
647
+                $this->cacheUserExists($name);
648
+                $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$name]);
649
+            }
650
+            return true;
651
+        }
652
+        return false;
653
+    }
654
+
655
+    /**
656
+     * gives back the user names as they are used ownClod internally
657
+     * @param array $ldapUsers as returned by fetchList()
658
+     * @return array an array with the user names to use in Nextcloud
659
+     *
660
+     * gives back the user names as they are used ownClod internally
661
+     */
662
+    public function nextcloudUserNames($ldapUsers) {
663
+        return $this->ldap2NextcloudNames($ldapUsers, true);
664
+    }
665
+
666
+    /**
667
+     * gives back the group names as they are used ownClod internally
668
+     * @param array $ldapGroups as returned by fetchList()
669
+     * @return array an array with the group names to use in Nextcloud
670
+     *
671
+     * gives back the group names as they are used ownClod internally
672
+     */
673
+    public function nextcloudGroupNames($ldapGroups) {
674
+        return $this->ldap2NextcloudNames($ldapGroups, false);
675
+    }
676
+
677
+    /**
678
+     * @param array $ldapObjects as returned by fetchList()
679
+     * @param bool $isUsers
680
+     * @return array
681
+     * @throws \Exception
682
+     */
683
+    private function ldap2NextcloudNames($ldapObjects, $isUsers) {
684
+        if($isUsers) {
685
+            $nameAttribute = $this->connection->ldapUserDisplayName;
686
+            $sndAttribute  = $this->connection->ldapUserDisplayName2;
687
+        } else {
688
+            $nameAttribute = $this->connection->ldapGroupDisplayName;
689
+        }
690
+        $nextcloudNames = [];
691
+
692
+        foreach($ldapObjects as $ldapObject) {
693
+            $nameByLDAP = null;
694
+            if(    isset($ldapObject[$nameAttribute])
695
+                && is_array($ldapObject[$nameAttribute])
696
+                && isset($ldapObject[$nameAttribute][0])
697
+            ) {
698
+                // might be set, but not necessarily. if so, we use it.
699
+                $nameByLDAP = $ldapObject[$nameAttribute][0];
700
+            }
701
+
702
+            $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers);
703
+            if($ncName) {
704
+                $nextcloudNames[] = $ncName;
705
+                if($isUsers) {
706
+                    $this->updateUserState($ncName);
707
+                    //cache the user names so it does not need to be retrieved
708
+                    //again later (e.g. sharing dialogue).
709
+                    if(is_null($nameByLDAP)) {
710
+                        continue;
711
+                    }
712
+                    $sndName = isset($ldapObject[$sndAttribute][0])
713
+                        ? $ldapObject[$sndAttribute][0] : '';
714
+                    $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName);
715
+                } else if($nameByLDAP !== null) {
716
+                    $this->cacheGroupDisplayName($ncName, $nameByLDAP);
717
+                }
718
+            }
719
+        }
720
+        return $nextcloudNames;
721
+    }
722
+
723
+    /**
724
+     * removes the deleted-flag of a user if it was set
725
+     *
726
+     * @param string $ncname
727
+     * @throws \Exception
728
+     */
729
+    public function updateUserState($ncname) {
730
+        $user = $this->userManager->get($ncname);
731
+        if($user instanceof OfflineUser) {
732
+            $user->unmark();
733
+        }
734
+    }
735
+
736
+    /**
737
+     * caches the user display name
738
+     * @param string $ocName the internal Nextcloud username
739
+     * @param string|false $home the home directory path
740
+     */
741
+    public function cacheUserHome($ocName, $home) {
742
+        $cacheKey = 'getHome'.$ocName;
743
+        $this->connection->writeToCache($cacheKey, $home);
744
+    }
745
+
746
+    /**
747
+     * caches a user as existing
748
+     * @param string $ocName the internal Nextcloud username
749
+     */
750
+    public function cacheUserExists($ocName) {
751
+        $this->connection->writeToCache('userExists'.$ocName, true);
752
+    }
753
+
754
+    /**
755
+     * caches the user display name
756
+     * @param string $ocName the internal Nextcloud username
757
+     * @param string $displayName the display name
758
+     * @param string $displayName2 the second display name
759
+     */
760
+    public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') {
761
+        $user = $this->userManager->get($ocName);
762
+        if($user === null) {
763
+            return;
764
+        }
765
+        $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
766
+        $cacheKeyTrunk = 'getDisplayName';
767
+        $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName);
768
+    }
769
+
770
+    public function cacheGroupDisplayName(string $ncName, string $displayName): void {
771
+        $cacheKey = 'group_getDisplayName' . $ncName;
772
+        $this->connection->writeToCache($cacheKey, $displayName);
773
+    }
774
+
775
+    /**
776
+     * creates a unique name for internal Nextcloud use for users. Don't call it directly.
777
+     * @param string $name the display name of the object
778
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
779
+     *
780
+     * Instead of using this method directly, call
781
+     * createAltInternalOwnCloudName($name, true)
782
+     */
783
+    private function _createAltInternalOwnCloudNameForUsers($name) {
784
+        $attempts = 0;
785
+        //while loop is just a precaution. If a name is not generated within
786
+        //20 attempts, something else is very wrong. Avoids infinite loop.
787
+        while($attempts < 20){
788
+            $altName = $name . '_' . rand(1000,9999);
789
+            if(!$this->ncUserManager->userExists($altName)) {
790
+                return $altName;
791
+            }
792
+            $attempts++;
793
+        }
794
+        return false;
795
+    }
796
+
797
+    /**
798
+     * creates a unique name for internal Nextcloud use for groups. Don't call it directly.
799
+     * @param string $name the display name of the object
800
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful.
801
+     *
802
+     * Instead of using this method directly, call
803
+     * createAltInternalOwnCloudName($name, false)
804
+     *
805
+     * Group names are also used as display names, so we do a sequential
806
+     * numbering, e.g. Developers_42 when there are 41 other groups called
807
+     * "Developers"
808
+     */
809
+    private function _createAltInternalOwnCloudNameForGroups($name) {
810
+        $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%');
811
+        if(!$usedNames || count($usedNames) === 0) {
812
+            $lastNo = 1; //will become name_2
813
+        } else {
814
+            natsort($usedNames);
815
+            $lastName = array_pop($usedNames);
816
+            $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1);
817
+        }
818
+        $altName = $name.'_'. (string)($lastNo+1);
819
+        unset($usedNames);
820
+
821
+        $attempts = 1;
822
+        while($attempts < 21){
823
+            // Check to be really sure it is unique
824
+            // while loop is just a precaution. If a name is not generated within
825
+            // 20 attempts, something else is very wrong. Avoids infinite loop.
826
+            if(!\OC::$server->getGroupManager()->groupExists($altName)) {
827
+                return $altName;
828
+            }
829
+            $altName = $name . '_' . ($lastNo + $attempts);
830
+            $attempts++;
831
+        }
832
+        return false;
833
+    }
834
+
835
+    /**
836
+     * creates a unique name for internal Nextcloud use.
837
+     * @param string $name the display name of the object
838
+     * @param boolean $isUser whether name should be created for a user (true) or a group (false)
839
+     * @return string|false with with the name to use in Nextcloud or false if unsuccessful
840
+     */
841
+    private function createAltInternalOwnCloudName($name, $isUser) {
842
+        $originalTTL = $this->connection->ldapCacheTTL;
843
+        $this->connection->setConfiguration(array('ldapCacheTTL' => 0));
844
+        if($isUser) {
845
+            $altName = $this->_createAltInternalOwnCloudNameForUsers($name);
846
+        } else {
847
+            $altName = $this->_createAltInternalOwnCloudNameForGroups($name);
848
+        }
849
+        $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL));
850
+
851
+        return $altName;
852
+    }
853
+
854
+    /**
855
+     * fetches a list of users according to a provided loginName and utilizing
856
+     * the login filter.
857
+     *
858
+     * @param string $loginName
859
+     * @param array $attributes optional, list of attributes to read
860
+     * @return array
861
+     */
862
+    public function fetchUsersByLoginName($loginName, $attributes = array('dn')) {
863
+        $loginName = $this->escapeFilterPart($loginName);
864
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
865
+        return $this->fetchListOfUsers($filter, $attributes);
866
+    }
867
+
868
+    /**
869
+     * counts the number of users according to a provided loginName and
870
+     * utilizing the login filter.
871
+     *
872
+     * @param string $loginName
873
+     * @return int
874
+     */
875
+    public function countUsersByLoginName($loginName) {
876
+        $loginName = $this->escapeFilterPart($loginName);
877
+        $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter);
878
+        return $this->countUsers($filter);
879
+    }
880
+
881
+    /**
882
+     * @param string $filter
883
+     * @param string|string[] $attr
884
+     * @param int $limit
885
+     * @param int $offset
886
+     * @param bool $forceApplyAttributes
887
+     * @return array
888
+     */
889
+    public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) {
890
+        $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset);
891
+        $recordsToUpdate = $ldapRecords;
892
+        if(!$forceApplyAttributes) {
893
+            $isBackgroundJobModeAjax = $this->config
894
+                    ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax';
895
+            $recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) {
896
+                $newlyMapped = false;
897
+                $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record);
898
+                if(is_string($uid)) {
899
+                    $this->cacheUserExists($uid);
900
+                }
901
+                return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax);
902
+            });
903
+        }
904
+        $this->batchApplyUserAttributes($recordsToUpdate);
905
+        return $this->fetchList($ldapRecords, $this->manyAttributes($attr));
906
+    }
907
+
908
+    /**
909
+     * provided with an array of LDAP user records the method will fetch the
910
+     * user object and requests it to process the freshly fetched attributes and
911
+     * and their values
912
+     *
913
+     * @param array $ldapRecords
914
+     * @throws \Exception
915
+     */
916
+    public function batchApplyUserAttributes(array $ldapRecords){
917
+        $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName);
918
+        foreach($ldapRecords as $userRecord) {
919
+            if(!isset($userRecord[$displayNameAttribute])) {
920
+                // displayName is obligatory
921
+                continue;
922
+            }
923
+            $ocName  = $this->dn2ocname($userRecord['dn'][0], null, true);
924
+            if($ocName === false) {
925
+                continue;
926
+            }
927
+            $this->updateUserState($ocName);
928
+            $user = $this->userManager->get($ocName);
929
+            if ($user !== null) {
930
+                $user->processAttributes($userRecord);
931
+            } else {
932
+                \OC::$server->getLogger()->debug(
933
+                    "The ldap user manager returned null for $ocName",
934
+                    ['app'=>'user_ldap']
935
+                );
936
+            }
937
+        }
938
+    }
939
+
940
+    /**
941
+     * @param string $filter
942
+     * @param string|string[] $attr
943
+     * @param int $limit
944
+     * @param int $offset
945
+     * @return array
946
+     */
947
+    public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) {
948
+        return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), $this->manyAttributes($attr));
949
+    }
950
+
951
+    /**
952
+     * @param array $list
953
+     * @param bool $manyAttributes
954
+     * @return array
955
+     */
956
+    private function fetchList($list, $manyAttributes) {
957
+        if(is_array($list)) {
958
+            if($manyAttributes) {
959
+                return $list;
960
+            } else {
961
+                $list = array_reduce($list, function($carry, $item) {
962
+                    $attribute = array_keys($item)[0];
963
+                    $carry[] = $item[$attribute][0];
964
+                    return $carry;
965
+                }, array());
966
+                return array_unique($list, SORT_LOCALE_STRING);
967
+            }
968
+        }
969
+
970
+        //error cause actually, maybe throw an exception in future.
971
+        return array();
972
+    }
973
+
974
+    /**
975
+     * executes an LDAP search, optimized for Users
976
+     * @param string $filter the LDAP filter for the search
977
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
978
+     * @param integer $limit
979
+     * @param integer $offset
980
+     * @return array with the search result
981
+     *
982
+     * Executes an LDAP search
983
+     */
984
+    public function searchUsers($filter, $attr = null, $limit = null, $offset = null) {
985
+        $result = [];
986
+        foreach($this->connection->ldapBaseUsers as $base) {
987
+            $result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
988
+        }
989
+        return $result;
990
+    }
991
+
992
+    /**
993
+     * @param string $filter
994
+     * @param string|string[] $attr
995
+     * @param int $limit
996
+     * @param int $offset
997
+     * @return false|int
998
+     */
999
+    public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) {
1000
+        $result = false;
1001
+        foreach($this->connection->ldapBaseUsers as $base) {
1002
+            $count = $this->count($filter, [$base], $attr, $limit, $offset);
1003
+            $result = is_int($count) ? (int)$result + $count : $result;
1004
+        }
1005
+        return $result;
1006
+    }
1007
+
1008
+    /**
1009
+     * executes an LDAP search, optimized for Groups
1010
+     * @param string $filter the LDAP filter for the search
1011
+     * @param string|string[] $attr optional, when a certain attribute shall be filtered out
1012
+     * @param integer $limit
1013
+     * @param integer $offset
1014
+     * @return array with the search result
1015
+     *
1016
+     * Executes an LDAP search
1017
+     */
1018
+    public function searchGroups($filter, $attr = null, $limit = null, $offset = null) {
1019
+        $result = [];
1020
+        foreach($this->connection->ldapBaseGroups as $base) {
1021
+            $result = array_merge($result, $this->search($filter, [$base], $attr, $limit, $offset));
1022
+        }
1023
+        return $result;
1024
+    }
1025
+
1026
+    /**
1027
+     * returns the number of available groups
1028
+     * @param string $filter the LDAP search filter
1029
+     * @param string[] $attr optional
1030
+     * @param int|null $limit
1031
+     * @param int|null $offset
1032
+     * @return int|bool
1033
+     */
1034
+    public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) {
1035
+        $result = false;
1036
+        foreach($this->connection->ldapBaseGroups as $base) {
1037
+            $count = $this->count($filter, [$base], $attr, $limit, $offset);
1038
+            $result = is_int($count) ? (int)$result + $count : $result;
1039
+        }
1040
+        return $result;
1041
+    }
1042
+
1043
+    /**
1044
+     * returns the number of available objects on the base DN
1045
+     *
1046
+     * @param int|null $limit
1047
+     * @param int|null $offset
1048
+     * @return int|bool
1049
+     */
1050
+    public function countObjects($limit = null, $offset = null) {
1051
+        $result = false;
1052
+        foreach($this->connection->ldapBase as $base) {
1053
+            $count = $this->count('objectclass=*', [$base], ['dn'], $limit, $offset);
1054
+            $result = is_int($count) ? (int)$result + $count : $result;
1055
+        }
1056
+        return $result;
1057
+    }
1058
+
1059
+    /**
1060
+     * Returns the LDAP handler
1061
+     * @throws \OC\ServerNotAvailableException
1062
+     */
1063
+
1064
+    /**
1065
+     * @return mixed
1066
+     * @throws \OC\ServerNotAvailableException
1067
+     */
1068
+    private function invokeLDAPMethod() {
1069
+        $arguments = func_get_args();
1070
+        $command = array_shift($arguments);
1071
+        $cr = array_shift($arguments);
1072
+        if (!method_exists($this->ldap, $command)) {
1073
+            return null;
1074
+        }
1075
+        array_unshift($arguments, $cr);
1076
+        // php no longer supports call-time pass-by-reference
1077
+        // thus cannot support controlPagedResultResponse as the third argument
1078
+        // is a reference
1079
+        $doMethod = function () use ($command, &$arguments) {
1080
+            if ($command == 'controlPagedResultResponse') {
1081
+                throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.');
1082
+            } else {
1083
+                return call_user_func_array(array($this->ldap, $command), $arguments);
1084
+            }
1085
+        };
1086
+        try {
1087
+            $ret = $doMethod();
1088
+        } catch (ServerNotAvailableException $e) {
1089
+            /* Server connection lost, attempt to reestablish it
1090 1090
 			 * Maybe implement exponential backoff?
1091 1091
 			 * This was enough to get solr indexer working which has large delays between LDAP fetches.
1092 1092
 			 */
1093
-			\OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1094
-			$this->connection->resetConnectionResource();
1095
-			$cr = $this->connection->getConnectionResource();
1096
-
1097
-			if(!$this->ldap->isResource($cr)) {
1098
-				// Seems like we didn't find any resource.
1099
-				\OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1100
-				throw $e;
1101
-			}
1102
-
1103
-			$arguments[0] = array_pad([], count($arguments[0]), $cr);
1104
-			$ret = $doMethod();
1105
-		}
1106
-		return $ret;
1107
-	}
1108
-
1109
-	/**
1110
-	 * retrieved. Results will according to the order in the array.
1111
-	 *
1112
-	 * @param $filter
1113
-	 * @param $base
1114
-	 * @param string[]|string|null $attr
1115
-	 * @param int $limit optional, maximum results to be counted
1116
-	 * @param int $offset optional, a starting point
1117
-	 * @return array|false array with the search result as first value and pagedSearchOK as
1118
-	 * second | false if not successful
1119
-	 * @throws ServerNotAvailableException
1120
-	 */
1121
-	private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1122
-		if(!is_null($attr) && !is_array($attr)) {
1123
-			$attr = array(mb_strtolower($attr, 'UTF-8'));
1124
-		}
1125
-
1126
-		// See if we have a resource, in case not cancel with message
1127
-		$cr = $this->connection->getConnectionResource();
1128
-		if(!$this->ldap->isResource($cr)) {
1129
-			// Seems like we didn't find any resource.
1130
-			// Return an empty array just like before.
1131
-			\OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1132
-			return false;
1133
-		}
1134
-
1135
-		//check whether paged search should be attempted
1136
-		$pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1137
-
1138
-		$linkResources = array_pad(array(), count($base), $cr);
1139
-		$sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1140
-		// cannot use $cr anymore, might have changed in the previous call!
1141
-		$error = $this->ldap->errno($this->connection->getConnectionResource());
1142
-		if(!is_array($sr) || $error !== 0) {
1143
-			\OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);
1144
-			return false;
1145
-		}
1146
-
1147
-		return array($sr, $pagedSearchOK);
1148
-	}
1149
-
1150
-	/**
1151
-	 * processes an LDAP paged search operation
1152
-	 * @param array $sr the array containing the LDAP search resources
1153
-	 * @param string $filter the LDAP filter for the search
1154
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1155
-	 * @param int $iFoundItems number of results in the single search operation
1156
-	 * @param int $limit maximum results to be counted
1157
-	 * @param int $offset a starting point
1158
-	 * @param bool $pagedSearchOK whether a paged search has been executed
1159
-	 * @param bool $skipHandling required for paged search when cookies to
1160
-	 * prior results need to be gained
1161
-	 * @return bool cookie validity, true if we have more pages, false otherwise.
1162
-	 */
1163
-	private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1164
-		$cookie = null;
1165
-		if($pagedSearchOK) {
1166
-			$cr = $this->connection->getConnectionResource();
1167
-			foreach($sr as $key => $res) {
1168
-				if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1169
-					$this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1170
-				}
1171
-			}
1172
-
1173
-			//browsing through prior pages to get the cookie for the new one
1174
-			if($skipHandling) {
1175
-				return false;
1176
-			}
1177
-			// if count is bigger, then the server does not support
1178
-			// paged search. Instead, he did a normal search. We set a
1179
-			// flag here, so the callee knows how to deal with it.
1180
-			if($iFoundItems <= $limit) {
1181
-				$this->pagedSearchedSuccessful = true;
1182
-			}
1183
-		} else {
1184
-			if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1185
-				\OC::$server->getLogger()->debug(
1186
-					'Paged search was not available',
1187
-					[ 'app' => 'user_ldap' ]
1188
-				);
1189
-			}
1190
-		}
1191
-		/* ++ Fixing RHDS searches with pages with zero results ++
1093
+            \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG);
1094
+            $this->connection->resetConnectionResource();
1095
+            $cr = $this->connection->getConnectionResource();
1096
+
1097
+            if(!$this->ldap->isResource($cr)) {
1098
+                // Seems like we didn't find any resource.
1099
+                \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG);
1100
+                throw $e;
1101
+            }
1102
+
1103
+            $arguments[0] = array_pad([], count($arguments[0]), $cr);
1104
+            $ret = $doMethod();
1105
+        }
1106
+        return $ret;
1107
+    }
1108
+
1109
+    /**
1110
+     * retrieved. Results will according to the order in the array.
1111
+     *
1112
+     * @param $filter
1113
+     * @param $base
1114
+     * @param string[]|string|null $attr
1115
+     * @param int $limit optional, maximum results to be counted
1116
+     * @param int $offset optional, a starting point
1117
+     * @return array|false array with the search result as first value and pagedSearchOK as
1118
+     * second | false if not successful
1119
+     * @throws ServerNotAvailableException
1120
+     */
1121
+    private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) {
1122
+        if(!is_null($attr) && !is_array($attr)) {
1123
+            $attr = array(mb_strtolower($attr, 'UTF-8'));
1124
+        }
1125
+
1126
+        // See if we have a resource, in case not cancel with message
1127
+        $cr = $this->connection->getConnectionResource();
1128
+        if(!$this->ldap->isResource($cr)) {
1129
+            // Seems like we didn't find any resource.
1130
+            // Return an empty array just like before.
1131
+            \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG);
1132
+            return false;
1133
+        }
1134
+
1135
+        //check whether paged search should be attempted
1136
+        $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset);
1137
+
1138
+        $linkResources = array_pad(array(), count($base), $cr);
1139
+        $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr);
1140
+        // cannot use $cr anymore, might have changed in the previous call!
1141
+        $error = $this->ldap->errno($this->connection->getConnectionResource());
1142
+        if(!is_array($sr) || $error !== 0) {
1143
+            \OCP\Util::writeLog('user_ldap', 'Attempt for Paging?  '.print_r($pagedSearchOK, true), ILogger::ERROR);
1144
+            return false;
1145
+        }
1146
+
1147
+        return array($sr, $pagedSearchOK);
1148
+    }
1149
+
1150
+    /**
1151
+     * processes an LDAP paged search operation
1152
+     * @param array $sr the array containing the LDAP search resources
1153
+     * @param string $filter the LDAP filter for the search
1154
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1155
+     * @param int $iFoundItems number of results in the single search operation
1156
+     * @param int $limit maximum results to be counted
1157
+     * @param int $offset a starting point
1158
+     * @param bool $pagedSearchOK whether a paged search has been executed
1159
+     * @param bool $skipHandling required for paged search when cookies to
1160
+     * prior results need to be gained
1161
+     * @return bool cookie validity, true if we have more pages, false otherwise.
1162
+     */
1163
+    private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) {
1164
+        $cookie = null;
1165
+        if($pagedSearchOK) {
1166
+            $cr = $this->connection->getConnectionResource();
1167
+            foreach($sr as $key => $res) {
1168
+                if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) {
1169
+                    $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie);
1170
+                }
1171
+            }
1172
+
1173
+            //browsing through prior pages to get the cookie for the new one
1174
+            if($skipHandling) {
1175
+                return false;
1176
+            }
1177
+            // if count is bigger, then the server does not support
1178
+            // paged search. Instead, he did a normal search. We set a
1179
+            // flag here, so the callee knows how to deal with it.
1180
+            if($iFoundItems <= $limit) {
1181
+                $this->pagedSearchedSuccessful = true;
1182
+            }
1183
+        } else {
1184
+            if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) {
1185
+                \OC::$server->getLogger()->debug(
1186
+                    'Paged search was not available',
1187
+                    [ 'app' => 'user_ldap' ]
1188
+                );
1189
+            }
1190
+        }
1191
+        /* ++ Fixing RHDS searches with pages with zero results ++
1192 1192
 		 * Return cookie status. If we don't have more pages, with RHDS
1193 1193
 		 * cookie is null, with openldap cookie is an empty string and
1194 1194
 		 * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0
1195 1195
 		 */
1196
-		return !empty($cookie) || $cookie === '0';
1197
-	}
1198
-
1199
-	/**
1200
-	 * executes an LDAP search, but counts the results only
1201
-	 *
1202
-	 * @param string $filter the LDAP filter for the search
1203
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1204
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1205
-	 * retrieved. Results will according to the order in the array.
1206
-	 * @param int $limit optional, maximum results to be counted
1207
-	 * @param int $offset optional, a starting point
1208
-	 * @param bool $skipHandling indicates whether the pages search operation is
1209
-	 * completed
1210
-	 * @return int|false Integer or false if the search could not be initialized
1211
-	 * @throws ServerNotAvailableException
1212
-	 */
1213
-	private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1214
-		\OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);
1215
-
1216
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1217
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1218
-			$limitPerPage = $limit;
1219
-		}
1220
-
1221
-		$counter = 0;
1222
-		$count = null;
1223
-		$this->connection->getConnectionResource();
1224
-
1225
-		do {
1226
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1227
-			if($search === false) {
1228
-				return $counter > 0 ? $counter : false;
1229
-			}
1230
-			list($sr, $pagedSearchOK) = $search;
1231
-
1232
-			/* ++ Fixing RHDS searches with pages with zero results ++
1196
+        return !empty($cookie) || $cookie === '0';
1197
+    }
1198
+
1199
+    /**
1200
+     * executes an LDAP search, but counts the results only
1201
+     *
1202
+     * @param string $filter the LDAP filter for the search
1203
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1204
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1205
+     * retrieved. Results will according to the order in the array.
1206
+     * @param int $limit optional, maximum results to be counted
1207
+     * @param int $offset optional, a starting point
1208
+     * @param bool $skipHandling indicates whether the pages search operation is
1209
+     * completed
1210
+     * @return int|false Integer or false if the search could not be initialized
1211
+     * @throws ServerNotAvailableException
1212
+     */
1213
+    private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1214
+        \OCP\Util::writeLog('user_ldap', 'Count filter:  '.print_r($filter, true), ILogger::DEBUG);
1215
+
1216
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1217
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1218
+            $limitPerPage = $limit;
1219
+        }
1220
+
1221
+        $counter = 0;
1222
+        $count = null;
1223
+        $this->connection->getConnectionResource();
1224
+
1225
+        do {
1226
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1227
+            if($search === false) {
1228
+                return $counter > 0 ? $counter : false;
1229
+            }
1230
+            list($sr, $pagedSearchOK) = $search;
1231
+
1232
+            /* ++ Fixing RHDS searches with pages with zero results ++
1233 1233
 			 * countEntriesInSearchResults() method signature changed
1234 1234
 			 * by removing $limit and &$hasHitLimit parameters
1235 1235
 			 */
1236
-			$count = $this->countEntriesInSearchResults($sr);
1237
-			$counter += $count;
1236
+            $count = $this->countEntriesInSearchResults($sr);
1237
+            $counter += $count;
1238 1238
 
1239
-			$hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1240
-										$offset, $pagedSearchOK, $skipHandling);
1241
-			$offset += $limitPerPage;
1242
-			/* ++ Fixing RHDS searches with pages with zero results ++
1239
+            $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage,
1240
+                                        $offset, $pagedSearchOK, $skipHandling);
1241
+            $offset += $limitPerPage;
1242
+            /* ++ Fixing RHDS searches with pages with zero results ++
1243 1243
 			 * Continue now depends on $hasMorePages value
1244 1244
 			 */
1245
-			$continue = $pagedSearchOK && $hasMorePages;
1246
-		} while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1247
-
1248
-		return $counter;
1249
-	}
1250
-
1251
-	/**
1252
-	 * @param array $searchResults
1253
-	 * @return int
1254
-	 */
1255
-	private function countEntriesInSearchResults($searchResults) {
1256
-		$counter = 0;
1257
-
1258
-		foreach($searchResults as $res) {
1259
-			$count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1260
-			$counter += $count;
1261
-		}
1262
-
1263
-		return $counter;
1264
-	}
1265
-
1266
-	/**
1267
-	 * Executes an LDAP search
1268
-	 *
1269
-	 * @param string $filter the LDAP filter for the search
1270
-	 * @param array $base an array containing the LDAP subtree(s) that shall be searched
1271
-	 * @param string|string[] $attr optional, array, one or more attributes that shall be
1272
-	 * @param int $limit
1273
-	 * @param int $offset
1274
-	 * @param bool $skipHandling
1275
-	 * @return array with the search result
1276
-	 * @throws ServerNotAvailableException
1277
-	 */
1278
-	public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1279
-		$limitPerPage = (int)$this->connection->ldapPagingSize;
1280
-		if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1281
-			$limitPerPage = $limit;
1282
-		}
1283
-
1284
-		/* ++ Fixing RHDS searches with pages with zero results ++
1245
+            $continue = $pagedSearchOK && $hasMorePages;
1246
+        } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter));
1247
+
1248
+        return $counter;
1249
+    }
1250
+
1251
+    /**
1252
+     * @param array $searchResults
1253
+     * @return int
1254
+     */
1255
+    private function countEntriesInSearchResults($searchResults) {
1256
+        $counter = 0;
1257
+
1258
+        foreach($searchResults as $res) {
1259
+            $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res);
1260
+            $counter += $count;
1261
+        }
1262
+
1263
+        return $counter;
1264
+    }
1265
+
1266
+    /**
1267
+     * Executes an LDAP search
1268
+     *
1269
+     * @param string $filter the LDAP filter for the search
1270
+     * @param array $base an array containing the LDAP subtree(s) that shall be searched
1271
+     * @param string|string[] $attr optional, array, one or more attributes that shall be
1272
+     * @param int $limit
1273
+     * @param int $offset
1274
+     * @param bool $skipHandling
1275
+     * @return array with the search result
1276
+     * @throws ServerNotAvailableException
1277
+     */
1278
+    public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) {
1279
+        $limitPerPage = (int)$this->connection->ldapPagingSize;
1280
+        if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) {
1281
+            $limitPerPage = $limit;
1282
+        }
1283
+
1284
+        /* ++ Fixing RHDS searches with pages with zero results ++
1285 1285
 		 * As we can have pages with zero results and/or pages with less
1286 1286
 		 * than $limit results but with a still valid server 'cookie',
1287 1287
 		 * loops through until we get $continue equals true and
1288 1288
 		 * $findings['count'] < $limit
1289 1289
 		 */
1290
-		$findings = [];
1291
-		$savedoffset = $offset;
1292
-		do {
1293
-			$search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1294
-			if($search === false) {
1295
-				return [];
1296
-			}
1297
-			list($sr, $pagedSearchOK) = $search;
1298
-			$cr = $this->connection->getConnectionResource();
1299
-
1300
-			if($skipHandling) {
1301
-				//i.e. result do not need to be fetched, we just need the cookie
1302
-				//thus pass 1 or any other value as $iFoundItems because it is not
1303
-				//used
1304
-				$this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1305
-								$offset, $pagedSearchOK,
1306
-								$skipHandling);
1307
-				return array();
1308
-			}
1309
-
1310
-			$iFoundItems = 0;
1311
-			foreach($sr as $res) {
1312
-				$findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1313
-				$iFoundItems = max($iFoundItems, $findings['count']);
1314
-				unset($findings['count']);
1315
-			}
1316
-
1317
-			$continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1318
-				$limitPerPage, $offset, $pagedSearchOK,
1319
-										$skipHandling);
1320
-			$offset += $limitPerPage;
1321
-		} while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1322
-		// reseting offset
1323
-		$offset = $savedoffset;
1324
-
1325
-		// if we're here, probably no connection resource is returned.
1326
-		// to make Nextcloud behave nicely, we simply give back an empty array.
1327
-		if(is_null($findings)) {
1328
-			return array();
1329
-		}
1330
-
1331
-		if(!is_null($attr)) {
1332
-			$selection = [];
1333
-			$i = 0;
1334
-			foreach($findings as $item) {
1335
-				if(!is_array($item)) {
1336
-					continue;
1337
-				}
1338
-				$item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1339
-				foreach($attr as $key) {
1340
-					if(isset($item[$key])) {
1341
-						if(is_array($item[$key]) && isset($item[$key]['count'])) {
1342
-							unset($item[$key]['count']);
1343
-						}
1344
-						if($key !== 'dn') {
1345
-							if($this->resemblesDN($key)) {
1346
-								$selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1347
-							} else if($key === 'objectguid' || $key === 'guid') {
1348
-								$selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1349
-							} else {
1350
-								$selection[$i][$key] = $item[$key];
1351
-							}
1352
-						} else {
1353
-							$selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1354
-						}
1355
-					}
1356
-
1357
-				}
1358
-				$i++;
1359
-			}
1360
-			$findings = $selection;
1361
-		}
1362
-		//we slice the findings, when
1363
-		//a) paged search unsuccessful, though attempted
1364
-		//b) no paged search, but limit set
1365
-		if((!$this->getPagedSearchResultState()
1366
-			&& $pagedSearchOK)
1367
-			|| (
1368
-				!$pagedSearchOK
1369
-				&& !is_null($limit)
1370
-			)
1371
-		) {
1372
-			$findings = array_slice($findings, (int)$offset, $limit);
1373
-		}
1374
-		return $findings;
1375
-	}
1376
-
1377
-	/**
1378
-	 * @param string $name
1379
-	 * @return string
1380
-	 * @throws \InvalidArgumentException
1381
-	 */
1382
-	public function sanitizeUsername($name) {
1383
-		$name = trim($name);
1384
-
1385
-		if($this->connection->ldapIgnoreNamingRules) {
1386
-			return $name;
1387
-		}
1388
-
1389
-		// Transliteration to ASCII
1390
-		$transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1391
-		if($transliterated !== false) {
1392
-			// depending on system config iconv can work or not
1393
-			$name = $transliterated;
1394
-		}
1395
-
1396
-		// Replacements
1397
-		$name = str_replace(' ', '_', $name);
1398
-
1399
-		// Every remaining disallowed characters will be removed
1400
-		$name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1401
-
1402
-		if($name === '') {
1403
-			throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1404
-		}
1405
-
1406
-		return $name;
1407
-	}
1408
-
1409
-	/**
1410
-	* escapes (user provided) parts for LDAP filter
1411
-	* @param string $input, the provided value
1412
-	* @param bool $allowAsterisk whether in * at the beginning should be preserved
1413
-	* @return string the escaped string
1414
-	*/
1415
-	public function escapeFilterPart($input, $allowAsterisk = false) {
1416
-		$asterisk = '';
1417
-		if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1418
-			$asterisk = '*';
1419
-			$input = mb_substr($input, 1, null, 'UTF-8');
1420
-		}
1421
-		$search  = array('*', '\\', '(', ')');
1422
-		$replace = array('\\*', '\\\\', '\\(', '\\)');
1423
-		return $asterisk . str_replace($search, $replace, $input);
1424
-	}
1425
-
1426
-	/**
1427
-	 * combines the input filters with AND
1428
-	 * @param string[] $filters the filters to connect
1429
-	 * @return string the combined filter
1430
-	 */
1431
-	public function combineFilterWithAnd($filters) {
1432
-		return $this->combineFilter($filters, '&');
1433
-	}
1434
-
1435
-	/**
1436
-	 * combines the input filters with OR
1437
-	 * @param string[] $filters the filters to connect
1438
-	 * @return string the combined filter
1439
-	 * Combines Filter arguments with OR
1440
-	 */
1441
-	public function combineFilterWithOr($filters) {
1442
-		return $this->combineFilter($filters, '|');
1443
-	}
1444
-
1445
-	/**
1446
-	 * combines the input filters with given operator
1447
-	 * @param string[] $filters the filters to connect
1448
-	 * @param string $operator either & or |
1449
-	 * @return string the combined filter
1450
-	 */
1451
-	private function combineFilter($filters, $operator) {
1452
-		$combinedFilter = '('.$operator;
1453
-		foreach($filters as $filter) {
1454
-			if ($filter !== '' && $filter[0] !== '(') {
1455
-				$filter = '('.$filter.')';
1456
-			}
1457
-			$combinedFilter.=$filter;
1458
-		}
1459
-		$combinedFilter.=')';
1460
-		return $combinedFilter;
1461
-	}
1462
-
1463
-	/**
1464
-	 * creates a filter part for to perform search for users
1465
-	 * @param string $search the search term
1466
-	 * @return string the final filter part to use in LDAP searches
1467
-	 */
1468
-	public function getFilterPartForUserSearch($search) {
1469
-		return $this->getFilterPartForSearch($search,
1470
-			$this->connection->ldapAttributesForUserSearch,
1471
-			$this->connection->ldapUserDisplayName);
1472
-	}
1473
-
1474
-	/**
1475
-	 * creates a filter part for to perform search for groups
1476
-	 * @param string $search the search term
1477
-	 * @return string the final filter part to use in LDAP searches
1478
-	 */
1479
-	public function getFilterPartForGroupSearch($search) {
1480
-		return $this->getFilterPartForSearch($search,
1481
-			$this->connection->ldapAttributesForGroupSearch,
1482
-			$this->connection->ldapGroupDisplayName);
1483
-	}
1484
-
1485
-	/**
1486
-	 * creates a filter part for searches by splitting up the given search
1487
-	 * string into single words
1488
-	 * @param string $search the search term
1489
-	 * @param string[] $searchAttributes needs to have at least two attributes,
1490
-	 * otherwise it does not make sense :)
1491
-	 * @return string the final filter part to use in LDAP searches
1492
-	 * @throws \Exception
1493
-	 */
1494
-	private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1495
-		if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1496
-			throw new \Exception('searchAttributes must be an array with at least two string');
1497
-		}
1498
-		$searchWords = explode(' ', trim($search));
1499
-		$wordFilters = array();
1500
-		foreach($searchWords as $word) {
1501
-			$word = $this->prepareSearchTerm($word);
1502
-			//every word needs to appear at least once
1503
-			$wordMatchOneAttrFilters = array();
1504
-			foreach($searchAttributes as $attr) {
1505
-				$wordMatchOneAttrFilters[] = $attr . '=' . $word;
1506
-			}
1507
-			$wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1508
-		}
1509
-		return $this->combineFilterWithAnd($wordFilters);
1510
-	}
1511
-
1512
-	/**
1513
-	 * creates a filter part for searches
1514
-	 * @param string $search the search term
1515
-	 * @param string[]|null $searchAttributes
1516
-	 * @param string $fallbackAttribute a fallback attribute in case the user
1517
-	 * did not define search attributes. Typically the display name attribute.
1518
-	 * @return string the final filter part to use in LDAP searches
1519
-	 */
1520
-	private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1521
-		$filter = array();
1522
-		$haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1523
-		if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1524
-			try {
1525
-				return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1526
-			} catch(\Exception $e) {
1527
-				\OCP\Util::writeLog(
1528
-					'user_ldap',
1529
-					'Creating advanced filter for search failed, falling back to simple method.',
1530
-					ILogger::INFO
1531
-				);
1532
-			}
1533
-		}
1534
-
1535
-		$search = $this->prepareSearchTerm($search);
1536
-		if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1537
-			if ($fallbackAttribute === '') {
1538
-				return '';
1539
-			}
1540
-			$filter[] = $fallbackAttribute . '=' . $search;
1541
-		} else {
1542
-			foreach($searchAttributes as $attribute) {
1543
-				$filter[] = $attribute . '=' . $search;
1544
-			}
1545
-		}
1546
-		if(count($filter) === 1) {
1547
-			return '('.$filter[0].')';
1548
-		}
1549
-		return $this->combineFilterWithOr($filter);
1550
-	}
1551
-
1552
-	/**
1553
-	 * returns the search term depending on whether we are allowed
1554
-	 * list users found by ldap with the current input appended by
1555
-	 * a *
1556
-	 * @return string
1557
-	 */
1558
-	private function prepareSearchTerm($term) {
1559
-		$config = \OC::$server->getConfig();
1560
-
1561
-		$allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1562
-
1563
-		$result = $term;
1564
-		if ($term === '') {
1565
-			$result = '*';
1566
-		} else if ($allowEnum !== 'no') {
1567
-			$result = $term . '*';
1568
-		}
1569
-		return $result;
1570
-	}
1571
-
1572
-	/**
1573
-	 * returns the filter used for counting users
1574
-	 * @return string
1575
-	 */
1576
-	public function getFilterForUserCount() {
1577
-		$filter = $this->combineFilterWithAnd(array(
1578
-			$this->connection->ldapUserFilter,
1579
-			$this->connection->ldapUserDisplayName . '=*'
1580
-		));
1581
-
1582
-		return $filter;
1583
-	}
1584
-
1585
-	/**
1586
-	 * @param string $name
1587
-	 * @param string $password
1588
-	 * @return bool
1589
-	 */
1590
-	public function areCredentialsValid($name, $password) {
1591
-		$name = $this->helper->DNasBaseParameter($name);
1592
-		$testConnection = clone $this->connection;
1593
-		$credentials = array(
1594
-			'ldapAgentName' => $name,
1595
-			'ldapAgentPassword' => $password
1596
-		);
1597
-		if(!$testConnection->setConfiguration($credentials)) {
1598
-			return false;
1599
-		}
1600
-		return $testConnection->bind();
1601
-	}
1602
-
1603
-	/**
1604
-	 * reverse lookup of a DN given a known UUID
1605
-	 *
1606
-	 * @param string $uuid
1607
-	 * @return string
1608
-	 * @throws \Exception
1609
-	 */
1610
-	public function getUserDnByUuid($uuid) {
1611
-		$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1612
-		$filter       = $this->connection->ldapUserFilter;
1613
-		$base         = $this->connection->ldapBaseUsers;
1614
-
1615
-		if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1616
-			// Sacrebleu! The UUID attribute is unknown :( We need first an
1617
-			// existing DN to be able to reliably detect it.
1618
-			$result = $this->search($filter, $base, ['dn'], 1);
1619
-			if(!isset($result[0]) || !isset($result[0]['dn'])) {
1620
-				throw new \Exception('Cannot determine UUID attribute');
1621
-			}
1622
-			$dn = $result[0]['dn'][0];
1623
-			if(!$this->detectUuidAttribute($dn, true)) {
1624
-				throw new \Exception('Cannot determine UUID attribute');
1625
-			}
1626
-		} else {
1627
-			// The UUID attribute is either known or an override is given.
1628
-			// By calling this method we ensure that $this->connection->$uuidAttr
1629
-			// is definitely set
1630
-			if(!$this->detectUuidAttribute('', true)) {
1631
-				throw new \Exception('Cannot determine UUID attribute');
1632
-			}
1633
-		}
1634
-
1635
-		$uuidAttr = $this->connection->ldapUuidUserAttribute;
1636
-		if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1637
-			$uuid = $this->formatGuid2ForFilterUser($uuid);
1638
-		}
1639
-
1640
-		$filter = $uuidAttr . '=' . $uuid;
1641
-		$result = $this->searchUsers($filter, ['dn'], 2);
1642
-		if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1643
-			// we put the count into account to make sure that this is
1644
-			// really unique
1645
-			return $result[0]['dn'][0];
1646
-		}
1647
-
1648
-		throw new \Exception('Cannot determine UUID attribute');
1649
-	}
1650
-
1651
-	/**
1652
-	 * auto-detects the directory's UUID attribute
1653
-	 *
1654
-	 * @param string $dn a known DN used to check against
1655
-	 * @param bool $isUser
1656
-	 * @param bool $force the detection should be run, even if it is not set to auto
1657
-	 * @param array|null $ldapRecord
1658
-	 * @return bool true on success, false otherwise
1659
-	 */
1660
-	private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1661
-		if($isUser) {
1662
-			$uuidAttr     = 'ldapUuidUserAttribute';
1663
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1664
-		} else {
1665
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1666
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1667
-		}
1668
-
1669
-		if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1670
-			return true;
1671
-		}
1672
-
1673
-		if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1674
-			$this->connection->$uuidAttr = $uuidOverride;
1675
-			return true;
1676
-		}
1677
-
1678
-		foreach(self::UUID_ATTRIBUTES as $attribute) {
1679
-			if($ldapRecord !== null) {
1680
-				// we have the info from LDAP already, we don't need to talk to the server again
1681
-				if(isset($ldapRecord[$attribute])) {
1682
-					$this->connection->$uuidAttr = $attribute;
1683
-					return true;
1684
-				} else {
1685
-					continue;
1686
-				}
1687
-			}
1688
-
1689
-			$value = $this->readAttribute($dn, $attribute);
1690
-			if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1691
-				\OCP\Util::writeLog(
1692
-					'user_ldap',
1693
-					'Setting '.$attribute.' as '.$uuidAttr,
1694
-					ILogger::DEBUG
1695
-				);
1696
-				$this->connection->$uuidAttr = $attribute;
1697
-				return true;
1698
-			}
1699
-		}
1700
-		\OCP\Util::writeLog(
1701
-			'user_ldap',
1702
-			'Could not autodetect the UUID attribute',
1703
-			ILogger::ERROR
1704
-		);
1705
-
1706
-		return false;
1707
-	}
1708
-
1709
-	/**
1710
-	 * @param string $dn
1711
-	 * @param bool $isUser
1712
-	 * @param null $ldapRecord
1713
-	 * @return bool|string
1714
-	 */
1715
-	public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1716
-		if($isUser) {
1717
-			$uuidAttr     = 'ldapUuidUserAttribute';
1718
-			$uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1719
-		} else {
1720
-			$uuidAttr     = 'ldapUuidGroupAttribute';
1721
-			$uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1722
-		}
1723
-
1724
-		$uuid = false;
1725
-		if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1726
-			$attr = $this->connection->$uuidAttr;
1727
-			$uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1728
-			if( !is_array($uuid)
1729
-				&& $uuidOverride !== ''
1730
-				&& $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1731
-			{
1732
-				$uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1733
-					? $ldapRecord[$this->connection->$uuidAttr]
1734
-					: $this->readAttribute($dn, $this->connection->$uuidAttr);
1735
-			}
1736
-			if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1737
-				$uuid = $uuid[0];
1738
-			}
1739
-		}
1740
-
1741
-		return $uuid;
1742
-	}
1743
-
1744
-	/**
1745
-	 * converts a binary ObjectGUID into a string representation
1746
-	 * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1747
-	 * @return string
1748
-	 * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1749
-	 */
1750
-	private function convertObjectGUID2Str($oguid) {
1751
-		$hex_guid = bin2hex($oguid);
1752
-		$hex_guid_to_guid_str = '';
1753
-		for($k = 1; $k <= 4; ++$k) {
1754
-			$hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1755
-		}
1756
-		$hex_guid_to_guid_str .= '-';
1757
-		for($k = 1; $k <= 2; ++$k) {
1758
-			$hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1759
-		}
1760
-		$hex_guid_to_guid_str .= '-';
1761
-		for($k = 1; $k <= 2; ++$k) {
1762
-			$hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1763
-		}
1764
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1765
-		$hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1766
-
1767
-		return strtoupper($hex_guid_to_guid_str);
1768
-	}
1769
-
1770
-	/**
1771
-	 * the first three blocks of the string-converted GUID happen to be in
1772
-	 * reverse order. In order to use it in a filter, this needs to be
1773
-	 * corrected. Furthermore the dashes need to be replaced and \\ preprended
1774
-	 * to every two hax figures.
1775
-	 *
1776
-	 * If an invalid string is passed, it will be returned without change.
1777
-	 *
1778
-	 * @param string $guid
1779
-	 * @return string
1780
-	 */
1781
-	public function formatGuid2ForFilterUser($guid) {
1782
-		if(!is_string($guid)) {
1783
-			throw new \InvalidArgumentException('String expected');
1784
-		}
1785
-		$blocks = explode('-', $guid);
1786
-		if(count($blocks) !== 5) {
1787
-			/*
1290
+        $findings = [];
1291
+        $savedoffset = $offset;
1292
+        do {
1293
+            $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset);
1294
+            if($search === false) {
1295
+                return [];
1296
+            }
1297
+            list($sr, $pagedSearchOK) = $search;
1298
+            $cr = $this->connection->getConnectionResource();
1299
+
1300
+            if($skipHandling) {
1301
+                //i.e. result do not need to be fetched, we just need the cookie
1302
+                //thus pass 1 or any other value as $iFoundItems because it is not
1303
+                //used
1304
+                $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage,
1305
+                                $offset, $pagedSearchOK,
1306
+                                $skipHandling);
1307
+                return array();
1308
+            }
1309
+
1310
+            $iFoundItems = 0;
1311
+            foreach($sr as $res) {
1312
+                $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res));
1313
+                $iFoundItems = max($iFoundItems, $findings['count']);
1314
+                unset($findings['count']);
1315
+            }
1316
+
1317
+            $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems,
1318
+                $limitPerPage, $offset, $pagedSearchOK,
1319
+                                        $skipHandling);
1320
+            $offset += $limitPerPage;
1321
+        } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit));
1322
+        // reseting offset
1323
+        $offset = $savedoffset;
1324
+
1325
+        // if we're here, probably no connection resource is returned.
1326
+        // to make Nextcloud behave nicely, we simply give back an empty array.
1327
+        if(is_null($findings)) {
1328
+            return array();
1329
+        }
1330
+
1331
+        if(!is_null($attr)) {
1332
+            $selection = [];
1333
+            $i = 0;
1334
+            foreach($findings as $item) {
1335
+                if(!is_array($item)) {
1336
+                    continue;
1337
+                }
1338
+                $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8');
1339
+                foreach($attr as $key) {
1340
+                    if(isset($item[$key])) {
1341
+                        if(is_array($item[$key]) && isset($item[$key]['count'])) {
1342
+                            unset($item[$key]['count']);
1343
+                        }
1344
+                        if($key !== 'dn') {
1345
+                            if($this->resemblesDN($key)) {
1346
+                                $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]);
1347
+                            } else if($key === 'objectguid' || $key === 'guid') {
1348
+                                $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])];
1349
+                            } else {
1350
+                                $selection[$i][$key] = $item[$key];
1351
+                            }
1352
+                        } else {
1353
+                            $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])];
1354
+                        }
1355
+                    }
1356
+
1357
+                }
1358
+                $i++;
1359
+            }
1360
+            $findings = $selection;
1361
+        }
1362
+        //we slice the findings, when
1363
+        //a) paged search unsuccessful, though attempted
1364
+        //b) no paged search, but limit set
1365
+        if((!$this->getPagedSearchResultState()
1366
+            && $pagedSearchOK)
1367
+            || (
1368
+                !$pagedSearchOK
1369
+                && !is_null($limit)
1370
+            )
1371
+        ) {
1372
+            $findings = array_slice($findings, (int)$offset, $limit);
1373
+        }
1374
+        return $findings;
1375
+    }
1376
+
1377
+    /**
1378
+     * @param string $name
1379
+     * @return string
1380
+     * @throws \InvalidArgumentException
1381
+     */
1382
+    public function sanitizeUsername($name) {
1383
+        $name = trim($name);
1384
+
1385
+        if($this->connection->ldapIgnoreNamingRules) {
1386
+            return $name;
1387
+        }
1388
+
1389
+        // Transliteration to ASCII
1390
+        $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name);
1391
+        if($transliterated !== false) {
1392
+            // depending on system config iconv can work or not
1393
+            $name = $transliterated;
1394
+        }
1395
+
1396
+        // Replacements
1397
+        $name = str_replace(' ', '_', $name);
1398
+
1399
+        // Every remaining disallowed characters will be removed
1400
+        $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name);
1401
+
1402
+        if($name === '') {
1403
+            throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters');
1404
+        }
1405
+
1406
+        return $name;
1407
+    }
1408
+
1409
+    /**
1410
+     * escapes (user provided) parts for LDAP filter
1411
+     * @param string $input, the provided value
1412
+     * @param bool $allowAsterisk whether in * at the beginning should be preserved
1413
+     * @return string the escaped string
1414
+     */
1415
+    public function escapeFilterPart($input, $allowAsterisk = false) {
1416
+        $asterisk = '';
1417
+        if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') {
1418
+            $asterisk = '*';
1419
+            $input = mb_substr($input, 1, null, 'UTF-8');
1420
+        }
1421
+        $search  = array('*', '\\', '(', ')');
1422
+        $replace = array('\\*', '\\\\', '\\(', '\\)');
1423
+        return $asterisk . str_replace($search, $replace, $input);
1424
+    }
1425
+
1426
+    /**
1427
+     * combines the input filters with AND
1428
+     * @param string[] $filters the filters to connect
1429
+     * @return string the combined filter
1430
+     */
1431
+    public function combineFilterWithAnd($filters) {
1432
+        return $this->combineFilter($filters, '&');
1433
+    }
1434
+
1435
+    /**
1436
+     * combines the input filters with OR
1437
+     * @param string[] $filters the filters to connect
1438
+     * @return string the combined filter
1439
+     * Combines Filter arguments with OR
1440
+     */
1441
+    public function combineFilterWithOr($filters) {
1442
+        return $this->combineFilter($filters, '|');
1443
+    }
1444
+
1445
+    /**
1446
+     * combines the input filters with given operator
1447
+     * @param string[] $filters the filters to connect
1448
+     * @param string $operator either & or |
1449
+     * @return string the combined filter
1450
+     */
1451
+    private function combineFilter($filters, $operator) {
1452
+        $combinedFilter = '('.$operator;
1453
+        foreach($filters as $filter) {
1454
+            if ($filter !== '' && $filter[0] !== '(') {
1455
+                $filter = '('.$filter.')';
1456
+            }
1457
+            $combinedFilter.=$filter;
1458
+        }
1459
+        $combinedFilter.=')';
1460
+        return $combinedFilter;
1461
+    }
1462
+
1463
+    /**
1464
+     * creates a filter part for to perform search for users
1465
+     * @param string $search the search term
1466
+     * @return string the final filter part to use in LDAP searches
1467
+     */
1468
+    public function getFilterPartForUserSearch($search) {
1469
+        return $this->getFilterPartForSearch($search,
1470
+            $this->connection->ldapAttributesForUserSearch,
1471
+            $this->connection->ldapUserDisplayName);
1472
+    }
1473
+
1474
+    /**
1475
+     * creates a filter part for to perform search for groups
1476
+     * @param string $search the search term
1477
+     * @return string the final filter part to use in LDAP searches
1478
+     */
1479
+    public function getFilterPartForGroupSearch($search) {
1480
+        return $this->getFilterPartForSearch($search,
1481
+            $this->connection->ldapAttributesForGroupSearch,
1482
+            $this->connection->ldapGroupDisplayName);
1483
+    }
1484
+
1485
+    /**
1486
+     * creates a filter part for searches by splitting up the given search
1487
+     * string into single words
1488
+     * @param string $search the search term
1489
+     * @param string[] $searchAttributes needs to have at least two attributes,
1490
+     * otherwise it does not make sense :)
1491
+     * @return string the final filter part to use in LDAP searches
1492
+     * @throws \Exception
1493
+     */
1494
+    private function getAdvancedFilterPartForSearch($search, $searchAttributes) {
1495
+        if(!is_array($searchAttributes) || count($searchAttributes) < 2) {
1496
+            throw new \Exception('searchAttributes must be an array with at least two string');
1497
+        }
1498
+        $searchWords = explode(' ', trim($search));
1499
+        $wordFilters = array();
1500
+        foreach($searchWords as $word) {
1501
+            $word = $this->prepareSearchTerm($word);
1502
+            //every word needs to appear at least once
1503
+            $wordMatchOneAttrFilters = array();
1504
+            foreach($searchAttributes as $attr) {
1505
+                $wordMatchOneAttrFilters[] = $attr . '=' . $word;
1506
+            }
1507
+            $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters);
1508
+        }
1509
+        return $this->combineFilterWithAnd($wordFilters);
1510
+    }
1511
+
1512
+    /**
1513
+     * creates a filter part for searches
1514
+     * @param string $search the search term
1515
+     * @param string[]|null $searchAttributes
1516
+     * @param string $fallbackAttribute a fallback attribute in case the user
1517
+     * did not define search attributes. Typically the display name attribute.
1518
+     * @return string the final filter part to use in LDAP searches
1519
+     */
1520
+    private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) {
1521
+        $filter = array();
1522
+        $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0);
1523
+        if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) {
1524
+            try {
1525
+                return $this->getAdvancedFilterPartForSearch($search, $searchAttributes);
1526
+            } catch(\Exception $e) {
1527
+                \OCP\Util::writeLog(
1528
+                    'user_ldap',
1529
+                    'Creating advanced filter for search failed, falling back to simple method.',
1530
+                    ILogger::INFO
1531
+                );
1532
+            }
1533
+        }
1534
+
1535
+        $search = $this->prepareSearchTerm($search);
1536
+        if(!is_array($searchAttributes) || count($searchAttributes) === 0) {
1537
+            if ($fallbackAttribute === '') {
1538
+                return '';
1539
+            }
1540
+            $filter[] = $fallbackAttribute . '=' . $search;
1541
+        } else {
1542
+            foreach($searchAttributes as $attribute) {
1543
+                $filter[] = $attribute . '=' . $search;
1544
+            }
1545
+        }
1546
+        if(count($filter) === 1) {
1547
+            return '('.$filter[0].')';
1548
+        }
1549
+        return $this->combineFilterWithOr($filter);
1550
+    }
1551
+
1552
+    /**
1553
+     * returns the search term depending on whether we are allowed
1554
+     * list users found by ldap with the current input appended by
1555
+     * a *
1556
+     * @return string
1557
+     */
1558
+    private function prepareSearchTerm($term) {
1559
+        $config = \OC::$server->getConfig();
1560
+
1561
+        $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes');
1562
+
1563
+        $result = $term;
1564
+        if ($term === '') {
1565
+            $result = '*';
1566
+        } else if ($allowEnum !== 'no') {
1567
+            $result = $term . '*';
1568
+        }
1569
+        return $result;
1570
+    }
1571
+
1572
+    /**
1573
+     * returns the filter used for counting users
1574
+     * @return string
1575
+     */
1576
+    public function getFilterForUserCount() {
1577
+        $filter = $this->combineFilterWithAnd(array(
1578
+            $this->connection->ldapUserFilter,
1579
+            $this->connection->ldapUserDisplayName . '=*'
1580
+        ));
1581
+
1582
+        return $filter;
1583
+    }
1584
+
1585
+    /**
1586
+     * @param string $name
1587
+     * @param string $password
1588
+     * @return bool
1589
+     */
1590
+    public function areCredentialsValid($name, $password) {
1591
+        $name = $this->helper->DNasBaseParameter($name);
1592
+        $testConnection = clone $this->connection;
1593
+        $credentials = array(
1594
+            'ldapAgentName' => $name,
1595
+            'ldapAgentPassword' => $password
1596
+        );
1597
+        if(!$testConnection->setConfiguration($credentials)) {
1598
+            return false;
1599
+        }
1600
+        return $testConnection->bind();
1601
+    }
1602
+
1603
+    /**
1604
+     * reverse lookup of a DN given a known UUID
1605
+     *
1606
+     * @param string $uuid
1607
+     * @return string
1608
+     * @throws \Exception
1609
+     */
1610
+    public function getUserDnByUuid($uuid) {
1611
+        $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1612
+        $filter       = $this->connection->ldapUserFilter;
1613
+        $base         = $this->connection->ldapBaseUsers;
1614
+
1615
+        if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') {
1616
+            // Sacrebleu! The UUID attribute is unknown :( We need first an
1617
+            // existing DN to be able to reliably detect it.
1618
+            $result = $this->search($filter, $base, ['dn'], 1);
1619
+            if(!isset($result[0]) || !isset($result[0]['dn'])) {
1620
+                throw new \Exception('Cannot determine UUID attribute');
1621
+            }
1622
+            $dn = $result[0]['dn'][0];
1623
+            if(!$this->detectUuidAttribute($dn, true)) {
1624
+                throw new \Exception('Cannot determine UUID attribute');
1625
+            }
1626
+        } else {
1627
+            // The UUID attribute is either known or an override is given.
1628
+            // By calling this method we ensure that $this->connection->$uuidAttr
1629
+            // is definitely set
1630
+            if(!$this->detectUuidAttribute('', true)) {
1631
+                throw new \Exception('Cannot determine UUID attribute');
1632
+            }
1633
+        }
1634
+
1635
+        $uuidAttr = $this->connection->ldapUuidUserAttribute;
1636
+        if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') {
1637
+            $uuid = $this->formatGuid2ForFilterUser($uuid);
1638
+        }
1639
+
1640
+        $filter = $uuidAttr . '=' . $uuid;
1641
+        $result = $this->searchUsers($filter, ['dn'], 2);
1642
+        if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) {
1643
+            // we put the count into account to make sure that this is
1644
+            // really unique
1645
+            return $result[0]['dn'][0];
1646
+        }
1647
+
1648
+        throw new \Exception('Cannot determine UUID attribute');
1649
+    }
1650
+
1651
+    /**
1652
+     * auto-detects the directory's UUID attribute
1653
+     *
1654
+     * @param string $dn a known DN used to check against
1655
+     * @param bool $isUser
1656
+     * @param bool $force the detection should be run, even if it is not set to auto
1657
+     * @param array|null $ldapRecord
1658
+     * @return bool true on success, false otherwise
1659
+     */
1660
+    private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) {
1661
+        if($isUser) {
1662
+            $uuidAttr     = 'ldapUuidUserAttribute';
1663
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1664
+        } else {
1665
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1666
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1667
+        }
1668
+
1669
+        if(($this->connection->$uuidAttr !== 'auto') && !$force) {
1670
+            return true;
1671
+        }
1672
+
1673
+        if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) {
1674
+            $this->connection->$uuidAttr = $uuidOverride;
1675
+            return true;
1676
+        }
1677
+
1678
+        foreach(self::UUID_ATTRIBUTES as $attribute) {
1679
+            if($ldapRecord !== null) {
1680
+                // we have the info from LDAP already, we don't need to talk to the server again
1681
+                if(isset($ldapRecord[$attribute])) {
1682
+                    $this->connection->$uuidAttr = $attribute;
1683
+                    return true;
1684
+                } else {
1685
+                    continue;
1686
+                }
1687
+            }
1688
+
1689
+            $value = $this->readAttribute($dn, $attribute);
1690
+            if(is_array($value) && isset($value[0]) && !empty($value[0])) {
1691
+                \OCP\Util::writeLog(
1692
+                    'user_ldap',
1693
+                    'Setting '.$attribute.' as '.$uuidAttr,
1694
+                    ILogger::DEBUG
1695
+                );
1696
+                $this->connection->$uuidAttr = $attribute;
1697
+                return true;
1698
+            }
1699
+        }
1700
+        \OCP\Util::writeLog(
1701
+            'user_ldap',
1702
+            'Could not autodetect the UUID attribute',
1703
+            ILogger::ERROR
1704
+        );
1705
+
1706
+        return false;
1707
+    }
1708
+
1709
+    /**
1710
+     * @param string $dn
1711
+     * @param bool $isUser
1712
+     * @param null $ldapRecord
1713
+     * @return bool|string
1714
+     */
1715
+    public function getUUID($dn, $isUser = true, $ldapRecord = null) {
1716
+        if($isUser) {
1717
+            $uuidAttr     = 'ldapUuidUserAttribute';
1718
+            $uuidOverride = $this->connection->ldapExpertUUIDUserAttr;
1719
+        } else {
1720
+            $uuidAttr     = 'ldapUuidGroupAttribute';
1721
+            $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr;
1722
+        }
1723
+
1724
+        $uuid = false;
1725
+        if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) {
1726
+            $attr = $this->connection->$uuidAttr;
1727
+            $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr);
1728
+            if( !is_array($uuid)
1729
+                && $uuidOverride !== ''
1730
+                && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord))
1731
+            {
1732
+                $uuid = isset($ldapRecord[$this->connection->$uuidAttr])
1733
+                    ? $ldapRecord[$this->connection->$uuidAttr]
1734
+                    : $this->readAttribute($dn, $this->connection->$uuidAttr);
1735
+            }
1736
+            if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) {
1737
+                $uuid = $uuid[0];
1738
+            }
1739
+        }
1740
+
1741
+        return $uuid;
1742
+    }
1743
+
1744
+    /**
1745
+     * converts a binary ObjectGUID into a string representation
1746
+     * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD
1747
+     * @return string
1748
+     * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198
1749
+     */
1750
+    private function convertObjectGUID2Str($oguid) {
1751
+        $hex_guid = bin2hex($oguid);
1752
+        $hex_guid_to_guid_str = '';
1753
+        for($k = 1; $k <= 4; ++$k) {
1754
+            $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2);
1755
+        }
1756
+        $hex_guid_to_guid_str .= '-';
1757
+        for($k = 1; $k <= 2; ++$k) {
1758
+            $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2);
1759
+        }
1760
+        $hex_guid_to_guid_str .= '-';
1761
+        for($k = 1; $k <= 2; ++$k) {
1762
+            $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2);
1763
+        }
1764
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4);
1765
+        $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20);
1766
+
1767
+        return strtoupper($hex_guid_to_guid_str);
1768
+    }
1769
+
1770
+    /**
1771
+     * the first three blocks of the string-converted GUID happen to be in
1772
+     * reverse order. In order to use it in a filter, this needs to be
1773
+     * corrected. Furthermore the dashes need to be replaced and \\ preprended
1774
+     * to every two hax figures.
1775
+     *
1776
+     * If an invalid string is passed, it will be returned without change.
1777
+     *
1778
+     * @param string $guid
1779
+     * @return string
1780
+     */
1781
+    public function formatGuid2ForFilterUser($guid) {
1782
+        if(!is_string($guid)) {
1783
+            throw new \InvalidArgumentException('String expected');
1784
+        }
1785
+        $blocks = explode('-', $guid);
1786
+        if(count($blocks) !== 5) {
1787
+            /*
1788 1788
 			 * Why not throw an Exception instead? This method is a utility
1789 1789
 			 * called only when trying to figure out whether a "missing" known
1790 1790
 			 * LDAP user was or was not renamed on the LDAP server. And this
@@ -1795,279 +1795,279 @@  discard block
 block discarded – undo
1795 1795
 			 * an exception here would kill the experience for a valid, acting
1796 1796
 			 * user. Instead we write a log message.
1797 1797
 			 */
1798
-			\OC::$server->getLogger()->info(
1799
-				'Passed string does not resemble a valid GUID. Known UUID ' .
1800
-				'({uuid}) probably does not match UUID configuration.',
1801
-				[ 'app' => 'user_ldap', 'uuid' => $guid ]
1802
-			);
1803
-			return $guid;
1804
-		}
1805
-		for($i=0; $i < 3; $i++) {
1806
-			$pairs = str_split($blocks[$i], 2);
1807
-			$pairs = array_reverse($pairs);
1808
-			$blocks[$i] = implode('', $pairs);
1809
-		}
1810
-		for($i=0; $i < 5; $i++) {
1811
-			$pairs = str_split($blocks[$i], 2);
1812
-			$blocks[$i] = '\\' . implode('\\', $pairs);
1813
-		}
1814
-		return implode('', $blocks);
1815
-	}
1816
-
1817
-	/**
1818
-	 * gets a SID of the domain of the given dn
1819
-	 * @param string $dn
1820
-	 * @return string|bool
1821
-	 */
1822
-	public function getSID($dn) {
1823
-		$domainDN = $this->getDomainDNFromDN($dn);
1824
-		$cacheKey = 'getSID-'.$domainDN;
1825
-		$sid = $this->connection->getFromCache($cacheKey);
1826
-		if(!is_null($sid)) {
1827
-			return $sid;
1828
-		}
1829
-
1830
-		$objectSid = $this->readAttribute($domainDN, 'objectsid');
1831
-		if(!is_array($objectSid) || empty($objectSid)) {
1832
-			$this->connection->writeToCache($cacheKey, false);
1833
-			return false;
1834
-		}
1835
-		$domainObjectSid = $this->convertSID2Str($objectSid[0]);
1836
-		$this->connection->writeToCache($cacheKey, $domainObjectSid);
1837
-
1838
-		return $domainObjectSid;
1839
-	}
1840
-
1841
-	/**
1842
-	 * converts a binary SID into a string representation
1843
-	 * @param string $sid
1844
-	 * @return string
1845
-	 */
1846
-	public function convertSID2Str($sid) {
1847
-		// The format of a SID binary string is as follows:
1848
-		// 1 byte for the revision level
1849
-		// 1 byte for the number n of variable sub-ids
1850
-		// 6 bytes for identifier authority value
1851
-		// n*4 bytes for n sub-ids
1852
-		//
1853
-		// Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1854
-		//  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1855
-		$revision = ord($sid[0]);
1856
-		$numberSubID = ord($sid[1]);
1857
-
1858
-		$subIdStart = 8; // 1 + 1 + 6
1859
-		$subIdLength = 4;
1860
-		if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1861
-			// Incorrect number of bytes present.
1862
-			return '';
1863
-		}
1864
-
1865
-		// 6 bytes = 48 bits can be represented using floats without loss of
1866
-		// precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1867
-		$iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1868
-
1869
-		$subIDs = array();
1870
-		for ($i = 0; $i < $numberSubID; $i++) {
1871
-			$subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1872
-			$subIDs[] = sprintf('%u', $subID[1]);
1873
-		}
1874
-
1875
-		// Result for example above: S-1-5-21-249921958-728525901-1594176202
1876
-		return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1877
-	}
1878
-
1879
-	/**
1880
-	 * checks if the given DN is part of the given base DN(s)
1881
-	 * @param string $dn the DN
1882
-	 * @param string[] $bases array containing the allowed base DN or DNs
1883
-	 * @return bool
1884
-	 */
1885
-	public function isDNPartOfBase($dn, $bases) {
1886
-		$belongsToBase = false;
1887
-		$bases = $this->helper->sanitizeDN($bases);
1888
-
1889
-		foreach($bases as $base) {
1890
-			$belongsToBase = true;
1891
-			if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1892
-				$belongsToBase = false;
1893
-			}
1894
-			if($belongsToBase) {
1895
-				break;
1896
-			}
1897
-		}
1898
-		return $belongsToBase;
1899
-	}
1900
-
1901
-	/**
1902
-	 * resets a running Paged Search operation
1903
-	 *
1904
-	 * @throws ServerNotAvailableException
1905
-	 */
1906
-	private function abandonPagedSearch() {
1907
-		$cr = $this->connection->getConnectionResource();
1908
-		$this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1909
-		$this->getPagedSearchResultState();
1910
-		$this->lastCookie = '';
1911
-		$this->cookies = [];
1912
-	}
1913
-
1914
-	/**
1915
-	 * get a cookie for the next LDAP paged search
1916
-	 * @param string $base a string with the base DN for the search
1917
-	 * @param string $filter the search filter to identify the correct search
1918
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1919
-	 * @param int $offset the offset for the new search to identify the correct search really good
1920
-	 * @return string containing the key or empty if none is cached
1921
-	 */
1922
-	private function getPagedResultCookie($base, $filter, $limit, $offset) {
1923
-		if($offset === 0) {
1924
-			return '';
1925
-		}
1926
-		$offset -= $limit;
1927
-		//we work with cache here
1928
-		$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1929
-		$cookie = '';
1930
-		if(isset($this->cookies[$cacheKey])) {
1931
-			$cookie = $this->cookies[$cacheKey];
1932
-			if(is_null($cookie)) {
1933
-				$cookie = '';
1934
-			}
1935
-		}
1936
-		return $cookie;
1937
-	}
1938
-
1939
-	/**
1940
-	 * checks whether an LDAP paged search operation has more pages that can be
1941
-	 * retrieved, typically when offset and limit are provided.
1942
-	 *
1943
-	 * Be very careful to use it: the last cookie value, which is inspected, can
1944
-	 * be reset by other operations. Best, call it immediately after a search(),
1945
-	 * searchUsers() or searchGroups() call. count-methods are probably safe as
1946
-	 * well. Don't rely on it with any fetchList-method.
1947
-	 * @return bool
1948
-	 */
1949
-	public function hasMoreResults() {
1950
-		if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1951
-			// as in RFC 2696, when all results are returned, the cookie will
1952
-			// be empty.
1953
-			return false;
1954
-		}
1955
-
1956
-		return true;
1957
-	}
1958
-
1959
-	/**
1960
-	 * set a cookie for LDAP paged search run
1961
-	 * @param string $base a string with the base DN for the search
1962
-	 * @param string $filter the search filter to identify the correct search
1963
-	 * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1964
-	 * @param int $offset the offset for the run search to identify the correct search really good
1965
-	 * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1966
-	 * @return void
1967
-	 */
1968
-	private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1969
-		// allow '0' for 389ds
1970
-		if(!empty($cookie) || $cookie === '0') {
1971
-			$cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1972
-			$this->cookies[$cacheKey] = $cookie;
1973
-			$this->lastCookie = $cookie;
1974
-		}
1975
-	}
1976
-
1977
-	/**
1978
-	 * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1979
-	 * @return boolean|null true on success, null or false otherwise
1980
-	 */
1981
-	public function getPagedSearchResultState() {
1982
-		$result = $this->pagedSearchedSuccessful;
1983
-		$this->pagedSearchedSuccessful = null;
1984
-		return $result;
1985
-	}
1986
-
1987
-	/**
1988
-	 * Prepares a paged search, if possible
1989
-	 * @param string $filter the LDAP filter for the search
1990
-	 * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1991
-	 * @param string[] $attr optional, when a certain attribute shall be filtered outside
1992
-	 * @param int $limit
1993
-	 * @param int $offset
1994
-	 * @return bool|true
1995
-	 */
1996
-	private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1997
-		$pagedSearchOK = false;
1998
-		if ($limit !== 0) {
1999
-			$offset = (int)$offset; //can be null
2000
-			\OCP\Util::writeLog('user_ldap',
2001
-				'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
2002
-				.' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
2003
-				ILogger::DEBUG);
2004
-			//get the cookie from the search for the previous search, required by LDAP
2005
-			foreach($bases as $base) {
2006
-
2007
-				$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2008
-				if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
2009
-					// no cookie known from a potential previous search. We need
2010
-					// to start from 0 to come to the desired page. cookie value
2011
-					// of '0' is valid, because 389ds
2012
-					$reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
2013
-					$this->search($filter, array($base), $attr, $limit, $reOffset, true);
2014
-					$cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2015
-					//still no cookie? obviously, the server does not like us. Let's skip paging efforts.
2016
-					// '0' is valid, because 389ds
2017
-					//TODO: remember this, probably does not change in the next request...
2018
-					if(empty($cookie) && $cookie !== '0') {
2019
-						$cookie = null;
2020
-					}
2021
-				}
2022
-				if(!is_null($cookie)) {
2023
-					//since offset = 0, this is a new search. We abandon other searches that might be ongoing.
2024
-					$this->abandonPagedSearch();
2025
-					$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2026
-						$this->connection->getConnectionResource(), $limit,
2027
-						false, $cookie);
2028
-					if(!$pagedSearchOK) {
2029
-						return false;
2030
-					}
2031
-					\OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);
2032
-				} else {
2033
-					$e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
2034
-					\OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
2035
-				}
2036
-
2037
-			}
2038
-		/* ++ Fixing RHDS searches with pages with zero results ++
1798
+            \OC::$server->getLogger()->info(
1799
+                'Passed string does not resemble a valid GUID. Known UUID ' .
1800
+                '({uuid}) probably does not match UUID configuration.',
1801
+                [ 'app' => 'user_ldap', 'uuid' => $guid ]
1802
+            );
1803
+            return $guid;
1804
+        }
1805
+        for($i=0; $i < 3; $i++) {
1806
+            $pairs = str_split($blocks[$i], 2);
1807
+            $pairs = array_reverse($pairs);
1808
+            $blocks[$i] = implode('', $pairs);
1809
+        }
1810
+        for($i=0; $i < 5; $i++) {
1811
+            $pairs = str_split($blocks[$i], 2);
1812
+            $blocks[$i] = '\\' . implode('\\', $pairs);
1813
+        }
1814
+        return implode('', $blocks);
1815
+    }
1816
+
1817
+    /**
1818
+     * gets a SID of the domain of the given dn
1819
+     * @param string $dn
1820
+     * @return string|bool
1821
+     */
1822
+    public function getSID($dn) {
1823
+        $domainDN = $this->getDomainDNFromDN($dn);
1824
+        $cacheKey = 'getSID-'.$domainDN;
1825
+        $sid = $this->connection->getFromCache($cacheKey);
1826
+        if(!is_null($sid)) {
1827
+            return $sid;
1828
+        }
1829
+
1830
+        $objectSid = $this->readAttribute($domainDN, 'objectsid');
1831
+        if(!is_array($objectSid) || empty($objectSid)) {
1832
+            $this->connection->writeToCache($cacheKey, false);
1833
+            return false;
1834
+        }
1835
+        $domainObjectSid = $this->convertSID2Str($objectSid[0]);
1836
+        $this->connection->writeToCache($cacheKey, $domainObjectSid);
1837
+
1838
+        return $domainObjectSid;
1839
+    }
1840
+
1841
+    /**
1842
+     * converts a binary SID into a string representation
1843
+     * @param string $sid
1844
+     * @return string
1845
+     */
1846
+    public function convertSID2Str($sid) {
1847
+        // The format of a SID binary string is as follows:
1848
+        // 1 byte for the revision level
1849
+        // 1 byte for the number n of variable sub-ids
1850
+        // 6 bytes for identifier authority value
1851
+        // n*4 bytes for n sub-ids
1852
+        //
1853
+        // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f
1854
+        //  Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444
1855
+        $revision = ord($sid[0]);
1856
+        $numberSubID = ord($sid[1]);
1857
+
1858
+        $subIdStart = 8; // 1 + 1 + 6
1859
+        $subIdLength = 4;
1860
+        if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) {
1861
+            // Incorrect number of bytes present.
1862
+            return '';
1863
+        }
1864
+
1865
+        // 6 bytes = 48 bits can be represented using floats without loss of
1866
+        // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71)
1867
+        $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', '');
1868
+
1869
+        $subIDs = array();
1870
+        for ($i = 0; $i < $numberSubID; $i++) {
1871
+            $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength));
1872
+            $subIDs[] = sprintf('%u', $subID[1]);
1873
+        }
1874
+
1875
+        // Result for example above: S-1-5-21-249921958-728525901-1594176202
1876
+        return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs));
1877
+    }
1878
+
1879
+    /**
1880
+     * checks if the given DN is part of the given base DN(s)
1881
+     * @param string $dn the DN
1882
+     * @param string[] $bases array containing the allowed base DN or DNs
1883
+     * @return bool
1884
+     */
1885
+    public function isDNPartOfBase($dn, $bases) {
1886
+        $belongsToBase = false;
1887
+        $bases = $this->helper->sanitizeDN($bases);
1888
+
1889
+        foreach($bases as $base) {
1890
+            $belongsToBase = true;
1891
+            if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) {
1892
+                $belongsToBase = false;
1893
+            }
1894
+            if($belongsToBase) {
1895
+                break;
1896
+            }
1897
+        }
1898
+        return $belongsToBase;
1899
+    }
1900
+
1901
+    /**
1902
+     * resets a running Paged Search operation
1903
+     *
1904
+     * @throws ServerNotAvailableException
1905
+     */
1906
+    private function abandonPagedSearch() {
1907
+        $cr = $this->connection->getConnectionResource();
1908
+        $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie);
1909
+        $this->getPagedSearchResultState();
1910
+        $this->lastCookie = '';
1911
+        $this->cookies = [];
1912
+    }
1913
+
1914
+    /**
1915
+     * get a cookie for the next LDAP paged search
1916
+     * @param string $base a string with the base DN for the search
1917
+     * @param string $filter the search filter to identify the correct search
1918
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1919
+     * @param int $offset the offset for the new search to identify the correct search really good
1920
+     * @return string containing the key or empty if none is cached
1921
+     */
1922
+    private function getPagedResultCookie($base, $filter, $limit, $offset) {
1923
+        if($offset === 0) {
1924
+            return '';
1925
+        }
1926
+        $offset -= $limit;
1927
+        //we work with cache here
1928
+        $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1929
+        $cookie = '';
1930
+        if(isset($this->cookies[$cacheKey])) {
1931
+            $cookie = $this->cookies[$cacheKey];
1932
+            if(is_null($cookie)) {
1933
+                $cookie = '';
1934
+            }
1935
+        }
1936
+        return $cookie;
1937
+    }
1938
+
1939
+    /**
1940
+     * checks whether an LDAP paged search operation has more pages that can be
1941
+     * retrieved, typically when offset and limit are provided.
1942
+     *
1943
+     * Be very careful to use it: the last cookie value, which is inspected, can
1944
+     * be reset by other operations. Best, call it immediately after a search(),
1945
+     * searchUsers() or searchGroups() call. count-methods are probably safe as
1946
+     * well. Don't rely on it with any fetchList-method.
1947
+     * @return bool
1948
+     */
1949
+    public function hasMoreResults() {
1950
+        if(empty($this->lastCookie) && $this->lastCookie !== '0') {
1951
+            // as in RFC 2696, when all results are returned, the cookie will
1952
+            // be empty.
1953
+            return false;
1954
+        }
1955
+
1956
+        return true;
1957
+    }
1958
+
1959
+    /**
1960
+     * set a cookie for LDAP paged search run
1961
+     * @param string $base a string with the base DN for the search
1962
+     * @param string $filter the search filter to identify the correct search
1963
+     * @param int $limit the limit (or 'pageSize'), to identify the correct search well
1964
+     * @param int $offset the offset for the run search to identify the correct search really good
1965
+     * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response
1966
+     * @return void
1967
+     */
1968
+    private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) {
1969
+        // allow '0' for 389ds
1970
+        if(!empty($cookie) || $cookie === '0') {
1971
+            $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset;
1972
+            $this->cookies[$cacheKey] = $cookie;
1973
+            $this->lastCookie = $cookie;
1974
+        }
1975
+    }
1976
+
1977
+    /**
1978
+     * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search.
1979
+     * @return boolean|null true on success, null or false otherwise
1980
+     */
1981
+    public function getPagedSearchResultState() {
1982
+        $result = $this->pagedSearchedSuccessful;
1983
+        $this->pagedSearchedSuccessful = null;
1984
+        return $result;
1985
+    }
1986
+
1987
+    /**
1988
+     * Prepares a paged search, if possible
1989
+     * @param string $filter the LDAP filter for the search
1990
+     * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched
1991
+     * @param string[] $attr optional, when a certain attribute shall be filtered outside
1992
+     * @param int $limit
1993
+     * @param int $offset
1994
+     * @return bool|true
1995
+     */
1996
+    private function initPagedSearch($filter, $bases, $attr, $limit, $offset) {
1997
+        $pagedSearchOK = false;
1998
+        if ($limit !== 0) {
1999
+            $offset = (int)$offset; //can be null
2000
+            \OCP\Util::writeLog('user_ldap',
2001
+                'initializing paged search for  Filter '.$filter.' base '.print_r($bases, true)
2002
+                .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset,
2003
+                ILogger::DEBUG);
2004
+            //get the cookie from the search for the previous search, required by LDAP
2005
+            foreach($bases as $base) {
2006
+
2007
+                $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2008
+                if(empty($cookie) && $cookie !== "0" && ($offset > 0)) {
2009
+                    // no cookie known from a potential previous search. We need
2010
+                    // to start from 0 to come to the desired page. cookie value
2011
+                    // of '0' is valid, because 389ds
2012
+                    $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit;
2013
+                    $this->search($filter, array($base), $attr, $limit, $reOffset, true);
2014
+                    $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset);
2015
+                    //still no cookie? obviously, the server does not like us. Let's skip paging efforts.
2016
+                    // '0' is valid, because 389ds
2017
+                    //TODO: remember this, probably does not change in the next request...
2018
+                    if(empty($cookie) && $cookie !== '0') {
2019
+                        $cookie = null;
2020
+                    }
2021
+                }
2022
+                if(!is_null($cookie)) {
2023
+                    //since offset = 0, this is a new search. We abandon other searches that might be ongoing.
2024
+                    $this->abandonPagedSearch();
2025
+                    $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2026
+                        $this->connection->getConnectionResource(), $limit,
2027
+                        false, $cookie);
2028
+                    if(!$pagedSearchOK) {
2029
+                        return false;
2030
+                    }
2031
+                    \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG);
2032
+                } else {
2033
+                    $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset);
2034
+                    \OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]);
2035
+                }
2036
+
2037
+            }
2038
+        /* ++ Fixing RHDS searches with pages with zero results ++
2039 2039
 		 * We coudn't get paged searches working with our RHDS for login ($limit = 0),
2040 2040
 		 * due to pages with zero results.
2041 2041
 		 * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination
2042 2042
 		 * if we don't have a previous paged search.
2043 2043
 		 */
2044
-		} else if ($limit === 0 && !empty($this->lastCookie)) {
2045
-			// a search without limit was requested. However, if we do use
2046
-			// Paged Search once, we always must do it. This requires us to
2047
-			// initialize it with the configured page size.
2048
-			$this->abandonPagedSearch();
2049
-			// in case someone set it to 0 … use 500, otherwise no results will
2050
-			// be returned.
2051
-			$pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2052
-			$pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2053
-				$this->connection->getConnectionResource(),
2054
-				$pageSize, false, '');
2055
-		}
2056
-
2057
-		return $pagedSearchOK;
2058
-	}
2059
-
2060
-	/**
2061
-	 * Is more than one $attr used for search?
2062
-	 *
2063
-	 * @param string|string[]|null $attr
2064
-	 * @return bool
2065
-	 */
2066
-	private function manyAttributes($attr): bool {
2067
-		if (\is_array($attr)) {
2068
-			return \count($attr) > 1;
2069
-		}
2070
-		return false;
2071
-	}
2044
+        } else if ($limit === 0 && !empty($this->lastCookie)) {
2045
+            // a search without limit was requested. However, if we do use
2046
+            // Paged Search once, we always must do it. This requires us to
2047
+            // initialize it with the configured page size.
2048
+            $this->abandonPagedSearch();
2049
+            // in case someone set it to 0 … use 500, otherwise no results will
2050
+            // be returned.
2051
+            $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500;
2052
+            $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult',
2053
+                $this->connection->getConnectionResource(),
2054
+                $pageSize, false, '');
2055
+        }
2056
+
2057
+        return $pagedSearchOK;
2058
+    }
2059
+
2060
+    /**
2061
+     * Is more than one $attr used for search?
2062
+     *
2063
+     * @param string|string[]|null $attr
2064
+     * @return bool
2065
+     */
2066
+    private function manyAttributes($attr): bool {
2067
+        if (\is_array($attr)) {
2068
+            return \count($attr) > 1;
2069
+        }
2070
+        return false;
2071
+    }
2072 2072
 
2073 2073
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 2 patches
Indentation   +1199 added lines, -1199 removed lines patch added patch discarded remove patch
@@ -47,1203 +47,1203 @@
 block discarded – undo
47 47
 use OCP\ILogger;
48 48
 
49 49
 class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP, IGetDisplayNameBackend {
50
-	protected $enabled = false;
51
-
52
-	/**
53
-	 * @var string[] $cachedGroupMembers array of users with gid as key
54
-	 */
55
-	protected $cachedGroupMembers;
56
-
57
-	/**
58
-	 * @var string[] $cachedGroupsByMember array of groups with uid as key
59
-	 */
60
-	protected $cachedGroupsByMember;
61
-
62
-	/**
63
-	 * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
64
-	 */
65
-	protected $cachedNestedGroups;
66
-
67
-	/** @var GroupPluginManager */
68
-	protected $groupPluginManager;
69
-
70
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
71
-		parent::__construct($access);
72
-		$filter = $this->access->connection->ldapGroupFilter;
73
-		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
74
-		if(!empty($filter) && !empty($gassoc)) {
75
-			$this->enabled = true;
76
-		}
77
-
78
-		$this->cachedGroupMembers = new CappedMemoryCache();
79
-		$this->cachedGroupsByMember = new CappedMemoryCache();
80
-		$this->cachedNestedGroups = new CappedMemoryCache();
81
-		$this->groupPluginManager = $groupPluginManager;
82
-	}
83
-
84
-	/**
85
-	 * is user in group?
86
-	 * @param string $uid uid of the user
87
-	 * @param string $gid gid of the group
88
-	 * @return bool
89
-	 *
90
-	 * Checks whether the user is member of a group or not.
91
-	 */
92
-	public function inGroup($uid, $gid) {
93
-		if(!$this->enabled) {
94
-			return false;
95
-		}
96
-		$cacheKey = 'inGroup'.$uid.':'.$gid;
97
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
98
-		if(!is_null($inGroup)) {
99
-			return (bool)$inGroup;
100
-		}
101
-
102
-		$userDN = $this->access->username2dn($uid);
103
-
104
-		if(isset($this->cachedGroupMembers[$gid])) {
105
-			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
106
-			return $isInGroup;
107
-		}
108
-
109
-		$cacheKeyMembers = 'inGroup-members:'.$gid;
110
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111
-		if(!is_null($members)) {
112
-			$this->cachedGroupMembers[$gid] = $members;
113
-			$isInGroup = in_array($userDN, $members, true);
114
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
115
-			return $isInGroup;
116
-		}
117
-
118
-		$groupDN = $this->access->groupname2dn($gid);
119
-		// just in case
120
-		if(!$groupDN || !$userDN) {
121
-			$this->access->connection->writeToCache($cacheKey, false);
122
-			return false;
123
-		}
124
-
125
-		//check primary group first
126
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
127
-			$this->access->connection->writeToCache($cacheKey, true);
128
-			return true;
129
-		}
130
-
131
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
-		$members = $this->_groupMembers($groupDN);
133
-		if(!is_array($members) || count($members) === 0) {
134
-			$this->access->connection->writeToCache($cacheKey, false);
135
-			return false;
136
-		}
137
-
138
-		//extra work if we don't get back user DNs
139
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140
-			$dns = array();
141
-			$filterParts = array();
142
-			$bytes = 0;
143
-			foreach($members as $mid) {
144
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
145
-				$filterParts[] = $filter;
146
-				$bytes += strlen($filter);
147
-				if($bytes >= 9000000) {
148
-					// AD has a default input buffer of 10 MB, we do not want
149
-					// to take even the chance to exceed it
150
-					$filter = $this->access->combineFilterWithOr($filterParts);
151
-					$bytes = 0;
152
-					$filterParts = array();
153
-					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
-					$dns = array_merge($dns, $users);
155
-				}
156
-			}
157
-			if(count($filterParts) > 0) {
158
-				$filter = $this->access->combineFilterWithOr($filterParts);
159
-				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
160
-				$dns = array_merge($dns, $users);
161
-			}
162
-			$members = $dns;
163
-		}
164
-
165
-		$isInGroup = in_array($userDN, $members);
166
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
167
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
168
-		$this->cachedGroupMembers[$gid] = $members;
169
-
170
-		return $isInGroup;
171
-	}
172
-
173
-	/**
174
-	 * @param string $dnGroup
175
-	 * @return array
176
-	 *
177
-	 * For a group that has user membership defined by an LDAP search url attribute returns the users
178
-	 * that match the search url otherwise returns an empty array.
179
-	 */
180
-	public function getDynamicGroupMembers($dnGroup) {
181
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
182
-
183
-		if (empty($dynamicGroupMemberURL)) {
184
-			return array();
185
-		}
186
-
187
-		$dynamicMembers = array();
188
-		$memberURLs = $this->access->readAttribute(
189
-			$dnGroup,
190
-			$dynamicGroupMemberURL,
191
-			$this->access->connection->ldapGroupFilter
192
-		);
193
-		if ($memberURLs !== false) {
194
-			// this group has the 'memberURL' attribute so this is a dynamic group
195
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
196
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
197
-			$pos = strpos($memberURLs[0], '(');
198
-			if ($pos !== false) {
199
-				$memberUrlFilter = substr($memberURLs[0], $pos);
200
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
201
-				$dynamicMembers = array();
202
-				foreach($foundMembers as $value) {
203
-					$dynamicMembers[$value['dn'][0]] = 1;
204
-				}
205
-			} else {
206
-				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
207
-					'of group ' . $dnGroup, ILogger::DEBUG);
208
-			}
209
-		}
210
-		return $dynamicMembers;
211
-	}
212
-
213
-	/**
214
-	 * @param string $dnGroup
215
-	 * @param array|null &$seen
216
-	 * @return array|mixed|null
217
-	 * @throws \OC\ServerNotAvailableException
218
-	 */
219
-	private function _groupMembers($dnGroup, &$seen = null) {
220
-		if ($seen === null) {
221
-			$seen = [];
222
-		}
223
-		$allMembers = [];
224
-		if (array_key_exists($dnGroup, $seen)) {
225
-			// avoid loops
226
-			return [];
227
-		}
228
-		// used extensively in cron job, caching makes sense for nested groups
229
-		$cacheKey = '_groupMembers'.$dnGroup;
230
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
231
-		if($groupMembers !== null) {
232
-			return $groupMembers;
233
-		}
234
-		$seen[$dnGroup] = 1;
235
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
236
-		if (is_array($members)) {
237
-			$fetcher = function($memberDN, &$seen) {
238
-				return $this->_groupMembers($memberDN, $seen);
239
-			};
240
-			$allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
241
-		}
242
-
243
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
244
-
245
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
246
-		return $allMembers;
247
-	}
248
-
249
-	/**
250
-	 * @param string $DN
251
-	 * @param array|null &$seen
252
-	 * @return array
253
-	 * @throws \OC\ServerNotAvailableException
254
-	 */
255
-	private function _getGroupDNsFromMemberOf($DN) {
256
-		$groups = $this->access->readAttribute($DN, 'memberOf');
257
-		if (!is_array($groups)) {
258
-			return [];
259
-		}
260
-
261
-		$fetcher = function($groupDN) {
262
-			if (isset($this->cachedNestedGroups[$groupDN])) {
263
-				$nestedGroups = $this->cachedNestedGroups[$groupDN];
264
-			} else {
265
-				$nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
266
-				if (!is_array($nestedGroups)) {
267
-					$nestedGroups = [];
268
-				}
269
-				$this->cachedNestedGroups[$groupDN] = $nestedGroups;
270
-			}
271
-			return $nestedGroups;
272
-		};
273
-
274
-		$groups = $this->walkNestedGroups($DN, $fetcher, $groups);
275
-		return $this->access->groupsMatchFilter($groups);
276
-	}
277
-
278
-	/**
279
-	 * @param string $dn
280
-	 * @param \Closure $fetcher args: string $dn, array $seen, returns: string[] of dns
281
-	 * @param array $list
282
-	 * @return array
283
-	 */
284
-	private function walkNestedGroups(string $dn, \Closure $fetcher, array $list): array {
285
-		$nesting = (int) $this->access->connection->ldapNestedGroups;
286
-		// depending on the input, we either have a list of DNs or a list of LDAP records
287
-		// also, the output expects either DNs or records. Testing the first element should suffice.
288
-		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
289
-
290
-		if ($nesting !== 1) {
291
-			if($recordMode) {
292
-				// the keys are numeric, but should hold the DN
293
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
294
-					if($record['dn'][0] != $dn) {
295
-						$transformed[$record['dn'][0]] = $record;
296
-					}
297
-					return $transformed;
298
-				}, []);
299
-			}
300
-			return $list;
301
-		}
302
-
303
-		$seen = [];
304
-		while ($record = array_pop($list)) {
305
-			$recordDN = $recordMode ? $record['dn'][0] : $record;
306
-			if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
307
-				// Prevent loops
308
-				continue;
309
-			}
310
-			$fetched = $fetcher($record, $seen);
311
-			$list = array_merge($list, $fetched);
312
-			$seen[$recordDN] = $record;
313
-		}
314
-
315
-		return $recordMode ? $seen : array_keys($seen);
316
-	}
317
-
318
-	/**
319
-	 * translates a gidNumber into an ownCloud internal name
320
-	 * @param string $gid as given by gidNumber on POSIX LDAP
321
-	 * @param string $dn a DN that belongs to the same domain as the group
322
-	 * @return string|bool
323
-	 */
324
-	public function gidNumber2Name($gid, $dn) {
325
-		$cacheKey = 'gidNumberToName' . $gid;
326
-		$groupName = $this->access->connection->getFromCache($cacheKey);
327
-		if(!is_null($groupName) && isset($groupName)) {
328
-			return $groupName;
329
-		}
330
-
331
-		//we need to get the DN from LDAP
332
-		$filter = $this->access->combineFilterWithAnd([
333
-			$this->access->connection->ldapGroupFilter,
334
-			'objectClass=posixGroup',
335
-			$this->access->connection->ldapGidNumber . '=' . $gid
336
-		]);
337
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
338
-		if(empty($result)) {
339
-			return false;
340
-		}
341
-		$dn = $result[0]['dn'][0];
342
-
343
-		//and now the group name
344
-		//NOTE once we have separate ownCloud group IDs and group names we can
345
-		//directly read the display name attribute instead of the DN
346
-		$name = $this->access->dn2groupname($dn);
347
-
348
-		$this->access->connection->writeToCache($cacheKey, $name);
349
-
350
-		return $name;
351
-	}
352
-
353
-	/**
354
-	 * returns the entry's gidNumber
355
-	 * @param string $dn
356
-	 * @param string $attribute
357
-	 * @return string|bool
358
-	 */
359
-	private function getEntryGidNumber($dn, $attribute) {
360
-		$value = $this->access->readAttribute($dn, $attribute);
361
-		if(is_array($value) && !empty($value)) {
362
-			return $value[0];
363
-		}
364
-		return false;
365
-	}
366
-
367
-	/**
368
-	 * returns the group's primary ID
369
-	 * @param string $dn
370
-	 * @return string|bool
371
-	 */
372
-	public function getGroupGidNumber($dn) {
373
-		return $this->getEntryGidNumber($dn, 'gidNumber');
374
-	}
375
-
376
-	/**
377
-	 * returns the user's gidNumber
378
-	 * @param string $dn
379
-	 * @return string|bool
380
-	 */
381
-	public function getUserGidNumber($dn) {
382
-		$gidNumber = false;
383
-		if($this->access->connection->hasGidNumber) {
384
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
385
-			if($gidNumber === false) {
386
-				$this->access->connection->hasGidNumber = false;
387
-			}
388
-		}
389
-		return $gidNumber;
390
-	}
391
-
392
-	/**
393
-	 * returns a filter for a "users has specific gid" search or count operation
394
-	 *
395
-	 * @param string $groupDN
396
-	 * @param string $search
397
-	 * @return string
398
-	 * @throws \Exception
399
-	 */
400
-	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
401
-		$groupID = $this->getGroupGidNumber($groupDN);
402
-		if($groupID === false) {
403
-			throw new \Exception('Not a valid group');
404
-		}
405
-
406
-		$filterParts = [];
407
-		$filterParts[] = $this->access->getFilterForUserCount();
408
-		if ($search !== '') {
409
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
410
-		}
411
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
412
-
413
-		return $this->access->combineFilterWithAnd($filterParts);
414
-	}
415
-
416
-	/**
417
-	 * returns a list of users that have the given group as gid number
418
-	 *
419
-	 * @param string $groupDN
420
-	 * @param string $search
421
-	 * @param int $limit
422
-	 * @param int $offset
423
-	 * @return string[]
424
-	 */
425
-	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
426
-		try {
427
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
428
-			$users = $this->access->fetchListOfUsers(
429
-				$filter,
430
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
431
-				$limit,
432
-				$offset
433
-			);
434
-			return $this->access->nextcloudUserNames($users);
435
-		} catch (\Exception $e) {
436
-			return [];
437
-		}
438
-	}
439
-
440
-	/**
441
-	 * returns the number of users that have the given group as gid number
442
-	 *
443
-	 * @param string $groupDN
444
-	 * @param string $search
445
-	 * @param int $limit
446
-	 * @param int $offset
447
-	 * @return int
448
-	 */
449
-	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
450
-		try {
451
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
452
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
453
-			return (int)$users;
454
-		} catch (\Exception $e) {
455
-			return 0;
456
-		}
457
-	}
458
-
459
-	/**
460
-	 * gets the gidNumber of a user
461
-	 * @param string $dn
462
-	 * @return string
463
-	 */
464
-	public function getUserGroupByGid($dn) {
465
-		$groupID = $this->getUserGidNumber($dn);
466
-		if($groupID !== false) {
467
-			$groupName = $this->gidNumber2Name($groupID, $dn);
468
-			if($groupName !== false) {
469
-				return $groupName;
470
-			}
471
-		}
472
-
473
-		return false;
474
-	}
475
-
476
-	/**
477
-	 * translates a primary group ID into an Nextcloud internal name
478
-	 * @param string $gid as given by primaryGroupID on AD
479
-	 * @param string $dn a DN that belongs to the same domain as the group
480
-	 * @return string|bool
481
-	 */
482
-	public function primaryGroupID2Name($gid, $dn) {
483
-		$cacheKey = 'primaryGroupIDtoName';
484
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
485
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
486
-			return $groupNames[$gid];
487
-		}
488
-
489
-		$domainObjectSid = $this->access->getSID($dn);
490
-		if($domainObjectSid === false) {
491
-			return false;
492
-		}
493
-
494
-		//we need to get the DN from LDAP
495
-		$filter = $this->access->combineFilterWithAnd(array(
496
-			$this->access->connection->ldapGroupFilter,
497
-			'objectsid=' . $domainObjectSid . '-' . $gid
498
-		));
499
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
500
-		if(empty($result)) {
501
-			return false;
502
-		}
503
-		$dn = $result[0]['dn'][0];
504
-
505
-		//and now the group name
506
-		//NOTE once we have separate Nextcloud group IDs and group names we can
507
-		//directly read the display name attribute instead of the DN
508
-		$name = $this->access->dn2groupname($dn);
509
-
510
-		$this->access->connection->writeToCache($cacheKey, $name);
511
-
512
-		return $name;
513
-	}
514
-
515
-	/**
516
-	 * returns the entry's primary group ID
517
-	 * @param string $dn
518
-	 * @param string $attribute
519
-	 * @return string|bool
520
-	 */
521
-	private function getEntryGroupID($dn, $attribute) {
522
-		$value = $this->access->readAttribute($dn, $attribute);
523
-		if(is_array($value) && !empty($value)) {
524
-			return $value[0];
525
-		}
526
-		return false;
527
-	}
528
-
529
-	/**
530
-	 * returns the group's primary ID
531
-	 * @param string $dn
532
-	 * @return string|bool
533
-	 */
534
-	public function getGroupPrimaryGroupID($dn) {
535
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
536
-	}
537
-
538
-	/**
539
-	 * returns the user's primary group ID
540
-	 * @param string $dn
541
-	 * @return string|bool
542
-	 */
543
-	public function getUserPrimaryGroupIDs($dn) {
544
-		$primaryGroupID = false;
545
-		if($this->access->connection->hasPrimaryGroups) {
546
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
547
-			if($primaryGroupID === false) {
548
-				$this->access->connection->hasPrimaryGroups = false;
549
-			}
550
-		}
551
-		return $primaryGroupID;
552
-	}
553
-
554
-	/**
555
-	 * returns a filter for a "users in primary group" search or count operation
556
-	 *
557
-	 * @param string $groupDN
558
-	 * @param string $search
559
-	 * @return string
560
-	 * @throws \Exception
561
-	 */
562
-	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
563
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
564
-		if($groupID === false) {
565
-			throw new \Exception('Not a valid group');
566
-		}
567
-
568
-		$filterParts = [];
569
-		$filterParts[] = $this->access->getFilterForUserCount();
570
-		if ($search !== '') {
571
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
572
-		}
573
-		$filterParts[] = 'primaryGroupID=' . $groupID;
574
-
575
-		return $this->access->combineFilterWithAnd($filterParts);
576
-	}
577
-
578
-	/**
579
-	 * returns a list of users that have the given group as primary group
580
-	 *
581
-	 * @param string $groupDN
582
-	 * @param string $search
583
-	 * @param int $limit
584
-	 * @param int $offset
585
-	 * @return string[]
586
-	 */
587
-	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
588
-		try {
589
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
590
-			$users = $this->access->fetchListOfUsers(
591
-				$filter,
592
-				array($this->access->connection->ldapUserDisplayName, 'dn'),
593
-				$limit,
594
-				$offset
595
-			);
596
-			return $this->access->nextcloudUserNames($users);
597
-		} catch (\Exception $e) {
598
-			return array();
599
-		}
600
-	}
601
-
602
-	/**
603
-	 * returns the number of users that have the given group as primary group
604
-	 *
605
-	 * @param string $groupDN
606
-	 * @param string $search
607
-	 * @param int $limit
608
-	 * @param int $offset
609
-	 * @return int
610
-	 */
611
-	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
612
-		try {
613
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
614
-			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
615
-			return (int)$users;
616
-		} catch (\Exception $e) {
617
-			return 0;
618
-		}
619
-	}
620
-
621
-	/**
622
-	 * gets the primary group of a user
623
-	 * @param string $dn
624
-	 * @return string
625
-	 */
626
-	public function getUserPrimaryGroup($dn) {
627
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
628
-		if($groupID !== false) {
629
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
630
-			if($groupName !== false) {
631
-				return $groupName;
632
-			}
633
-		}
634
-
635
-		return false;
636
-	}
637
-
638
-	/**
639
-	 * Get all groups a user belongs to
640
-	 * @param string $uid Name of the user
641
-	 * @return array with group names
642
-	 *
643
-	 * This function fetches all groups a user belongs to. It does not check
644
-	 * if the user exists at all.
645
-	 *
646
-	 * This function includes groups based on dynamic group membership.
647
-	 */
648
-	public function getUserGroups($uid) {
649
-		if(!$this->enabled) {
650
-			return array();
651
-		}
652
-		$cacheKey = 'getUserGroups'.$uid;
653
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
654
-		if(!is_null($userGroups)) {
655
-			return $userGroups;
656
-		}
657
-		$userDN = $this->access->username2dn($uid);
658
-		if(!$userDN) {
659
-			$this->access->connection->writeToCache($cacheKey, array());
660
-			return array();
661
-		}
662
-
663
-		$groups = [];
664
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
665
-		$gidGroupName = $this->getUserGroupByGid($userDN);
666
-
667
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
668
-
669
-		if (!empty($dynamicGroupMemberURL)) {
670
-			// look through dynamic groups to add them to the result array if needed
671
-			$groupsToMatch = $this->access->fetchListOfGroups(
672
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
673
-			foreach($groupsToMatch as $dynamicGroup) {
674
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
675
-					continue;
676
-				}
677
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
678
-				if ($pos !== false) {
679
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
680
-					// apply filter via ldap search to see if this user is in this
681
-					// dynamic group
682
-					$userMatch = $this->access->readAttribute(
683
-						$userDN,
684
-						$this->access->connection->ldapUserDisplayName,
685
-						$memberUrlFilter
686
-					);
687
-					if ($userMatch !== false) {
688
-						// match found so this user is in this group
689
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
690
-						if(is_string($groupName)) {
691
-							// be sure to never return false if the dn could not be
692
-							// resolved to a name, for whatever reason.
693
-							$groups[] = $groupName;
694
-						}
695
-					}
696
-				} else {
697
-					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
698
-						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
699
-				}
700
-			}
701
-		}
702
-
703
-		// if possible, read out membership via memberOf. It's far faster than
704
-		// performing a search, which still is a fallback later.
705
-		// memberof doesn't support memberuid, so skip it here.
706
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
707
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
708
-		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
709
-		    ) {
710
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
711
-			if (is_array($groupDNs)) {
712
-				foreach ($groupDNs as $dn) {
713
-					$groupName = $this->access->dn2groupname($dn);
714
-					if(is_string($groupName)) {
715
-						// be sure to never return false if the dn could not be
716
-						// resolved to a name, for whatever reason.
717
-						$groups[] = $groupName;
718
-					}
719
-				}
720
-			}
721
-
722
-			if($primaryGroup !== false) {
723
-				$groups[] = $primaryGroup;
724
-			}
725
-			if($gidGroupName !== false) {
726
-				$groups[] = $gidGroupName;
727
-			}
728
-			$this->access->connection->writeToCache($cacheKey, $groups);
729
-			return $groups;
730
-		}
731
-
732
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
733
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
734
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
735
-		) {
736
-			$uid = $userDN;
737
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
738
-			$result = $this->access->readAttribute($userDN, 'uid');
739
-			if ($result === false) {
740
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
741
-					$this->access->connection->ldapHost, ILogger::DEBUG);
742
-			}
743
-			$uid = $result[0];
744
-		} else {
745
-			// just in case
746
-			$uid = $userDN;
747
-		}
748
-
749
-		if(isset($this->cachedGroupsByMember[$uid])) {
750
-			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
751
-		} else {
752
-			$groupsByMember = array_values($this->getGroupsByMember($uid));
753
-			$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
754
-			$this->cachedGroupsByMember[$uid] = $groupsByMember;
755
-			$groups = array_merge($groups, $groupsByMember);
756
-		}
757
-
758
-		if($primaryGroup !== false) {
759
-			$groups[] = $primaryGroup;
760
-		}
761
-		if($gidGroupName !== false) {
762
-			$groups[] = $gidGroupName;
763
-		}
764
-
765
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
766
-		$this->access->connection->writeToCache($cacheKey, $groups);
767
-
768
-		return $groups;
769
-	}
770
-
771
-	/**
772
-	 * @param string $dn
773
-	 * @param array|null &$seen
774
-	 * @return array
775
-	 */
776
-	private function getGroupsByMember($dn, &$seen = null) {
777
-		if ($seen === null) {
778
-			$seen = [];
779
-		}
780
-		if (array_key_exists($dn, $seen)) {
781
-			// avoid loops
782
-			return [];
783
-		}
784
-		$allGroups = [];
785
-		$seen[$dn] = true;
786
-		$filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
787
-		$groups = $this->access->fetchListOfGroups($filter,
788
-			[$this->access->connection->ldapGroupDisplayName, 'dn']);
789
-		if (is_array($groups)) {
790
-			$fetcher = function ($dn, &$seen) {
791
-				if(is_array($dn) && isset($dn['dn'][0])) {
792
-					$dn = $dn['dn'][0];
793
-				}
794
-				return $this->getGroupsByMember($dn, $seen);
795
-			};
796
-			$allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
797
-		}
798
-		$visibleGroups = $this->access->groupsMatchFilter(array_keys($allGroups));
799
-		return array_intersect_key($allGroups, array_flip($visibleGroups));
800
-	}
801
-
802
-	/**
803
-	 * get a list of all users in a group
804
-	 *
805
-	 * @param string $gid
806
-	 * @param string $search
807
-	 * @param int $limit
808
-	 * @param int $offset
809
-	 * @return array with user ids
810
-	 */
811
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
812
-		if(!$this->enabled) {
813
-			return array();
814
-		}
815
-		if(!$this->groupExists($gid)) {
816
-			return array();
817
-		}
818
-		$search = $this->access->escapeFilterPart($search, true);
819
-		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
820
-		// check for cache of the exact query
821
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
822
-		if(!is_null($groupUsers)) {
823
-			return $groupUsers;
824
-		}
825
-
826
-		// check for cache of the query without limit and offset
827
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
828
-		if(!is_null($groupUsers)) {
829
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
830
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
831
-			return $groupUsers;
832
-		}
833
-
834
-		if($limit === -1) {
835
-			$limit = null;
836
-		}
837
-		$groupDN = $this->access->groupname2dn($gid);
838
-		if(!$groupDN) {
839
-			// group couldn't be found, return empty resultset
840
-			$this->access->connection->writeToCache($cacheKey, array());
841
-			return array();
842
-		}
843
-
844
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
845
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
846
-		$members = $this->_groupMembers($groupDN);
847
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
848
-			//in case users could not be retrieved, return empty result set
849
-			$this->access->connection->writeToCache($cacheKey, []);
850
-			return [];
851
-		}
852
-
853
-		$groupUsers = array();
854
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
855
-		$attrs = $this->access->userManager->getAttributes(true);
856
-		foreach($members as $member) {
857
-			if($isMemberUid) {
858
-				//we got uids, need to get their DNs to 'translate' them to user names
859
-				$filter = $this->access->combineFilterWithAnd(array(
860
-					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
861
-					$this->access->getFilterPartForUserSearch($search)
862
-				));
863
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
864
-				if(count($ldap_users) < 1) {
865
-					continue;
866
-				}
867
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
868
-			} else {
869
-				//we got DNs, check if we need to filter by search or we can give back all of them
870
-				if ($search !== '') {
871
-					if(!$this->access->readAttribute($member,
872
-						$this->access->connection->ldapUserDisplayName,
873
-						$this->access->getFilterPartForUserSearch($search))) {
874
-						continue;
875
-					}
876
-				}
877
-				// dn2username will also check if the users belong to the allowed base
878
-				if($ocname = $this->access->dn2username($member)) {
879
-					$groupUsers[] = $ocname;
880
-				}
881
-			}
882
-		}
883
-
884
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
885
-		natsort($groupUsers);
886
-		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
887
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
888
-
889
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
890
-
891
-		return $groupUsers;
892
-	}
893
-
894
-	/**
895
-	 * returns the number of users in a group, who match the search term
896
-	 * @param string $gid the internal group name
897
-	 * @param string $search optional, a search string
898
-	 * @return int|bool
899
-	 */
900
-	public function countUsersInGroup($gid, $search = '') {
901
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
902
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
903
-		}
904
-
905
-		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
906
-		if(!$this->enabled || !$this->groupExists($gid)) {
907
-			return false;
908
-		}
909
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
910
-		if(!is_null($groupUsers)) {
911
-			return $groupUsers;
912
-		}
913
-
914
-		$groupDN = $this->access->groupname2dn($gid);
915
-		if(!$groupDN) {
916
-			// group couldn't be found, return empty result set
917
-			$this->access->connection->writeToCache($cacheKey, false);
918
-			return false;
919
-		}
920
-
921
-		$members = $this->_groupMembers($groupDN);
922
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
923
-		if(!$members && $primaryUserCount === 0) {
924
-			//in case users could not be retrieved, return empty result set
925
-			$this->access->connection->writeToCache($cacheKey, false);
926
-			return false;
927
-		}
928
-
929
-		if ($search === '') {
930
-			$groupUsers = count($members) + $primaryUserCount;
931
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
932
-			return $groupUsers;
933
-		}
934
-		$search = $this->access->escapeFilterPart($search, true);
935
-		$isMemberUid =
936
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
937
-			=== 'memberuid');
938
-
939
-		//we need to apply the search filter
940
-		//alternatives that need to be checked:
941
-		//a) get all users by search filter and array_intersect them
942
-		//b) a, but only when less than 1k 10k ?k users like it is
943
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
944
-		//   and let it count.
945
-		//For now this is not important, because the only use of this method
946
-		//does not supply a search string
947
-		$groupUsers = array();
948
-		foreach($members as $member) {
949
-			if($isMemberUid) {
950
-				//we got uids, need to get their DNs to 'translate' them to user names
951
-				$filter = $this->access->combineFilterWithAnd(array(
952
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
953
-					$this->access->getFilterPartForUserSearch($search)
954
-				));
955
-				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
956
-				if(count($ldap_users) < 1) {
957
-					continue;
958
-				}
959
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
960
-			} else {
961
-				//we need to apply the search filter now
962
-				if(!$this->access->readAttribute($member,
963
-					$this->access->connection->ldapUserDisplayName,
964
-					$this->access->getFilterPartForUserSearch($search))) {
965
-					continue;
966
-				}
967
-				// dn2username will also check if the users belong to the allowed base
968
-				if($ocname = $this->access->dn2username($member)) {
969
-					$groupUsers[] = $ocname;
970
-				}
971
-			}
972
-		}
973
-
974
-		//and get users that have the group as primary
975
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
976
-
977
-		return count($groupUsers) + $primaryUsers;
978
-	}
979
-
980
-	/**
981
-	 * get a list of all groups
982
-	 *
983
-	 * @param string $search
984
-	 * @param $limit
985
-	 * @param int $offset
986
-	 * @return array with group names
987
-	 *
988
-	 * Returns a list with all groups (used by getGroups)
989
-	 */
990
-	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
991
-		if(!$this->enabled) {
992
-			return array();
993
-		}
994
-		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
995
-
996
-		//Check cache before driving unnecessary searches
997
-		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
998
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
-		if(!is_null($ldap_groups)) {
1000
-			return $ldap_groups;
1001
-		}
1002
-
1003
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
-		// error. With a limit of 0, we get 0 results. So we pass null.
1005
-		if($limit <= 0) {
1006
-			$limit = null;
1007
-		}
1008
-		$filter = $this->access->combineFilterWithAnd(array(
1009
-			$this->access->connection->ldapGroupFilter,
1010
-			$this->access->getFilterPartForGroupSearch($search)
1011
-		));
1012
-		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
1013
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
1014
-				array($this->access->connection->ldapGroupDisplayName, 'dn'),
1015
-				$limit,
1016
-				$offset);
1017
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1018
-
1019
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
1020
-		return $ldap_groups;
1021
-	}
1022
-
1023
-	/**
1024
-	 * get a list of all groups using a paged search
1025
-	 *
1026
-	 * @param string $search
1027
-	 * @param int $limit
1028
-	 * @param int $offset
1029
-	 * @return array with group names
1030
-	 *
1031
-	 * Returns a list with all groups
1032
-	 * Uses a paged search if available to override a
1033
-	 * server side search limit.
1034
-	 * (active directory has a limit of 1000 by default)
1035
-	 */
1036
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1037
-		if(!$this->enabled) {
1038
-			return array();
1039
-		}
1040
-		$search = $this->access->escapeFilterPart($search, true);
1041
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1042
-		if ($pagingSize <= 0) {
1043
-			return $this->getGroupsChunk($search, $limit, $offset);
1044
-		}
1045
-		$maxGroups = 100000; // limit max results (just for safety reasons)
1046
-		if ($limit > -1) {
1047
-		   $overallLimit = min($limit + $offset, $maxGroups);
1048
-		} else {
1049
-		   $overallLimit = $maxGroups;
1050
-		}
1051
-		$chunkOffset = $offset;
1052
-		$allGroups = array();
1053
-		while ($chunkOffset < $overallLimit) {
1054
-			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1055
-			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1056
-			$nread = count($ldapGroups);
1057
-			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1058
-			if ($nread) {
1059
-				$allGroups = array_merge($allGroups, $ldapGroups);
1060
-				$chunkOffset += $nread;
1061
-			}
1062
-			if ($nread < $chunkLimit) {
1063
-				break;
1064
-			}
1065
-		}
1066
-		return $allGroups;
1067
-	}
1068
-
1069
-	/**
1070
-	 * @param string $group
1071
-	 * @return bool
1072
-	 */
1073
-	public function groupMatchesFilter($group) {
1074
-		return (strripos($group, $this->groupSearch) !== false);
1075
-	}
1076
-
1077
-	/**
1078
-	 * check if a group exists
1079
-	 * @param string $gid
1080
-	 * @return bool
1081
-	 */
1082
-	public function groupExists($gid) {
1083
-		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1084
-		if(!is_null($groupExists)) {
1085
-			return (bool)$groupExists;
1086
-		}
1087
-
1088
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1089
-		//only, requires more checking.
1090
-		$dn = $this->access->groupname2dn($gid);
1091
-		if(!$dn) {
1092
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1093
-			return false;
1094
-		}
1095
-
1096
-		//if group really still exists, we will be able to read its objectclass
1097
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1098
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1099
-			return false;
1100
-		}
1101
-
1102
-		$this->access->connection->writeToCache('groupExists'.$gid, true);
1103
-		return true;
1104
-	}
1105
-
1106
-	/**
1107
-	* Check if backend implements actions
1108
-	* @param int $actions bitwise-or'ed actions
1109
-	* @return boolean
1110
-	*
1111
-	* Returns the supported actions as int to be
1112
-	* compared with GroupInterface::CREATE_GROUP etc.
1113
-	*/
1114
-	public function implementsActions($actions) {
1115
-		return (bool)((GroupInterface::COUNT_USERS |
1116
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1117
-	}
1118
-
1119
-	/**
1120
-	 * Return access for LDAP interaction.
1121
-	 * @return Access instance of Access for LDAP interaction
1122
-	 */
1123
-	public function getLDAPAccess($gid) {
1124
-		return $this->access;
1125
-	}
1126
-
1127
-	/**
1128
-	 * create a group
1129
-	 * @param string $gid
1130
-	 * @return bool
1131
-	 * @throws \Exception
1132
-	 */
1133
-	public function createGroup($gid) {
1134
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1135
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1136
-				//updates group mapping
1137
-				$this->access->dn2ocname($dn, $gid, false);
1138
-				$this->access->connection->writeToCache("groupExists".$gid, true);
1139
-			}
1140
-			return $dn != null;
1141
-		}
1142
-		throw new \Exception('Could not create group in LDAP backend.');
1143
-	}
1144
-
1145
-	/**
1146
-	 * delete a group
1147
-	 * @param string $gid gid of the group to delete
1148
-	 * @return bool
1149
-	 * @throws \Exception
1150
-	 */
1151
-	public function deleteGroup($gid) {
1152
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1153
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1154
-				#delete group in nextcloud internal db
1155
-				$this->access->getGroupMapper()->unmap($gid);
1156
-				$this->access->connection->writeToCache("groupExists".$gid, false);
1157
-			}
1158
-			return $ret;
1159
-		}
1160
-		throw new \Exception('Could not delete group in LDAP backend.');
1161
-	}
1162
-
1163
-	/**
1164
-	 * Add a user to a group
1165
-	 * @param string $uid Name of the user to add to group
1166
-	 * @param string $gid Name of the group in which add the user
1167
-	 * @return bool
1168
-	 * @throws \Exception
1169
-	 */
1170
-	public function addToGroup($uid, $gid) {
1171
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1172
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1173
-				$this->access->connection->clearCache();
1174
-				unset($this->cachedGroupMembers[$gid]);
1175
-			}
1176
-			return $ret;
1177
-		}
1178
-		throw new \Exception('Could not add user to group in LDAP backend.');
1179
-	}
1180
-
1181
-	/**
1182
-	 * Removes a user from a group
1183
-	 * @param string $uid Name of the user to remove from group
1184
-	 * @param string $gid Name of the group from which remove the user
1185
-	 * @return bool
1186
-	 * @throws \Exception
1187
-	 */
1188
-	public function removeFromGroup($uid, $gid) {
1189
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1190
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1191
-				$this->access->connection->clearCache();
1192
-				unset($this->cachedGroupMembers[$gid]);
1193
-			}
1194
-			return $ret;
1195
-		}
1196
-		throw new \Exception('Could not remove user from group in LDAP backend.');
1197
-	}
1198
-
1199
-	/**
1200
-	 * Gets group details
1201
-	 * @param string $gid Name of the group
1202
-	 * @return array | false
1203
-	 * @throws \Exception
1204
-	 */
1205
-	public function getGroupDetails($gid) {
1206
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1207
-			return $this->groupPluginManager->getGroupDetails($gid);
1208
-		}
1209
-		throw new \Exception('Could not get group details in LDAP backend.');
1210
-	}
1211
-
1212
-	/**
1213
-	 * Return LDAP connection resource from a cloned connection.
1214
-	 * The cloned connection needs to be closed manually.
1215
-	 * of the current access.
1216
-	 * @param string $gid
1217
-	 * @return resource of the LDAP connection
1218
-	 */
1219
-	public function getNewLDAPConnection($gid) {
1220
-		$connection = clone $this->access->getConnection();
1221
-		return $connection->getConnectionResource();
1222
-	}
1223
-
1224
-	/**
1225
-	 * @throws \OC\ServerNotAvailableException
1226
-	 */
1227
-	public function getDisplayName(string $gid): string {
1228
-		if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1229
-			return $this->groupPluginManager->getDisplayName($gid);
1230
-		}
1231
-
1232
-		$cacheKey = 'group_getDisplayName' . $gid;
1233
-		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1234
-			return $displayName;
1235
-		}
1236
-
1237
-		$displayName = $this->access->readAttribute(
1238
-			$this->access->groupname2dn($gid),
1239
-			$this->access->connection->ldapGroupDisplayName);
1240
-
1241
-		if ($displayName && (count($displayName) > 0)) {
1242
-			$displayName = $displayName[0];
1243
-			$this->access->connection->writeToCache($cacheKey, $displayName);
1244
-			return $displayName;
1245
-		}
1246
-
1247
-		return '';
1248
-	}
50
+    protected $enabled = false;
51
+
52
+    /**
53
+     * @var string[] $cachedGroupMembers array of users with gid as key
54
+     */
55
+    protected $cachedGroupMembers;
56
+
57
+    /**
58
+     * @var string[] $cachedGroupsByMember array of groups with uid as key
59
+     */
60
+    protected $cachedGroupsByMember;
61
+
62
+    /**
63
+     * @var string[] $cachedNestedGroups array of groups with gid (DN) as key
64
+     */
65
+    protected $cachedNestedGroups;
66
+
67
+    /** @var GroupPluginManager */
68
+    protected $groupPluginManager;
69
+
70
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
71
+        parent::__construct($access);
72
+        $filter = $this->access->connection->ldapGroupFilter;
73
+        $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
74
+        if(!empty($filter) && !empty($gassoc)) {
75
+            $this->enabled = true;
76
+        }
77
+
78
+        $this->cachedGroupMembers = new CappedMemoryCache();
79
+        $this->cachedGroupsByMember = new CappedMemoryCache();
80
+        $this->cachedNestedGroups = new CappedMemoryCache();
81
+        $this->groupPluginManager = $groupPluginManager;
82
+    }
83
+
84
+    /**
85
+     * is user in group?
86
+     * @param string $uid uid of the user
87
+     * @param string $gid gid of the group
88
+     * @return bool
89
+     *
90
+     * Checks whether the user is member of a group or not.
91
+     */
92
+    public function inGroup($uid, $gid) {
93
+        if(!$this->enabled) {
94
+            return false;
95
+        }
96
+        $cacheKey = 'inGroup'.$uid.':'.$gid;
97
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
98
+        if(!is_null($inGroup)) {
99
+            return (bool)$inGroup;
100
+        }
101
+
102
+        $userDN = $this->access->username2dn($uid);
103
+
104
+        if(isset($this->cachedGroupMembers[$gid])) {
105
+            $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
106
+            return $isInGroup;
107
+        }
108
+
109
+        $cacheKeyMembers = 'inGroup-members:'.$gid;
110
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
111
+        if(!is_null($members)) {
112
+            $this->cachedGroupMembers[$gid] = $members;
113
+            $isInGroup = in_array($userDN, $members, true);
114
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
115
+            return $isInGroup;
116
+        }
117
+
118
+        $groupDN = $this->access->groupname2dn($gid);
119
+        // just in case
120
+        if(!$groupDN || !$userDN) {
121
+            $this->access->connection->writeToCache($cacheKey, false);
122
+            return false;
123
+        }
124
+
125
+        //check primary group first
126
+        if($gid === $this->getUserPrimaryGroup($userDN)) {
127
+            $this->access->connection->writeToCache($cacheKey, true);
128
+            return true;
129
+        }
130
+
131
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132
+        $members = $this->_groupMembers($groupDN);
133
+        if(!is_array($members) || count($members) === 0) {
134
+            $this->access->connection->writeToCache($cacheKey, false);
135
+            return false;
136
+        }
137
+
138
+        //extra work if we don't get back user DNs
139
+        if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140
+            $dns = array();
141
+            $filterParts = array();
142
+            $bytes = 0;
143
+            foreach($members as $mid) {
144
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
145
+                $filterParts[] = $filter;
146
+                $bytes += strlen($filter);
147
+                if($bytes >= 9000000) {
148
+                    // AD has a default input buffer of 10 MB, we do not want
149
+                    // to take even the chance to exceed it
150
+                    $filter = $this->access->combineFilterWithOr($filterParts);
151
+                    $bytes = 0;
152
+                    $filterParts = array();
153
+                    $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
+                    $dns = array_merge($dns, $users);
155
+                }
156
+            }
157
+            if(count($filterParts) > 0) {
158
+                $filter = $this->access->combineFilterWithOr($filterParts);
159
+                $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
160
+                $dns = array_merge($dns, $users);
161
+            }
162
+            $members = $dns;
163
+        }
164
+
165
+        $isInGroup = in_array($userDN, $members);
166
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
167
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
168
+        $this->cachedGroupMembers[$gid] = $members;
169
+
170
+        return $isInGroup;
171
+    }
172
+
173
+    /**
174
+     * @param string $dnGroup
175
+     * @return array
176
+     *
177
+     * For a group that has user membership defined by an LDAP search url attribute returns the users
178
+     * that match the search url otherwise returns an empty array.
179
+     */
180
+    public function getDynamicGroupMembers($dnGroup) {
181
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
182
+
183
+        if (empty($dynamicGroupMemberURL)) {
184
+            return array();
185
+        }
186
+
187
+        $dynamicMembers = array();
188
+        $memberURLs = $this->access->readAttribute(
189
+            $dnGroup,
190
+            $dynamicGroupMemberURL,
191
+            $this->access->connection->ldapGroupFilter
192
+        );
193
+        if ($memberURLs !== false) {
194
+            // this group has the 'memberURL' attribute so this is a dynamic group
195
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
196
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
197
+            $pos = strpos($memberURLs[0], '(');
198
+            if ($pos !== false) {
199
+                $memberUrlFilter = substr($memberURLs[0], $pos);
200
+                $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
201
+                $dynamicMembers = array();
202
+                foreach($foundMembers as $value) {
203
+                    $dynamicMembers[$value['dn'][0]] = 1;
204
+                }
205
+            } else {
206
+                \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
207
+                    'of group ' . $dnGroup, ILogger::DEBUG);
208
+            }
209
+        }
210
+        return $dynamicMembers;
211
+    }
212
+
213
+    /**
214
+     * @param string $dnGroup
215
+     * @param array|null &$seen
216
+     * @return array|mixed|null
217
+     * @throws \OC\ServerNotAvailableException
218
+     */
219
+    private function _groupMembers($dnGroup, &$seen = null) {
220
+        if ($seen === null) {
221
+            $seen = [];
222
+        }
223
+        $allMembers = [];
224
+        if (array_key_exists($dnGroup, $seen)) {
225
+            // avoid loops
226
+            return [];
227
+        }
228
+        // used extensively in cron job, caching makes sense for nested groups
229
+        $cacheKey = '_groupMembers'.$dnGroup;
230
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
231
+        if($groupMembers !== null) {
232
+            return $groupMembers;
233
+        }
234
+        $seen[$dnGroup] = 1;
235
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr);
236
+        if (is_array($members)) {
237
+            $fetcher = function($memberDN, &$seen) {
238
+                return $this->_groupMembers($memberDN, $seen);
239
+            };
240
+            $allMembers = $this->walkNestedGroups($dnGroup, $fetcher, $members);
241
+        }
242
+
243
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
244
+
245
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
246
+        return $allMembers;
247
+    }
248
+
249
+    /**
250
+     * @param string $DN
251
+     * @param array|null &$seen
252
+     * @return array
253
+     * @throws \OC\ServerNotAvailableException
254
+     */
255
+    private function _getGroupDNsFromMemberOf($DN) {
256
+        $groups = $this->access->readAttribute($DN, 'memberOf');
257
+        if (!is_array($groups)) {
258
+            return [];
259
+        }
260
+
261
+        $fetcher = function($groupDN) {
262
+            if (isset($this->cachedNestedGroups[$groupDN])) {
263
+                $nestedGroups = $this->cachedNestedGroups[$groupDN];
264
+            } else {
265
+                $nestedGroups = $this->access->readAttribute($groupDN, 'memberOf');
266
+                if (!is_array($nestedGroups)) {
267
+                    $nestedGroups = [];
268
+                }
269
+                $this->cachedNestedGroups[$groupDN] = $nestedGroups;
270
+            }
271
+            return $nestedGroups;
272
+        };
273
+
274
+        $groups = $this->walkNestedGroups($DN, $fetcher, $groups);
275
+        return $this->access->groupsMatchFilter($groups);
276
+    }
277
+
278
+    /**
279
+     * @param string $dn
280
+     * @param \Closure $fetcher args: string $dn, array $seen, returns: string[] of dns
281
+     * @param array $list
282
+     * @return array
283
+     */
284
+    private function walkNestedGroups(string $dn, \Closure $fetcher, array $list): array {
285
+        $nesting = (int) $this->access->connection->ldapNestedGroups;
286
+        // depending on the input, we either have a list of DNs or a list of LDAP records
287
+        // also, the output expects either DNs or records. Testing the first element should suffice.
288
+        $recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
289
+
290
+        if ($nesting !== 1) {
291
+            if($recordMode) {
292
+                // the keys are numeric, but should hold the DN
293
+                return array_reduce($list, function ($transformed, $record) use ($dn) {
294
+                    if($record['dn'][0] != $dn) {
295
+                        $transformed[$record['dn'][0]] = $record;
296
+                    }
297
+                    return $transformed;
298
+                }, []);
299
+            }
300
+            return $list;
301
+        }
302
+
303
+        $seen = [];
304
+        while ($record = array_pop($list)) {
305
+            $recordDN = $recordMode ? $record['dn'][0] : $record;
306
+            if ($recordDN === $dn || array_key_exists($recordDN, $seen)) {
307
+                // Prevent loops
308
+                continue;
309
+            }
310
+            $fetched = $fetcher($record, $seen);
311
+            $list = array_merge($list, $fetched);
312
+            $seen[$recordDN] = $record;
313
+        }
314
+
315
+        return $recordMode ? $seen : array_keys($seen);
316
+    }
317
+
318
+    /**
319
+     * translates a gidNumber into an ownCloud internal name
320
+     * @param string $gid as given by gidNumber on POSIX LDAP
321
+     * @param string $dn a DN that belongs to the same domain as the group
322
+     * @return string|bool
323
+     */
324
+    public function gidNumber2Name($gid, $dn) {
325
+        $cacheKey = 'gidNumberToName' . $gid;
326
+        $groupName = $this->access->connection->getFromCache($cacheKey);
327
+        if(!is_null($groupName) && isset($groupName)) {
328
+            return $groupName;
329
+        }
330
+
331
+        //we need to get the DN from LDAP
332
+        $filter = $this->access->combineFilterWithAnd([
333
+            $this->access->connection->ldapGroupFilter,
334
+            'objectClass=posixGroup',
335
+            $this->access->connection->ldapGidNumber . '=' . $gid
336
+        ]);
337
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
338
+        if(empty($result)) {
339
+            return false;
340
+        }
341
+        $dn = $result[0]['dn'][0];
342
+
343
+        //and now the group name
344
+        //NOTE once we have separate ownCloud group IDs and group names we can
345
+        //directly read the display name attribute instead of the DN
346
+        $name = $this->access->dn2groupname($dn);
347
+
348
+        $this->access->connection->writeToCache($cacheKey, $name);
349
+
350
+        return $name;
351
+    }
352
+
353
+    /**
354
+     * returns the entry's gidNumber
355
+     * @param string $dn
356
+     * @param string $attribute
357
+     * @return string|bool
358
+     */
359
+    private function getEntryGidNumber($dn, $attribute) {
360
+        $value = $this->access->readAttribute($dn, $attribute);
361
+        if(is_array($value) && !empty($value)) {
362
+            return $value[0];
363
+        }
364
+        return false;
365
+    }
366
+
367
+    /**
368
+     * returns the group's primary ID
369
+     * @param string $dn
370
+     * @return string|bool
371
+     */
372
+    public function getGroupGidNumber($dn) {
373
+        return $this->getEntryGidNumber($dn, 'gidNumber');
374
+    }
375
+
376
+    /**
377
+     * returns the user's gidNumber
378
+     * @param string $dn
379
+     * @return string|bool
380
+     */
381
+    public function getUserGidNumber($dn) {
382
+        $gidNumber = false;
383
+        if($this->access->connection->hasGidNumber) {
384
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
385
+            if($gidNumber === false) {
386
+                $this->access->connection->hasGidNumber = false;
387
+            }
388
+        }
389
+        return $gidNumber;
390
+    }
391
+
392
+    /**
393
+     * returns a filter for a "users has specific gid" search or count operation
394
+     *
395
+     * @param string $groupDN
396
+     * @param string $search
397
+     * @return string
398
+     * @throws \Exception
399
+     */
400
+    private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
401
+        $groupID = $this->getGroupGidNumber($groupDN);
402
+        if($groupID === false) {
403
+            throw new \Exception('Not a valid group');
404
+        }
405
+
406
+        $filterParts = [];
407
+        $filterParts[] = $this->access->getFilterForUserCount();
408
+        if ($search !== '') {
409
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
410
+        }
411
+        $filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
412
+
413
+        return $this->access->combineFilterWithAnd($filterParts);
414
+    }
415
+
416
+    /**
417
+     * returns a list of users that have the given group as gid number
418
+     *
419
+     * @param string $groupDN
420
+     * @param string $search
421
+     * @param int $limit
422
+     * @param int $offset
423
+     * @return string[]
424
+     */
425
+    public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
426
+        try {
427
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
428
+            $users = $this->access->fetchListOfUsers(
429
+                $filter,
430
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
431
+                $limit,
432
+                $offset
433
+            );
434
+            return $this->access->nextcloudUserNames($users);
435
+        } catch (\Exception $e) {
436
+            return [];
437
+        }
438
+    }
439
+
440
+    /**
441
+     * returns the number of users that have the given group as gid number
442
+     *
443
+     * @param string $groupDN
444
+     * @param string $search
445
+     * @param int $limit
446
+     * @param int $offset
447
+     * @return int
448
+     */
449
+    public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
450
+        try {
451
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
452
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
453
+            return (int)$users;
454
+        } catch (\Exception $e) {
455
+            return 0;
456
+        }
457
+    }
458
+
459
+    /**
460
+     * gets the gidNumber of a user
461
+     * @param string $dn
462
+     * @return string
463
+     */
464
+    public function getUserGroupByGid($dn) {
465
+        $groupID = $this->getUserGidNumber($dn);
466
+        if($groupID !== false) {
467
+            $groupName = $this->gidNumber2Name($groupID, $dn);
468
+            if($groupName !== false) {
469
+                return $groupName;
470
+            }
471
+        }
472
+
473
+        return false;
474
+    }
475
+
476
+    /**
477
+     * translates a primary group ID into an Nextcloud internal name
478
+     * @param string $gid as given by primaryGroupID on AD
479
+     * @param string $dn a DN that belongs to the same domain as the group
480
+     * @return string|bool
481
+     */
482
+    public function primaryGroupID2Name($gid, $dn) {
483
+        $cacheKey = 'primaryGroupIDtoName';
484
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
485
+        if(!is_null($groupNames) && isset($groupNames[$gid])) {
486
+            return $groupNames[$gid];
487
+        }
488
+
489
+        $domainObjectSid = $this->access->getSID($dn);
490
+        if($domainObjectSid === false) {
491
+            return false;
492
+        }
493
+
494
+        //we need to get the DN from LDAP
495
+        $filter = $this->access->combineFilterWithAnd(array(
496
+            $this->access->connection->ldapGroupFilter,
497
+            'objectsid=' . $domainObjectSid . '-' . $gid
498
+        ));
499
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
500
+        if(empty($result)) {
501
+            return false;
502
+        }
503
+        $dn = $result[0]['dn'][0];
504
+
505
+        //and now the group name
506
+        //NOTE once we have separate Nextcloud group IDs and group names we can
507
+        //directly read the display name attribute instead of the DN
508
+        $name = $this->access->dn2groupname($dn);
509
+
510
+        $this->access->connection->writeToCache($cacheKey, $name);
511
+
512
+        return $name;
513
+    }
514
+
515
+    /**
516
+     * returns the entry's primary group ID
517
+     * @param string $dn
518
+     * @param string $attribute
519
+     * @return string|bool
520
+     */
521
+    private function getEntryGroupID($dn, $attribute) {
522
+        $value = $this->access->readAttribute($dn, $attribute);
523
+        if(is_array($value) && !empty($value)) {
524
+            return $value[0];
525
+        }
526
+        return false;
527
+    }
528
+
529
+    /**
530
+     * returns the group's primary ID
531
+     * @param string $dn
532
+     * @return string|bool
533
+     */
534
+    public function getGroupPrimaryGroupID($dn) {
535
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
536
+    }
537
+
538
+    /**
539
+     * returns the user's primary group ID
540
+     * @param string $dn
541
+     * @return string|bool
542
+     */
543
+    public function getUserPrimaryGroupIDs($dn) {
544
+        $primaryGroupID = false;
545
+        if($this->access->connection->hasPrimaryGroups) {
546
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
547
+            if($primaryGroupID === false) {
548
+                $this->access->connection->hasPrimaryGroups = false;
549
+            }
550
+        }
551
+        return $primaryGroupID;
552
+    }
553
+
554
+    /**
555
+     * returns a filter for a "users in primary group" search or count operation
556
+     *
557
+     * @param string $groupDN
558
+     * @param string $search
559
+     * @return string
560
+     * @throws \Exception
561
+     */
562
+    private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
563
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
564
+        if($groupID === false) {
565
+            throw new \Exception('Not a valid group');
566
+        }
567
+
568
+        $filterParts = [];
569
+        $filterParts[] = $this->access->getFilterForUserCount();
570
+        if ($search !== '') {
571
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
572
+        }
573
+        $filterParts[] = 'primaryGroupID=' . $groupID;
574
+
575
+        return $this->access->combineFilterWithAnd($filterParts);
576
+    }
577
+
578
+    /**
579
+     * returns a list of users that have the given group as primary group
580
+     *
581
+     * @param string $groupDN
582
+     * @param string $search
583
+     * @param int $limit
584
+     * @param int $offset
585
+     * @return string[]
586
+     */
587
+    public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
588
+        try {
589
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
590
+            $users = $this->access->fetchListOfUsers(
591
+                $filter,
592
+                array($this->access->connection->ldapUserDisplayName, 'dn'),
593
+                $limit,
594
+                $offset
595
+            );
596
+            return $this->access->nextcloudUserNames($users);
597
+        } catch (\Exception $e) {
598
+            return array();
599
+        }
600
+    }
601
+
602
+    /**
603
+     * returns the number of users that have the given group as primary group
604
+     *
605
+     * @param string $groupDN
606
+     * @param string $search
607
+     * @param int $limit
608
+     * @param int $offset
609
+     * @return int
610
+     */
611
+    public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
612
+        try {
613
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
614
+            $users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
615
+            return (int)$users;
616
+        } catch (\Exception $e) {
617
+            return 0;
618
+        }
619
+    }
620
+
621
+    /**
622
+     * gets the primary group of a user
623
+     * @param string $dn
624
+     * @return string
625
+     */
626
+    public function getUserPrimaryGroup($dn) {
627
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
628
+        if($groupID !== false) {
629
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
630
+            if($groupName !== false) {
631
+                return $groupName;
632
+            }
633
+        }
634
+
635
+        return false;
636
+    }
637
+
638
+    /**
639
+     * Get all groups a user belongs to
640
+     * @param string $uid Name of the user
641
+     * @return array with group names
642
+     *
643
+     * This function fetches all groups a user belongs to. It does not check
644
+     * if the user exists at all.
645
+     *
646
+     * This function includes groups based on dynamic group membership.
647
+     */
648
+    public function getUserGroups($uid) {
649
+        if(!$this->enabled) {
650
+            return array();
651
+        }
652
+        $cacheKey = 'getUserGroups'.$uid;
653
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
654
+        if(!is_null($userGroups)) {
655
+            return $userGroups;
656
+        }
657
+        $userDN = $this->access->username2dn($uid);
658
+        if(!$userDN) {
659
+            $this->access->connection->writeToCache($cacheKey, array());
660
+            return array();
661
+        }
662
+
663
+        $groups = [];
664
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
665
+        $gidGroupName = $this->getUserGroupByGid($userDN);
666
+
667
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
668
+
669
+        if (!empty($dynamicGroupMemberURL)) {
670
+            // look through dynamic groups to add them to the result array if needed
671
+            $groupsToMatch = $this->access->fetchListOfGroups(
672
+                $this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
673
+            foreach($groupsToMatch as $dynamicGroup) {
674
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
675
+                    continue;
676
+                }
677
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
678
+                if ($pos !== false) {
679
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
680
+                    // apply filter via ldap search to see if this user is in this
681
+                    // dynamic group
682
+                    $userMatch = $this->access->readAttribute(
683
+                        $userDN,
684
+                        $this->access->connection->ldapUserDisplayName,
685
+                        $memberUrlFilter
686
+                    );
687
+                    if ($userMatch !== false) {
688
+                        // match found so this user is in this group
689
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
690
+                        if(is_string($groupName)) {
691
+                            // be sure to never return false if the dn could not be
692
+                            // resolved to a name, for whatever reason.
693
+                            $groups[] = $groupName;
694
+                        }
695
+                    }
696
+                } else {
697
+                    \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
698
+                        'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
699
+                }
700
+            }
701
+        }
702
+
703
+        // if possible, read out membership via memberOf. It's far faster than
704
+        // performing a search, which still is a fallback later.
705
+        // memberof doesn't support memberuid, so skip it here.
706
+        if((int)$this->access->connection->hasMemberOfFilterSupport === 1
707
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
708
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
709
+            ) {
710
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
711
+            if (is_array($groupDNs)) {
712
+                foreach ($groupDNs as $dn) {
713
+                    $groupName = $this->access->dn2groupname($dn);
714
+                    if(is_string($groupName)) {
715
+                        // be sure to never return false if the dn could not be
716
+                        // resolved to a name, for whatever reason.
717
+                        $groups[] = $groupName;
718
+                    }
719
+                }
720
+            }
721
+
722
+            if($primaryGroup !== false) {
723
+                $groups[] = $primaryGroup;
724
+            }
725
+            if($gidGroupName !== false) {
726
+                $groups[] = $gidGroupName;
727
+            }
728
+            $this->access->connection->writeToCache($cacheKey, $groups);
729
+            return $groups;
730
+        }
731
+
732
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
733
+        if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
734
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
735
+        ) {
736
+            $uid = $userDN;
737
+        } else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
738
+            $result = $this->access->readAttribute($userDN, 'uid');
739
+            if ($result === false) {
740
+                \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
741
+                    $this->access->connection->ldapHost, ILogger::DEBUG);
742
+            }
743
+            $uid = $result[0];
744
+        } else {
745
+            // just in case
746
+            $uid = $userDN;
747
+        }
748
+
749
+        if(isset($this->cachedGroupsByMember[$uid])) {
750
+            $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
751
+        } else {
752
+            $groupsByMember = array_values($this->getGroupsByMember($uid));
753
+            $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
754
+            $this->cachedGroupsByMember[$uid] = $groupsByMember;
755
+            $groups = array_merge($groups, $groupsByMember);
756
+        }
757
+
758
+        if($primaryGroup !== false) {
759
+            $groups[] = $primaryGroup;
760
+        }
761
+        if($gidGroupName !== false) {
762
+            $groups[] = $gidGroupName;
763
+        }
764
+
765
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
766
+        $this->access->connection->writeToCache($cacheKey, $groups);
767
+
768
+        return $groups;
769
+    }
770
+
771
+    /**
772
+     * @param string $dn
773
+     * @param array|null &$seen
774
+     * @return array
775
+     */
776
+    private function getGroupsByMember($dn, &$seen = null) {
777
+        if ($seen === null) {
778
+            $seen = [];
779
+        }
780
+        if (array_key_exists($dn, $seen)) {
781
+            // avoid loops
782
+            return [];
783
+        }
784
+        $allGroups = [];
785
+        $seen[$dn] = true;
786
+        $filter = $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn;
787
+        $groups = $this->access->fetchListOfGroups($filter,
788
+            [$this->access->connection->ldapGroupDisplayName, 'dn']);
789
+        if (is_array($groups)) {
790
+            $fetcher = function ($dn, &$seen) {
791
+                if(is_array($dn) && isset($dn['dn'][0])) {
792
+                    $dn = $dn['dn'][0];
793
+                }
794
+                return $this->getGroupsByMember($dn, $seen);
795
+            };
796
+            $allGroups = $this->walkNestedGroups($dn, $fetcher, $groups);
797
+        }
798
+        $visibleGroups = $this->access->groupsMatchFilter(array_keys($allGroups));
799
+        return array_intersect_key($allGroups, array_flip($visibleGroups));
800
+    }
801
+
802
+    /**
803
+     * get a list of all users in a group
804
+     *
805
+     * @param string $gid
806
+     * @param string $search
807
+     * @param int $limit
808
+     * @param int $offset
809
+     * @return array with user ids
810
+     */
811
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
812
+        if(!$this->enabled) {
813
+            return array();
814
+        }
815
+        if(!$this->groupExists($gid)) {
816
+            return array();
817
+        }
818
+        $search = $this->access->escapeFilterPart($search, true);
819
+        $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
820
+        // check for cache of the exact query
821
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
822
+        if(!is_null($groupUsers)) {
823
+            return $groupUsers;
824
+        }
825
+
826
+        // check for cache of the query without limit and offset
827
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
828
+        if(!is_null($groupUsers)) {
829
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
830
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
831
+            return $groupUsers;
832
+        }
833
+
834
+        if($limit === -1) {
835
+            $limit = null;
836
+        }
837
+        $groupDN = $this->access->groupname2dn($gid);
838
+        if(!$groupDN) {
839
+            // group couldn't be found, return empty resultset
840
+            $this->access->connection->writeToCache($cacheKey, array());
841
+            return array();
842
+        }
843
+
844
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
845
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
846
+        $members = $this->_groupMembers($groupDN);
847
+        if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
848
+            //in case users could not be retrieved, return empty result set
849
+            $this->access->connection->writeToCache($cacheKey, []);
850
+            return [];
851
+        }
852
+
853
+        $groupUsers = array();
854
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
855
+        $attrs = $this->access->userManager->getAttributes(true);
856
+        foreach($members as $member) {
857
+            if($isMemberUid) {
858
+                //we got uids, need to get their DNs to 'translate' them to user names
859
+                $filter = $this->access->combineFilterWithAnd(array(
860
+                    str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
861
+                    $this->access->getFilterPartForUserSearch($search)
862
+                ));
863
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
864
+                if(count($ldap_users) < 1) {
865
+                    continue;
866
+                }
867
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
868
+            } else {
869
+                //we got DNs, check if we need to filter by search or we can give back all of them
870
+                if ($search !== '') {
871
+                    if(!$this->access->readAttribute($member,
872
+                        $this->access->connection->ldapUserDisplayName,
873
+                        $this->access->getFilterPartForUserSearch($search))) {
874
+                        continue;
875
+                    }
876
+                }
877
+                // dn2username will also check if the users belong to the allowed base
878
+                if($ocname = $this->access->dn2username($member)) {
879
+                    $groupUsers[] = $ocname;
880
+                }
881
+            }
882
+        }
883
+
884
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
885
+        natsort($groupUsers);
886
+        $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
887
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
888
+
889
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
890
+
891
+        return $groupUsers;
892
+    }
893
+
894
+    /**
895
+     * returns the number of users in a group, who match the search term
896
+     * @param string $gid the internal group name
897
+     * @param string $search optional, a search string
898
+     * @return int|bool
899
+     */
900
+    public function countUsersInGroup($gid, $search = '') {
901
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
902
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
903
+        }
904
+
905
+        $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
906
+        if(!$this->enabled || !$this->groupExists($gid)) {
907
+            return false;
908
+        }
909
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
910
+        if(!is_null($groupUsers)) {
911
+            return $groupUsers;
912
+        }
913
+
914
+        $groupDN = $this->access->groupname2dn($gid);
915
+        if(!$groupDN) {
916
+            // group couldn't be found, return empty result set
917
+            $this->access->connection->writeToCache($cacheKey, false);
918
+            return false;
919
+        }
920
+
921
+        $members = $this->_groupMembers($groupDN);
922
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
923
+        if(!$members && $primaryUserCount === 0) {
924
+            //in case users could not be retrieved, return empty result set
925
+            $this->access->connection->writeToCache($cacheKey, false);
926
+            return false;
927
+        }
928
+
929
+        if ($search === '') {
930
+            $groupUsers = count($members) + $primaryUserCount;
931
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
932
+            return $groupUsers;
933
+        }
934
+        $search = $this->access->escapeFilterPart($search, true);
935
+        $isMemberUid =
936
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
937
+            === 'memberuid');
938
+
939
+        //we need to apply the search filter
940
+        //alternatives that need to be checked:
941
+        //a) get all users by search filter and array_intersect them
942
+        //b) a, but only when less than 1k 10k ?k users like it is
943
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
944
+        //   and let it count.
945
+        //For now this is not important, because the only use of this method
946
+        //does not supply a search string
947
+        $groupUsers = array();
948
+        foreach($members as $member) {
949
+            if($isMemberUid) {
950
+                //we got uids, need to get their DNs to 'translate' them to user names
951
+                $filter = $this->access->combineFilterWithAnd(array(
952
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
953
+                    $this->access->getFilterPartForUserSearch($search)
954
+                ));
955
+                $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
956
+                if(count($ldap_users) < 1) {
957
+                    continue;
958
+                }
959
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
960
+            } else {
961
+                //we need to apply the search filter now
962
+                if(!$this->access->readAttribute($member,
963
+                    $this->access->connection->ldapUserDisplayName,
964
+                    $this->access->getFilterPartForUserSearch($search))) {
965
+                    continue;
966
+                }
967
+                // dn2username will also check if the users belong to the allowed base
968
+                if($ocname = $this->access->dn2username($member)) {
969
+                    $groupUsers[] = $ocname;
970
+                }
971
+            }
972
+        }
973
+
974
+        //and get users that have the group as primary
975
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
976
+
977
+        return count($groupUsers) + $primaryUsers;
978
+    }
979
+
980
+    /**
981
+     * get a list of all groups
982
+     *
983
+     * @param string $search
984
+     * @param $limit
985
+     * @param int $offset
986
+     * @return array with group names
987
+     *
988
+     * Returns a list with all groups (used by getGroups)
989
+     */
990
+    protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
991
+        if(!$this->enabled) {
992
+            return array();
993
+        }
994
+        $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
995
+
996
+        //Check cache before driving unnecessary searches
997
+        \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
998
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
+        if(!is_null($ldap_groups)) {
1000
+            return $ldap_groups;
1001
+        }
1002
+
1003
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004
+        // error. With a limit of 0, we get 0 results. So we pass null.
1005
+        if($limit <= 0) {
1006
+            $limit = null;
1007
+        }
1008
+        $filter = $this->access->combineFilterWithAnd(array(
1009
+            $this->access->connection->ldapGroupFilter,
1010
+            $this->access->getFilterPartForGroupSearch($search)
1011
+        ));
1012
+        \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
1013
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
1014
+                array($this->access->connection->ldapGroupDisplayName, 'dn'),
1015
+                $limit,
1016
+                $offset);
1017
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
1018
+
1019
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
1020
+        return $ldap_groups;
1021
+    }
1022
+
1023
+    /**
1024
+     * get a list of all groups using a paged search
1025
+     *
1026
+     * @param string $search
1027
+     * @param int $limit
1028
+     * @param int $offset
1029
+     * @return array with group names
1030
+     *
1031
+     * Returns a list with all groups
1032
+     * Uses a paged search if available to override a
1033
+     * server side search limit.
1034
+     * (active directory has a limit of 1000 by default)
1035
+     */
1036
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1037
+        if(!$this->enabled) {
1038
+            return array();
1039
+        }
1040
+        $search = $this->access->escapeFilterPart($search, true);
1041
+        $pagingSize = (int)$this->access->connection->ldapPagingSize;
1042
+        if ($pagingSize <= 0) {
1043
+            return $this->getGroupsChunk($search, $limit, $offset);
1044
+        }
1045
+        $maxGroups = 100000; // limit max results (just for safety reasons)
1046
+        if ($limit > -1) {
1047
+            $overallLimit = min($limit + $offset, $maxGroups);
1048
+        } else {
1049
+            $overallLimit = $maxGroups;
1050
+        }
1051
+        $chunkOffset = $offset;
1052
+        $allGroups = array();
1053
+        while ($chunkOffset < $overallLimit) {
1054
+            $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1055
+            $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1056
+            $nread = count($ldapGroups);
1057
+            \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1058
+            if ($nread) {
1059
+                $allGroups = array_merge($allGroups, $ldapGroups);
1060
+                $chunkOffset += $nread;
1061
+            }
1062
+            if ($nread < $chunkLimit) {
1063
+                break;
1064
+            }
1065
+        }
1066
+        return $allGroups;
1067
+    }
1068
+
1069
+    /**
1070
+     * @param string $group
1071
+     * @return bool
1072
+     */
1073
+    public function groupMatchesFilter($group) {
1074
+        return (strripos($group, $this->groupSearch) !== false);
1075
+    }
1076
+
1077
+    /**
1078
+     * check if a group exists
1079
+     * @param string $gid
1080
+     * @return bool
1081
+     */
1082
+    public function groupExists($gid) {
1083
+        $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1084
+        if(!is_null($groupExists)) {
1085
+            return (bool)$groupExists;
1086
+        }
1087
+
1088
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1089
+        //only, requires more checking.
1090
+        $dn = $this->access->groupname2dn($gid);
1091
+        if(!$dn) {
1092
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1093
+            return false;
1094
+        }
1095
+
1096
+        //if group really still exists, we will be able to read its objectclass
1097
+        if(!is_array($this->access->readAttribute($dn, ''))) {
1098
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1099
+            return false;
1100
+        }
1101
+
1102
+        $this->access->connection->writeToCache('groupExists'.$gid, true);
1103
+        return true;
1104
+    }
1105
+
1106
+    /**
1107
+     * Check if backend implements actions
1108
+     * @param int $actions bitwise-or'ed actions
1109
+     * @return boolean
1110
+     *
1111
+     * Returns the supported actions as int to be
1112
+     * compared with GroupInterface::CREATE_GROUP etc.
1113
+     */
1114
+    public function implementsActions($actions) {
1115
+        return (bool)((GroupInterface::COUNT_USERS |
1116
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1117
+    }
1118
+
1119
+    /**
1120
+     * Return access for LDAP interaction.
1121
+     * @return Access instance of Access for LDAP interaction
1122
+     */
1123
+    public function getLDAPAccess($gid) {
1124
+        return $this->access;
1125
+    }
1126
+
1127
+    /**
1128
+     * create a group
1129
+     * @param string $gid
1130
+     * @return bool
1131
+     * @throws \Exception
1132
+     */
1133
+    public function createGroup($gid) {
1134
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1135
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1136
+                //updates group mapping
1137
+                $this->access->dn2ocname($dn, $gid, false);
1138
+                $this->access->connection->writeToCache("groupExists".$gid, true);
1139
+            }
1140
+            return $dn != null;
1141
+        }
1142
+        throw new \Exception('Could not create group in LDAP backend.');
1143
+    }
1144
+
1145
+    /**
1146
+     * delete a group
1147
+     * @param string $gid gid of the group to delete
1148
+     * @return bool
1149
+     * @throws \Exception
1150
+     */
1151
+    public function deleteGroup($gid) {
1152
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1153
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1154
+                #delete group in nextcloud internal db
1155
+                $this->access->getGroupMapper()->unmap($gid);
1156
+                $this->access->connection->writeToCache("groupExists".$gid, false);
1157
+            }
1158
+            return $ret;
1159
+        }
1160
+        throw new \Exception('Could not delete group in LDAP backend.');
1161
+    }
1162
+
1163
+    /**
1164
+     * Add a user to a group
1165
+     * @param string $uid Name of the user to add to group
1166
+     * @param string $gid Name of the group in which add the user
1167
+     * @return bool
1168
+     * @throws \Exception
1169
+     */
1170
+    public function addToGroup($uid, $gid) {
1171
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1172
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1173
+                $this->access->connection->clearCache();
1174
+                unset($this->cachedGroupMembers[$gid]);
1175
+            }
1176
+            return $ret;
1177
+        }
1178
+        throw new \Exception('Could not add user to group in LDAP backend.');
1179
+    }
1180
+
1181
+    /**
1182
+     * Removes a user from a group
1183
+     * @param string $uid Name of the user to remove from group
1184
+     * @param string $gid Name of the group from which remove the user
1185
+     * @return bool
1186
+     * @throws \Exception
1187
+     */
1188
+    public function removeFromGroup($uid, $gid) {
1189
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1190
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1191
+                $this->access->connection->clearCache();
1192
+                unset($this->cachedGroupMembers[$gid]);
1193
+            }
1194
+            return $ret;
1195
+        }
1196
+        throw new \Exception('Could not remove user from group in LDAP backend.');
1197
+    }
1198
+
1199
+    /**
1200
+     * Gets group details
1201
+     * @param string $gid Name of the group
1202
+     * @return array | false
1203
+     * @throws \Exception
1204
+     */
1205
+    public function getGroupDetails($gid) {
1206
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1207
+            return $this->groupPluginManager->getGroupDetails($gid);
1208
+        }
1209
+        throw new \Exception('Could not get group details in LDAP backend.');
1210
+    }
1211
+
1212
+    /**
1213
+     * Return LDAP connection resource from a cloned connection.
1214
+     * The cloned connection needs to be closed manually.
1215
+     * of the current access.
1216
+     * @param string $gid
1217
+     * @return resource of the LDAP connection
1218
+     */
1219
+    public function getNewLDAPConnection($gid) {
1220
+        $connection = clone $this->access->getConnection();
1221
+        return $connection->getConnectionResource();
1222
+    }
1223
+
1224
+    /**
1225
+     * @throws \OC\ServerNotAvailableException
1226
+     */
1227
+    public function getDisplayName(string $gid): string {
1228
+        if ($this->groupPluginManager instanceof IGetDisplayNameBackend) {
1229
+            return $this->groupPluginManager->getDisplayName($gid);
1230
+        }
1231
+
1232
+        $cacheKey = 'group_getDisplayName' . $gid;
1233
+        if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1234
+            return $displayName;
1235
+        }
1236
+
1237
+        $displayName = $this->access->readAttribute(
1238
+            $this->access->groupname2dn($gid),
1239
+            $this->access->connection->ldapGroupDisplayName);
1240
+
1241
+        if ($displayName && (count($displayName) > 0)) {
1242
+            $displayName = $displayName[0];
1243
+            $this->access->connection->writeToCache($cacheKey, $displayName);
1244
+            return $displayName;
1245
+        }
1246
+
1247
+        return '';
1248
+    }
1249 1249
 }
Please login to merge, or discard this patch.
Spacing   +97 added lines, -97 removed lines patch added patch discarded remove patch
@@ -71,7 +71,7 @@  discard block
 block discarded – undo
71 71
 		parent::__construct($access);
72 72
 		$filter = $this->access->connection->ldapGroupFilter;
73 73
 		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
74
-		if(!empty($filter) && !empty($gassoc)) {
74
+		if (!empty($filter) && !empty($gassoc)) {
75 75
 			$this->enabled = true;
76 76
 		}
77 77
 
@@ -90,25 +90,25 @@  discard block
 block discarded – undo
90 90
 	 * Checks whether the user is member of a group or not.
91 91
 	 */
92 92
 	public function inGroup($uid, $gid) {
93
-		if(!$this->enabled) {
93
+		if (!$this->enabled) {
94 94
 			return false;
95 95
 		}
96 96
 		$cacheKey = 'inGroup'.$uid.':'.$gid;
97 97
 		$inGroup = $this->access->connection->getFromCache($cacheKey);
98
-		if(!is_null($inGroup)) {
99
-			return (bool)$inGroup;
98
+		if (!is_null($inGroup)) {
99
+			return (bool) $inGroup;
100 100
 		}
101 101
 
102 102
 		$userDN = $this->access->username2dn($uid);
103 103
 
104
-		if(isset($this->cachedGroupMembers[$gid])) {
104
+		if (isset($this->cachedGroupMembers[$gid])) {
105 105
 			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
106 106
 			return $isInGroup;
107 107
 		}
108 108
 
109 109
 		$cacheKeyMembers = 'inGroup-members:'.$gid;
110 110
 		$members = $this->access->connection->getFromCache($cacheKeyMembers);
111
-		if(!is_null($members)) {
111
+		if (!is_null($members)) {
112 112
 			$this->cachedGroupMembers[$gid] = $members;
113 113
 			$isInGroup = in_array($userDN, $members, true);
114 114
 			$this->access->connection->writeToCache($cacheKey, $isInGroup);
@@ -117,34 +117,34 @@  discard block
 block discarded – undo
117 117
 
118 118
 		$groupDN = $this->access->groupname2dn($gid);
119 119
 		// just in case
120
-		if(!$groupDN || !$userDN) {
120
+		if (!$groupDN || !$userDN) {
121 121
 			$this->access->connection->writeToCache($cacheKey, false);
122 122
 			return false;
123 123
 		}
124 124
 
125 125
 		//check primary group first
126
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
126
+		if ($gid === $this->getUserPrimaryGroup($userDN)) {
127 127
 			$this->access->connection->writeToCache($cacheKey, true);
128 128
 			return true;
129 129
 		}
130 130
 
131 131
 		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
132 132
 		$members = $this->_groupMembers($groupDN);
133
-		if(!is_array($members) || count($members) === 0) {
133
+		if (!is_array($members) || count($members) === 0) {
134 134
 			$this->access->connection->writeToCache($cacheKey, false);
135 135
 			return false;
136 136
 		}
137 137
 
138 138
 		//extra work if we don't get back user DNs
139
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
139
+		if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
140 140
 			$dns = array();
141 141
 			$filterParts = array();
142 142
 			$bytes = 0;
143
-			foreach($members as $mid) {
143
+			foreach ($members as $mid) {
144 144
 				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
145 145
 				$filterParts[] = $filter;
146 146
 				$bytes += strlen($filter);
147
-				if($bytes >= 9000000) {
147
+				if ($bytes >= 9000000) {
148 148
 					// AD has a default input buffer of 10 MB, we do not want
149 149
 					// to take even the chance to exceed it
150 150
 					$filter = $this->access->combineFilterWithOr($filterParts);
@@ -154,7 +154,7 @@  discard block
 block discarded – undo
154 154
 					$dns = array_merge($dns, $users);
155 155
 				}
156 156
 			}
157
-			if(count($filterParts) > 0) {
157
+			if (count($filterParts) > 0) {
158 158
 				$filter = $this->access->combineFilterWithOr($filterParts);
159 159
 				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
160 160
 				$dns = array_merge($dns, $users);
@@ -197,14 +197,14 @@  discard block
 block discarded – undo
197 197
 			$pos = strpos($memberURLs[0], '(');
198 198
 			if ($pos !== false) {
199 199
 				$memberUrlFilter = substr($memberURLs[0], $pos);
200
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
200
+				$foundMembers = $this->access->searchUsers($memberUrlFilter, 'dn');
201 201
 				$dynamicMembers = array();
202
-				foreach($foundMembers as $value) {
202
+				foreach ($foundMembers as $value) {
203 203
 					$dynamicMembers[$value['dn'][0]] = 1;
204 204
 				}
205 205
 			} else {
206 206
 				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
207
-					'of group ' . $dnGroup, ILogger::DEBUG);
207
+					'of group '.$dnGroup, ILogger::DEBUG);
208 208
 			}
209 209
 		}
210 210
 		return $dynamicMembers;
@@ -228,7 +228,7 @@  discard block
 block discarded – undo
228 228
 		// used extensively in cron job, caching makes sense for nested groups
229 229
 		$cacheKey = '_groupMembers'.$dnGroup;
230 230
 		$groupMembers = $this->access->connection->getFromCache($cacheKey);
231
-		if($groupMembers !== null) {
231
+		if ($groupMembers !== null) {
232 232
 			return $groupMembers;
233 233
 		}
234 234
 		$seen[$dnGroup] = 1;
@@ -288,10 +288,10 @@  discard block
 block discarded – undo
288 288
 		$recordMode = is_array($list) && isset($list[0]) && is_array($list[0]) && isset($list[0]['dn'][0]);
289 289
 
290 290
 		if ($nesting !== 1) {
291
-			if($recordMode) {
291
+			if ($recordMode) {
292 292
 				// the keys are numeric, but should hold the DN
293
-				return array_reduce($list, function ($transformed, $record) use ($dn) {
294
-					if($record['dn'][0] != $dn) {
293
+				return array_reduce($list, function($transformed, $record) use ($dn) {
294
+					if ($record['dn'][0] != $dn) {
295 295
 						$transformed[$record['dn'][0]] = $record;
296 296
 					}
297 297
 					return $transformed;
@@ -322,9 +322,9 @@  discard block
 block discarded – undo
322 322
 	 * @return string|bool
323 323
 	 */
324 324
 	public function gidNumber2Name($gid, $dn) {
325
-		$cacheKey = 'gidNumberToName' . $gid;
325
+		$cacheKey = 'gidNumberToName'.$gid;
326 326
 		$groupName = $this->access->connection->getFromCache($cacheKey);
327
-		if(!is_null($groupName) && isset($groupName)) {
327
+		if (!is_null($groupName) && isset($groupName)) {
328 328
 			return $groupName;
329 329
 		}
330 330
 
@@ -332,10 +332,10 @@  discard block
 block discarded – undo
332 332
 		$filter = $this->access->combineFilterWithAnd([
333 333
 			$this->access->connection->ldapGroupFilter,
334 334
 			'objectClass=posixGroup',
335
-			$this->access->connection->ldapGidNumber . '=' . $gid
335
+			$this->access->connection->ldapGidNumber.'='.$gid
336 336
 		]);
337 337
 		$result = $this->access->searchGroups($filter, array('dn'), 1);
338
-		if(empty($result)) {
338
+		if (empty($result)) {
339 339
 			return false;
340 340
 		}
341 341
 		$dn = $result[0]['dn'][0];
@@ -358,7 +358,7 @@  discard block
 block discarded – undo
358 358
 	 */
359 359
 	private function getEntryGidNumber($dn, $attribute) {
360 360
 		$value = $this->access->readAttribute($dn, $attribute);
361
-		if(is_array($value) && !empty($value)) {
361
+		if (is_array($value) && !empty($value)) {
362 362
 			return $value[0];
363 363
 		}
364 364
 		return false;
@@ -380,9 +380,9 @@  discard block
 block discarded – undo
380 380
 	 */
381 381
 	public function getUserGidNumber($dn) {
382 382
 		$gidNumber = false;
383
-		if($this->access->connection->hasGidNumber) {
383
+		if ($this->access->connection->hasGidNumber) {
384 384
 			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
385
-			if($gidNumber === false) {
385
+			if ($gidNumber === false) {
386 386
 				$this->access->connection->hasGidNumber = false;
387 387
 			}
388 388
 		}
@@ -399,7 +399,7 @@  discard block
 block discarded – undo
399 399
 	 */
400 400
 	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
401 401
 		$groupID = $this->getGroupGidNumber($groupDN);
402
-		if($groupID === false) {
402
+		if ($groupID === false) {
403 403
 			throw new \Exception('Not a valid group');
404 404
 		}
405 405
 
@@ -408,7 +408,7 @@  discard block
 block discarded – undo
408 408
 		if ($search !== '') {
409 409
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
410 410
 		}
411
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
411
+		$filterParts[] = $this->access->connection->ldapGidNumber.'='.$groupID;
412 412
 
413 413
 		return $this->access->combineFilterWithAnd($filterParts);
414 414
 	}
@@ -450,7 +450,7 @@  discard block
 block discarded – undo
450 450
 		try {
451 451
 			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
452 452
 			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
453
-			return (int)$users;
453
+			return (int) $users;
454 454
 		} catch (\Exception $e) {
455 455
 			return 0;
456 456
 		}
@@ -463,9 +463,9 @@  discard block
 block discarded – undo
463 463
 	 */
464 464
 	public function getUserGroupByGid($dn) {
465 465
 		$groupID = $this->getUserGidNumber($dn);
466
-		if($groupID !== false) {
466
+		if ($groupID !== false) {
467 467
 			$groupName = $this->gidNumber2Name($groupID, $dn);
468
-			if($groupName !== false) {
468
+			if ($groupName !== false) {
469 469
 				return $groupName;
470 470
 			}
471 471
 		}
@@ -482,22 +482,22 @@  discard block
 block discarded – undo
482 482
 	public function primaryGroupID2Name($gid, $dn) {
483 483
 		$cacheKey = 'primaryGroupIDtoName';
484 484
 		$groupNames = $this->access->connection->getFromCache($cacheKey);
485
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
485
+		if (!is_null($groupNames) && isset($groupNames[$gid])) {
486 486
 			return $groupNames[$gid];
487 487
 		}
488 488
 
489 489
 		$domainObjectSid = $this->access->getSID($dn);
490
-		if($domainObjectSid === false) {
490
+		if ($domainObjectSid === false) {
491 491
 			return false;
492 492
 		}
493 493
 
494 494
 		//we need to get the DN from LDAP
495 495
 		$filter = $this->access->combineFilterWithAnd(array(
496 496
 			$this->access->connection->ldapGroupFilter,
497
-			'objectsid=' . $domainObjectSid . '-' . $gid
497
+			'objectsid='.$domainObjectSid.'-'.$gid
498 498
 		));
499 499
 		$result = $this->access->searchGroups($filter, array('dn'), 1);
500
-		if(empty($result)) {
500
+		if (empty($result)) {
501 501
 			return false;
502 502
 		}
503 503
 		$dn = $result[0]['dn'][0];
@@ -520,7 +520,7 @@  discard block
 block discarded – undo
520 520
 	 */
521 521
 	private function getEntryGroupID($dn, $attribute) {
522 522
 		$value = $this->access->readAttribute($dn, $attribute);
523
-		if(is_array($value) && !empty($value)) {
523
+		if (is_array($value) && !empty($value)) {
524 524
 			return $value[0];
525 525
 		}
526 526
 		return false;
@@ -542,9 +542,9 @@  discard block
 block discarded – undo
542 542
 	 */
543 543
 	public function getUserPrimaryGroupIDs($dn) {
544 544
 		$primaryGroupID = false;
545
-		if($this->access->connection->hasPrimaryGroups) {
545
+		if ($this->access->connection->hasPrimaryGroups) {
546 546
 			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
547
-			if($primaryGroupID === false) {
547
+			if ($primaryGroupID === false) {
548 548
 				$this->access->connection->hasPrimaryGroups = false;
549 549
 			}
550 550
 		}
@@ -561,7 +561,7 @@  discard block
 block discarded – undo
561 561
 	 */
562 562
 	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
563 563
 		$groupID = $this->getGroupPrimaryGroupID($groupDN);
564
-		if($groupID === false) {
564
+		if ($groupID === false) {
565 565
 			throw new \Exception('Not a valid group');
566 566
 		}
567 567
 
@@ -570,7 +570,7 @@  discard block
 block discarded – undo
570 570
 		if ($search !== '') {
571 571
 			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
572 572
 		}
573
-		$filterParts[] = 'primaryGroupID=' . $groupID;
573
+		$filterParts[] = 'primaryGroupID='.$groupID;
574 574
 
575 575
 		return $this->access->combineFilterWithAnd($filterParts);
576 576
 	}
@@ -612,7 +612,7 @@  discard block
 block discarded – undo
612 612
 		try {
613 613
 			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
614 614
 			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
615
-			return (int)$users;
615
+			return (int) $users;
616 616
 		} catch (\Exception $e) {
617 617
 			return 0;
618 618
 		}
@@ -625,9 +625,9 @@  discard block
 block discarded – undo
625 625
 	 */
626 626
 	public function getUserPrimaryGroup($dn) {
627 627
 		$groupID = $this->getUserPrimaryGroupIDs($dn);
628
-		if($groupID !== false) {
628
+		if ($groupID !== false) {
629 629
 			$groupName = $this->primaryGroupID2Name($groupID, $dn);
630
-			if($groupName !== false) {
630
+			if ($groupName !== false) {
631 631
 				return $groupName;
632 632
 			}
633 633
 		}
@@ -646,16 +646,16 @@  discard block
 block discarded – undo
646 646
 	 * This function includes groups based on dynamic group membership.
647 647
 	 */
648 648
 	public function getUserGroups($uid) {
649
-		if(!$this->enabled) {
649
+		if (!$this->enabled) {
650 650
 			return array();
651 651
 		}
652 652
 		$cacheKey = 'getUserGroups'.$uid;
653 653
 		$userGroups = $this->access->connection->getFromCache($cacheKey);
654
-		if(!is_null($userGroups)) {
654
+		if (!is_null($userGroups)) {
655 655
 			return $userGroups;
656 656
 		}
657 657
 		$userDN = $this->access->username2dn($uid);
658
-		if(!$userDN) {
658
+		if (!$userDN) {
659 659
 			$this->access->connection->writeToCache($cacheKey, array());
660 660
 			return array();
661 661
 		}
@@ -669,14 +669,14 @@  discard block
 block discarded – undo
669 669
 		if (!empty($dynamicGroupMemberURL)) {
670 670
 			// look through dynamic groups to add them to the result array if needed
671 671
 			$groupsToMatch = $this->access->fetchListOfGroups(
672
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
673
-			foreach($groupsToMatch as $dynamicGroup) {
672
+				$this->access->connection->ldapGroupFilter, array('dn', $dynamicGroupMemberURL));
673
+			foreach ($groupsToMatch as $dynamicGroup) {
674 674
 				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
675 675
 					continue;
676 676
 				}
677 677
 				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
678 678
 				if ($pos !== false) {
679
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
679
+					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0], $pos);
680 680
 					// apply filter via ldap search to see if this user is in this
681 681
 					// dynamic group
682 682
 					$userMatch = $this->access->readAttribute(
@@ -687,7 +687,7 @@  discard block
 block discarded – undo
687 687
 					if ($userMatch !== false) {
688 688
 						// match found so this user is in this group
689 689
 						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
690
-						if(is_string($groupName)) {
690
+						if (is_string($groupName)) {
691 691
 							// be sure to never return false if the dn could not be
692 692
 							// resolved to a name, for whatever reason.
693 693
 							$groups[] = $groupName;
@@ -695,7 +695,7 @@  discard block
 block discarded – undo
695 695
 					}
696 696
 				} else {
697 697
 					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
698
-						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
698
+						'of group '.print_r($dynamicGroup, true), ILogger::DEBUG);
699 699
 				}
700 700
 			}
701 701
 		}
@@ -703,15 +703,15 @@  discard block
 block discarded – undo
703 703
 		// if possible, read out membership via memberOf. It's far faster than
704 704
 		// performing a search, which still is a fallback later.
705 705
 		// memberof doesn't support memberuid, so skip it here.
706
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
707
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
706
+		if ((int) $this->access->connection->hasMemberOfFilterSupport === 1
707
+			&& (int) $this->access->connection->useMemberOfToDetectMembership === 1
708 708
 		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
709 709
 		    ) {
710 710
 			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
711 711
 			if (is_array($groupDNs)) {
712 712
 				foreach ($groupDNs as $dn) {
713 713
 					$groupName = $this->access->dn2groupname($dn);
714
-					if(is_string($groupName)) {
714
+					if (is_string($groupName)) {
715 715
 						// be sure to never return false if the dn could not be
716 716
 						// resolved to a name, for whatever reason.
717 717
 						$groups[] = $groupName;
@@ -719,10 +719,10 @@  discard block
 block discarded – undo
719 719
 				}
720 720
 			}
721 721
 
722
-			if($primaryGroup !== false) {
722
+			if ($primaryGroup !== false) {
723 723
 				$groups[] = $primaryGroup;
724 724
 			}
725
-			if($gidGroupName !== false) {
725
+			if ($gidGroupName !== false) {
726 726
 				$groups[] = $gidGroupName;
727 727
 			}
728 728
 			$this->access->connection->writeToCache($cacheKey, $groups);
@@ -730,14 +730,14 @@  discard block
 block discarded – undo
730 730
 		}
731 731
 
732 732
 		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
733
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
733
+		if ((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
734 734
 			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
735 735
 		) {
736 736
 			$uid = $userDN;
737
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
737
+		} else if (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
738 738
 			$result = $this->access->readAttribute($userDN, 'uid');
739 739
 			if ($result === false) {
740
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
740
+				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN '.$userDN.' on '.
741 741
 					$this->access->connection->ldapHost, ILogger::DEBUG);
742 742
 			}
743 743
 			$uid = $result[0];
@@ -746,7 +746,7 @@  discard block
 block discarded – undo
746 746
 			$uid = $userDN;
747 747
 		}
748 748
 
749
-		if(isset($this->cachedGroupsByMember[$uid])) {
749
+		if (isset($this->cachedGroupsByMember[$uid])) {
750 750
 			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
751 751
 		} else {
752 752
 			$groupsByMember = array_values($this->getGroupsByMember($uid));
@@ -755,10 +755,10 @@  discard block
 block discarded – undo
755 755
 			$groups = array_merge($groups, $groupsByMember);
756 756
 		}
757 757
 
758
-		if($primaryGroup !== false) {
758
+		if ($primaryGroup !== false) {
759 759
 			$groups[] = $primaryGroup;
760 760
 		}
761
-		if($gidGroupName !== false) {
761
+		if ($gidGroupName !== false) {
762 762
 			$groups[] = $gidGroupName;
763 763
 		}
764 764
 
@@ -787,8 +787,8 @@  discard block
 block discarded – undo
787 787
 		$groups = $this->access->fetchListOfGroups($filter,
788 788
 			[$this->access->connection->ldapGroupDisplayName, 'dn']);
789 789
 		if (is_array($groups)) {
790
-			$fetcher = function ($dn, &$seen) {
791
-				if(is_array($dn) && isset($dn['dn'][0])) {
790
+			$fetcher = function($dn, &$seen) {
791
+				if (is_array($dn) && isset($dn['dn'][0])) {
792 792
 					$dn = $dn['dn'][0];
793 793
 				}
794 794
 				return $this->getGroupsByMember($dn, $seen);
@@ -809,33 +809,33 @@  discard block
 block discarded – undo
809 809
 	 * @return array with user ids
810 810
 	 */
811 811
 	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
812
-		if(!$this->enabled) {
812
+		if (!$this->enabled) {
813 813
 			return array();
814 814
 		}
815
-		if(!$this->groupExists($gid)) {
815
+		if (!$this->groupExists($gid)) {
816 816
 			return array();
817 817
 		}
818 818
 		$search = $this->access->escapeFilterPart($search, true);
819 819
 		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
820 820
 		// check for cache of the exact query
821 821
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
822
-		if(!is_null($groupUsers)) {
822
+		if (!is_null($groupUsers)) {
823 823
 			return $groupUsers;
824 824
 		}
825 825
 
826 826
 		// check for cache of the query without limit and offset
827 827
 		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
828
-		if(!is_null($groupUsers)) {
828
+		if (!is_null($groupUsers)) {
829 829
 			$groupUsers = array_slice($groupUsers, $offset, $limit);
830 830
 			$this->access->connection->writeToCache($cacheKey, $groupUsers);
831 831
 			return $groupUsers;
832 832
 		}
833 833
 
834
-		if($limit === -1) {
834
+		if ($limit === -1) {
835 835
 			$limit = null;
836 836
 		}
837 837
 		$groupDN = $this->access->groupname2dn($gid);
838
-		if(!$groupDN) {
838
+		if (!$groupDN) {
839 839
 			// group couldn't be found, return empty resultset
840 840
 			$this->access->connection->writeToCache($cacheKey, array());
841 841
 			return array();
@@ -844,7 +844,7 @@  discard block
 block discarded – undo
844 844
 		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
845 845
 		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
846 846
 		$members = $this->_groupMembers($groupDN);
847
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
847
+		if (!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
848 848
 			//in case users could not be retrieved, return empty result set
849 849
 			$this->access->connection->writeToCache($cacheKey, []);
850 850
 			return [];
@@ -853,29 +853,29 @@  discard block
 block discarded – undo
853 853
 		$groupUsers = array();
854 854
 		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
855 855
 		$attrs = $this->access->userManager->getAttributes(true);
856
-		foreach($members as $member) {
857
-			if($isMemberUid) {
856
+		foreach ($members as $member) {
857
+			if ($isMemberUid) {
858 858
 				//we got uids, need to get their DNs to 'translate' them to user names
859 859
 				$filter = $this->access->combineFilterWithAnd(array(
860 860
 					str_replace('%uid', trim($member), $this->access->connection->ldapLoginFilter),
861 861
 					$this->access->getFilterPartForUserSearch($search)
862 862
 				));
863 863
 				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
864
-				if(count($ldap_users) < 1) {
864
+				if (count($ldap_users) < 1) {
865 865
 					continue;
866 866
 				}
867 867
 				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
868 868
 			} else {
869 869
 				//we got DNs, check if we need to filter by search or we can give back all of them
870 870
 				if ($search !== '') {
871
-					if(!$this->access->readAttribute($member,
871
+					if (!$this->access->readAttribute($member,
872 872
 						$this->access->connection->ldapUserDisplayName,
873 873
 						$this->access->getFilterPartForUserSearch($search))) {
874 874
 						continue;
875 875
 					}
876 876
 				}
877 877
 				// dn2username will also check if the users belong to the allowed base
878
-				if($ocname = $this->access->dn2username($member)) {
878
+				if ($ocname = $this->access->dn2username($member)) {
879 879
 					$groupUsers[] = $ocname;
880 880
 				}
881 881
 			}
@@ -903,16 +903,16 @@  discard block
 block discarded – undo
903 903
 		}
904 904
 
905 905
 		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
906
-		if(!$this->enabled || !$this->groupExists($gid)) {
906
+		if (!$this->enabled || !$this->groupExists($gid)) {
907 907
 			return false;
908 908
 		}
909 909
 		$groupUsers = $this->access->connection->getFromCache($cacheKey);
910
-		if(!is_null($groupUsers)) {
910
+		if (!is_null($groupUsers)) {
911 911
 			return $groupUsers;
912 912
 		}
913 913
 
914 914
 		$groupDN = $this->access->groupname2dn($gid);
915
-		if(!$groupDN) {
915
+		if (!$groupDN) {
916 916
 			// group couldn't be found, return empty result set
917 917
 			$this->access->connection->writeToCache($cacheKey, false);
918 918
 			return false;
@@ -920,7 +920,7 @@  discard block
 block discarded – undo
920 920
 
921 921
 		$members = $this->_groupMembers($groupDN);
922 922
 		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
923
-		if(!$members && $primaryUserCount === 0) {
923
+		if (!$members && $primaryUserCount === 0) {
924 924
 			//in case users could not be retrieved, return empty result set
925 925
 			$this->access->connection->writeToCache($cacheKey, false);
926 926
 			return false;
@@ -945,27 +945,27 @@  discard block
 block discarded – undo
945 945
 		//For now this is not important, because the only use of this method
946 946
 		//does not supply a search string
947 947
 		$groupUsers = array();
948
-		foreach($members as $member) {
949
-			if($isMemberUid) {
948
+		foreach ($members as $member) {
949
+			if ($isMemberUid) {
950 950
 				//we got uids, need to get their DNs to 'translate' them to user names
951 951
 				$filter = $this->access->combineFilterWithAnd(array(
952 952
 					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
953 953
 					$this->access->getFilterPartForUserSearch($search)
954 954
 				));
955 955
 				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
956
-				if(count($ldap_users) < 1) {
956
+				if (count($ldap_users) < 1) {
957 957
 					continue;
958 958
 				}
959 959
 				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
960 960
 			} else {
961 961
 				//we need to apply the search filter now
962
-				if(!$this->access->readAttribute($member,
962
+				if (!$this->access->readAttribute($member,
963 963
 					$this->access->connection->ldapUserDisplayName,
964 964
 					$this->access->getFilterPartForUserSearch($search))) {
965 965
 					continue;
966 966
 				}
967 967
 				// dn2username will also check if the users belong to the allowed base
968
-				if($ocname = $this->access->dn2username($member)) {
968
+				if ($ocname = $this->access->dn2username($member)) {
969 969
 					$groupUsers[] = $ocname;
970 970
 				}
971 971
 			}
@@ -988,7 +988,7 @@  discard block
 block discarded – undo
988 988
 	 * Returns a list with all groups (used by getGroups)
989 989
 	 */
990 990
 	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
991
-		if(!$this->enabled) {
991
+		if (!$this->enabled) {
992 992
 			return array();
993 993
 		}
994 994
 		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
@@ -996,13 +996,13 @@  discard block
 block discarded – undo
996 996
 		//Check cache before driving unnecessary searches
997 997
 		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
998 998
 		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
999
-		if(!is_null($ldap_groups)) {
999
+		if (!is_null($ldap_groups)) {
1000 1000
 			return $ldap_groups;
1001 1001
 		}
1002 1002
 
1003 1003
 		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
1004 1004
 		// error. With a limit of 0, we get 0 results. So we pass null.
1005
-		if($limit <= 0) {
1005
+		if ($limit <= 0) {
1006 1006
 			$limit = null;
1007 1007
 		}
1008 1008
 		$filter = $this->access->combineFilterWithAnd(array(
@@ -1034,11 +1034,11 @@  discard block
 block discarded – undo
1034 1034
 	 * (active directory has a limit of 1000 by default)
1035 1035
 	 */
1036 1036
 	public function getGroups($search = '', $limit = -1, $offset = 0) {
1037
-		if(!$this->enabled) {
1037
+		if (!$this->enabled) {
1038 1038
 			return array();
1039 1039
 		}
1040 1040
 		$search = $this->access->escapeFilterPart($search, true);
1041
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1041
+		$pagingSize = (int) $this->access->connection->ldapPagingSize;
1042 1042
 		if ($pagingSize <= 0) {
1043 1043
 			return $this->getGroupsChunk($search, $limit, $offset);
1044 1044
 		}
@@ -1081,20 +1081,20 @@  discard block
 block discarded – undo
1081 1081
 	 */
1082 1082
 	public function groupExists($gid) {
1083 1083
 		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1084
-		if(!is_null($groupExists)) {
1085
-			return (bool)$groupExists;
1084
+		if (!is_null($groupExists)) {
1085
+			return (bool) $groupExists;
1086 1086
 		}
1087 1087
 
1088 1088
 		//getting dn, if false the group does not exist. If dn, it may be mapped
1089 1089
 		//only, requires more checking.
1090 1090
 		$dn = $this->access->groupname2dn($gid);
1091
-		if(!$dn) {
1091
+		if (!$dn) {
1092 1092
 			$this->access->connection->writeToCache('groupExists'.$gid, false);
1093 1093
 			return false;
1094 1094
 		}
1095 1095
 
1096 1096
 		//if group really still exists, we will be able to read its objectclass
1097
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1097
+		if (!is_array($this->access->readAttribute($dn, ''))) {
1098 1098
 			$this->access->connection->writeToCache('groupExists'.$gid, false);
1099 1099
 			return false;
1100 1100
 		}
@@ -1112,7 +1112,7 @@  discard block
 block discarded – undo
1112 1112
 	* compared with GroupInterface::CREATE_GROUP etc.
1113 1113
 	*/
1114 1114
 	public function implementsActions($actions) {
1115
-		return (bool)((GroupInterface::COUNT_USERS |
1115
+		return (bool) ((GroupInterface::COUNT_USERS |
1116 1116
 				$this->groupPluginManager->getImplementedActions()) & $actions);
1117 1117
 	}
1118 1118
 
@@ -1229,7 +1229,7 @@  discard block
 block discarded – undo
1229 1229
 			return $this->groupPluginManager->getDisplayName($gid);
1230 1230
 		}
1231 1231
 
1232
-		$cacheKey = 'group_getDisplayName' . $gid;
1232
+		$cacheKey = 'group_getDisplayName'.$gid;
1233 1233
 		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
1234 1234
 			return $displayName;
1235 1235
 		}
Please login to merge, or discard this patch.
apps/user_ldap/lib/User_LDAP.php 2 patches
Indentation   +595 added lines, -595 removed lines patch added patch discarded remove patch
@@ -52,603 +52,603 @@
 block discarded – undo
52 52
 use OCP\Util;
53 53
 
54 54
 class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserInterface, IUserLDAP {
55
-	/** @var \OCP\IConfig */
56
-	protected $ocConfig;
57
-
58
-	/** @var INotificationManager */
59
-	protected $notificationManager;
60
-
61
-	/** @var string */
62
-	protected $currentUserInDeletionProcess;
63
-
64
-	/** @var UserPluginManager */
65
-	protected $userPluginManager;
66
-
67
-	/**
68
-	 * @param Access $access
69
-	 * @param \OCP\IConfig $ocConfig
70
-	 * @param \OCP\Notification\IManager $notificationManager
71
-	 * @param IUserSession $userSession
72
-	 */
73
-	public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, IUserSession $userSession, UserPluginManager $userPluginManager) {
74
-		parent::__construct($access);
75
-		$this->ocConfig = $ocConfig;
76
-		$this->notificationManager = $notificationManager;
77
-		$this->userPluginManager = $userPluginManager;
78
-		$this->registerHooks($userSession);
79
-	}
80
-
81
-	protected function registerHooks(IUserSession $userSession) {
82
-		$userSession->listen('\OC\User', 'preDelete', [$this, 'preDeleteUser']);
83
-		$userSession->listen('\OC\User', 'postDelete', [$this, 'postDeleteUser']);
84
-	}
85
-
86
-	public function preDeleteUser(IUser $user) {
87
-		$this->currentUserInDeletionProcess = $user->getUID();
88
-	}
89
-
90
-	public function postDeleteUser() {
91
-		$this->currentUserInDeletionProcess = null;
92
-	}
93
-
94
-	/**
95
-	 * checks whether the user is allowed to change his avatar in Nextcloud
96
-	 *
97
-	 * @param string $uid the Nextcloud user name
98
-	 * @return boolean either the user can or cannot
99
-	 * @throws \Exception
100
-	 */
101
-	public function canChangeAvatar($uid) {
102
-		if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
103
-			return $this->userPluginManager->canChangeAvatar($uid);
104
-		}
105
-
106
-		if(!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
107
-			return true;
108
-		}
109
-
110
-		$user = $this->access->userManager->get($uid);
111
-		if(!$user instanceof User) {
112
-			return false;
113
-		}
114
-		$imageData = $user->getAvatarImage();
115
-		if($imageData === false) {
116
-			return true;
117
-		}
118
-		return !$user->updateAvatar(true);
119
-	}
120
-
121
-	/**
122
-	 * Return the username for the given login name, if available
123
-	 *
124
-	 * @param string $loginName
125
-	 * @return string|false
126
-	 * @throws \Exception
127
-	 */
128
-	public function loginName2UserName($loginName) {
129
-		$cacheKey = 'loginName2UserName-' . $loginName;
130
-		$username = $this->access->connection->getFromCache($cacheKey);
131
-
132
-		if ($username !== null) {
133
-			return $username;
134
-		}
135
-
136
-		try {
137
-			$ldapRecord = $this->getLDAPUserByLoginName($loginName);
138
-			$user = $this->access->userManager->get($ldapRecord['dn'][0]);
139
-			if ($user === null || $user instanceof OfflineUser) {
140
-				// this path is not really possible, however get() is documented
141
-				// to return User, OfflineUser or null so we are very defensive here.
142
-				$this->access->connection->writeToCache($cacheKey, false);
143
-				return false;
144
-			}
145
-			$username = $user->getUsername();
146
-			$this->access->connection->writeToCache($cacheKey, $username);
147
-			return $username;
148
-		} catch (NotOnLDAP $e) {
149
-			$this->access->connection->writeToCache($cacheKey, false);
150
-			return false;
151
-		}
152
-	}
55
+    /** @var \OCP\IConfig */
56
+    protected $ocConfig;
57
+
58
+    /** @var INotificationManager */
59
+    protected $notificationManager;
60
+
61
+    /** @var string */
62
+    protected $currentUserInDeletionProcess;
63
+
64
+    /** @var UserPluginManager */
65
+    protected $userPluginManager;
66
+
67
+    /**
68
+     * @param Access $access
69
+     * @param \OCP\IConfig $ocConfig
70
+     * @param \OCP\Notification\IManager $notificationManager
71
+     * @param IUserSession $userSession
72
+     */
73
+    public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, IUserSession $userSession, UserPluginManager $userPluginManager) {
74
+        parent::__construct($access);
75
+        $this->ocConfig = $ocConfig;
76
+        $this->notificationManager = $notificationManager;
77
+        $this->userPluginManager = $userPluginManager;
78
+        $this->registerHooks($userSession);
79
+    }
80
+
81
+    protected function registerHooks(IUserSession $userSession) {
82
+        $userSession->listen('\OC\User', 'preDelete', [$this, 'preDeleteUser']);
83
+        $userSession->listen('\OC\User', 'postDelete', [$this, 'postDeleteUser']);
84
+    }
85
+
86
+    public function preDeleteUser(IUser $user) {
87
+        $this->currentUserInDeletionProcess = $user->getUID();
88
+    }
89
+
90
+    public function postDeleteUser() {
91
+        $this->currentUserInDeletionProcess = null;
92
+    }
93
+
94
+    /**
95
+     * checks whether the user is allowed to change his avatar in Nextcloud
96
+     *
97
+     * @param string $uid the Nextcloud user name
98
+     * @return boolean either the user can or cannot
99
+     * @throws \Exception
100
+     */
101
+    public function canChangeAvatar($uid) {
102
+        if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
103
+            return $this->userPluginManager->canChangeAvatar($uid);
104
+        }
105
+
106
+        if(!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
107
+            return true;
108
+        }
109
+
110
+        $user = $this->access->userManager->get($uid);
111
+        if(!$user instanceof User) {
112
+            return false;
113
+        }
114
+        $imageData = $user->getAvatarImage();
115
+        if($imageData === false) {
116
+            return true;
117
+        }
118
+        return !$user->updateAvatar(true);
119
+    }
120
+
121
+    /**
122
+     * Return the username for the given login name, if available
123
+     *
124
+     * @param string $loginName
125
+     * @return string|false
126
+     * @throws \Exception
127
+     */
128
+    public function loginName2UserName($loginName) {
129
+        $cacheKey = 'loginName2UserName-' . $loginName;
130
+        $username = $this->access->connection->getFromCache($cacheKey);
131
+
132
+        if ($username !== null) {
133
+            return $username;
134
+        }
135
+
136
+        try {
137
+            $ldapRecord = $this->getLDAPUserByLoginName($loginName);
138
+            $user = $this->access->userManager->get($ldapRecord['dn'][0]);
139
+            if ($user === null || $user instanceof OfflineUser) {
140
+                // this path is not really possible, however get() is documented
141
+                // to return User, OfflineUser or null so we are very defensive here.
142
+                $this->access->connection->writeToCache($cacheKey, false);
143
+                return false;
144
+            }
145
+            $username = $user->getUsername();
146
+            $this->access->connection->writeToCache($cacheKey, $username);
147
+            return $username;
148
+        } catch (NotOnLDAP $e) {
149
+            $this->access->connection->writeToCache($cacheKey, false);
150
+            return false;
151
+        }
152
+    }
153 153
 	
154
-	/**
155
-	 * returns the username for the given LDAP DN, if available
156
-	 *
157
-	 * @param string $dn
158
-	 * @return string|false with the username
159
-	 */
160
-	public function dn2UserName($dn) {
161
-		return $this->access->dn2username($dn);
162
-	}
163
-
164
-	/**
165
-	 * returns an LDAP record based on a given login name
166
-	 *
167
-	 * @param string $loginName
168
-	 * @return array
169
-	 * @throws NotOnLDAP
170
-	 */
171
-	public function getLDAPUserByLoginName($loginName) {
172
-		//find out dn of the user name
173
-		$attrs = $this->access->userManager->getAttributes();
174
-		$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
175
-		if(count($users) < 1) {
176
-			throw new NotOnLDAP('No user available for the given login name on ' .
177
-				$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
178
-		}
179
-		return $users[0];
180
-	}
181
-
182
-	/**
183
-	 * Check if the password is correct without logging in the user
184
-	 *
185
-	 * @param string $uid The username
186
-	 * @param string $password The password
187
-	 * @return false|string
188
-	 */
189
-	public function checkPassword($uid, $password) {
190
-		try {
191
-			$ldapRecord = $this->getLDAPUserByLoginName($uid);
192
-		} catch(NotOnLDAP $e) {
193
-			\OC::$server->getLogger()->logException($e, ['app' => 'user_ldap', 'level' => ILogger::DEBUG]);
194
-			return false;
195
-		}
196
-		$dn = $ldapRecord['dn'][0];
197
-		$user = $this->access->userManager->get($dn);
198
-
199
-		if(!$user instanceof User) {
200
-			Util::writeLog('user_ldap',
201
-				'LDAP Login: Could not get user object for DN ' . $dn .
202
-				'. Maybe the LDAP entry has no set display name attribute?',
203
-				ILogger::WARN);
204
-			return false;
205
-		}
206
-		if($user->getUsername() !== false) {
207
-			//are the credentials OK?
208
-			if(!$this->access->areCredentialsValid($dn, $password)) {
209
-				return false;
210
-			}
211
-
212
-			$this->access->cacheUserExists($user->getUsername());
213
-			$user->processAttributes($ldapRecord);
214
-			$user->markLogin();
215
-
216
-			return $user->getUsername();
217
-		}
218
-
219
-		return false;
220
-	}
221
-
222
-	/**
223
-	 * Set password
224
-	 * @param string $uid The username
225
-	 * @param string $password The new password
226
-	 * @return bool
227
-	 */
228
-	public function setPassword($uid, $password) {
229
-		if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
230
-			return $this->userPluginManager->setPassword($uid, $password);
231
-		}
232
-
233
-		$user = $this->access->userManager->get($uid);
234
-
235
-		if(!$user instanceof User) {
236
-			throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
237
-				'. Maybe the LDAP entry has no set display name attribute?');
238
-		}
239
-		if($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
240
-			$ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
241
-			$turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
242
-			if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
243
-				//remove last password expiry warning if any
244
-				$notification = $this->notificationManager->createNotification();
245
-				$notification->setApp('user_ldap')
246
-					->setUser($uid)
247
-					->setObject('pwd_exp_warn', $uid)
248
-				;
249
-				$this->notificationManager->markProcessed($notification);
250
-			}
251
-			return true;
252
-		}
253
-
254
-		return false;
255
-	}
256
-
257
-	/**
258
-	 * Get a list of all users
259
-	 *
260
-	 * @param string $search
261
-	 * @param integer $limit
262
-	 * @param integer $offset
263
-	 * @return string[] an array of all uids
264
-	 */
265
-	public function getUsers($search = '', $limit = 10, $offset = 0) {
266
-		$search = $this->access->escapeFilterPart($search, true);
267
-		$cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
268
-
269
-		//check if users are cached, if so return
270
-		$ldap_users = $this->access->connection->getFromCache($cachekey);
271
-		if(!is_null($ldap_users)) {
272
-			return $ldap_users;
273
-		}
274
-
275
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
276
-		// error. With a limit of 0, we get 0 results. So we pass null.
277
-		if($limit <= 0) {
278
-			$limit = null;
279
-		}
280
-		$filter = $this->access->combineFilterWithAnd(array(
281
-			$this->access->connection->ldapUserFilter,
282
-			$this->access->connection->ldapUserDisplayName . '=*',
283
-			$this->access->getFilterPartForUserSearch($search)
284
-		));
285
-
286
-		Util::writeLog('user_ldap',
287
-			'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
288
-			ILogger::DEBUG);
289
-		//do the search and translate results to Nextcloud names
290
-		$ldap_users = $this->access->fetchListOfUsers(
291
-			$filter,
292
-			$this->access->userManager->getAttributes(true),
293
-			$limit, $offset);
294
-		$ldap_users = $this->access->nextcloudUserNames($ldap_users);
295
-		Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', ILogger::DEBUG);
296
-
297
-		$this->access->connection->writeToCache($cachekey, $ldap_users);
298
-		return $ldap_users;
299
-	}
300
-
301
-	/**
302
-	 * checks whether a user is still available on LDAP
303
-	 *
304
-	 * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
305
-	 * name or an instance of that user
306
-	 * @return bool
307
-	 * @throws \Exception
308
-	 * @throws \OC\ServerNotAvailableException
309
-	 */
310
-	public function userExistsOnLDAP($user) {
311
-		if(is_string($user)) {
312
-			$user = $this->access->userManager->get($user);
313
-		}
314
-		if(is_null($user)) {
315
-			return false;
316
-		}
317
-
318
-		$dn = $user->getDN();
319
-		//check if user really still exists by reading its entry
320
-		if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
321
-			try {
322
-				$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
323
-				if (!$uuid) {
324
-					return false;
325
-				}
326
-				$newDn = $this->access->getUserDnByUuid($uuid);
327
-				//check if renamed user is still valid by reapplying the ldap filter
328
-				if ($newDn === $dn || !is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
329
-					return false;
330
-				}
331
-				$this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
332
-				return true;
333
-			} catch (ServerNotAvailableException $e) {
334
-				throw $e;
335
-			} catch (\Exception $e) {
336
-				return false;
337
-			}
338
-		}
339
-
340
-		if($user instanceof OfflineUser) {
341
-			$user->unmark();
342
-		}
343
-
344
-		return true;
345
-	}
346
-
347
-	/**
348
-	 * check if a user exists
349
-	 * @param string $uid the username
350
-	 * @return boolean
351
-	 * @throws \Exception when connection could not be established
352
-	 */
353
-	public function userExists($uid) {
354
-		$userExists = $this->access->connection->getFromCache('userExists'.$uid);
355
-		if(!is_null($userExists)) {
356
-			return (bool)$userExists;
357
-		}
358
-		//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
359
-		$user = $this->access->userManager->get($uid);
360
-
361
-		if(is_null($user)) {
362
-			Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
363
-				$this->access->connection->ldapHost, ILogger::DEBUG);
364
-			$this->access->connection->writeToCache('userExists'.$uid, false);
365
-			return false;
366
-		} else if($user instanceof OfflineUser) {
367
-			//express check for users marked as deleted. Returning true is
368
-			//necessary for cleanup
369
-			return true;
370
-		}
371
-
372
-		$result = $this->userExistsOnLDAP($user);
373
-		$this->access->connection->writeToCache('userExists'.$uid, $result);
374
-		return $result;
375
-	}
376
-
377
-	/**
378
-	* returns whether a user was deleted in LDAP
379
-	*
380
-	* @param string $uid The username of the user to delete
381
-	* @return bool
382
-	*/
383
-	public function deleteUser($uid) {
384
-		if ($this->userPluginManager->canDeleteUser()) {
385
-			return $this->userPluginManager->deleteUser($uid);
386
-		}
387
-
388
-		$marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
389
-		if((int)$marked === 0) {
390
-			\OC::$server->getLogger()->notice(
391
-				'User '.$uid . ' is not marked as deleted, not cleaning up.',
392
-				array('app' => 'user_ldap'));
393
-			return false;
394
-		}
395
-		\OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
396
-			array('app' => 'user_ldap'));
397
-
398
-		$this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
399
-		$this->access->userManager->invalidate($uid);
400
-		return true;
401
-	}
402
-
403
-	/**
404
-	 * get the user's home directory
405
-	 *
406
-	 * @param string $uid the username
407
-	 * @return bool|string
408
-	 * @throws NoUserException
409
-	 * @throws \Exception
410
-	 */
411
-	public function getHome($uid) {
412
-		// user Exists check required as it is not done in user proxy!
413
-		if(!$this->userExists($uid)) {
414
-			return false;
415
-		}
416
-
417
-		if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
418
-			return $this->userPluginManager->getHome($uid);
419
-		}
420
-
421
-		$cacheKey = 'getHome'.$uid;
422
-		$path = $this->access->connection->getFromCache($cacheKey);
423
-		if(!is_null($path)) {
424
-			return $path;
425
-		}
426
-
427
-		// early return path if it is a deleted user
428
-		$user = $this->access->userManager->get($uid);
429
-		if($user instanceof OfflineUser) {
430
-			if($this->currentUserInDeletionProcess !== null
431
-				&& $this->currentUserInDeletionProcess === $user->getOCName()
432
-			) {
433
-				return $user->getHomePath();
434
-			} else {
435
-				throw new NoUserException($uid . ' is not a valid user anymore');
436
-			}
437
-		} else if ($user === null) {
438
-			throw new NoUserException($uid . ' is not a valid user anymore');
439
-		}
440
-
441
-		$path = $user->getHomePath();
442
-		$this->access->cacheUserHome($uid, $path);
443
-
444
-		return $path;
445
-	}
446
-
447
-	/**
448
-	 * get display name of the user
449
-	 * @param string $uid user ID of the user
450
-	 * @return string|false display name
451
-	 */
452
-	public function getDisplayName($uid) {
453
-		if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
454
-			return $this->userPluginManager->getDisplayName($uid);
455
-		}
456
-
457
-		if(!$this->userExists($uid)) {
458
-			return false;
459
-		}
460
-
461
-		$cacheKey = 'getDisplayName'.$uid;
462
-		if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
463
-			return $displayName;
464
-		}
465
-
466
-		//Check whether the display name is configured to have a 2nd feature
467
-		$additionalAttribute = $this->access->connection->ldapUserDisplayName2;
468
-		$displayName2 = '';
469
-		if ($additionalAttribute !== '') {
470
-			$displayName2 = $this->access->readAttribute(
471
-				$this->access->username2dn($uid),
472
-				$additionalAttribute);
473
-		}
474
-
475
-		$displayName = $this->access->readAttribute(
476
-			$this->access->username2dn($uid),
477
-			$this->access->connection->ldapUserDisplayName);
478
-
479
-		if($displayName && (count($displayName) > 0)) {
480
-			$displayName = $displayName[0];
481
-
482
-			if (is_array($displayName2)){
483
-				$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
484
-			}
485
-
486
-			$user = $this->access->userManager->get($uid);
487
-			if ($user instanceof User) {
488
-				$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
489
-				$this->access->connection->writeToCache($cacheKey, $displayName);
490
-			}
491
-			if ($user instanceof OfflineUser) {
492
-				/** @var OfflineUser $user*/
493
-				$displayName = $user->getDisplayName();
494
-			}
495
-			return $displayName;
496
-		}
497
-
498
-		return null;
499
-	}
500
-
501
-	/**
502
-	 * set display name of the user
503
-	 * @param string $uid user ID of the user
504
-	 * @param string $displayName new display name of the user
505
-	 * @return string|false display name
506
-	 */
507
-	public function setDisplayName($uid, $displayName) {
508
-		if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
509
-			$this->userPluginManager->setDisplayName($uid, $displayName);
510
-			$this->access->cacheUserDisplayName($uid, $displayName);
511
-			return $displayName;
512
-		}
513
-		return false;
514
-	}
515
-
516
-	/**
517
-	 * Get a list of all display names
518
-	 *
519
-	 * @param string $search
520
-	 * @param string|null $limit
521
-	 * @param string|null $offset
522
-	 * @return array an array of all displayNames (value) and the corresponding uids (key)
523
-	 */
524
-	public function getDisplayNames($search = '', $limit = null, $offset = null) {
525
-		$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
526
-		if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
527
-			return $displayNames;
528
-		}
529
-
530
-		$displayNames = array();
531
-		$users = $this->getUsers($search, $limit, $offset);
532
-		foreach ($users as $user) {
533
-			$displayNames[$user] = $this->getDisplayName($user);
534
-		}
535
-		$this->access->connection->writeToCache($cacheKey, $displayNames);
536
-		return $displayNames;
537
-	}
538
-
539
-	/**
540
-	* Check if backend implements actions
541
-	* @param int $actions bitwise-or'ed actions
542
-	* @return boolean
543
-	*
544
-	* Returns the supported actions as int to be
545
-	* compared with \OC\User\Backend::CREATE_USER etc.
546
-	*/
547
-	public function implementsActions($actions) {
548
-		return (bool)((Backend::CHECK_PASSWORD
549
-			| Backend::GET_HOME
550
-			| Backend::GET_DISPLAYNAME
551
-			| (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0)
552
-			| Backend::COUNT_USERS
553
-			| (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
554
-			| $this->userPluginManager->getImplementedActions())
555
-			& $actions);
556
-	}
557
-
558
-	/**
559
-	 * @return bool
560
-	 */
561
-	public function hasUserListings() {
562
-		return true;
563
-	}
564
-
565
-	/**
566
-	 * counts the users in LDAP
567
-	 *
568
-	 * @return int|bool
569
-	 */
570
-	public function countUsers() {
571
-		if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
572
-			return $this->userPluginManager->countUsers();
573
-		}
574
-
575
-		$filter = $this->access->getFilterForUserCount();
576
-		$cacheKey = 'countUsers-'.$filter;
577
-		if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
578
-			return $entries;
579
-		}
580
-		$entries = $this->access->countUsers($filter);
581
-		$this->access->connection->writeToCache($cacheKey, $entries);
582
-		return $entries;
583
-	}
584
-
585
-	/**
586
-	 * Backend name to be shown in user management
587
-	 * @return string the name of the backend to be shown
588
-	 */
589
-	public function getBackendName(){
590
-		return 'LDAP';
591
-	}
154
+    /**
155
+     * returns the username for the given LDAP DN, if available
156
+     *
157
+     * @param string $dn
158
+     * @return string|false with the username
159
+     */
160
+    public function dn2UserName($dn) {
161
+        return $this->access->dn2username($dn);
162
+    }
163
+
164
+    /**
165
+     * returns an LDAP record based on a given login name
166
+     *
167
+     * @param string $loginName
168
+     * @return array
169
+     * @throws NotOnLDAP
170
+     */
171
+    public function getLDAPUserByLoginName($loginName) {
172
+        //find out dn of the user name
173
+        $attrs = $this->access->userManager->getAttributes();
174
+        $users = $this->access->fetchUsersByLoginName($loginName, $attrs);
175
+        if(count($users) < 1) {
176
+            throw new NotOnLDAP('No user available for the given login name on ' .
177
+                $this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
178
+        }
179
+        return $users[0];
180
+    }
181
+
182
+    /**
183
+     * Check if the password is correct without logging in the user
184
+     *
185
+     * @param string $uid The username
186
+     * @param string $password The password
187
+     * @return false|string
188
+     */
189
+    public function checkPassword($uid, $password) {
190
+        try {
191
+            $ldapRecord = $this->getLDAPUserByLoginName($uid);
192
+        } catch(NotOnLDAP $e) {
193
+            \OC::$server->getLogger()->logException($e, ['app' => 'user_ldap', 'level' => ILogger::DEBUG]);
194
+            return false;
195
+        }
196
+        $dn = $ldapRecord['dn'][0];
197
+        $user = $this->access->userManager->get($dn);
198
+
199
+        if(!$user instanceof User) {
200
+            Util::writeLog('user_ldap',
201
+                'LDAP Login: Could not get user object for DN ' . $dn .
202
+                '. Maybe the LDAP entry has no set display name attribute?',
203
+                ILogger::WARN);
204
+            return false;
205
+        }
206
+        if($user->getUsername() !== false) {
207
+            //are the credentials OK?
208
+            if(!$this->access->areCredentialsValid($dn, $password)) {
209
+                return false;
210
+            }
211
+
212
+            $this->access->cacheUserExists($user->getUsername());
213
+            $user->processAttributes($ldapRecord);
214
+            $user->markLogin();
215
+
216
+            return $user->getUsername();
217
+        }
218
+
219
+        return false;
220
+    }
221
+
222
+    /**
223
+     * Set password
224
+     * @param string $uid The username
225
+     * @param string $password The new password
226
+     * @return bool
227
+     */
228
+    public function setPassword($uid, $password) {
229
+        if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
230
+            return $this->userPluginManager->setPassword($uid, $password);
231
+        }
232
+
233
+        $user = $this->access->userManager->get($uid);
234
+
235
+        if(!$user instanceof User) {
236
+            throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
237
+                '. Maybe the LDAP entry has no set display name attribute?');
238
+        }
239
+        if($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
240
+            $ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
241
+            $turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
242
+            if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
243
+                //remove last password expiry warning if any
244
+                $notification = $this->notificationManager->createNotification();
245
+                $notification->setApp('user_ldap')
246
+                    ->setUser($uid)
247
+                    ->setObject('pwd_exp_warn', $uid)
248
+                ;
249
+                $this->notificationManager->markProcessed($notification);
250
+            }
251
+            return true;
252
+        }
253
+
254
+        return false;
255
+    }
256
+
257
+    /**
258
+     * Get a list of all users
259
+     *
260
+     * @param string $search
261
+     * @param integer $limit
262
+     * @param integer $offset
263
+     * @return string[] an array of all uids
264
+     */
265
+    public function getUsers($search = '', $limit = 10, $offset = 0) {
266
+        $search = $this->access->escapeFilterPart($search, true);
267
+        $cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
268
+
269
+        //check if users are cached, if so return
270
+        $ldap_users = $this->access->connection->getFromCache($cachekey);
271
+        if(!is_null($ldap_users)) {
272
+            return $ldap_users;
273
+        }
274
+
275
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
276
+        // error. With a limit of 0, we get 0 results. So we pass null.
277
+        if($limit <= 0) {
278
+            $limit = null;
279
+        }
280
+        $filter = $this->access->combineFilterWithAnd(array(
281
+            $this->access->connection->ldapUserFilter,
282
+            $this->access->connection->ldapUserDisplayName . '=*',
283
+            $this->access->getFilterPartForUserSearch($search)
284
+        ));
285
+
286
+        Util::writeLog('user_ldap',
287
+            'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
288
+            ILogger::DEBUG);
289
+        //do the search and translate results to Nextcloud names
290
+        $ldap_users = $this->access->fetchListOfUsers(
291
+            $filter,
292
+            $this->access->userManager->getAttributes(true),
293
+            $limit, $offset);
294
+        $ldap_users = $this->access->nextcloudUserNames($ldap_users);
295
+        Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', ILogger::DEBUG);
296
+
297
+        $this->access->connection->writeToCache($cachekey, $ldap_users);
298
+        return $ldap_users;
299
+    }
300
+
301
+    /**
302
+     * checks whether a user is still available on LDAP
303
+     *
304
+     * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
305
+     * name or an instance of that user
306
+     * @return bool
307
+     * @throws \Exception
308
+     * @throws \OC\ServerNotAvailableException
309
+     */
310
+    public function userExistsOnLDAP($user) {
311
+        if(is_string($user)) {
312
+            $user = $this->access->userManager->get($user);
313
+        }
314
+        if(is_null($user)) {
315
+            return false;
316
+        }
317
+
318
+        $dn = $user->getDN();
319
+        //check if user really still exists by reading its entry
320
+        if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
321
+            try {
322
+                $uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
323
+                if (!$uuid) {
324
+                    return false;
325
+                }
326
+                $newDn = $this->access->getUserDnByUuid($uuid);
327
+                //check if renamed user is still valid by reapplying the ldap filter
328
+                if ($newDn === $dn || !is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
329
+                    return false;
330
+                }
331
+                $this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
332
+                return true;
333
+            } catch (ServerNotAvailableException $e) {
334
+                throw $e;
335
+            } catch (\Exception $e) {
336
+                return false;
337
+            }
338
+        }
339
+
340
+        if($user instanceof OfflineUser) {
341
+            $user->unmark();
342
+        }
343
+
344
+        return true;
345
+    }
346
+
347
+    /**
348
+     * check if a user exists
349
+     * @param string $uid the username
350
+     * @return boolean
351
+     * @throws \Exception when connection could not be established
352
+     */
353
+    public function userExists($uid) {
354
+        $userExists = $this->access->connection->getFromCache('userExists'.$uid);
355
+        if(!is_null($userExists)) {
356
+            return (bool)$userExists;
357
+        }
358
+        //getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
359
+        $user = $this->access->userManager->get($uid);
360
+
361
+        if(is_null($user)) {
362
+            Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
363
+                $this->access->connection->ldapHost, ILogger::DEBUG);
364
+            $this->access->connection->writeToCache('userExists'.$uid, false);
365
+            return false;
366
+        } else if($user instanceof OfflineUser) {
367
+            //express check for users marked as deleted. Returning true is
368
+            //necessary for cleanup
369
+            return true;
370
+        }
371
+
372
+        $result = $this->userExistsOnLDAP($user);
373
+        $this->access->connection->writeToCache('userExists'.$uid, $result);
374
+        return $result;
375
+    }
376
+
377
+    /**
378
+     * returns whether a user was deleted in LDAP
379
+     *
380
+     * @param string $uid The username of the user to delete
381
+     * @return bool
382
+     */
383
+    public function deleteUser($uid) {
384
+        if ($this->userPluginManager->canDeleteUser()) {
385
+            return $this->userPluginManager->deleteUser($uid);
386
+        }
387
+
388
+        $marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
389
+        if((int)$marked === 0) {
390
+            \OC::$server->getLogger()->notice(
391
+                'User '.$uid . ' is not marked as deleted, not cleaning up.',
392
+                array('app' => 'user_ldap'));
393
+            return false;
394
+        }
395
+        \OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
396
+            array('app' => 'user_ldap'));
397
+
398
+        $this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
399
+        $this->access->userManager->invalidate($uid);
400
+        return true;
401
+    }
402
+
403
+    /**
404
+     * get the user's home directory
405
+     *
406
+     * @param string $uid the username
407
+     * @return bool|string
408
+     * @throws NoUserException
409
+     * @throws \Exception
410
+     */
411
+    public function getHome($uid) {
412
+        // user Exists check required as it is not done in user proxy!
413
+        if(!$this->userExists($uid)) {
414
+            return false;
415
+        }
416
+
417
+        if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
418
+            return $this->userPluginManager->getHome($uid);
419
+        }
420
+
421
+        $cacheKey = 'getHome'.$uid;
422
+        $path = $this->access->connection->getFromCache($cacheKey);
423
+        if(!is_null($path)) {
424
+            return $path;
425
+        }
426
+
427
+        // early return path if it is a deleted user
428
+        $user = $this->access->userManager->get($uid);
429
+        if($user instanceof OfflineUser) {
430
+            if($this->currentUserInDeletionProcess !== null
431
+                && $this->currentUserInDeletionProcess === $user->getOCName()
432
+            ) {
433
+                return $user->getHomePath();
434
+            } else {
435
+                throw new NoUserException($uid . ' is not a valid user anymore');
436
+            }
437
+        } else if ($user === null) {
438
+            throw new NoUserException($uid . ' is not a valid user anymore');
439
+        }
440
+
441
+        $path = $user->getHomePath();
442
+        $this->access->cacheUserHome($uid, $path);
443
+
444
+        return $path;
445
+    }
446
+
447
+    /**
448
+     * get display name of the user
449
+     * @param string $uid user ID of the user
450
+     * @return string|false display name
451
+     */
452
+    public function getDisplayName($uid) {
453
+        if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
454
+            return $this->userPluginManager->getDisplayName($uid);
455
+        }
456
+
457
+        if(!$this->userExists($uid)) {
458
+            return false;
459
+        }
460
+
461
+        $cacheKey = 'getDisplayName'.$uid;
462
+        if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
463
+            return $displayName;
464
+        }
465
+
466
+        //Check whether the display name is configured to have a 2nd feature
467
+        $additionalAttribute = $this->access->connection->ldapUserDisplayName2;
468
+        $displayName2 = '';
469
+        if ($additionalAttribute !== '') {
470
+            $displayName2 = $this->access->readAttribute(
471
+                $this->access->username2dn($uid),
472
+                $additionalAttribute);
473
+        }
474
+
475
+        $displayName = $this->access->readAttribute(
476
+            $this->access->username2dn($uid),
477
+            $this->access->connection->ldapUserDisplayName);
478
+
479
+        if($displayName && (count($displayName) > 0)) {
480
+            $displayName = $displayName[0];
481
+
482
+            if (is_array($displayName2)){
483
+                $displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
484
+            }
485
+
486
+            $user = $this->access->userManager->get($uid);
487
+            if ($user instanceof User) {
488
+                $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
489
+                $this->access->connection->writeToCache($cacheKey, $displayName);
490
+            }
491
+            if ($user instanceof OfflineUser) {
492
+                /** @var OfflineUser $user*/
493
+                $displayName = $user->getDisplayName();
494
+            }
495
+            return $displayName;
496
+        }
497
+
498
+        return null;
499
+    }
500
+
501
+    /**
502
+     * set display name of the user
503
+     * @param string $uid user ID of the user
504
+     * @param string $displayName new display name of the user
505
+     * @return string|false display name
506
+     */
507
+    public function setDisplayName($uid, $displayName) {
508
+        if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
509
+            $this->userPluginManager->setDisplayName($uid, $displayName);
510
+            $this->access->cacheUserDisplayName($uid, $displayName);
511
+            return $displayName;
512
+        }
513
+        return false;
514
+    }
515
+
516
+    /**
517
+     * Get a list of all display names
518
+     *
519
+     * @param string $search
520
+     * @param string|null $limit
521
+     * @param string|null $offset
522
+     * @return array an array of all displayNames (value) and the corresponding uids (key)
523
+     */
524
+    public function getDisplayNames($search = '', $limit = null, $offset = null) {
525
+        $cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
526
+        if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
527
+            return $displayNames;
528
+        }
529
+
530
+        $displayNames = array();
531
+        $users = $this->getUsers($search, $limit, $offset);
532
+        foreach ($users as $user) {
533
+            $displayNames[$user] = $this->getDisplayName($user);
534
+        }
535
+        $this->access->connection->writeToCache($cacheKey, $displayNames);
536
+        return $displayNames;
537
+    }
538
+
539
+    /**
540
+     * Check if backend implements actions
541
+     * @param int $actions bitwise-or'ed actions
542
+     * @return boolean
543
+     *
544
+     * Returns the supported actions as int to be
545
+     * compared with \OC\User\Backend::CREATE_USER etc.
546
+     */
547
+    public function implementsActions($actions) {
548
+        return (bool)((Backend::CHECK_PASSWORD
549
+            | Backend::GET_HOME
550
+            | Backend::GET_DISPLAYNAME
551
+            | (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0)
552
+            | Backend::COUNT_USERS
553
+            | (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
554
+            | $this->userPluginManager->getImplementedActions())
555
+            & $actions);
556
+    }
557
+
558
+    /**
559
+     * @return bool
560
+     */
561
+    public function hasUserListings() {
562
+        return true;
563
+    }
564
+
565
+    /**
566
+     * counts the users in LDAP
567
+     *
568
+     * @return int|bool
569
+     */
570
+    public function countUsers() {
571
+        if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
572
+            return $this->userPluginManager->countUsers();
573
+        }
574
+
575
+        $filter = $this->access->getFilterForUserCount();
576
+        $cacheKey = 'countUsers-'.$filter;
577
+        if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
578
+            return $entries;
579
+        }
580
+        $entries = $this->access->countUsers($filter);
581
+        $this->access->connection->writeToCache($cacheKey, $entries);
582
+        return $entries;
583
+    }
584
+
585
+    /**
586
+     * Backend name to be shown in user management
587
+     * @return string the name of the backend to be shown
588
+     */
589
+    public function getBackendName(){
590
+        return 'LDAP';
591
+    }
592 592
 	
593
-	/**
594
-	 * Return access for LDAP interaction.
595
-	 * @param string $uid
596
-	 * @return Access instance of Access for LDAP interaction
597
-	 */
598
-	public function getLDAPAccess($uid) {
599
-		return $this->access;
600
-	}
593
+    /**
594
+     * Return access for LDAP interaction.
595
+     * @param string $uid
596
+     * @return Access instance of Access for LDAP interaction
597
+     */
598
+    public function getLDAPAccess($uid) {
599
+        return $this->access;
600
+    }
601 601
 	
602
-	/**
603
-	 * Return LDAP connection resource from a cloned connection.
604
-	 * The cloned connection needs to be closed manually.
605
-	 * of the current access.
606
-	 * @param string $uid
607
-	 * @return resource of the LDAP connection
608
-	 */
609
-	public function getNewLDAPConnection($uid) {
610
-		$connection = clone $this->access->getConnection();
611
-		return $connection->getConnectionResource();
612
-	}
613
-
614
-	/**
615
-	 * create new user
616
-	 * @param string $username username of the new user
617
-	 * @param string $password password of the new user
618
-	 * @throws \UnexpectedValueException
619
-	 * @return bool
620
-	 */
621
-	public function createUser($username, $password) {
622
-		if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
623
-			if ($dn = $this->userPluginManager->createUser($username, $password)) {
624
-				if (is_string($dn)) {
625
-					// the NC user creation work flow requires a know user id up front
626
-					$uuid = $this->access->getUUID($dn, true);
627
-					if(is_string($uuid)) {
628
-						$this->access->mapAndAnnounceIfApplicable(
629
-							$this->access->getUserMapper(),
630
-							$dn,
631
-							$username,
632
-							$uuid,
633
-							true
634
-						);
635
-						$this->access->cacheUserExists($username);
636
-					} else {
637
-						\OC::$server->getLogger()->warning(
638
-							'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
639
-							[
640
-								'app' => 'user_ldap',
641
-								'userid' => $username,
642
-							]
643
-						);
644
-					}
645
-				} else {
646
-					throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean.");
647
-				}
648
-			}
649
-			return (bool) $dn;
650
-		}
651
-		return false;
652
-	}
602
+    /**
603
+     * Return LDAP connection resource from a cloned connection.
604
+     * The cloned connection needs to be closed manually.
605
+     * of the current access.
606
+     * @param string $uid
607
+     * @return resource of the LDAP connection
608
+     */
609
+    public function getNewLDAPConnection($uid) {
610
+        $connection = clone $this->access->getConnection();
611
+        return $connection->getConnectionResource();
612
+    }
613
+
614
+    /**
615
+     * create new user
616
+     * @param string $username username of the new user
617
+     * @param string $password password of the new user
618
+     * @throws \UnexpectedValueException
619
+     * @return bool
620
+     */
621
+    public function createUser($username, $password) {
622
+        if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
623
+            if ($dn = $this->userPluginManager->createUser($username, $password)) {
624
+                if (is_string($dn)) {
625
+                    // the NC user creation work flow requires a know user id up front
626
+                    $uuid = $this->access->getUUID($dn, true);
627
+                    if(is_string($uuid)) {
628
+                        $this->access->mapAndAnnounceIfApplicable(
629
+                            $this->access->getUserMapper(),
630
+                            $dn,
631
+                            $username,
632
+                            $uuid,
633
+                            true
634
+                        );
635
+                        $this->access->cacheUserExists($username);
636
+                    } else {
637
+                        \OC::$server->getLogger()->warning(
638
+                            'Failed to map created LDAP user with userid {userid}, because UUID could not be determined',
639
+                            [
640
+                                'app' => 'user_ldap',
641
+                                'userid' => $username,
642
+                            ]
643
+                        );
644
+                    }
645
+                } else {
646
+                    throw new \UnexpectedValueException("LDAP Plugin: Method createUser changed to return the user DN instead of boolean.");
647
+                }
648
+            }
649
+            return (bool) $dn;
650
+        }
651
+        return false;
652
+    }
653 653
 
654 654
 }
Please login to merge, or discard this patch.
Spacing   +47 added lines, -47 removed lines patch added patch discarded remove patch
@@ -103,16 +103,16 @@  discard block
 block discarded – undo
103 103
 			return $this->userPluginManager->canChangeAvatar($uid);
104 104
 		}
105 105
 
106
-		if(!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
106
+		if (!$this->implementsActions(Backend::PROVIDE_AVATAR)) {
107 107
 			return true;
108 108
 		}
109 109
 
110 110
 		$user = $this->access->userManager->get($uid);
111
-		if(!$user instanceof User) {
111
+		if (!$user instanceof User) {
112 112
 			return false;
113 113
 		}
114 114
 		$imageData = $user->getAvatarImage();
115
-		if($imageData === false) {
115
+		if ($imageData === false) {
116 116
 			return true;
117 117
 		}
118 118
 		return !$user->updateAvatar(true);
@@ -126,7 +126,7 @@  discard block
 block discarded – undo
126 126
 	 * @throws \Exception
127 127
 	 */
128 128
 	public function loginName2UserName($loginName) {
129
-		$cacheKey = 'loginName2UserName-' . $loginName;
129
+		$cacheKey = 'loginName2UserName-'.$loginName;
130 130
 		$username = $this->access->connection->getFromCache($cacheKey);
131 131
 
132 132
 		if ($username !== null) {
@@ -172,9 +172,9 @@  discard block
 block discarded – undo
172 172
 		//find out dn of the user name
173 173
 		$attrs = $this->access->userManager->getAttributes();
174 174
 		$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
175
-		if(count($users) < 1) {
176
-			throw new NotOnLDAP('No user available for the given login name on ' .
177
-				$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
175
+		if (count($users) < 1) {
176
+			throw new NotOnLDAP('No user available for the given login name on '.
177
+				$this->access->connection->ldapHost.':'.$this->access->connection->ldapPort);
178 178
 		}
179 179
 		return $users[0];
180 180
 	}
@@ -189,23 +189,23 @@  discard block
 block discarded – undo
189 189
 	public function checkPassword($uid, $password) {
190 190
 		try {
191 191
 			$ldapRecord = $this->getLDAPUserByLoginName($uid);
192
-		} catch(NotOnLDAP $e) {
192
+		} catch (NotOnLDAP $e) {
193 193
 			\OC::$server->getLogger()->logException($e, ['app' => 'user_ldap', 'level' => ILogger::DEBUG]);
194 194
 			return false;
195 195
 		}
196 196
 		$dn = $ldapRecord['dn'][0];
197 197
 		$user = $this->access->userManager->get($dn);
198 198
 
199
-		if(!$user instanceof User) {
199
+		if (!$user instanceof User) {
200 200
 			Util::writeLog('user_ldap',
201
-				'LDAP Login: Could not get user object for DN ' . $dn .
201
+				'LDAP Login: Could not get user object for DN '.$dn.
202 202
 				'. Maybe the LDAP entry has no set display name attribute?',
203 203
 				ILogger::WARN);
204 204
 			return false;
205 205
 		}
206
-		if($user->getUsername() !== false) {
206
+		if ($user->getUsername() !== false) {
207 207
 			//are the credentials OK?
208
-			if(!$this->access->areCredentialsValid($dn, $password)) {
208
+			if (!$this->access->areCredentialsValid($dn, $password)) {
209 209
 				return false;
210 210
 			}
211 211
 
@@ -232,14 +232,14 @@  discard block
 block discarded – undo
232 232
 
233 233
 		$user = $this->access->userManager->get($uid);
234 234
 
235
-		if(!$user instanceof User) {
236
-			throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
235
+		if (!$user instanceof User) {
236
+			throw new \Exception('LDAP setPassword: Could not get user object for uid '.$uid.
237 237
 				'. Maybe the LDAP entry has no set display name attribute?');
238 238
 		}
239
-		if($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
239
+		if ($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
240 240
 			$ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
241 241
 			$turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
242
-			if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
242
+			if (!empty($ldapDefaultPPolicyDN) && ((int) $turnOnPasswordChange === 1)) {
243 243
 				//remove last password expiry warning if any
244 244
 				$notification = $this->notificationManager->createNotification();
245 245
 				$notification->setApp('user_ldap')
@@ -268,18 +268,18 @@  discard block
 block discarded – undo
268 268
 
269 269
 		//check if users are cached, if so return
270 270
 		$ldap_users = $this->access->connection->getFromCache($cachekey);
271
-		if(!is_null($ldap_users)) {
271
+		if (!is_null($ldap_users)) {
272 272
 			return $ldap_users;
273 273
 		}
274 274
 
275 275
 		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
276 276
 		// error. With a limit of 0, we get 0 results. So we pass null.
277
-		if($limit <= 0) {
277
+		if ($limit <= 0) {
278 278
 			$limit = null;
279 279
 		}
280 280
 		$filter = $this->access->combineFilterWithAnd(array(
281 281
 			$this->access->connection->ldapUserFilter,
282
-			$this->access->connection->ldapUserDisplayName . '=*',
282
+			$this->access->connection->ldapUserDisplayName.'=*',
283 283
 			$this->access->getFilterPartForUserSearch($search)
284 284
 		));
285 285
 
@@ -292,7 +292,7 @@  discard block
 block discarded – undo
292 292
 			$this->access->userManager->getAttributes(true),
293 293
 			$limit, $offset);
294 294
 		$ldap_users = $this->access->nextcloudUserNames($ldap_users);
295
-		Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', ILogger::DEBUG);
295
+		Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users).' Users found', ILogger::DEBUG);
296 296
 
297 297
 		$this->access->connection->writeToCache($cachekey, $ldap_users);
298 298
 		return $ldap_users;
@@ -308,16 +308,16 @@  discard block
 block discarded – undo
308 308
 	 * @throws \OC\ServerNotAvailableException
309 309
 	 */
310 310
 	public function userExistsOnLDAP($user) {
311
-		if(is_string($user)) {
311
+		if (is_string($user)) {
312 312
 			$user = $this->access->userManager->get($user);
313 313
 		}
314
-		if(is_null($user)) {
314
+		if (is_null($user)) {
315 315
 			return false;
316 316
 		}
317 317
 
318 318
 		$dn = $user->getDN();
319 319
 		//check if user really still exists by reading its entry
320
-		if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
320
+		if (!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
321 321
 			try {
322 322
 				$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
323 323
 				if (!$uuid) {
@@ -337,7 +337,7 @@  discard block
 block discarded – undo
337 337
 			}
338 338
 		}
339 339
 
340
-		if($user instanceof OfflineUser) {
340
+		if ($user instanceof OfflineUser) {
341 341
 			$user->unmark();
342 342
 		}
343 343
 
@@ -352,18 +352,18 @@  discard block
 block discarded – undo
352 352
 	 */
353 353
 	public function userExists($uid) {
354 354
 		$userExists = $this->access->connection->getFromCache('userExists'.$uid);
355
-		if(!is_null($userExists)) {
356
-			return (bool)$userExists;
355
+		if (!is_null($userExists)) {
356
+			return (bool) $userExists;
357 357
 		}
358 358
 		//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
359 359
 		$user = $this->access->userManager->get($uid);
360 360
 
361
-		if(is_null($user)) {
361
+		if (is_null($user)) {
362 362
 			Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
363 363
 				$this->access->connection->ldapHost, ILogger::DEBUG);
364 364
 			$this->access->connection->writeToCache('userExists'.$uid, false);
365 365
 			return false;
366
-		} else if($user instanceof OfflineUser) {
366
+		} else if ($user instanceof OfflineUser) {
367 367
 			//express check for users marked as deleted. Returning true is
368 368
 			//necessary for cleanup
369 369
 			return true;
@@ -386,13 +386,13 @@  discard block
 block discarded – undo
386 386
 		}
387 387
 
388 388
 		$marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
389
-		if((int)$marked === 0) {
389
+		if ((int) $marked === 0) {
390 390
 			\OC::$server->getLogger()->notice(
391
-				'User '.$uid . ' is not marked as deleted, not cleaning up.',
391
+				'User '.$uid.' is not marked as deleted, not cleaning up.',
392 392
 				array('app' => 'user_ldap'));
393 393
 			return false;
394 394
 		}
395
-		\OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
395
+		\OC::$server->getLogger()->info('Cleaning up after user '.$uid,
396 396
 			array('app' => 'user_ldap'));
397 397
 
398 398
 		$this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
@@ -410,7 +410,7 @@  discard block
 block discarded – undo
410 410
 	 */
411 411
 	public function getHome($uid) {
412 412
 		// user Exists check required as it is not done in user proxy!
413
-		if(!$this->userExists($uid)) {
413
+		if (!$this->userExists($uid)) {
414 414
 			return false;
415 415
 		}
416 416
 
@@ -420,22 +420,22 @@  discard block
 block discarded – undo
420 420
 
421 421
 		$cacheKey = 'getHome'.$uid;
422 422
 		$path = $this->access->connection->getFromCache($cacheKey);
423
-		if(!is_null($path)) {
423
+		if (!is_null($path)) {
424 424
 			return $path;
425 425
 		}
426 426
 
427 427
 		// early return path if it is a deleted user
428 428
 		$user = $this->access->userManager->get($uid);
429
-		if($user instanceof OfflineUser) {
430
-			if($this->currentUserInDeletionProcess !== null
429
+		if ($user instanceof OfflineUser) {
430
+			if ($this->currentUserInDeletionProcess !== null
431 431
 				&& $this->currentUserInDeletionProcess === $user->getOCName()
432 432
 			) {
433 433
 				return $user->getHomePath();
434 434
 			} else {
435
-				throw new NoUserException($uid . ' is not a valid user anymore');
435
+				throw new NoUserException($uid.' is not a valid user anymore');
436 436
 			}
437 437
 		} else if ($user === null) {
438
-			throw new NoUserException($uid . ' is not a valid user anymore');
438
+			throw new NoUserException($uid.' is not a valid user anymore');
439 439
 		}
440 440
 
441 441
 		$path = $user->getHomePath();
@@ -454,12 +454,12 @@  discard block
 block discarded – undo
454 454
 			return $this->userPluginManager->getDisplayName($uid);
455 455
 		}
456 456
 
457
-		if(!$this->userExists($uid)) {
457
+		if (!$this->userExists($uid)) {
458 458
 			return false;
459 459
 		}
460 460
 
461 461
 		$cacheKey = 'getDisplayName'.$uid;
462
-		if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
462
+		if (!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
463 463
 			return $displayName;
464 464
 		}
465 465
 
@@ -476,10 +476,10 @@  discard block
 block discarded – undo
476 476
 			$this->access->username2dn($uid),
477 477
 			$this->access->connection->ldapUserDisplayName);
478 478
 
479
-		if($displayName && (count($displayName) > 0)) {
479
+		if ($displayName && (count($displayName) > 0)) {
480 480
 			$displayName = $displayName[0];
481 481
 
482
-			if (is_array($displayName2)){
482
+			if (is_array($displayName2)) {
483 483
 				$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
484 484
 			}
485 485
 
@@ -523,7 +523,7 @@  discard block
 block discarded – undo
523 523
 	 */
524 524
 	public function getDisplayNames($search = '', $limit = null, $offset = null) {
525 525
 		$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
526
-		if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
526
+		if (!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
527 527
 			return $displayNames;
528 528
 		}
529 529
 
@@ -545,12 +545,12 @@  discard block
 block discarded – undo
545 545
 	* compared with \OC\User\Backend::CREATE_USER etc.
546 546
 	*/
547 547
 	public function implementsActions($actions) {
548
-		return (bool)((Backend::CHECK_PASSWORD
548
+		return (bool) ((Backend::CHECK_PASSWORD
549 549
 			| Backend::GET_HOME
550 550
 			| Backend::GET_DISPLAYNAME
551 551
 			| (($this->access->connection->ldapUserAvatarRule !== 'none') ? Backend::PROVIDE_AVATAR : 0)
552 552
 			| Backend::COUNT_USERS
553
-			| (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
553
+			| (((int) $this->access->connection->turnOnPasswordChange === 1) ? Backend::SET_PASSWORD : 0)
554 554
 			| $this->userPluginManager->getImplementedActions())
555 555
 			& $actions);
556 556
 	}
@@ -574,7 +574,7 @@  discard block
 block discarded – undo
574 574
 
575 575
 		$filter = $this->access->getFilterForUserCount();
576 576
 		$cacheKey = 'countUsers-'.$filter;
577
-		if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
577
+		if (!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
578 578
 			return $entries;
579 579
 		}
580 580
 		$entries = $this->access->countUsers($filter);
@@ -586,7 +586,7 @@  discard block
 block discarded – undo
586 586
 	 * Backend name to be shown in user management
587 587
 	 * @return string the name of the backend to be shown
588 588
 	 */
589
-	public function getBackendName(){
589
+	public function getBackendName() {
590 590
 		return 'LDAP';
591 591
 	}
592 592
 	
@@ -624,7 +624,7 @@  discard block
 block discarded – undo
624 624
 				if (is_string($dn)) {
625 625
 					// the NC user creation work flow requires a know user id up front
626 626
 					$uuid = $this->access->getUUID($dn, true);
627
-					if(is_string($uuid)) {
627
+					if (is_string($uuid)) {
628 628
 						$this->access->mapAndAnnounceIfApplicable(
629 629
 							$this->access->getUserMapper(),
630 630
 							$dn,
Please login to merge, or discard this patch.
apps/provisioning_api/lib/Controller/UsersController.php 2 patches
Indentation   +879 added lines, -879 removed lines patch added patch discarded remove patch
@@ -53,883 +53,883 @@
 block discarded – undo
53 53
 
54 54
 class UsersController extends AUserData {
55 55
 
56
-	/** @var IAppManager */
57
-	private $appManager;
58
-	/** @var ILogger */
59
-	private $logger;
60
-	/** @var IFactory */
61
-	private $l10nFactory;
62
-	/** @var NewUserMailHelper */
63
-	private $newUserMailHelper;
64
-	/** @var FederatedFileSharingFactory */
65
-	private $federatedFileSharingFactory;
66
-	/** @var ISecureRandom */
67
-	private $secureRandom;
68
-
69
-	/**
70
-	 * @param string $appName
71
-	 * @param IRequest $request
72
-	 * @param IUserManager $userManager
73
-	 * @param IConfig $config
74
-	 * @param IAppManager $appManager
75
-	 * @param IGroupManager $groupManager
76
-	 * @param IUserSession $userSession
77
-	 * @param AccountManager $accountManager
78
-	 * @param ILogger $logger
79
-	 * @param IFactory $l10nFactory
80
-	 * @param NewUserMailHelper $newUserMailHelper
81
-	 * @param FederatedFileSharingFactory $federatedFileSharingFactory
82
-	 * @param ISecureRandom $secureRandom
83
-	 */
84
-	public function __construct(string $appName,
85
-								IRequest $request,
86
-								IUserManager $userManager,
87
-								IConfig $config,
88
-								IAppManager $appManager,
89
-								IGroupManager $groupManager,
90
-								IUserSession $userSession,
91
-								AccountManager $accountManager,
92
-								ILogger $logger,
93
-								IFactory $l10nFactory,
94
-								NewUserMailHelper $newUserMailHelper,
95
-								FederatedFileSharingFactory $federatedFileSharingFactory,
96
-								ISecureRandom $secureRandom) {
97
-		parent::__construct($appName,
98
-							$request,
99
-							$userManager,
100
-							$config,
101
-							$groupManager,
102
-							$userSession,
103
-							$accountManager);
104
-
105
-		$this->appManager = $appManager;
106
-		$this->logger = $logger;
107
-		$this->l10nFactory = $l10nFactory;
108
-		$this->newUserMailHelper = $newUserMailHelper;
109
-		$this->federatedFileSharingFactory = $federatedFileSharingFactory;
110
-		$this->secureRandom = $secureRandom;
111
-	}
112
-
113
-	/**
114
-	 * @NoAdminRequired
115
-	 *
116
-	 * returns a list of users
117
-	 *
118
-	 * @param string $search
119
-	 * @param int $limit
120
-	 * @param int $offset
121
-	 * @return DataResponse
122
-	 */
123
-	public function getUsers(string $search = '', $limit = null, $offset = 0): DataResponse {
124
-		$user = $this->userSession->getUser();
125
-		$users = [];
126
-
127
-		// Admin? Or SubAdmin?
128
-		$uid = $user->getUID();
129
-		$subAdminManager = $this->groupManager->getSubAdmin();
130
-		if ($this->groupManager->isAdmin($uid)){
131
-			$users = $this->userManager->search($search, $limit, $offset);
132
-		} else if ($subAdminManager->isSubAdmin($user)) {
133
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
134
-			foreach ($subAdminOfGroups as $key => $group) {
135
-				$subAdminOfGroups[$key] = $group->getGID();
136
-			}
137
-
138
-			$users = [];
139
-			foreach ($subAdminOfGroups as $group) {
140
-				$users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
141
-			}
142
-		}
143
-
144
-		$users = array_keys($users);
145
-
146
-		return new DataResponse([
147
-			'users' => $users
148
-		]);
149
-	}
150
-
151
-	/**
152
-	 * @NoAdminRequired
153
-	 *
154
-	 * returns a list of users and their data
155
-	 */
156
-	public function getUsersDetails(string $search = '', $limit = null, $offset = 0): DataResponse {
157
-		$currentUser = $this->userSession->getUser();
158
-		$users = [];
159
-
160
-		// Admin? Or SubAdmin?
161
-		$uid = $currentUser->getUID();
162
-		$subAdminManager = $this->groupManager->getSubAdmin();
163
-		if ($this->groupManager->isAdmin($uid)){
164
-			$users = $this->userManager->search($search, $limit, $offset);
165
-			$users = array_keys($users);
166
-		} else if ($subAdminManager->isSubAdmin($currentUser)) {
167
-			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
168
-			foreach ($subAdminOfGroups as $key => $group) {
169
-				$subAdminOfGroups[$key] = $group->getGID();
170
-			}
171
-
172
-			$users = [];
173
-			foreach ($subAdminOfGroups as $group) {
174
-				$users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
175
-			}
176
-			$users = array_merge(...$users);
177
-		}
178
-
179
-		$usersDetails = [];
180
-		foreach ($users as $userId) {
181
-			$userId = (string) $userId;
182
-			$userData = $this->getUserData($userId);
183
-			// Do not insert empty entry
184
-			if (!empty($userData)) {
185
-				$usersDetails[$userId] = $userData;
186
-			} else {
187
-				// Logged user does not have permissions to see this user
188
-				// only showing its id
189
-				$usersDetails[$userId] = ['id' => $userId];
190
-			}
191
-		}
192
-
193
-		return new DataResponse([
194
-			'users' => $usersDetails
195
-		]);
196
-	}
197
-
198
-	/**
199
-	 * @throws OCSException
200
-	 */
201
-	private function createNewUserId(): string {
202
-		$attempts = 0;
203
-		do {
204
-			$uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
205
-			if (!$this->userManager->userExists($uidCandidate)) {
206
-				return $uidCandidate;
207
-			}
208
-			$attempts++;
209
-		} while ($attempts < 10);
210
-		throw new OCSException('Could not create non-existing user id', 111);
211
-	}
212
-
213
-	/**
214
-	 * @PasswordConfirmationRequired
215
-	 * @NoAdminRequired
216
-	 *
217
-	 * @param string $userid
218
-	 * @param string $password
219
-	 * @param string $displayName
220
-	 * @param string $email
221
-	 * @param array $groups
222
-	 * @param array $subadmin
223
-	 * @param string $quota
224
-	 * @param string $language
225
-	 * @return DataResponse
226
-	 * @throws OCSException
227
-	 */
228
-	public function addUser(string $userid,
229
-							string $password = '',
230
-							string $displayName = '',
231
-							string $email = '',
232
-							array $groups = [],
233
-							array $subadmin = [],
234
-							string $quota = '',
235
-							string $language = ''): DataResponse {
236
-		$user = $this->userSession->getUser();
237
-		$isAdmin = $this->groupManager->isAdmin($user->getUID());
238
-		$subAdminManager = $this->groupManager->getSubAdmin();
239
-
240
-		if(empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
241
-			$userid = $this->createNewUserId();
242
-		}
243
-
244
-		if ($this->userManager->userExists($userid)) {
245
-			$this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
246
-			throw new OCSException('User already exists', 102);
247
-		}
248
-
249
-		if ($groups !== []) {
250
-			foreach ($groups as $group) {
251
-				if (!$this->groupManager->groupExists($group)) {
252
-					throw new OCSException('group '.$group.' does not exist', 104);
253
-				}
254
-				if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
255
-					throw new OCSException('insufficient privileges for group '. $group, 105);
256
-				}
257
-			}
258
-		} else {
259
-			if (!$isAdmin) {
260
-				throw new OCSException('no group specified (required for subadmins)', 106);
261
-			}
262
-		}
263
-
264
-		$subadminGroups = [];
265
-		if ($subadmin !== []) {
266
-			foreach ($subadmin as $groupid) {
267
-				$group = $this->groupManager->get($groupid);
268
-				// Check if group exists
269
-				if ($group === null) {
270
-					throw new OCSException('Subadmin group does not exist',  102);
271
-				}
272
-				// Check if trying to make subadmin of admin group
273
-				if ($group->getGID() === 'admin') {
274
-					throw new OCSException('Cannot create subadmins for admin group', 103);
275
-				}
276
-				// Check if has permission to promote subadmins
277
-				if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) {
278
-					throw new OCSForbiddenException('No permissions to promote subadmins');
279
-				}
280
-				$subadminGroups[] = $group;
281
-			}
282
-		}
283
-
284
-		$generatePasswordResetToken = false;
285
-		if ($password === '') {
286
-			if ($email === '') {
287
-				throw new OCSException('To send a password link to the user an email address is required.', 108);
288
-			}
289
-
290
-			$password = $this->secureRandom->generate(10);
291
-			// Make sure we pass the password_policy
292
-			$password .= $this->secureRandom->generate(2, '$!.,;:-~+*[]{}()');
293
-			$generatePasswordResetToken = true;
294
-		}
295
-
296
-		if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
297
-			throw new OCSException('Required email address was not provided', 110);
298
-		}
299
-
300
-		try {
301
-			$newUser = $this->userManager->createUser($userid, $password);
302
-			$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
303
-
304
-			foreach ($groups as $group) {
305
-				$this->groupManager->get($group)->addUser($newUser);
306
-				$this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
307
-			}
308
-			foreach ($subadminGroups as $group) {
309
-				$subAdminManager->createSubAdmin($newUser, $group);
310
-			}
311
-
312
-			if ($displayName !== '') {
313
-				$this->editUser($userid, 'display', $displayName);
314
-			}
315
-
316
-			if ($quota !== '') {
317
-				$this->editUser($userid, 'quota', $quota);
318
-			}
319
-
320
-			if ($language !== '') {
321
-				$this->editUser($userid, 'language', $language);
322
-			}
323
-
324
-			// Send new user mail only if a mail is set
325
-			if ($email !== '') {
326
-				$newUser->setEMailAddress($email);
327
-				try {
328
-					$emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
329
-					$this->newUserMailHelper->sendMail($newUser, $emailTemplate);
330
-				} catch (\Exception $e) {
331
-					$this->logger->logException($e, [
332
-						'message' => "Can't send new user mail to $email",
333
-						'level' => ILogger::ERROR,
334
-						'app' => 'ocs_api',
335
-					]);
336
-					throw new OCSException('Unable to send the invitation mail', 109);
337
-				}
338
-			}
339
-
340
-			return new DataResponse(['UserID' => $userid]);
341
-
342
-		} catch (HintException $e ) {
343
-			$this->logger->logException($e, [
344
-				'message' => 'Failed addUser attempt with hint exception.',
345
-				'level' => ILogger::WARN,
346
-				'app' => 'ocs_api',
347
-			]);
348
-			throw new OCSException($e->getHint(), 107);
349
-		} catch (\Exception $e) {
350
-			$this->logger->logException($e, [
351
-				'message' => 'Failed addUser attempt with exception.',
352
-				'level' => ILogger::ERROR,
353
-				'app' => 'ocs_api',
354
-			]);
355
-			throw new OCSException('Bad request', 101);
356
-		}
357
-	}
358
-
359
-	/**
360
-	 * @NoAdminRequired
361
-	 * @NoSubAdminRequired
362
-	 *
363
-	 * gets user info
364
-	 *
365
-	 * @param string $userId
366
-	 * @return DataResponse
367
-	 * @throws OCSException
368
-	 */
369
-	public function getUser(string $userId): DataResponse {
370
-		$data = $this->getUserData($userId);
371
-		// getUserData returns empty array if not enough permissions
372
-		if (empty($data)) {
373
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
374
-		}
375
-		return new DataResponse($data);
376
-	}
377
-
378
-	/**
379
-	 * @NoAdminRequired
380
-	 * @NoSubAdminRequired
381
-	 *
382
-	 * gets user info from the currently logged in user
383
-	 *
384
-	 * @return DataResponse
385
-	 * @throws OCSException
386
-	 */
387
-	public function getCurrentUser(): DataResponse {
388
-		$user = $this->userSession->getUser();
389
-		if ($user) {
390
-			$data =  $this->getUserData($user->getUID());
391
-			// rename "displayname" to "display-name" only for this call to keep
392
-			// the API stable.
393
-			$data['display-name'] = $data['displayname'];
394
-			unset($data['displayname']);
395
-			return new DataResponse($data);
396
-
397
-		}
398
-
399
-		throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
400
-	}
401
-
402
-	/**
403
-	 * @NoAdminRequired
404
-	 * @NoSubAdminRequired
405
-	 */
406
-	public function getEditableFields(): DataResponse {
407
-		$permittedFields = [];
408
-
409
-		// Editing self (display, email)
410
-		if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
411
-			$permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
412
-			$permittedFields[] = AccountManager::PROPERTY_EMAIL;
413
-		}
414
-
415
-		if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
416
-			$federatedFileSharing = $this->federatedFileSharingFactory->get();
417
-			$shareProvider = $federatedFileSharing->getFederatedShareProvider();
418
-			if ($shareProvider->isLookupServerUploadEnabled()) {
419
-				$permittedFields[] = AccountManager::PROPERTY_PHONE;
420
-				$permittedFields[] = AccountManager::PROPERTY_ADDRESS;
421
-				$permittedFields[] = AccountManager::PROPERTY_WEBSITE;
422
-				$permittedFields[] = AccountManager::PROPERTY_TWITTER;
423
-			}
424
-		}
425
-
426
-		return new DataResponse($permittedFields);
427
-	}
428
-
429
-	/**
430
-	 * @NoAdminRequired
431
-	 * @NoSubAdminRequired
432
-	 * @PasswordConfirmationRequired
433
-	 *
434
-	 * edit users
435
-	 *
436
-	 * @param string $userId
437
-	 * @param string $key
438
-	 * @param string $value
439
-	 * @return DataResponse
440
-	 * @throws OCSException
441
-	 */
442
-	public function editUser(string $userId, string $key, string $value): DataResponse {
443
-		$currentLoggedInUser = $this->userSession->getUser();
444
-
445
-		$targetUser = $this->userManager->get($userId);
446
-		if ($targetUser === null) {
447
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
448
-		}
449
-
450
-		$permittedFields = [];
451
-		if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
452
-			// Editing self (display, email)
453
-			if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
454
-				$permittedFields[] = 'display';
455
-				$permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
456
-				$permittedFields[] = AccountManager::PROPERTY_EMAIL;
457
-			}
458
-
459
-			$permittedFields[] = 'password';
460
-			if ($this->config->getSystemValue('force_language', false) === false ||
461
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
462
-				$permittedFields[] = 'language';
463
-			}
464
-
465
-			if ($this->config->getSystemValue('force_locale', false) === false ||
466
-				$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
467
-				$permittedFields[] = 'locale';
468
-			}
469
-
470
-			if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
471
-				$federatedFileSharing = new \OCA\FederatedFileSharing\AppInfo\Application();
472
-				$shareProvider = $federatedFileSharing->getFederatedShareProvider();
473
-				if ($shareProvider->isLookupServerUploadEnabled()) {
474
-					$permittedFields[] = AccountManager::PROPERTY_PHONE;
475
-					$permittedFields[] = AccountManager::PROPERTY_ADDRESS;
476
-					$permittedFields[] = AccountManager::PROPERTY_WEBSITE;
477
-					$permittedFields[] = AccountManager::PROPERTY_TWITTER;
478
-				}
479
-			}
480
-
481
-			// If admin they can edit their own quota
482
-			if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
483
-				$permittedFields[] = 'quota';
484
-			}
485
-		} else {
486
-			// Check if admin / subadmin
487
-			$subAdminManager = $this->groupManager->getSubAdmin();
488
-			if ($subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
489
-			|| $this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
490
-				// They have permissions over the user
491
-				$permittedFields[] = 'display';
492
-				$permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
493
-				$permittedFields[] = AccountManager::PROPERTY_EMAIL;
494
-				$permittedFields[] = 'password';
495
-				$permittedFields[] = 'language';
496
-				$permittedFields[] = 'locale';
497
-				$permittedFields[] = AccountManager::PROPERTY_PHONE;
498
-				$permittedFields[] = AccountManager::PROPERTY_ADDRESS;
499
-				$permittedFields[] = AccountManager::PROPERTY_WEBSITE;
500
-				$permittedFields[] = AccountManager::PROPERTY_TWITTER;
501
-				$permittedFields[] = 'quota';
502
-			} else {
503
-				// No rights
504
-				throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
505
-			}
506
-		}
507
-		// Check if permitted to edit this field
508
-		if (!in_array($key, $permittedFields)) {
509
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
510
-		}
511
-		// Process the edit
512
-		switch($key) {
513
-			case 'display':
514
-			case AccountManager::PROPERTY_DISPLAYNAME:
515
-				$targetUser->setDisplayName($value);
516
-				break;
517
-			case 'quota':
518
-				$quota = $value;
519
-				if ($quota !== 'none' && $quota !== 'default') {
520
-					if (is_numeric($quota)) {
521
-						$quota = (float) $quota;
522
-					} else {
523
-						$quota = \OCP\Util::computerFileSize($quota);
524
-					}
525
-					if ($quota === false) {
526
-						throw new OCSException('Invalid quota value '.$value, 103);
527
-					}
528
-					if ($quota === -1) {
529
-						$quota = 'none';
530
-					} else {
531
-						$quota = \OCP\Util::humanFileSize($quota);
532
-					}
533
-				}
534
-				$targetUser->setQuota($quota);
535
-				break;
536
-			case 'password':
537
-				try {
538
-					if (!$targetUser->canChangePassword()) {
539
-						throw new OCSException('Setting the password is not supported by the users backend', 103);
540
-					}
541
-					$targetUser->setPassword($value);
542
-				} catch (HintException $e) { // password policy error
543
-					throw new OCSException($e->getMessage(), 103);
544
-				}
545
-				break;
546
-			case 'language':
547
-				$languagesCodes = $this->l10nFactory->findAvailableLanguages();
548
-				if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
549
-					throw new OCSException('Invalid language', 102);
550
-				}
551
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
552
-				break;
553
-			case 'locale':
554
-				if (!$this->l10nFactory->localeExists($value)) {
555
-					throw new OCSException('Invalid locale', 102);
556
-				}
557
-				$this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
558
-				break;
559
-			case AccountManager::PROPERTY_EMAIL:
560
-				if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
561
-					$targetUser->setEMailAddress($value);
562
-				} else {
563
-					throw new OCSException('', 102);
564
-				}
565
-				break;
566
-			case AccountManager::PROPERTY_PHONE:
567
-			case AccountManager::PROPERTY_ADDRESS:
568
-			case AccountManager::PROPERTY_WEBSITE:
569
-			case AccountManager::PROPERTY_TWITTER:
570
-				$userAccount = $this->accountManager->getUser($targetUser);
571
-				if ($userAccount[$key]['value'] !== $value) {
572
-					$userAccount[$key]['value'] = $value;
573
-					$this->accountManager->updateUser($targetUser, $userAccount);
574
-				}
575
-				break;
576
-			default:
577
-				throw new OCSException('', 103);
578
-		}
579
-		return new DataResponse();
580
-	}
581
-
582
-	/**
583
-	 * @PasswordConfirmationRequired
584
-	 * @NoAdminRequired
585
-	 *
586
-	 * @param string $userId
587
-	 * @return DataResponse
588
-	 * @throws OCSException
589
-	 */
590
-	public function deleteUser(string $userId): DataResponse {
591
-		$currentLoggedInUser = $this->userSession->getUser();
592
-
593
-		$targetUser = $this->userManager->get($userId);
594
-
595
-		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
596
-			throw new OCSException('', 101);
597
-		}
598
-
599
-		// If not permitted
600
-		$subAdminManager = $this->groupManager->getSubAdmin();
601
-		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
602
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
603
-		}
604
-
605
-		// Go ahead with the delete
606
-		if ($targetUser->delete()) {
607
-			return new DataResponse();
608
-		} else {
609
-			throw new OCSException('', 101);
610
-		}
611
-	}
612
-
613
-	/**
614
-	 * @PasswordConfirmationRequired
615
-	 * @NoAdminRequired
616
-	 *
617
-	 * @param string $userId
618
-	 * @return DataResponse
619
-	 * @throws OCSException
620
-	 * @throws OCSForbiddenException
621
-	 */
622
-	public function disableUser(string $userId): DataResponse {
623
-		return $this->setEnabled($userId, false);
624
-	}
625
-
626
-	/**
627
-	 * @PasswordConfirmationRequired
628
-	 * @NoAdminRequired
629
-	 *
630
-	 * @param string $userId
631
-	 * @return DataResponse
632
-	 * @throws OCSException
633
-	 * @throws OCSForbiddenException
634
-	 */
635
-	public function enableUser(string $userId): DataResponse {
636
-		return $this->setEnabled($userId, true);
637
-	}
638
-
639
-	/**
640
-	 * @param string $userId
641
-	 * @param bool $value
642
-	 * @return DataResponse
643
-	 * @throws OCSException
644
-	 */
645
-	private function setEnabled(string $userId, bool $value): DataResponse {
646
-		$currentLoggedInUser = $this->userSession->getUser();
647
-
648
-		$targetUser = $this->userManager->get($userId);
649
-		if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
650
-			throw new OCSException('', 101);
651
-		}
652
-
653
-		// If not permitted
654
-		$subAdminManager = $this->groupManager->getSubAdmin();
655
-		if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
656
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
657
-		}
658
-
659
-		// enable/disable the user now
660
-		$targetUser->setEnabled($value);
661
-		return new DataResponse();
662
-	}
663
-
664
-	/**
665
-	 * @NoAdminRequired
666
-	 * @NoSubAdminRequired
667
-	 *
668
-	 * @param string $userId
669
-	 * @return DataResponse
670
-	 * @throws OCSException
671
-	 */
672
-	public function getUsersGroups(string $userId): DataResponse {
673
-		$loggedInUser = $this->userSession->getUser();
674
-
675
-		$targetUser = $this->userManager->get($userId);
676
-		if ($targetUser === null) {
677
-			throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
678
-		}
679
-
680
-		if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) {
681
-			// Self lookup or admin lookup
682
-			return new DataResponse([
683
-				'groups' => $this->groupManager->getUserGroupIds($targetUser)
684
-			]);
685
-		} else {
686
-			$subAdminManager = $this->groupManager->getSubAdmin();
687
-
688
-			// Looking up someone else
689
-			if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
690
-				// Return the group that the method caller is subadmin of for the user in question
691
-				/** @var IGroup[] $getSubAdminsGroups */
692
-				$getSubAdminsGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
693
-				foreach ($getSubAdminsGroups as $key => $group) {
694
-					$getSubAdminsGroups[$key] = $group->getGID();
695
-				}
696
-				$groups = array_intersect(
697
-					$getSubAdminsGroups,
698
-					$this->groupManager->getUserGroupIds($targetUser)
699
-				);
700
-				return new DataResponse(['groups' => $groups]);
701
-			} else {
702
-				// Not permitted
703
-				throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
704
-			}
705
-		}
706
-
707
-	}
708
-
709
-	/**
710
-	 * @PasswordConfirmationRequired
711
-	 * @NoAdminRequired
712
-	 *
713
-	 * @param string $userId
714
-	 * @param string $groupid
715
-	 * @return DataResponse
716
-	 * @throws OCSException
717
-	 */
718
-	public function addToGroup(string $userId, string $groupid = ''): DataResponse {
719
-		if ($groupid === '') {
720
-			throw new OCSException('', 101);
721
-		}
722
-
723
-		$group = $this->groupManager->get($groupid);
724
-		$targetUser = $this->userManager->get($userId);
725
-		if ($group === null) {
726
-			throw new OCSException('', 102);
727
-		}
728
-		if ($targetUser === null) {
729
-			throw new OCSException('', 103);
730
-		}
731
-
732
-		// If they're not an admin, check they are a subadmin of the group in question
733
-		$loggedInUser = $this->userSession->getUser();
734
-		$subAdminManager = $this->groupManager->getSubAdmin();
735
-		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
736
-			throw new OCSException('', 104);
737
-		}
738
-
739
-		// Add user to group
740
-		$group->addUser($targetUser);
741
-		return new DataResponse();
742
-	}
743
-
744
-	/**
745
-	 * @PasswordConfirmationRequired
746
-	 * @NoAdminRequired
747
-	 *
748
-	 * @param string $userId
749
-	 * @param string $groupid
750
-	 * @return DataResponse
751
-	 * @throws OCSException
752
-	 */
753
-	public function removeFromGroup(string $userId, string $groupid): DataResponse {
754
-		$loggedInUser = $this->userSession->getUser();
755
-
756
-		if ($groupid === null || trim($groupid) === '') {
757
-			throw new OCSException('', 101);
758
-		}
759
-
760
-		$group = $this->groupManager->get($groupid);
761
-		if ($group === null) {
762
-			throw new OCSException('', 102);
763
-		}
764
-
765
-		$targetUser = $this->userManager->get($userId);
766
-		if ($targetUser === null) {
767
-			throw new OCSException('', 103);
768
-		}
769
-
770
-		// If they're not an admin, check they are a subadmin of the group in question
771
-		$subAdminManager = $this->groupManager->getSubAdmin();
772
-		if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
773
-			throw new OCSException('', 104);
774
-		}
775
-
776
-		// Check they aren't removing themselves from 'admin' or their 'subadmin; group
777
-		if ($targetUser->getUID() === $loggedInUser->getUID()) {
778
-			if ($this->groupManager->isAdmin($loggedInUser->getUID())) {
779
-				if ($group->getGID() === 'admin') {
780
-					throw new OCSException('Cannot remove yourself from the admin group', 105);
781
-				}
782
-			} else {
783
-				// Not an admin, so the user must be a subadmin of this group, but that is not allowed.
784
-				throw new OCSException('Cannot remove yourself from this group as you are a SubAdmin', 105);
785
-			}
786
-
787
-		} else if (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
788
-			/** @var IGroup[] $subAdminGroups */
789
-			$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
790
-			$subAdminGroups = array_map(function (IGroup $subAdminGroup) {
791
-				return $subAdminGroup->getGID();
792
-			}, $subAdminGroups);
793
-			$userGroups = $this->groupManager->getUserGroupIds($targetUser);
794
-			$userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
795
-
796
-			if (count($userSubAdminGroups) <= 1) {
797
-				// Subadmin must not be able to remove a user from all their subadmin groups.
798
-				throw new OCSException('Not viable to remove user from the last group you are SubAdmin of', 105);
799
-			}
800
-		}
801
-
802
-		// Remove user from group
803
-		$group->removeUser($targetUser);
804
-		return new DataResponse();
805
-	}
806
-
807
-	/**
808
-	 * Creates a subadmin
809
-	 *
810
-	 * @PasswordConfirmationRequired
811
-	 *
812
-	 * @param string $userId
813
-	 * @param string $groupid
814
-	 * @return DataResponse
815
-	 * @throws OCSException
816
-	 */
817
-	public function addSubAdmin(string $userId, string $groupid): DataResponse {
818
-		$group = $this->groupManager->get($groupid);
819
-		$user = $this->userManager->get($userId);
820
-
821
-		// Check if the user exists
822
-		if ($user === null) {
823
-			throw new OCSException('User does not exist', 101);
824
-		}
825
-		// Check if group exists
826
-		if ($group === null) {
827
-			throw new OCSException('Group does not exist',  102);
828
-		}
829
-		// Check if trying to make subadmin of admin group
830
-		if ($group->getGID() === 'admin') {
831
-			throw new OCSException('Cannot create subadmins for admin group', 103);
832
-		}
833
-
834
-		$subAdminManager = $this->groupManager->getSubAdmin();
835
-
836
-		// We cannot be subadmin twice
837
-		if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
838
-			return new DataResponse();
839
-		}
840
-		// Go
841
-		$subAdminManager->createSubAdmin($user, $group);
842
-		return new DataResponse();
843
-	}
844
-
845
-	/**
846
-	 * Removes a subadmin from a group
847
-	 *
848
-	 * @PasswordConfirmationRequired
849
-	 *
850
-	 * @param string $userId
851
-	 * @param string $groupid
852
-	 * @return DataResponse
853
-	 * @throws OCSException
854
-	 */
855
-	public function removeSubAdmin(string $userId, string $groupid): DataResponse {
856
-		$group = $this->groupManager->get($groupid);
857
-		$user = $this->userManager->get($userId);
858
-		$subAdminManager = $this->groupManager->getSubAdmin();
859
-
860
-		// Check if the user exists
861
-		if ($user === null) {
862
-			throw new OCSException('User does not exist', 101);
863
-		}
864
-		// Check if the group exists
865
-		if ($group === null) {
866
-			throw new OCSException('Group does not exist', 101);
867
-		}
868
-		// Check if they are a subadmin of this said group
869
-		if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
870
-			throw new OCSException('User is not a subadmin of this group', 102);
871
-		}
872
-
873
-		// Go
874
-		$subAdminManager->deleteSubAdmin($user, $group);
875
-		return new DataResponse();
876
-	}
877
-
878
-	/**
879
-	 * Get the groups a user is a subadmin of
880
-	 *
881
-	 * @param string $userId
882
-	 * @return DataResponse
883
-	 * @throws OCSException
884
-	 */
885
-	public function getUserSubAdminGroups(string $userId): DataResponse {
886
-		$groups = $this->getUserSubAdminGroupsData($userId);
887
-		return new DataResponse($groups);
888
-	}
889
-
890
-	/**
891
-	 * @NoAdminRequired
892
-	 * @PasswordConfirmationRequired
893
-	 *
894
-	 * resend welcome message
895
-	 *
896
-	 * @param string $userId
897
-	 * @return DataResponse
898
-	 * @throws OCSException
899
-	 */
900
-	public function resendWelcomeMessage(string $userId): DataResponse {
901
-		$currentLoggedInUser = $this->userSession->getUser();
902
-
903
-		$targetUser = $this->userManager->get($userId);
904
-		if ($targetUser === null) {
905
-			throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
906
-		}
907
-
908
-		// Check if admin / subadmin
909
-		$subAdminManager = $this->groupManager->getSubAdmin();
910
-		if (!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
911
-			&& !$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
912
-			// No rights
913
-			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
914
-		}
915
-
916
-		$email = $targetUser->getEMailAddress();
917
-		if ($email === '' || $email === null) {
918
-			throw new OCSException('Email address not available', 101);
919
-		}
920
-
921
-		try {
922
-			$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
923
-			$this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
924
-		} catch(\Exception $e) {
925
-			$this->logger->logException($e, [
926
-				'message' => "Can't send new user mail to $email",
927
-				'level' => ILogger::ERROR,
928
-				'app' => 'settings',
929
-			]);
930
-			throw new OCSException('Sending email failed', 102);
931
-		}
932
-
933
-		return new DataResponse();
934
-	}
56
+    /** @var IAppManager */
57
+    private $appManager;
58
+    /** @var ILogger */
59
+    private $logger;
60
+    /** @var IFactory */
61
+    private $l10nFactory;
62
+    /** @var NewUserMailHelper */
63
+    private $newUserMailHelper;
64
+    /** @var FederatedFileSharingFactory */
65
+    private $federatedFileSharingFactory;
66
+    /** @var ISecureRandom */
67
+    private $secureRandom;
68
+
69
+    /**
70
+     * @param string $appName
71
+     * @param IRequest $request
72
+     * @param IUserManager $userManager
73
+     * @param IConfig $config
74
+     * @param IAppManager $appManager
75
+     * @param IGroupManager $groupManager
76
+     * @param IUserSession $userSession
77
+     * @param AccountManager $accountManager
78
+     * @param ILogger $logger
79
+     * @param IFactory $l10nFactory
80
+     * @param NewUserMailHelper $newUserMailHelper
81
+     * @param FederatedFileSharingFactory $federatedFileSharingFactory
82
+     * @param ISecureRandom $secureRandom
83
+     */
84
+    public function __construct(string $appName,
85
+                                IRequest $request,
86
+                                IUserManager $userManager,
87
+                                IConfig $config,
88
+                                IAppManager $appManager,
89
+                                IGroupManager $groupManager,
90
+                                IUserSession $userSession,
91
+                                AccountManager $accountManager,
92
+                                ILogger $logger,
93
+                                IFactory $l10nFactory,
94
+                                NewUserMailHelper $newUserMailHelper,
95
+                                FederatedFileSharingFactory $federatedFileSharingFactory,
96
+                                ISecureRandom $secureRandom) {
97
+        parent::__construct($appName,
98
+                            $request,
99
+                            $userManager,
100
+                            $config,
101
+                            $groupManager,
102
+                            $userSession,
103
+                            $accountManager);
104
+
105
+        $this->appManager = $appManager;
106
+        $this->logger = $logger;
107
+        $this->l10nFactory = $l10nFactory;
108
+        $this->newUserMailHelper = $newUserMailHelper;
109
+        $this->federatedFileSharingFactory = $federatedFileSharingFactory;
110
+        $this->secureRandom = $secureRandom;
111
+    }
112
+
113
+    /**
114
+     * @NoAdminRequired
115
+     *
116
+     * returns a list of users
117
+     *
118
+     * @param string $search
119
+     * @param int $limit
120
+     * @param int $offset
121
+     * @return DataResponse
122
+     */
123
+    public function getUsers(string $search = '', $limit = null, $offset = 0): DataResponse {
124
+        $user = $this->userSession->getUser();
125
+        $users = [];
126
+
127
+        // Admin? Or SubAdmin?
128
+        $uid = $user->getUID();
129
+        $subAdminManager = $this->groupManager->getSubAdmin();
130
+        if ($this->groupManager->isAdmin($uid)){
131
+            $users = $this->userManager->search($search, $limit, $offset);
132
+        } else if ($subAdminManager->isSubAdmin($user)) {
133
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
134
+            foreach ($subAdminOfGroups as $key => $group) {
135
+                $subAdminOfGroups[$key] = $group->getGID();
136
+            }
137
+
138
+            $users = [];
139
+            foreach ($subAdminOfGroups as $group) {
140
+                $users = array_merge($users, $this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
141
+            }
142
+        }
143
+
144
+        $users = array_keys($users);
145
+
146
+        return new DataResponse([
147
+            'users' => $users
148
+        ]);
149
+    }
150
+
151
+    /**
152
+     * @NoAdminRequired
153
+     *
154
+     * returns a list of users and their data
155
+     */
156
+    public function getUsersDetails(string $search = '', $limit = null, $offset = 0): DataResponse {
157
+        $currentUser = $this->userSession->getUser();
158
+        $users = [];
159
+
160
+        // Admin? Or SubAdmin?
161
+        $uid = $currentUser->getUID();
162
+        $subAdminManager = $this->groupManager->getSubAdmin();
163
+        if ($this->groupManager->isAdmin($uid)){
164
+            $users = $this->userManager->search($search, $limit, $offset);
165
+            $users = array_keys($users);
166
+        } else if ($subAdminManager->isSubAdmin($currentUser)) {
167
+            $subAdminOfGroups = $subAdminManager->getSubAdminsGroups($currentUser);
168
+            foreach ($subAdminOfGroups as $key => $group) {
169
+                $subAdminOfGroups[$key] = $group->getGID();
170
+            }
171
+
172
+            $users = [];
173
+            foreach ($subAdminOfGroups as $group) {
174
+                $users[] = array_keys($this->groupManager->displayNamesInGroup($group, $search, $limit, $offset));
175
+            }
176
+            $users = array_merge(...$users);
177
+        }
178
+
179
+        $usersDetails = [];
180
+        foreach ($users as $userId) {
181
+            $userId = (string) $userId;
182
+            $userData = $this->getUserData($userId);
183
+            // Do not insert empty entry
184
+            if (!empty($userData)) {
185
+                $usersDetails[$userId] = $userData;
186
+            } else {
187
+                // Logged user does not have permissions to see this user
188
+                // only showing its id
189
+                $usersDetails[$userId] = ['id' => $userId];
190
+            }
191
+        }
192
+
193
+        return new DataResponse([
194
+            'users' => $usersDetails
195
+        ]);
196
+    }
197
+
198
+    /**
199
+     * @throws OCSException
200
+     */
201
+    private function createNewUserId(): string {
202
+        $attempts = 0;
203
+        do {
204
+            $uidCandidate = $this->secureRandom->generate(10, ISecureRandom::CHAR_HUMAN_READABLE);
205
+            if (!$this->userManager->userExists($uidCandidate)) {
206
+                return $uidCandidate;
207
+            }
208
+            $attempts++;
209
+        } while ($attempts < 10);
210
+        throw new OCSException('Could not create non-existing user id', 111);
211
+    }
212
+
213
+    /**
214
+     * @PasswordConfirmationRequired
215
+     * @NoAdminRequired
216
+     *
217
+     * @param string $userid
218
+     * @param string $password
219
+     * @param string $displayName
220
+     * @param string $email
221
+     * @param array $groups
222
+     * @param array $subadmin
223
+     * @param string $quota
224
+     * @param string $language
225
+     * @return DataResponse
226
+     * @throws OCSException
227
+     */
228
+    public function addUser(string $userid,
229
+                            string $password = '',
230
+                            string $displayName = '',
231
+                            string $email = '',
232
+                            array $groups = [],
233
+                            array $subadmin = [],
234
+                            string $quota = '',
235
+                            string $language = ''): DataResponse {
236
+        $user = $this->userSession->getUser();
237
+        $isAdmin = $this->groupManager->isAdmin($user->getUID());
238
+        $subAdminManager = $this->groupManager->getSubAdmin();
239
+
240
+        if(empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
241
+            $userid = $this->createNewUserId();
242
+        }
243
+
244
+        if ($this->userManager->userExists($userid)) {
245
+            $this->logger->error('Failed addUser attempt: User already exists.', ['app' => 'ocs_api']);
246
+            throw new OCSException('User already exists', 102);
247
+        }
248
+
249
+        if ($groups !== []) {
250
+            foreach ($groups as $group) {
251
+                if (!$this->groupManager->groupExists($group)) {
252
+                    throw new OCSException('group '.$group.' does not exist', 104);
253
+                }
254
+                if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
255
+                    throw new OCSException('insufficient privileges for group '. $group, 105);
256
+                }
257
+            }
258
+        } else {
259
+            if (!$isAdmin) {
260
+                throw new OCSException('no group specified (required for subadmins)', 106);
261
+            }
262
+        }
263
+
264
+        $subadminGroups = [];
265
+        if ($subadmin !== []) {
266
+            foreach ($subadmin as $groupid) {
267
+                $group = $this->groupManager->get($groupid);
268
+                // Check if group exists
269
+                if ($group === null) {
270
+                    throw new OCSException('Subadmin group does not exist',  102);
271
+                }
272
+                // Check if trying to make subadmin of admin group
273
+                if ($group->getGID() === 'admin') {
274
+                    throw new OCSException('Cannot create subadmins for admin group', 103);
275
+                }
276
+                // Check if has permission to promote subadmins
277
+                if (!$subAdminManager->isSubAdminOfGroup($user, $group) && !$isAdmin) {
278
+                    throw new OCSForbiddenException('No permissions to promote subadmins');
279
+                }
280
+                $subadminGroups[] = $group;
281
+            }
282
+        }
283
+
284
+        $generatePasswordResetToken = false;
285
+        if ($password === '') {
286
+            if ($email === '') {
287
+                throw new OCSException('To send a password link to the user an email address is required.', 108);
288
+            }
289
+
290
+            $password = $this->secureRandom->generate(10);
291
+            // Make sure we pass the password_policy
292
+            $password .= $this->secureRandom->generate(2, '$!.,;:-~+*[]{}()');
293
+            $generatePasswordResetToken = true;
294
+        }
295
+
296
+        if ($email === '' && $this->config->getAppValue('core', 'newUser.requireEmail', 'no') === 'yes') {
297
+            throw new OCSException('Required email address was not provided', 110);
298
+        }
299
+
300
+        try {
301
+            $newUser = $this->userManager->createUser($userid, $password);
302
+            $this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
303
+
304
+            foreach ($groups as $group) {
305
+                $this->groupManager->get($group)->addUser($newUser);
306
+                $this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
307
+            }
308
+            foreach ($subadminGroups as $group) {
309
+                $subAdminManager->createSubAdmin($newUser, $group);
310
+            }
311
+
312
+            if ($displayName !== '') {
313
+                $this->editUser($userid, 'display', $displayName);
314
+            }
315
+
316
+            if ($quota !== '') {
317
+                $this->editUser($userid, 'quota', $quota);
318
+            }
319
+
320
+            if ($language !== '') {
321
+                $this->editUser($userid, 'language', $language);
322
+            }
323
+
324
+            // Send new user mail only if a mail is set
325
+            if ($email !== '') {
326
+                $newUser->setEMailAddress($email);
327
+                try {
328
+                    $emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken);
329
+                    $this->newUserMailHelper->sendMail($newUser, $emailTemplate);
330
+                } catch (\Exception $e) {
331
+                    $this->logger->logException($e, [
332
+                        'message' => "Can't send new user mail to $email",
333
+                        'level' => ILogger::ERROR,
334
+                        'app' => 'ocs_api',
335
+                    ]);
336
+                    throw new OCSException('Unable to send the invitation mail', 109);
337
+                }
338
+            }
339
+
340
+            return new DataResponse(['UserID' => $userid]);
341
+
342
+        } catch (HintException $e ) {
343
+            $this->logger->logException($e, [
344
+                'message' => 'Failed addUser attempt with hint exception.',
345
+                'level' => ILogger::WARN,
346
+                'app' => 'ocs_api',
347
+            ]);
348
+            throw new OCSException($e->getHint(), 107);
349
+        } catch (\Exception $e) {
350
+            $this->logger->logException($e, [
351
+                'message' => 'Failed addUser attempt with exception.',
352
+                'level' => ILogger::ERROR,
353
+                'app' => 'ocs_api',
354
+            ]);
355
+            throw new OCSException('Bad request', 101);
356
+        }
357
+    }
358
+
359
+    /**
360
+     * @NoAdminRequired
361
+     * @NoSubAdminRequired
362
+     *
363
+     * gets user info
364
+     *
365
+     * @param string $userId
366
+     * @return DataResponse
367
+     * @throws OCSException
368
+     */
369
+    public function getUser(string $userId): DataResponse {
370
+        $data = $this->getUserData($userId);
371
+        // getUserData returns empty array if not enough permissions
372
+        if (empty($data)) {
373
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
374
+        }
375
+        return new DataResponse($data);
376
+    }
377
+
378
+    /**
379
+     * @NoAdminRequired
380
+     * @NoSubAdminRequired
381
+     *
382
+     * gets user info from the currently logged in user
383
+     *
384
+     * @return DataResponse
385
+     * @throws OCSException
386
+     */
387
+    public function getCurrentUser(): DataResponse {
388
+        $user = $this->userSession->getUser();
389
+        if ($user) {
390
+            $data =  $this->getUserData($user->getUID());
391
+            // rename "displayname" to "display-name" only for this call to keep
392
+            // the API stable.
393
+            $data['display-name'] = $data['displayname'];
394
+            unset($data['displayname']);
395
+            return new DataResponse($data);
396
+
397
+        }
398
+
399
+        throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
400
+    }
401
+
402
+    /**
403
+     * @NoAdminRequired
404
+     * @NoSubAdminRequired
405
+     */
406
+    public function getEditableFields(): DataResponse {
407
+        $permittedFields = [];
408
+
409
+        // Editing self (display, email)
410
+        if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
411
+            $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
412
+            $permittedFields[] = AccountManager::PROPERTY_EMAIL;
413
+        }
414
+
415
+        if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
416
+            $federatedFileSharing = $this->federatedFileSharingFactory->get();
417
+            $shareProvider = $federatedFileSharing->getFederatedShareProvider();
418
+            if ($shareProvider->isLookupServerUploadEnabled()) {
419
+                $permittedFields[] = AccountManager::PROPERTY_PHONE;
420
+                $permittedFields[] = AccountManager::PROPERTY_ADDRESS;
421
+                $permittedFields[] = AccountManager::PROPERTY_WEBSITE;
422
+                $permittedFields[] = AccountManager::PROPERTY_TWITTER;
423
+            }
424
+        }
425
+
426
+        return new DataResponse($permittedFields);
427
+    }
428
+
429
+    /**
430
+     * @NoAdminRequired
431
+     * @NoSubAdminRequired
432
+     * @PasswordConfirmationRequired
433
+     *
434
+     * edit users
435
+     *
436
+     * @param string $userId
437
+     * @param string $key
438
+     * @param string $value
439
+     * @return DataResponse
440
+     * @throws OCSException
441
+     */
442
+    public function editUser(string $userId, string $key, string $value): DataResponse {
443
+        $currentLoggedInUser = $this->userSession->getUser();
444
+
445
+        $targetUser = $this->userManager->get($userId);
446
+        if ($targetUser === null) {
447
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
448
+        }
449
+
450
+        $permittedFields = [];
451
+        if ($targetUser->getUID() === $currentLoggedInUser->getUID()) {
452
+            // Editing self (display, email)
453
+            if ($this->config->getSystemValue('allow_user_to_change_display_name', true) !== false) {
454
+                $permittedFields[] = 'display';
455
+                $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
456
+                $permittedFields[] = AccountManager::PROPERTY_EMAIL;
457
+            }
458
+
459
+            $permittedFields[] = 'password';
460
+            if ($this->config->getSystemValue('force_language', false) === false ||
461
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
462
+                $permittedFields[] = 'language';
463
+            }
464
+
465
+            if ($this->config->getSystemValue('force_locale', false) === false ||
466
+                $this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
467
+                $permittedFields[] = 'locale';
468
+            }
469
+
470
+            if ($this->appManager->isEnabledForUser('federatedfilesharing')) {
471
+                $federatedFileSharing = new \OCA\FederatedFileSharing\AppInfo\Application();
472
+                $shareProvider = $federatedFileSharing->getFederatedShareProvider();
473
+                if ($shareProvider->isLookupServerUploadEnabled()) {
474
+                    $permittedFields[] = AccountManager::PROPERTY_PHONE;
475
+                    $permittedFields[] = AccountManager::PROPERTY_ADDRESS;
476
+                    $permittedFields[] = AccountManager::PROPERTY_WEBSITE;
477
+                    $permittedFields[] = AccountManager::PROPERTY_TWITTER;
478
+                }
479
+            }
480
+
481
+            // If admin they can edit their own quota
482
+            if ($this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
483
+                $permittedFields[] = 'quota';
484
+            }
485
+        } else {
486
+            // Check if admin / subadmin
487
+            $subAdminManager = $this->groupManager->getSubAdmin();
488
+            if ($subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
489
+            || $this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
490
+                // They have permissions over the user
491
+                $permittedFields[] = 'display';
492
+                $permittedFields[] = AccountManager::PROPERTY_DISPLAYNAME;
493
+                $permittedFields[] = AccountManager::PROPERTY_EMAIL;
494
+                $permittedFields[] = 'password';
495
+                $permittedFields[] = 'language';
496
+                $permittedFields[] = 'locale';
497
+                $permittedFields[] = AccountManager::PROPERTY_PHONE;
498
+                $permittedFields[] = AccountManager::PROPERTY_ADDRESS;
499
+                $permittedFields[] = AccountManager::PROPERTY_WEBSITE;
500
+                $permittedFields[] = AccountManager::PROPERTY_TWITTER;
501
+                $permittedFields[] = 'quota';
502
+            } else {
503
+                // No rights
504
+                throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
505
+            }
506
+        }
507
+        // Check if permitted to edit this field
508
+        if (!in_array($key, $permittedFields)) {
509
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
510
+        }
511
+        // Process the edit
512
+        switch($key) {
513
+            case 'display':
514
+            case AccountManager::PROPERTY_DISPLAYNAME:
515
+                $targetUser->setDisplayName($value);
516
+                break;
517
+            case 'quota':
518
+                $quota = $value;
519
+                if ($quota !== 'none' && $quota !== 'default') {
520
+                    if (is_numeric($quota)) {
521
+                        $quota = (float) $quota;
522
+                    } else {
523
+                        $quota = \OCP\Util::computerFileSize($quota);
524
+                    }
525
+                    if ($quota === false) {
526
+                        throw new OCSException('Invalid quota value '.$value, 103);
527
+                    }
528
+                    if ($quota === -1) {
529
+                        $quota = 'none';
530
+                    } else {
531
+                        $quota = \OCP\Util::humanFileSize($quota);
532
+                    }
533
+                }
534
+                $targetUser->setQuota($quota);
535
+                break;
536
+            case 'password':
537
+                try {
538
+                    if (!$targetUser->canChangePassword()) {
539
+                        throw new OCSException('Setting the password is not supported by the users backend', 103);
540
+                    }
541
+                    $targetUser->setPassword($value);
542
+                } catch (HintException $e) { // password policy error
543
+                    throw new OCSException($e->getMessage(), 103);
544
+                }
545
+                break;
546
+            case 'language':
547
+                $languagesCodes = $this->l10nFactory->findAvailableLanguages();
548
+                if (!in_array($value, $languagesCodes, true) && $value !== 'en') {
549
+                    throw new OCSException('Invalid language', 102);
550
+                }
551
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'lang', $value);
552
+                break;
553
+            case 'locale':
554
+                if (!$this->l10nFactory->localeExists($value)) {
555
+                    throw new OCSException('Invalid locale', 102);
556
+                }
557
+                $this->config->setUserValue($targetUser->getUID(), 'core', 'locale', $value);
558
+                break;
559
+            case AccountManager::PROPERTY_EMAIL:
560
+                if (filter_var($value, FILTER_VALIDATE_EMAIL) || $value === '') {
561
+                    $targetUser->setEMailAddress($value);
562
+                } else {
563
+                    throw new OCSException('', 102);
564
+                }
565
+                break;
566
+            case AccountManager::PROPERTY_PHONE:
567
+            case AccountManager::PROPERTY_ADDRESS:
568
+            case AccountManager::PROPERTY_WEBSITE:
569
+            case AccountManager::PROPERTY_TWITTER:
570
+                $userAccount = $this->accountManager->getUser($targetUser);
571
+                if ($userAccount[$key]['value'] !== $value) {
572
+                    $userAccount[$key]['value'] = $value;
573
+                    $this->accountManager->updateUser($targetUser, $userAccount);
574
+                }
575
+                break;
576
+            default:
577
+                throw new OCSException('', 103);
578
+        }
579
+        return new DataResponse();
580
+    }
581
+
582
+    /**
583
+     * @PasswordConfirmationRequired
584
+     * @NoAdminRequired
585
+     *
586
+     * @param string $userId
587
+     * @return DataResponse
588
+     * @throws OCSException
589
+     */
590
+    public function deleteUser(string $userId): DataResponse {
591
+        $currentLoggedInUser = $this->userSession->getUser();
592
+
593
+        $targetUser = $this->userManager->get($userId);
594
+
595
+        if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
596
+            throw new OCSException('', 101);
597
+        }
598
+
599
+        // If not permitted
600
+        $subAdminManager = $this->groupManager->getSubAdmin();
601
+        if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
602
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
603
+        }
604
+
605
+        // Go ahead with the delete
606
+        if ($targetUser->delete()) {
607
+            return new DataResponse();
608
+        } else {
609
+            throw new OCSException('', 101);
610
+        }
611
+    }
612
+
613
+    /**
614
+     * @PasswordConfirmationRequired
615
+     * @NoAdminRequired
616
+     *
617
+     * @param string $userId
618
+     * @return DataResponse
619
+     * @throws OCSException
620
+     * @throws OCSForbiddenException
621
+     */
622
+    public function disableUser(string $userId): DataResponse {
623
+        return $this->setEnabled($userId, false);
624
+    }
625
+
626
+    /**
627
+     * @PasswordConfirmationRequired
628
+     * @NoAdminRequired
629
+     *
630
+     * @param string $userId
631
+     * @return DataResponse
632
+     * @throws OCSException
633
+     * @throws OCSForbiddenException
634
+     */
635
+    public function enableUser(string $userId): DataResponse {
636
+        return $this->setEnabled($userId, true);
637
+    }
638
+
639
+    /**
640
+     * @param string $userId
641
+     * @param bool $value
642
+     * @return DataResponse
643
+     * @throws OCSException
644
+     */
645
+    private function setEnabled(string $userId, bool $value): DataResponse {
646
+        $currentLoggedInUser = $this->userSession->getUser();
647
+
648
+        $targetUser = $this->userManager->get($userId);
649
+        if ($targetUser === null || $targetUser->getUID() === $currentLoggedInUser->getUID()) {
650
+            throw new OCSException('', 101);
651
+        }
652
+
653
+        // If not permitted
654
+        $subAdminManager = $this->groupManager->getSubAdmin();
655
+        if (!$this->groupManager->isAdmin($currentLoggedInUser->getUID()) && !$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)) {
656
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
657
+        }
658
+
659
+        // enable/disable the user now
660
+        $targetUser->setEnabled($value);
661
+        return new DataResponse();
662
+    }
663
+
664
+    /**
665
+     * @NoAdminRequired
666
+     * @NoSubAdminRequired
667
+     *
668
+     * @param string $userId
669
+     * @return DataResponse
670
+     * @throws OCSException
671
+     */
672
+    public function getUsersGroups(string $userId): DataResponse {
673
+        $loggedInUser = $this->userSession->getUser();
674
+
675
+        $targetUser = $this->userManager->get($userId);
676
+        if ($targetUser === null) {
677
+            throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
678
+        }
679
+
680
+        if ($targetUser->getUID() === $loggedInUser->getUID() || $this->groupManager->isAdmin($loggedInUser->getUID())) {
681
+            // Self lookup or admin lookup
682
+            return new DataResponse([
683
+                'groups' => $this->groupManager->getUserGroupIds($targetUser)
684
+            ]);
685
+        } else {
686
+            $subAdminManager = $this->groupManager->getSubAdmin();
687
+
688
+            // Looking up someone else
689
+            if ($subAdminManager->isUserAccessible($loggedInUser, $targetUser)) {
690
+                // Return the group that the method caller is subadmin of for the user in question
691
+                /** @var IGroup[] $getSubAdminsGroups */
692
+                $getSubAdminsGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
693
+                foreach ($getSubAdminsGroups as $key => $group) {
694
+                    $getSubAdminsGroups[$key] = $group->getGID();
695
+                }
696
+                $groups = array_intersect(
697
+                    $getSubAdminsGroups,
698
+                    $this->groupManager->getUserGroupIds($targetUser)
699
+                );
700
+                return new DataResponse(['groups' => $groups]);
701
+            } else {
702
+                // Not permitted
703
+                throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
704
+            }
705
+        }
706
+
707
+    }
708
+
709
+    /**
710
+     * @PasswordConfirmationRequired
711
+     * @NoAdminRequired
712
+     *
713
+     * @param string $userId
714
+     * @param string $groupid
715
+     * @return DataResponse
716
+     * @throws OCSException
717
+     */
718
+    public function addToGroup(string $userId, string $groupid = ''): DataResponse {
719
+        if ($groupid === '') {
720
+            throw new OCSException('', 101);
721
+        }
722
+
723
+        $group = $this->groupManager->get($groupid);
724
+        $targetUser = $this->userManager->get($userId);
725
+        if ($group === null) {
726
+            throw new OCSException('', 102);
727
+        }
728
+        if ($targetUser === null) {
729
+            throw new OCSException('', 103);
730
+        }
731
+
732
+        // If they're not an admin, check they are a subadmin of the group in question
733
+        $loggedInUser = $this->userSession->getUser();
734
+        $subAdminManager = $this->groupManager->getSubAdmin();
735
+        if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
736
+            throw new OCSException('', 104);
737
+        }
738
+
739
+        // Add user to group
740
+        $group->addUser($targetUser);
741
+        return new DataResponse();
742
+    }
743
+
744
+    /**
745
+     * @PasswordConfirmationRequired
746
+     * @NoAdminRequired
747
+     *
748
+     * @param string $userId
749
+     * @param string $groupid
750
+     * @return DataResponse
751
+     * @throws OCSException
752
+     */
753
+    public function removeFromGroup(string $userId, string $groupid): DataResponse {
754
+        $loggedInUser = $this->userSession->getUser();
755
+
756
+        if ($groupid === null || trim($groupid) === '') {
757
+            throw new OCSException('', 101);
758
+        }
759
+
760
+        $group = $this->groupManager->get($groupid);
761
+        if ($group === null) {
762
+            throw new OCSException('', 102);
763
+        }
764
+
765
+        $targetUser = $this->userManager->get($userId);
766
+        if ($targetUser === null) {
767
+            throw new OCSException('', 103);
768
+        }
769
+
770
+        // If they're not an admin, check they are a subadmin of the group in question
771
+        $subAdminManager = $this->groupManager->getSubAdmin();
772
+        if (!$this->groupManager->isAdmin($loggedInUser->getUID()) && !$subAdminManager->isSubAdminOfGroup($loggedInUser, $group)) {
773
+            throw new OCSException('', 104);
774
+        }
775
+
776
+        // Check they aren't removing themselves from 'admin' or their 'subadmin; group
777
+        if ($targetUser->getUID() === $loggedInUser->getUID()) {
778
+            if ($this->groupManager->isAdmin($loggedInUser->getUID())) {
779
+                if ($group->getGID() === 'admin') {
780
+                    throw new OCSException('Cannot remove yourself from the admin group', 105);
781
+                }
782
+            } else {
783
+                // Not an admin, so the user must be a subadmin of this group, but that is not allowed.
784
+                throw new OCSException('Cannot remove yourself from this group as you are a SubAdmin', 105);
785
+            }
786
+
787
+        } else if (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
788
+            /** @var IGroup[] $subAdminGroups */
789
+            $subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
790
+            $subAdminGroups = array_map(function (IGroup $subAdminGroup) {
791
+                return $subAdminGroup->getGID();
792
+            }, $subAdminGroups);
793
+            $userGroups = $this->groupManager->getUserGroupIds($targetUser);
794
+            $userSubAdminGroups = array_intersect($subAdminGroups, $userGroups);
795
+
796
+            if (count($userSubAdminGroups) <= 1) {
797
+                // Subadmin must not be able to remove a user from all their subadmin groups.
798
+                throw new OCSException('Not viable to remove user from the last group you are SubAdmin of', 105);
799
+            }
800
+        }
801
+
802
+        // Remove user from group
803
+        $group->removeUser($targetUser);
804
+        return new DataResponse();
805
+    }
806
+
807
+    /**
808
+     * Creates a subadmin
809
+     *
810
+     * @PasswordConfirmationRequired
811
+     *
812
+     * @param string $userId
813
+     * @param string $groupid
814
+     * @return DataResponse
815
+     * @throws OCSException
816
+     */
817
+    public function addSubAdmin(string $userId, string $groupid): DataResponse {
818
+        $group = $this->groupManager->get($groupid);
819
+        $user = $this->userManager->get($userId);
820
+
821
+        // Check if the user exists
822
+        if ($user === null) {
823
+            throw new OCSException('User does not exist', 101);
824
+        }
825
+        // Check if group exists
826
+        if ($group === null) {
827
+            throw new OCSException('Group does not exist',  102);
828
+        }
829
+        // Check if trying to make subadmin of admin group
830
+        if ($group->getGID() === 'admin') {
831
+            throw new OCSException('Cannot create subadmins for admin group', 103);
832
+        }
833
+
834
+        $subAdminManager = $this->groupManager->getSubAdmin();
835
+
836
+        // We cannot be subadmin twice
837
+        if ($subAdminManager->isSubAdminOfGroup($user, $group)) {
838
+            return new DataResponse();
839
+        }
840
+        // Go
841
+        $subAdminManager->createSubAdmin($user, $group);
842
+        return new DataResponse();
843
+    }
844
+
845
+    /**
846
+     * Removes a subadmin from a group
847
+     *
848
+     * @PasswordConfirmationRequired
849
+     *
850
+     * @param string $userId
851
+     * @param string $groupid
852
+     * @return DataResponse
853
+     * @throws OCSException
854
+     */
855
+    public function removeSubAdmin(string $userId, string $groupid): DataResponse {
856
+        $group = $this->groupManager->get($groupid);
857
+        $user = $this->userManager->get($userId);
858
+        $subAdminManager = $this->groupManager->getSubAdmin();
859
+
860
+        // Check if the user exists
861
+        if ($user === null) {
862
+            throw new OCSException('User does not exist', 101);
863
+        }
864
+        // Check if the group exists
865
+        if ($group === null) {
866
+            throw new OCSException('Group does not exist', 101);
867
+        }
868
+        // Check if they are a subadmin of this said group
869
+        if (!$subAdminManager->isSubAdminOfGroup($user, $group)) {
870
+            throw new OCSException('User is not a subadmin of this group', 102);
871
+        }
872
+
873
+        // Go
874
+        $subAdminManager->deleteSubAdmin($user, $group);
875
+        return new DataResponse();
876
+    }
877
+
878
+    /**
879
+     * Get the groups a user is a subadmin of
880
+     *
881
+     * @param string $userId
882
+     * @return DataResponse
883
+     * @throws OCSException
884
+     */
885
+    public function getUserSubAdminGroups(string $userId): DataResponse {
886
+        $groups = $this->getUserSubAdminGroupsData($userId);
887
+        return new DataResponse($groups);
888
+    }
889
+
890
+    /**
891
+     * @NoAdminRequired
892
+     * @PasswordConfirmationRequired
893
+     *
894
+     * resend welcome message
895
+     *
896
+     * @param string $userId
897
+     * @return DataResponse
898
+     * @throws OCSException
899
+     */
900
+    public function resendWelcomeMessage(string $userId): DataResponse {
901
+        $currentLoggedInUser = $this->userSession->getUser();
902
+
903
+        $targetUser = $this->userManager->get($userId);
904
+        if ($targetUser === null) {
905
+            throw new OCSException('', \OCP\API::RESPOND_NOT_FOUND);
906
+        }
907
+
908
+        // Check if admin / subadmin
909
+        $subAdminManager = $this->groupManager->getSubAdmin();
910
+        if (!$subAdminManager->isUserAccessible($currentLoggedInUser, $targetUser)
911
+            && !$this->groupManager->isAdmin($currentLoggedInUser->getUID())) {
912
+            // No rights
913
+            throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
914
+        }
915
+
916
+        $email = $targetUser->getEMailAddress();
917
+        if ($email === '' || $email === null) {
918
+            throw new OCSException('Email address not available', 101);
919
+        }
920
+
921
+        try {
922
+            $emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
923
+            $this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
924
+        } catch(\Exception $e) {
925
+            $this->logger->logException($e, [
926
+                'message' => "Can't send new user mail to $email",
927
+                'level' => ILogger::ERROR,
928
+                'app' => 'settings',
929
+            ]);
930
+            throw new OCSException('Sending email failed', 102);
931
+        }
932
+
933
+        return new DataResponse();
934
+    }
935 935
 }
Please login to merge, or discard this patch.
Spacing   +13 added lines, -13 removed lines patch added patch discarded remove patch
@@ -127,7 +127,7 @@  discard block
 block discarded – undo
127 127
 		// Admin? Or SubAdmin?
128 128
 		$uid = $user->getUID();
129 129
 		$subAdminManager = $this->groupManager->getSubAdmin();
130
-		if ($this->groupManager->isAdmin($uid)){
130
+		if ($this->groupManager->isAdmin($uid)) {
131 131
 			$users = $this->userManager->search($search, $limit, $offset);
132 132
 		} else if ($subAdminManager->isSubAdmin($user)) {
133 133
 			$subAdminOfGroups = $subAdminManager->getSubAdminsGroups($user);
@@ -160,7 +160,7 @@  discard block
 block discarded – undo
160 160
 		// Admin? Or SubAdmin?
161 161
 		$uid = $currentUser->getUID();
162 162
 		$subAdminManager = $this->groupManager->getSubAdmin();
163
-		if ($this->groupManager->isAdmin($uid)){
163
+		if ($this->groupManager->isAdmin($uid)) {
164 164
 			$users = $this->userManager->search($search, $limit, $offset);
165 165
 			$users = array_keys($users);
166 166
 		} else if ($subAdminManager->isSubAdmin($currentUser)) {
@@ -237,7 +237,7 @@  discard block
 block discarded – undo
237 237
 		$isAdmin = $this->groupManager->isAdmin($user->getUID());
238 238
 		$subAdminManager = $this->groupManager->getSubAdmin();
239 239
 
240
-		if(empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
240
+		if (empty($userid) && $this->config->getAppValue('core', 'newUser.generateUserID', 'no') === 'yes') {
241 241
 			$userid = $this->createNewUserId();
242 242
 		}
243 243
 
@@ -252,7 +252,7 @@  discard block
 block discarded – undo
252 252
 					throw new OCSException('group '.$group.' does not exist', 104);
253 253
 				}
254 254
 				if (!$isAdmin && !$subAdminManager->isSubAdminOfGroup($user, $this->groupManager->get($group))) {
255
-					throw new OCSException('insufficient privileges for group '. $group, 105);
255
+					throw new OCSException('insufficient privileges for group '.$group, 105);
256 256
 				}
257 257
 			}
258 258
 		} else {
@@ -267,7 +267,7 @@  discard block
 block discarded – undo
267 267
 				$group = $this->groupManager->get($groupid);
268 268
 				// Check if group exists
269 269
 				if ($group === null) {
270
-					throw new OCSException('Subadmin group does not exist',  102);
270
+					throw new OCSException('Subadmin group does not exist', 102);
271 271
 				}
272 272
 				// Check if trying to make subadmin of admin group
273 273
 				if ($group->getGID() === 'admin') {
@@ -299,11 +299,11 @@  discard block
 block discarded – undo
299 299
 
300 300
 		try {
301 301
 			$newUser = $this->userManager->createUser($userid, $password);
302
-			$this->logger->info('Successful addUser call with userid: ' . $userid, ['app' => 'ocs_api']);
302
+			$this->logger->info('Successful addUser call with userid: '.$userid, ['app' => 'ocs_api']);
303 303
 
304 304
 			foreach ($groups as $group) {
305 305
 				$this->groupManager->get($group)->addUser($newUser);
306
-				$this->logger->info('Added userid ' . $userid . ' to group ' . $group, ['app' => 'ocs_api']);
306
+				$this->logger->info('Added userid '.$userid.' to group '.$group, ['app' => 'ocs_api']);
307 307
 			}
308 308
 			foreach ($subadminGroups as $group) {
309 309
 				$subAdminManager->createSubAdmin($newUser, $group);
@@ -339,7 +339,7 @@  discard block
 block discarded – undo
339 339
 
340 340
 			return new DataResponse(['UserID' => $userid]);
341 341
 
342
-		} catch (HintException $e ) {
342
+		} catch (HintException $e) {
343 343
 			$this->logger->logException($e, [
344 344
 				'message' => 'Failed addUser attempt with hint exception.',
345 345
 				'level' => ILogger::WARN,
@@ -387,7 +387,7 @@  discard block
 block discarded – undo
387 387
 	public function getCurrentUser(): DataResponse {
388 388
 		$user = $this->userSession->getUser();
389 389
 		if ($user) {
390
-			$data =  $this->getUserData($user->getUID());
390
+			$data = $this->getUserData($user->getUID());
391 391
 			// rename "displayname" to "display-name" only for this call to keep
392 392
 			// the API stable.
393 393
 			$data['display-name'] = $data['displayname'];
@@ -509,7 +509,7 @@  discard block
 block discarded – undo
509 509
 			throw new OCSException('', \OCP\API::RESPOND_UNAUTHORISED);
510 510
 		}
511 511
 		// Process the edit
512
-		switch($key) {
512
+		switch ($key) {
513 513
 			case 'display':
514 514
 			case AccountManager::PROPERTY_DISPLAYNAME:
515 515
 				$targetUser->setDisplayName($value);
@@ -787,7 +787,7 @@  discard block
 block discarded – undo
787 787
 		} else if (!$this->groupManager->isAdmin($loggedInUser->getUID())) {
788 788
 			/** @var IGroup[] $subAdminGroups */
789 789
 			$subAdminGroups = $subAdminManager->getSubAdminsGroups($loggedInUser);
790
-			$subAdminGroups = array_map(function (IGroup $subAdminGroup) {
790
+			$subAdminGroups = array_map(function(IGroup $subAdminGroup) {
791 791
 				return $subAdminGroup->getGID();
792 792
 			}, $subAdminGroups);
793 793
 			$userGroups = $this->groupManager->getUserGroupIds($targetUser);
@@ -824,7 +824,7 @@  discard block
 block discarded – undo
824 824
 		}
825 825
 		// Check if group exists
826 826
 		if ($group === null) {
827
-			throw new OCSException('Group does not exist',  102);
827
+			throw new OCSException('Group does not exist', 102);
828 828
 		}
829 829
 		// Check if trying to make subadmin of admin group
830 830
 		if ($group->getGID() === 'admin') {
@@ -921,7 +921,7 @@  discard block
 block discarded – undo
921 921
 		try {
922 922
 			$emailTemplate = $this->newUserMailHelper->generateTemplate($targetUser, false);
923 923
 			$this->newUserMailHelper->sendMail($targetUser, $emailTemplate);
924
-		} catch(\Exception $e) {
924
+		} catch (\Exception $e) {
925 925
 			$this->logger->logException($e, [
926 926
 				'message' => "Can't send new user mail to $email",
927 927
 				'level' => ILogger::ERROR,
Please login to merge, or discard this patch.