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