Passed
Push — master ( 25ab12...62403d )
by Blizzz
12:17 queued 22s
created
apps/user_ldap/lib/User/User.php 1 patch
Indentation   +741 added lines, -741 removed lines patch added patch discarded remove patch
@@ -49,745 +49,745 @@
 block discarded – undo
49 49
  * represents an LDAP user, gets and holds user-specific information from LDAP
50 50
  */
51 51
 class User {
52
-	/**
53
-	 * @var Access
54
-	 */
55
-	protected $access;
56
-	/**
57
-	 * @var Connection
58
-	 */
59
-	protected $connection;
60
-	/**
61
-	 * @var IConfig
62
-	 */
63
-	protected $config;
64
-	/**
65
-	 * @var FilesystemHelper
66
-	 */
67
-	protected $fs;
68
-	/**
69
-	 * @var Image
70
-	 */
71
-	protected $image;
72
-	/**
73
-	 * @var LogWrapper
74
-	 */
75
-	protected $log;
76
-	/**
77
-	 * @var IAvatarManager
78
-	 */
79
-	protected $avatarManager;
80
-	/**
81
-	 * @var IUserManager
82
-	 */
83
-	protected $userManager;
84
-	/**
85
-	 * @var INotificationManager
86
-	 */
87
-	protected $notificationManager;
88
-	/**
89
-	 * @var string
90
-	 */
91
-	protected $dn;
92
-	/**
93
-	 * @var string
94
-	 */
95
-	protected $uid;
96
-	/**
97
-	 * @var string[]
98
-	 */
99
-	protected $refreshedFeatures = array();
100
-	/**
101
-	 * @var string
102
-	 */
103
-	protected $avatarImage;
104
-
105
-	/**
106
-	 * DB config keys for user preferences
107
-	 */
108
-	const USER_PREFKEY_FIRSTLOGIN  = 'firstLoginAccomplished';
109
-	const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh';
110
-
111
-	/**
112
-	 * @brief constructor, make sure the subclasses call this one!
113
-	 * @param string $username the internal username
114
-	 * @param string $dn the LDAP DN
115
-	 * @param Access $access
116
-	 * @param IConfig $config
117
-	 * @param FilesystemHelper $fs
118
-	 * @param Image $image any empty instance
119
-	 * @param LogWrapper $log
120
-	 * @param IAvatarManager $avatarManager
121
-	 * @param IUserManager $userManager
122
-	 * @param INotificationManager $notificationManager
123
-	 */
124
-	public function __construct($username, $dn, Access $access,
125
-		IConfig $config, FilesystemHelper $fs, Image $image,
126
-		LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager,
127
-		INotificationManager $notificationManager) {
128
-
129
-		if ($username === null) {
130
-			$log->log("uid for '$dn' must not be null!", ILogger::ERROR);
131
-			throw new \InvalidArgumentException('uid must not be null!');
132
-		} else if ($username === '') {
133
-			$log->log("uid for '$dn' must not be an empty string", ILogger::ERROR);
134
-			throw new \InvalidArgumentException('uid must not be an empty string!');
135
-		}
136
-
137
-		$this->access              = $access;
138
-		$this->connection          = $access->getConnection();
139
-		$this->config              = $config;
140
-		$this->fs                  = $fs;
141
-		$this->dn                  = $dn;
142
-		$this->uid                 = $username;
143
-		$this->image               = $image;
144
-		$this->log                 = $log;
145
-		$this->avatarManager       = $avatarManager;
146
-		$this->userManager         = $userManager;
147
-		$this->notificationManager = $notificationManager;
148
-
149
-		\OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
150
-	}
151
-
152
-	/**
153
-	 * @brief updates properties like email, quota or avatar provided by LDAP
154
-	 * @return null
155
-	 */
156
-	public function update() {
157
-		if(is_null($this->dn)) {
158
-			return null;
159
-		}
160
-
161
-		$hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap',
162
-				self::USER_PREFKEY_FIRSTLOGIN, 0);
163
-
164
-		if($this->needsRefresh()) {
165
-			$this->updateEmail();
166
-			$this->updateQuota();
167
-			if($hasLoggedIn !== 0) {
168
-				//we do not need to try it, when the user has not been logged in
169
-				//before, because the file system will not be ready.
170
-				$this->updateAvatar();
171
-				//in order to get an avatar as soon as possible, mark the user
172
-				//as refreshed only when updating the avatar did happen
173
-				$this->markRefreshTime();
174
-			}
175
-		}
176
-	}
177
-
178
-	/**
179
-	 * marks a user as deleted
180
-	 *
181
-	 * @throws \OCP\PreConditionNotMetException
182
-	 */
183
-	public function markUser() {
184
-		$curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0');
185
-		if($curValue === '1') {
186
-			// the user is already marked, do not write to DB again
187
-			return;
188
-		}
189
-		$this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1');
190
-		$this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time());
191
-	}
192
-
193
-	/**
194
-	 * processes results from LDAP for attributes as returned by getAttributesToRead()
195
-	 * @param array $ldapEntry the user entry as retrieved from LDAP
196
-	 */
197
-	public function processAttributes($ldapEntry) {
198
-		$this->markRefreshTime();
199
-		//Quota
200
-		$attr = strtolower($this->connection->ldapQuotaAttribute);
201
-		if(isset($ldapEntry[$attr])) {
202
-			$this->updateQuota($ldapEntry[$attr][0]);
203
-		} else {
204
-			if ($this->connection->ldapQuotaDefault !== '') {
205
-				$this->updateQuota();
206
-			}
207
-		}
208
-		unset($attr);
209
-
210
-		//displayName
211
-		$displayName = $displayName2 = '';
212
-		$attr = strtolower($this->connection->ldapUserDisplayName);
213
-		if(isset($ldapEntry[$attr])) {
214
-			$displayName = (string)$ldapEntry[$attr][0];
215
-		}
216
-		$attr = strtolower($this->connection->ldapUserDisplayName2);
217
-		if(isset($ldapEntry[$attr])) {
218
-			$displayName2 = (string)$ldapEntry[$attr][0];
219
-		}
220
-		if ($displayName !== '') {
221
-			$this->composeAndStoreDisplayName($displayName, $displayName2);
222
-			$this->access->cacheUserDisplayName(
223
-				$this->getUsername(),
224
-				$displayName,
225
-				$displayName2
226
-			);
227
-		}
228
-		unset($attr);
229
-
230
-		//Email
231
-		//email must be stored after displayname, because it would cause a user
232
-		//change event that will trigger fetching the display name again
233
-		$attr = strtolower($this->connection->ldapEmailAttribute);
234
-		if(isset($ldapEntry[$attr])) {
235
-			$this->updateEmail($ldapEntry[$attr][0]);
236
-		}
237
-		unset($attr);
238
-
239
-		// LDAP Username, needed for s2s sharing
240
-		if(isset($ldapEntry['uid'])) {
241
-			$this->storeLDAPUserName($ldapEntry['uid'][0]);
242
-		} else if(isset($ldapEntry['samaccountname'])) {
243
-			$this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
244
-		}
245
-
246
-		//homePath
247
-		if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) {
248
-			$attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
249
-			if(isset($ldapEntry[$attr])) {
250
-				$this->access->cacheUserHome(
251
-					$this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
252
-			}
253
-		}
254
-
255
-		//memberOf groups
256
-		$cacheKey = 'getMemberOf'.$this->getUsername();
257
-		$groups = false;
258
-		if(isset($ldapEntry['memberof'])) {
259
-			$groups = $ldapEntry['memberof'];
260
-		}
261
-		$this->connection->writeToCache($cacheKey, $groups);
262
-
263
-		//external storage var
264
-		$attr = strtolower($this->connection->ldapExtStorageHomeAttribute);
265
-		if(isset($ldapEntry[$attr])) {
266
-			$this->updateExtStorageHome($ldapEntry[$attr][0]);
267
-		}
268
-		unset($attr);
269
-
270
-		//Avatar
271
-		/** @var Connection $connection */
272
-		$connection = $this->access->getConnection();
273
-		$attributes = $connection->resolveRule('avatar');
274
-		foreach ($attributes as $attribute)  {
275
-			if(isset($ldapEntry[$attribute])) {
276
-				$this->avatarImage = $ldapEntry[$attribute][0];
277
-				// the call to the method that saves the avatar in the file
278
-				// system must be postponed after the login. It is to ensure
279
-				// external mounts are mounted properly (e.g. with login
280
-				// credentials from the session).
281
-				\OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
282
-				break;
283
-			}
284
-		}
285
-	}
286
-
287
-	/**
288
-	 * @brief returns the LDAP DN of the user
289
-	 * @return string
290
-	 */
291
-	public function getDN() {
292
-		return $this->dn;
293
-	}
294
-
295
-	/**
296
-	 * @brief returns the Nextcloud internal username of the user
297
-	 * @return string
298
-	 */
299
-	public function getUsername() {
300
-		return $this->uid;
301
-	}
302
-
303
-	/**
304
-	 * returns the home directory of the user if specified by LDAP settings
305
-	 * @param string $valueFromLDAP
306
-	 * @return bool|string
307
-	 * @throws \Exception
308
-	 */
309
-	public function getHomePath($valueFromLDAP = null) {
310
-		$path = (string)$valueFromLDAP;
311
-		$attr = null;
312
-
313
-		if (is_null($valueFromLDAP)
314
-		   && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0
315
-		   && $this->access->connection->homeFolderNamingRule !== 'attr:')
316
-		{
317
-			$attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
318
-			$homedir = $this->access->readAttribute(
319
-				$this->access->username2dn($this->getUsername()), $attr);
320
-			if ($homedir && isset($homedir[0])) {
321
-				$path = $homedir[0];
322
-			}
323
-		}
324
-
325
-		if ($path !== '') {
326
-			//if attribute's value is an absolute path take this, otherwise append it to data dir
327
-			//check for / at the beginning or pattern c:\ resp. c:/
328
-			if(   '/' !== $path[0]
329
-			   && !(3 < strlen($path) && ctype_alpha($path[0])
330
-			       && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2]))
331
-			) {
332
-				$path = $this->config->getSystemValue('datadirectory',
333
-						\OC::$SERVERROOT.'/data' ) . '/' . $path;
334
-			}
335
-			//we need it to store it in the DB as well in case a user gets
336
-			//deleted so we can clean up afterwards
337
-			$this->config->setUserValue(
338
-				$this->getUsername(), 'user_ldap', 'homePath', $path
339
-			);
340
-			return $path;
341
-		}
342
-
343
-		if(    !is_null($attr)
344
-			&& $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true)
345
-		) {
346
-			// a naming rule attribute is defined, but it doesn't exist for that LDAP user
347
-			throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
348
-		}
349
-
350
-		//false will apply default behaviour as defined and done by OC_User
351
-		$this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
352
-		return false;
353
-	}
354
-
355
-	public function getMemberOfGroups() {
356
-		$cacheKey = 'getMemberOf'.$this->getUsername();
357
-		$memberOfGroups = $this->connection->getFromCache($cacheKey);
358
-		if(!is_null($memberOfGroups)) {
359
-			return $memberOfGroups;
360
-		}
361
-		$groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
362
-		$this->connection->writeToCache($cacheKey, $groupDNs);
363
-		return $groupDNs;
364
-	}
365
-
366
-	/**
367
-	 * @brief reads the image from LDAP that shall be used as Avatar
368
-	 * @return string data (provided by LDAP) | false
369
-	 */
370
-	public function getAvatarImage() {
371
-		if(!is_null($this->avatarImage)) {
372
-			return $this->avatarImage;
373
-		}
374
-
375
-		$this->avatarImage = false;
376
-		/** @var Connection $connection */
377
-		$connection = $this->access->getConnection();
378
-		$attributes = $connection->resolveRule('avatar');
379
-		foreach($attributes as $attribute) {
380
-			$result = $this->access->readAttribute($this->dn, $attribute);
381
-			if($result !== false && is_array($result) && isset($result[0])) {
382
-				$this->avatarImage = $result[0];
383
-				break;
384
-			}
385
-		}
386
-
387
-		return $this->avatarImage;
388
-	}
389
-
390
-	/**
391
-	 * @brief marks the user as having logged in at least once
392
-	 * @return null
393
-	 */
394
-	public function markLogin() {
395
-		$this->config->setUserValue(
396
-			$this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1);
397
-	}
398
-
399
-	/**
400
-	 * @brief marks the time when user features like email have been updated
401
-	 * @return null
402
-	 */
403
-	public function markRefreshTime() {
404
-		$this->config->setUserValue(
405
-			$this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time());
406
-	}
407
-
408
-	/**
409
-	 * @brief checks whether user features needs to be updated again by
410
-	 * comparing the difference of time of the last refresh to now with the
411
-	 * desired interval
412
-	 * @return bool
413
-	 */
414
-	private function needsRefresh() {
415
-		$lastChecked = $this->config->getUserValue($this->uid, 'user_ldap',
416
-			self::USER_PREFKEY_LASTREFRESH, 0);
417
-
418
-		if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) {
419
-			return false;
420
-		}
421
-		return  true;
422
-	}
423
-
424
-	/**
425
-	 * Stores a key-value pair in relation to this user
426
-	 *
427
-	 * @param string $key
428
-	 * @param string $value
429
-	 */
430
-	private function store($key, $value) {
431
-		$this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
432
-	}
433
-
434
-	/**
435
-	 * Composes the display name and stores it in the database. The final
436
-	 * display name is returned.
437
-	 *
438
-	 * @param string $displayName
439
-	 * @param string $displayName2
440
-	 * @return string the effective display name
441
-	 */
442
-	public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
443
-		$displayName2 = (string)$displayName2;
444
-		if($displayName2 !== '') {
445
-			$displayName .= ' (' . $displayName2 . ')';
446
-		}
447
-		$oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null);
448
-		if ($oldName !== $displayName)  {
449
-			$this->store('displayName', $displayName);
450
-			$user = $this->userManager->get($this->getUsername());
451
-			if (!empty($oldName) && $user instanceof \OC\User\User) {
452
-				// if it was empty, it would be a new record, not a change emitting the trigger could
453
-				// potentially cause a UniqueConstraintViolationException, depending on some factors.
454
-				$user->triggerChange('displayName', $displayName, $oldName);
455
-			}
456
-		}
457
-		return $displayName;
458
-	}
459
-
460
-	/**
461
-	 * Stores the LDAP Username in the Database
462
-	 * @param string $userName
463
-	 */
464
-	public function storeLDAPUserName($userName) {
465
-		$this->store('uid', $userName);
466
-	}
467
-
468
-	/**
469
-	 * @brief checks whether an update method specified by feature was run
470
-	 * already. If not, it will marked like this, because it is expected that
471
-	 * the method will be run, when false is returned.
472
-	 * @param string $feature email | quota | avatar (can be extended)
473
-	 * @return bool
474
-	 */
475
-	private function wasRefreshed($feature) {
476
-		if(isset($this->refreshedFeatures[$feature])) {
477
-			return true;
478
-		}
479
-		$this->refreshedFeatures[$feature] = 1;
480
-		return false;
481
-	}
482
-
483
-	/**
484
-	 * fetches the email from LDAP and stores it as Nextcloud user value
485
-	 * @param string $valueFromLDAP if known, to save an LDAP read request
486
-	 * @return null
487
-	 */
488
-	public function updateEmail($valueFromLDAP = null) {
489
-		if($this->wasRefreshed('email')) {
490
-			return;
491
-		}
492
-		$email = (string)$valueFromLDAP;
493
-		if(is_null($valueFromLDAP)) {
494
-			$emailAttribute = $this->connection->ldapEmailAttribute;
495
-			if ($emailAttribute !== '') {
496
-				$aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
497
-				if(is_array($aEmail) && (count($aEmail) > 0)) {
498
-					$email = (string)$aEmail[0];
499
-				}
500
-			}
501
-		}
502
-		if ($email !== '') {
503
-			$user = $this->userManager->get($this->uid);
504
-			if (!is_null($user)) {
505
-				$currentEmail = (string)$user->getEMailAddress();
506
-				if ($currentEmail !== $email) {
507
-					$user->setEMailAddress($email);
508
-				}
509
-			}
510
-		}
511
-	}
512
-
513
-	/**
514
-	 * Overall process goes as follow:
515
-	 * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
516
-	 * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
517
-	 * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
518
-	 * 4. check if the target user exists and set the quota for the user.
519
-	 *
520
-	 * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
521
-	 * parameter can be passed with the value of the attribute. This value will be considered as the
522
-	 * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
523
-	 * fetch all the user's attributes in one call and use the fetched values in this function.
524
-	 * The expected value for that parameter is a string describing the quota for the user. Valid
525
-	 * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
526
-	 * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
527
-	 *
528
-	 * fetches the quota from LDAP and stores it as Nextcloud user value
529
-	 * @param string $valueFromLDAP the quota attribute's value can be passed,
530
-	 * to save the readAttribute request
531
-	 * @return null
532
-	 */
533
-	public function updateQuota($valueFromLDAP = null) {
534
-		if($this->wasRefreshed('quota')) {
535
-			return;
536
-		}
537
-
538
-		$quotaAttribute = $this->connection->ldapQuotaAttribute;
539
-		$defaultQuota = $this->connection->ldapQuotaDefault;
540
-		if($quotaAttribute === '' && $defaultQuota === '') {
541
-			return;
542
-		}
543
-
544
-		$quota = false;
545
-		if(is_null($valueFromLDAP) && $quotaAttribute !== '') {
546
-			$aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
547
-			if($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) {
548
-				$quota = $aQuota[0];
549
-			} else if(is_array($aQuota) && isset($aQuota[0])) {
550
-				$this->log->log('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::DEBUG);
551
-			}
552
-		} else if ($this->verifyQuotaValue($valueFromLDAP)) {
553
-			$quota = $valueFromLDAP;
554
-		} else {
555
-			$this->log->log('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::DEBUG);
556
-		}
557
-
558
-		if ($quota === false && $this->verifyQuotaValue($defaultQuota)) {
559
-			// quota not found using the LDAP attribute (or not parseable). Try the default quota
560
-			$quota = $defaultQuota;
561
-		} else if($quota === false) {
562
-			$this->log->log('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::DEBUG);
563
-			return;
564
-		}
565
-
566
-		$targetUser = $this->userManager->get($this->uid);
567
-		if ($targetUser instanceof IUser) {
568
-			$targetUser->setQuota($quota);
569
-		} else {
570
-			$this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::INFO);
571
-		}
572
-	}
573
-
574
-	private function verifyQuotaValue($quotaValue) {
575
-		return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
576
-	}
577
-
578
-	/**
579
-	 * called by a post_login hook to save the avatar picture
580
-	 *
581
-	 * @param array $params
582
-	 */
583
-	public function updateAvatarPostLogin($params) {
584
-		if(isset($params['uid']) && $params['uid'] === $this->getUsername()) {
585
-			$this->updateAvatar();
586
-		}
587
-	}
588
-
589
-	/**
590
-	 * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
591
-	 * @return bool
592
-	 */
593
-	public function updateAvatar($force = false) {
594
-		if(!$force && $this->wasRefreshed('avatar')) {
595
-			return false;
596
-		}
597
-		$avatarImage = $this->getAvatarImage();
598
-		if($avatarImage === false) {
599
-			//not set, nothing left to do;
600
-			return false;
601
-		}
602
-
603
-		if(!$this->image->loadFromBase64(base64_encode($avatarImage))) {
604
-			return false;
605
-		}
606
-
607
-		// use the checksum before modifications
608
-		$checksum = md5($this->image->data());
609
-
610
-		if($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '')) {
611
-			return true;
612
-		}
613
-
614
-		$isSet = $this->setOwnCloudAvatar();
615
-
616
-		if($isSet) {
617
-			// save checksum only after successful setting
618
-			$this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum);
619
-		}
620
-
621
-		return $isSet;
622
-	}
623
-
624
-	/**
625
-	 * @brief sets an image as Nextcloud avatar
626
-	 * @return bool
627
-	 */
628
-	private function setOwnCloudAvatar() {
629
-		if(!$this->image->valid()) {
630
-			$this->log->log('avatar image data from LDAP invalid for '.$this->dn, ILogger::ERROR);
631
-			return false;
632
-		}
633
-
634
-
635
-		//make sure it is a square and not bigger than 128x128
636
-		$size = min([$this->image->width(), $this->image->height(), 128]);
637
-		if(!$this->image->centerCrop($size)) {
638
-			$this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR);
639
-			return false;
640
-		}
641
-
642
-		if(!$this->fs->isLoaded()) {
643
-			$this->fs->setup($this->uid);
644
-		}
645
-
646
-		try {
647
-			$avatar = $this->avatarManager->getAvatar($this->uid);
648
-			$avatar->set($this->image);
649
-			return true;
650
-		} catch (\Exception $e) {
651
-			\OC::$server->getLogger()->logException($e, [
652
-				'message' => 'Could not set avatar for ' . $this->dn,
653
-				'level' => ILogger::INFO,
654
-				'app' => 'user_ldap',
655
-			]);
656
-		}
657
-		return false;
658
-	}
659
-
660
-	/**
661
-	 * @throws AttributeNotSet
662
-	 * @throws \OC\ServerNotAvailableException
663
-	 * @throws \OCP\PreConditionNotMetException
664
-	 */
665
-	public function getExtStorageHome():string {
666
-		$value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', '');
667
-		if ($value !== '') {
668
-			return $value;
669
-		}
670
-
671
-		$value = $this->updateExtStorageHome();
672
-		if ($value !== '') {
673
-			return $value;
674
-		}
675
-
676
-		throw new AttributeNotSet(sprintf(
677
-			'external home storage attribute yield no value for %s', $this->getUsername()
678
-		));
679
-	}
680
-
681
-	/**
682
-	 * @throws \OCP\PreConditionNotMetException
683
-	 * @throws \OC\ServerNotAvailableException
684
-	 */
685
-	public function updateExtStorageHome(string $valueFromLDAP = null):string {
686
-		if ($valueFromLDAP === null) {
687
-			$extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute);
688
-		} else {
689
-			$extHomeValues = [$valueFromLDAP];
690
-		}
691
-		if ($extHomeValues && isset($extHomeValues[0])) {
692
-			$extHome = $extHomeValues[0];
693
-			$this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome);
694
-			return $extHome;
695
-		} else {
696
-			$this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome');
697
-			return '';
698
-		}
699
-	}
700
-
701
-	/**
702
-	 * called by a post_login hook to handle password expiry
703
-	 *
704
-	 * @param array $params
705
-	 */
706
-	public function handlePasswordExpiry($params) {
707
-		$ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
708
-		if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
709
-			return;//password expiry handling disabled
710
-		}
711
-		$uid = $params['uid'];
712
-		if (isset($uid) && $uid === $this->getUsername()) {
713
-			//retrieve relevant user attributes
714
-			$result = $this->access->search('objectclass=*', array($this->dn), ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
715
-
716
-			if (array_key_exists('pwdpolicysubentry', $result[0])) {
717
-				$pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
718
-				if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){
719
-					$ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
720
-				}
721
-			}
722
-
723
-			$pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : [];
724
-			$pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : [];
725
-			$pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : [];
726
-
727
-			//retrieve relevant password policy attributes
728
-			$cacheKey = 'ppolicyAttributes' . $ppolicyDN;
729
-			$result = $this->connection->getFromCache($cacheKey);
730
-			if(is_null($result)) {
731
-				$result = $this->access->search('objectclass=*', array($ppolicyDN), ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
732
-				$this->connection->writeToCache($cacheKey, $result);
733
-			}
734
-
735
-			$pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : [];
736
-			$pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : [];
737
-			$pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : [];
738
-
739
-			//handle grace login
740
-			if (!empty($pwdGraceUseTime)) { //was this a grace login?
741
-				if (!empty($pwdGraceAuthNLimit)
742
-					&& count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available?
743
-					$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
744
-					header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
745
-					'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
746
-				} else { //no more grace login available
747
-					header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
748
-					'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid)));
749
-				}
750
-				exit();
751
-			}
752
-			//handle pwdReset attribute
753
-			if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change his password
754
-				$this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
755
-				header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
756
-				'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
757
-				exit();
758
-			}
759
-			//handle password expiry warning
760
-			if (!empty($pwdChangedTime)) {
761
-				if (!empty($pwdMaxAge)
762
-					&& !empty($pwdExpireWarning)) {
763
-					$pwdMaxAgeInt = (int)$pwdMaxAge[0];
764
-					$pwdExpireWarningInt = (int)$pwdExpireWarning[0];
765
-					if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){
766
-						$pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
767
-						$pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S'));
768
-						$currentDateTime = new \DateTime();
769
-						$secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
770
-						if ($secondsToExpiry <= $pwdExpireWarningInt) {
771
-							//remove last password expiry warning if any
772
-							$notification = $this->notificationManager->createNotification();
773
-							$notification->setApp('user_ldap')
774
-								->setUser($uid)
775
-								->setObject('pwd_exp_warn', $uid)
776
-							;
777
-							$this->notificationManager->markProcessed($notification);
778
-							//create new password expiry warning
779
-							$notification = $this->notificationManager->createNotification();
780
-							$notification->setApp('user_ldap')
781
-								->setUser($uid)
782
-								->setDateTime($currentDateTime)
783
-								->setObject('pwd_exp_warn', $uid)
784
-								->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)])
785
-							;
786
-							$this->notificationManager->notify($notification);
787
-						}
788
-					}
789
-				}
790
-			}
791
-		}
792
-	}
52
+    /**
53
+     * @var Access
54
+     */
55
+    protected $access;
56
+    /**
57
+     * @var Connection
58
+     */
59
+    protected $connection;
60
+    /**
61
+     * @var IConfig
62
+     */
63
+    protected $config;
64
+    /**
65
+     * @var FilesystemHelper
66
+     */
67
+    protected $fs;
68
+    /**
69
+     * @var Image
70
+     */
71
+    protected $image;
72
+    /**
73
+     * @var LogWrapper
74
+     */
75
+    protected $log;
76
+    /**
77
+     * @var IAvatarManager
78
+     */
79
+    protected $avatarManager;
80
+    /**
81
+     * @var IUserManager
82
+     */
83
+    protected $userManager;
84
+    /**
85
+     * @var INotificationManager
86
+     */
87
+    protected $notificationManager;
88
+    /**
89
+     * @var string
90
+     */
91
+    protected $dn;
92
+    /**
93
+     * @var string
94
+     */
95
+    protected $uid;
96
+    /**
97
+     * @var string[]
98
+     */
99
+    protected $refreshedFeatures = array();
100
+    /**
101
+     * @var string
102
+     */
103
+    protected $avatarImage;
104
+
105
+    /**
106
+     * DB config keys for user preferences
107
+     */
108
+    const USER_PREFKEY_FIRSTLOGIN  = 'firstLoginAccomplished';
109
+    const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh';
110
+
111
+    /**
112
+     * @brief constructor, make sure the subclasses call this one!
113
+     * @param string $username the internal username
114
+     * @param string $dn the LDAP DN
115
+     * @param Access $access
116
+     * @param IConfig $config
117
+     * @param FilesystemHelper $fs
118
+     * @param Image $image any empty instance
119
+     * @param LogWrapper $log
120
+     * @param IAvatarManager $avatarManager
121
+     * @param IUserManager $userManager
122
+     * @param INotificationManager $notificationManager
123
+     */
124
+    public function __construct($username, $dn, Access $access,
125
+        IConfig $config, FilesystemHelper $fs, Image $image,
126
+        LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager,
127
+        INotificationManager $notificationManager) {
128
+
129
+        if ($username === null) {
130
+            $log->log("uid for '$dn' must not be null!", ILogger::ERROR);
131
+            throw new \InvalidArgumentException('uid must not be null!');
132
+        } else if ($username === '') {
133
+            $log->log("uid for '$dn' must not be an empty string", ILogger::ERROR);
134
+            throw new \InvalidArgumentException('uid must not be an empty string!');
135
+        }
136
+
137
+        $this->access              = $access;
138
+        $this->connection          = $access->getConnection();
139
+        $this->config              = $config;
140
+        $this->fs                  = $fs;
141
+        $this->dn                  = $dn;
142
+        $this->uid                 = $username;
143
+        $this->image               = $image;
144
+        $this->log                 = $log;
145
+        $this->avatarManager       = $avatarManager;
146
+        $this->userManager         = $userManager;
147
+        $this->notificationManager = $notificationManager;
148
+
149
+        \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry');
150
+    }
151
+
152
+    /**
153
+     * @brief updates properties like email, quota or avatar provided by LDAP
154
+     * @return null
155
+     */
156
+    public function update() {
157
+        if(is_null($this->dn)) {
158
+            return null;
159
+        }
160
+
161
+        $hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap',
162
+                self::USER_PREFKEY_FIRSTLOGIN, 0);
163
+
164
+        if($this->needsRefresh()) {
165
+            $this->updateEmail();
166
+            $this->updateQuota();
167
+            if($hasLoggedIn !== 0) {
168
+                //we do not need to try it, when the user has not been logged in
169
+                //before, because the file system will not be ready.
170
+                $this->updateAvatar();
171
+                //in order to get an avatar as soon as possible, mark the user
172
+                //as refreshed only when updating the avatar did happen
173
+                $this->markRefreshTime();
174
+            }
175
+        }
176
+    }
177
+
178
+    /**
179
+     * marks a user as deleted
180
+     *
181
+     * @throws \OCP\PreConditionNotMetException
182
+     */
183
+    public function markUser() {
184
+        $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0');
185
+        if($curValue === '1') {
186
+            // the user is already marked, do not write to DB again
187
+            return;
188
+        }
189
+        $this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1');
190
+        $this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time());
191
+    }
192
+
193
+    /**
194
+     * processes results from LDAP for attributes as returned by getAttributesToRead()
195
+     * @param array $ldapEntry the user entry as retrieved from LDAP
196
+     */
197
+    public function processAttributes($ldapEntry) {
198
+        $this->markRefreshTime();
199
+        //Quota
200
+        $attr = strtolower($this->connection->ldapQuotaAttribute);
201
+        if(isset($ldapEntry[$attr])) {
202
+            $this->updateQuota($ldapEntry[$attr][0]);
203
+        } else {
204
+            if ($this->connection->ldapQuotaDefault !== '') {
205
+                $this->updateQuota();
206
+            }
207
+        }
208
+        unset($attr);
209
+
210
+        //displayName
211
+        $displayName = $displayName2 = '';
212
+        $attr = strtolower($this->connection->ldapUserDisplayName);
213
+        if(isset($ldapEntry[$attr])) {
214
+            $displayName = (string)$ldapEntry[$attr][0];
215
+        }
216
+        $attr = strtolower($this->connection->ldapUserDisplayName2);
217
+        if(isset($ldapEntry[$attr])) {
218
+            $displayName2 = (string)$ldapEntry[$attr][0];
219
+        }
220
+        if ($displayName !== '') {
221
+            $this->composeAndStoreDisplayName($displayName, $displayName2);
222
+            $this->access->cacheUserDisplayName(
223
+                $this->getUsername(),
224
+                $displayName,
225
+                $displayName2
226
+            );
227
+        }
228
+        unset($attr);
229
+
230
+        //Email
231
+        //email must be stored after displayname, because it would cause a user
232
+        //change event that will trigger fetching the display name again
233
+        $attr = strtolower($this->connection->ldapEmailAttribute);
234
+        if(isset($ldapEntry[$attr])) {
235
+            $this->updateEmail($ldapEntry[$attr][0]);
236
+        }
237
+        unset($attr);
238
+
239
+        // LDAP Username, needed for s2s sharing
240
+        if(isset($ldapEntry['uid'])) {
241
+            $this->storeLDAPUserName($ldapEntry['uid'][0]);
242
+        } else if(isset($ldapEntry['samaccountname'])) {
243
+            $this->storeLDAPUserName($ldapEntry['samaccountname'][0]);
244
+        }
245
+
246
+        //homePath
247
+        if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) {
248
+            $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:')));
249
+            if(isset($ldapEntry[$attr])) {
250
+                $this->access->cacheUserHome(
251
+                    $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0]));
252
+            }
253
+        }
254
+
255
+        //memberOf groups
256
+        $cacheKey = 'getMemberOf'.$this->getUsername();
257
+        $groups = false;
258
+        if(isset($ldapEntry['memberof'])) {
259
+            $groups = $ldapEntry['memberof'];
260
+        }
261
+        $this->connection->writeToCache($cacheKey, $groups);
262
+
263
+        //external storage var
264
+        $attr = strtolower($this->connection->ldapExtStorageHomeAttribute);
265
+        if(isset($ldapEntry[$attr])) {
266
+            $this->updateExtStorageHome($ldapEntry[$attr][0]);
267
+        }
268
+        unset($attr);
269
+
270
+        //Avatar
271
+        /** @var Connection $connection */
272
+        $connection = $this->access->getConnection();
273
+        $attributes = $connection->resolveRule('avatar');
274
+        foreach ($attributes as $attribute)  {
275
+            if(isset($ldapEntry[$attribute])) {
276
+                $this->avatarImage = $ldapEntry[$attribute][0];
277
+                // the call to the method that saves the avatar in the file
278
+                // system must be postponed after the login. It is to ensure
279
+                // external mounts are mounted properly (e.g. with login
280
+                // credentials from the session).
281
+                \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin');
282
+                break;
283
+            }
284
+        }
285
+    }
286
+
287
+    /**
288
+     * @brief returns the LDAP DN of the user
289
+     * @return string
290
+     */
291
+    public function getDN() {
292
+        return $this->dn;
293
+    }
294
+
295
+    /**
296
+     * @brief returns the Nextcloud internal username of the user
297
+     * @return string
298
+     */
299
+    public function getUsername() {
300
+        return $this->uid;
301
+    }
302
+
303
+    /**
304
+     * returns the home directory of the user if specified by LDAP settings
305
+     * @param string $valueFromLDAP
306
+     * @return bool|string
307
+     * @throws \Exception
308
+     */
309
+    public function getHomePath($valueFromLDAP = null) {
310
+        $path = (string)$valueFromLDAP;
311
+        $attr = null;
312
+
313
+        if (is_null($valueFromLDAP)
314
+           && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0
315
+           && $this->access->connection->homeFolderNamingRule !== 'attr:')
316
+        {
317
+            $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:'));
318
+            $homedir = $this->access->readAttribute(
319
+                $this->access->username2dn($this->getUsername()), $attr);
320
+            if ($homedir && isset($homedir[0])) {
321
+                $path = $homedir[0];
322
+            }
323
+        }
324
+
325
+        if ($path !== '') {
326
+            //if attribute's value is an absolute path take this, otherwise append it to data dir
327
+            //check for / at the beginning or pattern c:\ resp. c:/
328
+            if(   '/' !== $path[0]
329
+               && !(3 < strlen($path) && ctype_alpha($path[0])
330
+                   && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2]))
331
+            ) {
332
+                $path = $this->config->getSystemValue('datadirectory',
333
+                        \OC::$SERVERROOT.'/data' ) . '/' . $path;
334
+            }
335
+            //we need it to store it in the DB as well in case a user gets
336
+            //deleted so we can clean up afterwards
337
+            $this->config->setUserValue(
338
+                $this->getUsername(), 'user_ldap', 'homePath', $path
339
+            );
340
+            return $path;
341
+        }
342
+
343
+        if(    !is_null($attr)
344
+            && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true)
345
+        ) {
346
+            // a naming rule attribute is defined, but it doesn't exist for that LDAP user
347
+            throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername());
348
+        }
349
+
350
+        //false will apply default behaviour as defined and done by OC_User
351
+        $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', '');
352
+        return false;
353
+    }
354
+
355
+    public function getMemberOfGroups() {
356
+        $cacheKey = 'getMemberOf'.$this->getUsername();
357
+        $memberOfGroups = $this->connection->getFromCache($cacheKey);
358
+        if(!is_null($memberOfGroups)) {
359
+            return $memberOfGroups;
360
+        }
361
+        $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf');
362
+        $this->connection->writeToCache($cacheKey, $groupDNs);
363
+        return $groupDNs;
364
+    }
365
+
366
+    /**
367
+     * @brief reads the image from LDAP that shall be used as Avatar
368
+     * @return string data (provided by LDAP) | false
369
+     */
370
+    public function getAvatarImage() {
371
+        if(!is_null($this->avatarImage)) {
372
+            return $this->avatarImage;
373
+        }
374
+
375
+        $this->avatarImage = false;
376
+        /** @var Connection $connection */
377
+        $connection = $this->access->getConnection();
378
+        $attributes = $connection->resolveRule('avatar');
379
+        foreach($attributes as $attribute) {
380
+            $result = $this->access->readAttribute($this->dn, $attribute);
381
+            if($result !== false && is_array($result) && isset($result[0])) {
382
+                $this->avatarImage = $result[0];
383
+                break;
384
+            }
385
+        }
386
+
387
+        return $this->avatarImage;
388
+    }
389
+
390
+    /**
391
+     * @brief marks the user as having logged in at least once
392
+     * @return null
393
+     */
394
+    public function markLogin() {
395
+        $this->config->setUserValue(
396
+            $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1);
397
+    }
398
+
399
+    /**
400
+     * @brief marks the time when user features like email have been updated
401
+     * @return null
402
+     */
403
+    public function markRefreshTime() {
404
+        $this->config->setUserValue(
405
+            $this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time());
406
+    }
407
+
408
+    /**
409
+     * @brief checks whether user features needs to be updated again by
410
+     * comparing the difference of time of the last refresh to now with the
411
+     * desired interval
412
+     * @return bool
413
+     */
414
+    private function needsRefresh() {
415
+        $lastChecked = $this->config->getUserValue($this->uid, 'user_ldap',
416
+            self::USER_PREFKEY_LASTREFRESH, 0);
417
+
418
+        if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) {
419
+            return false;
420
+        }
421
+        return  true;
422
+    }
423
+
424
+    /**
425
+     * Stores a key-value pair in relation to this user
426
+     *
427
+     * @param string $key
428
+     * @param string $value
429
+     */
430
+    private function store($key, $value) {
431
+        $this->config->setUserValue($this->uid, 'user_ldap', $key, $value);
432
+    }
433
+
434
+    /**
435
+     * Composes the display name and stores it in the database. The final
436
+     * display name is returned.
437
+     *
438
+     * @param string $displayName
439
+     * @param string $displayName2
440
+     * @return string the effective display name
441
+     */
442
+    public function composeAndStoreDisplayName($displayName, $displayName2 = '') {
443
+        $displayName2 = (string)$displayName2;
444
+        if($displayName2 !== '') {
445
+            $displayName .= ' (' . $displayName2 . ')';
446
+        }
447
+        $oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null);
448
+        if ($oldName !== $displayName)  {
449
+            $this->store('displayName', $displayName);
450
+            $user = $this->userManager->get($this->getUsername());
451
+            if (!empty($oldName) && $user instanceof \OC\User\User) {
452
+                // if it was empty, it would be a new record, not a change emitting the trigger could
453
+                // potentially cause a UniqueConstraintViolationException, depending on some factors.
454
+                $user->triggerChange('displayName', $displayName, $oldName);
455
+            }
456
+        }
457
+        return $displayName;
458
+    }
459
+
460
+    /**
461
+     * Stores the LDAP Username in the Database
462
+     * @param string $userName
463
+     */
464
+    public function storeLDAPUserName($userName) {
465
+        $this->store('uid', $userName);
466
+    }
467
+
468
+    /**
469
+     * @brief checks whether an update method specified by feature was run
470
+     * already. If not, it will marked like this, because it is expected that
471
+     * the method will be run, when false is returned.
472
+     * @param string $feature email | quota | avatar (can be extended)
473
+     * @return bool
474
+     */
475
+    private function wasRefreshed($feature) {
476
+        if(isset($this->refreshedFeatures[$feature])) {
477
+            return true;
478
+        }
479
+        $this->refreshedFeatures[$feature] = 1;
480
+        return false;
481
+    }
482
+
483
+    /**
484
+     * fetches the email from LDAP and stores it as Nextcloud user value
485
+     * @param string $valueFromLDAP if known, to save an LDAP read request
486
+     * @return null
487
+     */
488
+    public function updateEmail($valueFromLDAP = null) {
489
+        if($this->wasRefreshed('email')) {
490
+            return;
491
+        }
492
+        $email = (string)$valueFromLDAP;
493
+        if(is_null($valueFromLDAP)) {
494
+            $emailAttribute = $this->connection->ldapEmailAttribute;
495
+            if ($emailAttribute !== '') {
496
+                $aEmail = $this->access->readAttribute($this->dn, $emailAttribute);
497
+                if(is_array($aEmail) && (count($aEmail) > 0)) {
498
+                    $email = (string)$aEmail[0];
499
+                }
500
+            }
501
+        }
502
+        if ($email !== '') {
503
+            $user = $this->userManager->get($this->uid);
504
+            if (!is_null($user)) {
505
+                $currentEmail = (string)$user->getEMailAddress();
506
+                if ($currentEmail !== $email) {
507
+                    $user->setEMailAddress($email);
508
+                }
509
+            }
510
+        }
511
+    }
512
+
513
+    /**
514
+     * Overall process goes as follow:
515
+     * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function
516
+     * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota
517
+     * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default')
518
+     * 4. check if the target user exists and set the quota for the user.
519
+     *
520
+     * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP
521
+     * parameter can be passed with the value of the attribute. This value will be considered as the
522
+     * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to
523
+     * fetch all the user's attributes in one call and use the fetched values in this function.
524
+     * The expected value for that parameter is a string describing the quota for the user. Valid
525
+     * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in
526
+     * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info)
527
+     *
528
+     * fetches the quota from LDAP and stores it as Nextcloud user value
529
+     * @param string $valueFromLDAP the quota attribute's value can be passed,
530
+     * to save the readAttribute request
531
+     * @return null
532
+     */
533
+    public function updateQuota($valueFromLDAP = null) {
534
+        if($this->wasRefreshed('quota')) {
535
+            return;
536
+        }
537
+
538
+        $quotaAttribute = $this->connection->ldapQuotaAttribute;
539
+        $defaultQuota = $this->connection->ldapQuotaDefault;
540
+        if($quotaAttribute === '' && $defaultQuota === '') {
541
+            return;
542
+        }
543
+
544
+        $quota = false;
545
+        if(is_null($valueFromLDAP) && $quotaAttribute !== '') {
546
+            $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute);
547
+            if($aQuota && (count($aQuota) > 0) && $this->verifyQuotaValue($aQuota[0])) {
548
+                $quota = $aQuota[0];
549
+            } else if(is_array($aQuota) && isset($aQuota[0])) {
550
+                $this->log->log('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::DEBUG);
551
+            }
552
+        } else if ($this->verifyQuotaValue($valueFromLDAP)) {
553
+            $quota = $valueFromLDAP;
554
+        } else {
555
+            $this->log->log('no suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::DEBUG);
556
+        }
557
+
558
+        if ($quota === false && $this->verifyQuotaValue($defaultQuota)) {
559
+            // quota not found using the LDAP attribute (or not parseable). Try the default quota
560
+            $quota = $defaultQuota;
561
+        } else if($quota === false) {
562
+            $this->log->log('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::DEBUG);
563
+            return;
564
+        }
565
+
566
+        $targetUser = $this->userManager->get($this->uid);
567
+        if ($targetUser instanceof IUser) {
568
+            $targetUser->setQuota($quota);
569
+        } else {
570
+            $this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::INFO);
571
+        }
572
+    }
573
+
574
+    private function verifyQuotaValue($quotaValue) {
575
+        return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false;
576
+    }
577
+
578
+    /**
579
+     * called by a post_login hook to save the avatar picture
580
+     *
581
+     * @param array $params
582
+     */
583
+    public function updateAvatarPostLogin($params) {
584
+        if(isset($params['uid']) && $params['uid'] === $this->getUsername()) {
585
+            $this->updateAvatar();
586
+        }
587
+    }
588
+
589
+    /**
590
+     * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar
591
+     * @return bool
592
+     */
593
+    public function updateAvatar($force = false) {
594
+        if(!$force && $this->wasRefreshed('avatar')) {
595
+            return false;
596
+        }
597
+        $avatarImage = $this->getAvatarImage();
598
+        if($avatarImage === false) {
599
+            //not set, nothing left to do;
600
+            return false;
601
+        }
602
+
603
+        if(!$this->image->loadFromBase64(base64_encode($avatarImage))) {
604
+            return false;
605
+        }
606
+
607
+        // use the checksum before modifications
608
+        $checksum = md5($this->image->data());
609
+
610
+        if($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '')) {
611
+            return true;
612
+        }
613
+
614
+        $isSet = $this->setOwnCloudAvatar();
615
+
616
+        if($isSet) {
617
+            // save checksum only after successful setting
618
+            $this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum);
619
+        }
620
+
621
+        return $isSet;
622
+    }
623
+
624
+    /**
625
+     * @brief sets an image as Nextcloud avatar
626
+     * @return bool
627
+     */
628
+    private function setOwnCloudAvatar() {
629
+        if(!$this->image->valid()) {
630
+            $this->log->log('avatar image data from LDAP invalid for '.$this->dn, ILogger::ERROR);
631
+            return false;
632
+        }
633
+
634
+
635
+        //make sure it is a square and not bigger than 128x128
636
+        $size = min([$this->image->width(), $this->image->height(), 128]);
637
+        if(!$this->image->centerCrop($size)) {
638
+            $this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR);
639
+            return false;
640
+        }
641
+
642
+        if(!$this->fs->isLoaded()) {
643
+            $this->fs->setup($this->uid);
644
+        }
645
+
646
+        try {
647
+            $avatar = $this->avatarManager->getAvatar($this->uid);
648
+            $avatar->set($this->image);
649
+            return true;
650
+        } catch (\Exception $e) {
651
+            \OC::$server->getLogger()->logException($e, [
652
+                'message' => 'Could not set avatar for ' . $this->dn,
653
+                'level' => ILogger::INFO,
654
+                'app' => 'user_ldap',
655
+            ]);
656
+        }
657
+        return false;
658
+    }
659
+
660
+    /**
661
+     * @throws AttributeNotSet
662
+     * @throws \OC\ServerNotAvailableException
663
+     * @throws \OCP\PreConditionNotMetException
664
+     */
665
+    public function getExtStorageHome():string {
666
+        $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', '');
667
+        if ($value !== '') {
668
+            return $value;
669
+        }
670
+
671
+        $value = $this->updateExtStorageHome();
672
+        if ($value !== '') {
673
+            return $value;
674
+        }
675
+
676
+        throw new AttributeNotSet(sprintf(
677
+            'external home storage attribute yield no value for %s', $this->getUsername()
678
+        ));
679
+    }
680
+
681
+    /**
682
+     * @throws \OCP\PreConditionNotMetException
683
+     * @throws \OC\ServerNotAvailableException
684
+     */
685
+    public function updateExtStorageHome(string $valueFromLDAP = null):string {
686
+        if ($valueFromLDAP === null) {
687
+            $extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute);
688
+        } else {
689
+            $extHomeValues = [$valueFromLDAP];
690
+        }
691
+        if ($extHomeValues && isset($extHomeValues[0])) {
692
+            $extHome = $extHomeValues[0];
693
+            $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome);
694
+            return $extHome;
695
+        } else {
696
+            $this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome');
697
+            return '';
698
+        }
699
+    }
700
+
701
+    /**
702
+     * called by a post_login hook to handle password expiry
703
+     *
704
+     * @param array $params
705
+     */
706
+    public function handlePasswordExpiry($params) {
707
+        $ppolicyDN = $this->connection->ldapDefaultPPolicyDN;
708
+        if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) {
709
+            return;//password expiry handling disabled
710
+        }
711
+        $uid = $params['uid'];
712
+        if (isset($uid) && $uid === $this->getUsername()) {
713
+            //retrieve relevant user attributes
714
+            $result = $this->access->search('objectclass=*', array($this->dn), ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']);
715
+
716
+            if (array_key_exists('pwdpolicysubentry', $result[0])) {
717
+                $pwdPolicySubentry = $result[0]['pwdpolicysubentry'];
718
+                if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){
719
+                    $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN
720
+                }
721
+            }
722
+
723
+            $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : [];
724
+            $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : [];
725
+            $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : [];
726
+
727
+            //retrieve relevant password policy attributes
728
+            $cacheKey = 'ppolicyAttributes' . $ppolicyDN;
729
+            $result = $this->connection->getFromCache($cacheKey);
730
+            if(is_null($result)) {
731
+                $result = $this->access->search('objectclass=*', array($ppolicyDN), ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']);
732
+                $this->connection->writeToCache($cacheKey, $result);
733
+            }
734
+
735
+            $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : [];
736
+            $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : [];
737
+            $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : [];
738
+
739
+            //handle grace login
740
+            if (!empty($pwdGraceUseTime)) { //was this a grace login?
741
+                if (!empty($pwdGraceAuthNLimit)
742
+                    && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available?
743
+                    $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
744
+                    header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
745
+                    'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
746
+                } else { //no more grace login available
747
+                    header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
748
+                    'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid)));
749
+                }
750
+                exit();
751
+            }
752
+            //handle pwdReset attribute
753
+            if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change his password
754
+                $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true');
755
+                header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute(
756
+                'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid)));
757
+                exit();
758
+            }
759
+            //handle password expiry warning
760
+            if (!empty($pwdChangedTime)) {
761
+                if (!empty($pwdMaxAge)
762
+                    && !empty($pwdExpireWarning)) {
763
+                    $pwdMaxAgeInt = (int)$pwdMaxAge[0];
764
+                    $pwdExpireWarningInt = (int)$pwdExpireWarning[0];
765
+                    if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){
766
+                        $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]);
767
+                        $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S'));
768
+                        $currentDateTime = new \DateTime();
769
+                        $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp();
770
+                        if ($secondsToExpiry <= $pwdExpireWarningInt) {
771
+                            //remove last password expiry warning if any
772
+                            $notification = $this->notificationManager->createNotification();
773
+                            $notification->setApp('user_ldap')
774
+                                ->setUser($uid)
775
+                                ->setObject('pwd_exp_warn', $uid)
776
+                            ;
777
+                            $this->notificationManager->markProcessed($notification);
778
+                            //create new password expiry warning
779
+                            $notification = $this->notificationManager->createNotification();
780
+                            $notification->setApp('user_ldap')
781
+                                ->setUser($uid)
782
+                                ->setDateTime($currentDateTime)
783
+                                ->setObject('pwd_exp_warn', $uid)
784
+                                ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)])
785
+                            ;
786
+                            $this->notificationManager->notify($notification);
787
+                        }
788
+                    }
789
+                }
790
+            }
791
+        }
792
+    }
793 793
 }
Please login to merge, or discard this patch.