1: <?php
2: /**
3: * This code is licensed under AGPLv3 license or Afterlogic Software License
4: * if commercial version of the product was purchased.
5: * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
6: */
7:
8: namespace Aurora\Modules\StandardResetPassword;
9:
10: use Aurora\System\Api;
11: use Aurora\System\Application;
12: use PHPMailer\PHPMailer\PHPMailer;
13: use Aurora\Modules\Core\Models\User;
14:
15: /**
16: * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
17: * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
18: * @copyright Copyright (c) 2023, Afterlogic Corp.
19: *
20: * @property Settings $oModuleSettings
21: *
22: * @package Modules
23: */
24: class Module extends \Aurora\System\Module\AbstractWebclientModule
25: {
26: /***** private functions *****/
27: /**
28: * Initializes Module.
29: *
30: * @ignore
31: */
32: public function init()
33: {
34: $this->extendObject(
35: 'Aurora\Modules\Core\Classes\User',
36: array(
37: 'RecoveryEmail' => array('string', ''),
38: 'PasswordResetHash' => array('string', ''),
39: 'ConfirmRecoveryEmailHash' => array('string', ''),
40: )
41: );
42:
43: $this->aErrors = [
44: Enums\ErrorCodes::WrongPassword => $this->i18N('ERROR_WRONG_PASSWORD'),
45: ];
46:
47: $this->AddEntry('confirm-recovery-email', 'EntryConfirmRecoveryEmail');
48: }
49:
50: /**
51: * @return Module
52: */
53: public static function getInstance()
54: {
55: return parent::getInstance();
56: }
57:
58: /**
59: * @return Module
60: */
61: public static function Decorator()
62: {
63: return parent::Decorator();
64: }
65:
66: /**
67: * @return Settings
68: */
69: public function getModuleSettings()
70: {
71: return $this->oModuleSettings;
72: }
73:
74: public function EntryConfirmRecoveryEmail()
75: {
76: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
77: $sHash = (string) \Aurora\System\Router::getItemByIndex(1, '');
78: $oModuleManager = Api::GetModuleManager();
79: $sSiteName = $oModuleManager->getModuleConfigValue('Core', 'SiteName');
80: $sTheme = $oModuleManager->getModuleConfigValue('CoreWebclient', 'Theme');
81:
82: $oUser = null;
83: try {
84: $oUser = $this->getUserByHash($sHash, 'confirm-recovery-email');
85: } catch (\Exception $oEx) {
86: Api::LogException($oEx);
87: }
88: $ConfirmRecoveryEmailHeading = '';
89: $ConfirmRecoveryEmailInfo = '';
90: if ($oUser instanceof User && $sHash === $oUser->getExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash')) {
91: $ConfirmRecoveryEmailHeading = $this->i18N('HEADING_CONFIRM_EMAIL_RECOVERY_HASH');
92: $ConfirmRecoveryEmailInfo = \strtr($this->i18N('INFO_CONFIRM_EMAIL_RECOVERY_HASH'), [
93: '%SITE_NAME%' => $sSiteName,
94: '%RECOVERY_EMAIL%' => $oUser->getExtendedProp(self::GetName() . '::RecoveryEmail'),
95: ]);
96: $oMin = \Aurora\Modules\Min\Module::Decorator();
97: if ($oMin) {
98: $oMin->DeleteMinByHash($sHash);
99: }
100: $oUser->setExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash', '');
101: $oCoreDecorator = \Aurora\Modules\Core\Module::Decorator();
102: $oCoreDecorator->UpdateUserObject($oUser);
103: } else {
104: $ConfirmRecoveryEmailHeading = $this->i18N('HEADING_CONFIRM_EMAIL_RECOVERY_HASH');
105: $ConfirmRecoveryEmailInfo = $this->i18N('ERROR_LINK_NOT_VALID');
106: }
107: $sConfirmRecoveryEmailTemplate = \file_get_contents($this->GetPath() . '/templates/EntryConfirmRecoveryEmail.html');
108:
109: \Aurora\Modules\CoreWebclient\Module::Decorator()->SetHtmlOutputHeaders();
110: return \strtr($sConfirmRecoveryEmailTemplate, array(
111: '{{SiteName}}' => $sSiteName . ' - ' . $ConfirmRecoveryEmailHeading,
112: '{{Theme}}' => $sTheme,
113: '{{ConfirmRecoveryEmailHeading}}' => $ConfirmRecoveryEmailHeading,
114: '{{ConfirmRecoveryEmailInfo}}' => $ConfirmRecoveryEmailInfo,
115: '{{ActionOpenApp}}' => \strtr($this->i18N('ACTION_OPEN_SITENAME'), ['%SITE_NAME%' => $sSiteName]),
116: '{{OpenAppUrl}}' => Application::getBaseUrl(),
117: ));
118: }
119:
120: protected function getMinId($iUserId, $sType, $sFunction = '')
121: {
122: return \implode('|', array(self::GetName(), $iUserId, \md5($iUserId), $sType, $sFunction));
123: }
124:
125: protected function generateHash($iUserId, $sType, $sFunction = '')
126: {
127: $mHash = '';
128: $oMin = \Aurora\Modules\Min\Module::Decorator();
129: if ($oMin) {
130: $sMinId = $this->getMinId($iUserId, $sType, $sFunction);
131: $mHash = $oMin->GetMinByID($sMinId);
132:
133: if ($mHash) {
134: $mHash = $oMin->DeleteMinByID($sMinId);
135: }
136:
137: $iRecoveryLinkLifetimeMinutes = $this->oModuleSettings->RecoveryLinkLifetimeMinutes;
138: $iExpiresSeconds = time() + $iRecoveryLinkLifetimeMinutes * 60;
139: $mHash = $oMin->CreateMin(
140: $sMinId,
141: array(
142: 'UserId' => $iUserId,
143: 'Type' => $sType
144: ),
145: $iUserId,
146: $iExpiresSeconds
147: );
148: }
149:
150: return $mHash;
151: }
152:
153: protected function getSmtpConfig()
154: {
155: return [
156: 'Host' => $this->oModuleSettings->NotificationHost,
157: 'Port' => $this->oModuleSettings->NotificationPort,
158: 'UseSsl' => !empty($this->oModuleSettings->NotificationSMTPSecure),
159: 'SMTPAuth' => (bool) $this->oModuleSettings->NotificationUseAuth,
160: 'SMTPSecure' => $this->oModuleSettings->NotificationSMTPSecure,
161: 'Username' => $this->oModuleSettings->NotificationLogin,
162: 'Password' => \Aurora\System\Utils::DecryptValue($this->oModuleSettings->NotificationPassword),
163: ];
164: }
165:
166: protected function getMailAccountByEmail($sEmail)
167: {
168: $oAccount = null;
169: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUserByPublicId($sEmail);
170: if ($oUser instanceof User) {
171: $bPrevState = \Aurora\Api::skipCheckUserRole(true);
172: if (class_exists('\Aurora\Modules\Mail\Module')) {
173: $oAccount = \Aurora\Modules\Mail\Module::Decorator()->GetAccountByEmail($sEmail, $oUser->Id);
174: }
175: \Aurora\Api::skipCheckUserRole($bPrevState);
176: }
177: return $oAccount;
178: }
179:
180: protected function getMailAccountConfig($sEmail)
181: {
182: $aConfig = [
183: 'Host' => '',
184: 'Port' => '',
185: 'UseSsl' => false,
186: 'SMTPSecure' => 'ssl',
187: 'SMTPAuth' => false,
188: 'Username' => '',
189: 'Password' => '',
190: ];
191:
192: if (class_exists('\Aurora\Modules\Mail\Enums\SmtpAuthType')) {
193: $oSendAccount = $this->getMailAccountByEmail($sEmail);
194: $oSendServer = $oSendAccount ? $oSendAccount->getServer() : null;
195: if ($oSendServer) {
196: $aConfig['Host'] = $oSendServer->OutgoingServer;
197: $aConfig['Port'] = $oSendServer->OutgoingPort;
198: switch ($oSendServer->SmtpAuthType) {
199: case \Aurora\Modules\Mail\Enums\SmtpAuthType::NoAuthentication:
200: break;
201: case \Aurora\Modules\Mail\Enums\SmtpAuthType::UseSpecifiedCredentials:
202: $aConfig['UseSsl'] = $oSendServer->OutgoingUseSsl;
203: $aConfig['SMTPAuth'] = true;
204: $aConfig['Username'] = $oSendServer->SmtpLogin;
205: $aConfig['Password'] = $oSendServer->SmtpPassword;
206: break;
207: case \Aurora\Modules\Mail\Enums\SmtpAuthType::UseUserCredentials:
208: $aConfig['UseSsl'] = $oSendServer->OutgoingUseSsl;
209: $aConfig['SMTPAuth'] = true;
210: $aConfig['Username'] = $oSendAccount->IncomingLogin;
211: $aConfig['Password'] = $oSendAccount->getPassword();
212: break;
213: }
214: }
215: }
216:
217: return $aConfig;
218: }
219:
220: /**
221: * Sends notification email.
222: * @param string $sRecipientEmail
223: * @param string $sSubject
224: * @param string $sBody
225: * @param bool $bIsHtmlBody
226: * @param string $sSiteName
227: * @return bool
228: * @throws \Exception
229: */
230: protected function sendMessage($sRecipientEmail, $sSubject, $sBody, $bIsHtmlBody, $sSiteName)
231: {
232: $bResult = false;
233:
234: $oMail = new PHPMailer();
235:
236: $sFrom = $this->oModuleSettings->NotificationEmail;
237: $sType = \strtolower($this->oModuleSettings->NotificationType);
238: switch ($sType) {
239: case 'mail':
240: $oMail->isMail();
241: break;
242: case 'smtp':
243: case 'account':
244: $oMail->isSMTP();
245: $aConfig = $sType === 'smtp' ? $this->getSmtpConfig() : $this->getMailAccountConfig($sFrom);
246: $oMail->Host = $aConfig['Host'];
247: $oMail->Port = $aConfig['Port'];
248: $oMail->SMTPAuth = $aConfig['SMTPAuth'];
249: if ($aConfig['UseSsl']) {
250: $oMail->SMTPSecure = $aConfig['SMTPSecure'];
251: }
252: $oMail->Username = $aConfig['Username'];
253: $oMail->Password = $aConfig['Password'];
254: $oMail->SMTPOptions = array(
255: 'ssl' => array(
256: 'verify_peer' => false,
257: 'verify_peer_name' => false,
258: 'allow_self_signed' => true
259: )
260: );
261: break;
262: }
263:
264: //$oMail->Timeout = 10; // seconds
265: $oMail->setFrom($sFrom);
266: $oMail->addAddress($sRecipientEmail);
267: $oMail->addReplyTo($sFrom, $sSiteName);
268:
269: $oMail->Subject = $sSubject;
270: $oMail->Body = $sBody;
271: $oMail->isHTML($bIsHtmlBody);
272:
273: try {
274: $bResult = $oMail->send();
275: } catch (\Exception $oEx) {
276: Api::LogException($oEx);
277: throw new \Exception($oEx->getMessage());
278: }
279: if (!$bResult && !empty($oMail->ErrorInfo)) {
280: Api::Log("Message could not be sent. Mailer Error: {$oMail->ErrorInfo}");
281: throw new \Exception($oMail->ErrorInfo);
282: }
283:
284: return $bResult;
285: }
286:
287: protected function getHashModuleName()
288: {
289: return $this->oModuleSettings->HashModuleName;
290: }
291: /**
292: * Sends password reset message.
293: * @param string $sRecipientEmail
294: * @param string $sHash
295: * @return boolean
296: */
297: protected function sendPasswordResetMessage($sRecipientEmail, $sHash)
298: {
299: $oModuleManager = Api::GetModuleManager();
300: $sSiteName = $oModuleManager->getModuleConfigValue('Core', 'SiteName');
301:
302: $sBody = \file_get_contents($this->GetPath() . '/templates/mail/Message.html');
303: if (\is_string($sBody)) {
304: $sGreeting = $this->i18N('LABEL_MESSAGE_GREETING');
305: $sMessage = \strtr($this->i18N('LABEL_RESET_PASSWORD_MESSAGE'), [
306: '%SITE_NAME%' => $sSiteName,
307: '%RESET_PASSWORD_URL%' => \rtrim(Application::getBaseUrl(), '\\/ ') . '/#' . $this->getHashModuleName() . '/' . $sHash,
308: ]);
309: $sSignature = \strtr($this->i18N('LABEL_MESSAGE_SIGNATURE'), ['%SITE_NAME%' => $sSiteName]);
310: $sBody = \strtr($sBody, array(
311: '{{GREETING}}' => $sGreeting,
312: '{{MESSAGE}}' => $sMessage,
313: '{{SIGNATURE}}' => $sSignature,
314: ));
315: }
316: $bIsHtmlBody = true;
317: $sSubject = $this->i18N('LABEL_RESET_PASSWORD_SUBJECT');
318: return $this->sendMessage($sRecipientEmail, $sSubject, $sBody, $bIsHtmlBody, $sSiteName);
319: }
320:
321: /**
322: * Sends recovery email confirmation message.
323: * @param string $sRecipientEmail
324: * @param string $sHash
325: * @return bool
326: */
327: protected function sendRecoveryEmailConfirmationMessage($sRecipientEmail, $sHash)
328: {
329: $oModuleManager = Api::GetModuleManager();
330: $sSiteName = $oModuleManager->getModuleConfigValue('Core', 'SiteName');
331:
332: $sBody = \file_get_contents($this->GetPath() . '/templates/mail/Message.html');
333: if (\is_string($sBody)) {
334: $sGreeting = $this->i18N('LABEL_MESSAGE_GREETING');
335: $sMessage = \strtr($this->i18N('LABEL_CONFIRM_EMAIL_MESSAGE'), [
336: '%RECOVERY_EMAIL%' => $sRecipientEmail,
337: '%SITE_NAME%' => $sSiteName,
338: '%RESET_PASSWORD_URL%' => \rtrim(Application::getBaseUrl(), '\\/ ') . '?/confirm-recovery-email/' . $sHash,
339: ]);
340: $sSignature = \strtr($this->i18N('LABEL_MESSAGE_SIGNATURE'), ['%SITE_NAME%' => $sSiteName]);
341: $sBody = \strtr($sBody, array(
342: '{{GREETING}}' => $sGreeting,
343: '{{MESSAGE}}' => $sMessage,
344: '{{SIGNATURE}}' => $sSignature,
345: ));
346: }
347: $bIsHtmlBody = true;
348: $sSubject = \strtr($this->i18N('LABEL_CONFIRM_EMAIL_SUBJECT'), ['%RECOVERY_EMAIL%' => $sRecipientEmail]);
349: return $this->sendMessage($sRecipientEmail, $sSubject, $sBody, $bIsHtmlBody, $sSiteName);
350: }
351:
352: /**
353: * Returns user with identifier obtained from the hash.
354: *
355: * @param string $sHash
356: * @param string $sType
357: * @param boolean $bAdd5Min
358: * @return \Aurora\Modules\Core\Models\User
359: */
360: protected function getUserByHash($sHash, $sType, $bAdd5Min = false)
361: {
362: $oUser = null;
363: $oMin = \Aurora\Modules\Min\Module::Decorator();
364: $mHash = $oMin ? $oMin->GetMinByHash($sHash) : null;
365: if (!empty($mHash) && isset($mHash['__hash__'], $mHash['UserId'], $mHash['Type']) && $mHash['Type'] === $sType) {
366: $iRecoveryLinkLifetimeMinutes = $this->oModuleSettings->RecoveryLinkLifetimeMinutes;
367: $bRecoveryLinkPrmament = ($iRecoveryLinkLifetimeMinutes === 0);
368: if (!$bRecoveryLinkPrmament) {
369: $iExpiresSeconds = $mHash['ExpireDate'];
370: if ($bAdd5Min) {
371: $iExpiresSeconds += 5 * 60;
372: }
373: if ($iExpiresSeconds > time()) {
374: $bRecoveryLinkPrmament = true;
375: } else {
376: throw new \Exception($this->i18N('ERROR_LINK_NOT_VALID'));
377: }
378: }
379: if ($bRecoveryLinkPrmament) {
380: $iUserId = $mHash['UserId'];
381: $bPrevState = \Aurora\Api::skipCheckUserRole(true);
382: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUser($iUserId);
383: \Aurora\Api::skipCheckUserRole($bPrevState);
384: }
385: }
386: return $oUser;
387: }
388:
389: /**
390: * Get recovery email address partly replaced with stars.
391: * @param \Aurora\Modules\Core\Models\User $oUser
392: * @return string
393: */
394: protected function getStarredRecoveryEmail($oUser)
395: {
396: $sResult = '';
397:
398: if ($oUser instanceof User) {
399: $sRecoveryEmail = $oUser->getExtendedProp(self::GetName() . '::RecoveryEmail');
400: if (!empty($sRecoveryEmail)) {
401: $aRecoveryEmailParts = explode('@', $sRecoveryEmail);
402: $iPartsCount = count($aRecoveryEmailParts);
403: if ($iPartsCount > 0) {
404: $sResult = substr($aRecoveryEmailParts[0], 0, 3) . '***';
405: }
406: if ($iPartsCount > 1) {
407: $sResult .= '@' . $aRecoveryEmailParts[$iPartsCount - 1];
408: }
409: }
410: }
411:
412: return $sResult;
413: }
414:
415: private function getAuthenticatedAccount()
416: {
417: $aUserInfo = Api::getAuthenticatedUserInfo();
418:
419: $oAccount = null;
420:
421: if (isset($aUserInfo['account']) && isset($aUserInfo['accountType']) && class_exists($aUserInfo['accountType'])) {
422: $oAccount = call_user_func_array([$aUserInfo['accountType'], 'find'], [(int)$aUserInfo['account']]);
423: }
424:
425: return $oAccount;
426: }
427:
428: private function getAccountById($iUserId, $iAccountId, $sAccountType)
429: {
430: $oAccount = null;
431:
432: $aAccounts = \Aurora\Modules\Core\Module::Decorator()->GetUserAccounts($iUserId);
433:
434: foreach ($aAccounts as $oItem) {
435: if ($oItem['Id'] === $iAccountId && $oItem['Type'] === $sAccountType && class_exists($oItem['Type'])) {
436: $oAccount = call_user_func_array([ $oItem['Type'], 'find'], [(int)$oItem['Id']]);
437: }
438: }
439:
440: return $oAccount;
441: }
442: /***** private functions *****/
443:
444: /***** public functions might be called with web API *****/
445: /**
446: * Obtains list of module settings for authenticated user.
447: *
448: * @return array
449: */
450: public function GetSettings()
451: {
452:
453: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
454:
455: $aSettings = [
456: 'HashModuleName' => $this->oModuleSettings->HashModuleName,
457: 'CustomLogoUrl' => $this->oModuleSettings->CustomLogoUrl,
458: 'BottomInfoHtmlText' => $this->oModuleSettings->BottomInfoHtmlText,
459: ];
460:
461: $oAuthenticatedUser = Api::getAuthenticatedUser();
462: if ($oAuthenticatedUser instanceof User) {
463: if ($oAuthenticatedUser->isNormalOrTenant()) {
464: $iRecoveryAccountId = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::RecoveryAccountId');
465: $sRecoveryAccountType = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::RecoveryAccountType');
466: $oAccount = $this->getAccountById($oAuthenticatedUser->Id, $iRecoveryAccountId, $sRecoveryAccountType);
467:
468: $aSettings['RecoveryEmail'] = $this->getStarredRecoveryEmail($oAuthenticatedUser);
469: $aSettings['RecoveryEmailConfirmed'] = empty($oAuthenticatedUser->getExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash'));
470: $aSettings['RecoveryAccount'] = $oAccount ? $oAccount->getLogin() : '';
471: }
472: if ($oAuthenticatedUser->Role === \Aurora\System\Enums\UserRole::SuperAdmin) {
473: $aSettings['RecoveryLinkLifetimeMinutes'] = $this->oModuleSettings->RecoveryLinkLifetimeMinutes;
474: $aSettings['NotificationEmail'] = $this->oModuleSettings->NotificationEmail;
475: $aSettings['NotificationType'] = $this->oModuleSettings->NotificationType;
476: $aSettings['NotificationHost'] = $this->oModuleSettings->NotificationHost;
477: $aSettings['NotificationPort'] = $this->oModuleSettings->NotificationPort;
478: $aSettings['NotificationSMTPSecure'] = $this->oModuleSettings->NotificationSMTPSecure;
479: $aSettings['NotificationUseAuth'] = $this->oModuleSettings->NotificationUseAuth;
480: $aSettings['NotificationLogin'] = $this->oModuleSettings->NotificationLogin;
481: $aSettings['HasNotificationPassword'] = !empty($this->oModuleSettings->NotificationPassword);
482: }
483: }
484:
485: return $aSettings;
486: }
487:
488: /**
489: * Updates per user settings.
490: * @param string $RecoveryEmail
491: * @param string $Password
492: * @return boolean|string
493: * @throws \Aurora\System\Exceptions\ApiException
494: * @throws \Aurora\Modules\StandardResetPassword\Exceptions\Exception
495: */
496: public function UpdateSettings($RecoveryEmail = null, $Password = null)
497: {
498: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::NormalUser);
499:
500: if ($RecoveryEmail === null || $Password === null) {
501: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
502: }
503:
504: $oAuthenticatedUser = Api::getAuthenticatedUser();
505: if ($oAuthenticatedUser instanceof User && $oAuthenticatedUser->isNormalOrTenant()) {
506: $oAccount = $this->getAuthenticatedAccount();
507:
508: if ($Password === null || ($oAccount && $oAccount->getPassword() !== $Password)) {
509: throw new \Aurora\Modules\StandardResetPassword\Exceptions\Exception(Enums\ErrorCodes::WrongPassword);
510: }
511:
512: $sPrevRecoveryEmail = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::RecoveryEmail');
513: $sPrevConfirmRecoveryEmail = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::ConfirmRecoveryEmail');
514: $sPrevAccountId = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::RecoveryAccountId');
515: $sPrevAccountType = $oAuthenticatedUser->getExtendedProp(self::GetName() . '::RecoveryAccountType');
516:
517: $sConfirmRecoveryEmailHash = !empty($RecoveryEmail) ? $this->generateHash($oAuthenticatedUser->Id, 'confirm-recovery-email', __FUNCTION__) : '';
518:
519: $oAuthenticatedUser->setExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash', $sConfirmRecoveryEmailHash);
520: $oAuthenticatedUser->setExtendedProp(self::GetName() . '::RecoveryEmail', $RecoveryEmail);
521: $oAuthenticatedUser->setExtendedProp(self::GetName() . '::RecoveryAccountId', $oAccount->Id);
522: $oAuthenticatedUser->setExtendedProp(self::GetName() . '::RecoveryAccountType', $oAccount->getName());
523:
524: $bResult = \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oAuthenticatedUser);
525: if ($bResult) {
526:
527: if (!empty($RecoveryEmail)) {
528: $oSentException = null;
529: try {
530: // Send message to confirm recovery email if it's not empty.
531: $bResult = $this->sendRecoveryEmailConfirmationMessage($RecoveryEmail, $sConfirmRecoveryEmailHash);
532: } catch (\Exception $oException) {
533: $bResult = false;
534: $oSentException = $oException;
535: }
536:
537: if (!$bResult) {
538: $oAuthenticatedUser->setExtendedProps([
539: self::GetName() . '::ConfirmRecoveryEmailHash' => $sPrevConfirmRecoveryEmail,
540: self::GetName() . '::RecoveryEmail', $sPrevRecoveryEmail,
541: self::GetName() . '::RecoveryAccountId', $sPrevAccountId,
542: self::GetName() . '::RecoveryAccountType', $sPrevAccountType
543: ]);
544: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oAuthenticatedUser);
545: }
546:
547: if ($oSentException !== null) {
548: throw $oSentException;
549: }
550: }
551: }
552:
553: if ($bResult) {
554: return [
555: 'RecoveryEmail' => $this->getStarredRecoveryEmail($oAuthenticatedUser),
556: 'RecoveryAccount' => $oAccount->getLogin()
557: ];
558: } else {
559: return false;
560: }
561: }
562:
563: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::AccessDenied);
564: }
565:
566: /**
567: * Updates per user settings.
568: * @param int $RecoveryLinkLifetimeMinutes
569: * @param string $NotificationEmail
570: * @param string $NotificationType
571: * @param string $NotificationHost
572: * @param int $NotificationPort
573: * @param string $NotificationSMTPSecure
574: * @param boolean $NotificationUseAuth
575: * @param string $NotificationLogin
576: * @param string $NotificationPassword
577: * @return boolean|string
578: * @throws \Aurora\System\Exceptions\ApiException
579: * @throws \Aurora\Modules\StandardResetPassword\Exceptions\Exception
580: */
581: public function UpdateAdminSettings(
582: $RecoveryLinkLifetimeMinutes,
583: $NotificationEmail,
584: $NotificationType,
585: $NotificationHost = null,
586: $NotificationPort = null,
587: $NotificationSMTPSecure = null,
588: $NotificationUseAuth = null,
589: $NotificationLogin = null,
590: $NotificationPassword = null
591: ) {
592: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::SuperAdmin);
593:
594: $this->setConfig('RecoveryLinkLifetimeMinutes', $RecoveryLinkLifetimeMinutes);
595: $this->setConfig('NotificationEmail', $NotificationEmail);
596: $this->setConfig('NotificationType', $NotificationType);
597: if ($NotificationType === 'smtp') {
598: $this->setConfig('NotificationHost', $NotificationHost);
599: $this->setConfig('NotificationPort', $NotificationPort);
600: $this->setConfig('NotificationSMTPSecure', $NotificationSMTPSecure);
601: $this->setConfig('NotificationUseAuth', $NotificationUseAuth);
602: if ($NotificationUseAuth) {
603: $this->setConfig('NotificationLogin', $NotificationLogin);
604: $this->setConfig('NotificationPassword', \Aurora\System\Utils::EncryptValue($NotificationPassword));
605: }
606: }
607: return $this->saveModuleConfig();
608: }
609:
610: /**
611: * Get recovery email address partly replaced with stars.
612: * @param string $UserPublicId
613: * @return string
614: */
615: public function GetStarredRecoveryEmailAddress($UserPublicId)
616: {
617: $sRecoveryEmail = '';
618: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUserByPublicId($UserPublicId);
619: if ($oUser) {
620: $sRecoveryEmail = $this->getStarredRecoveryEmail($oUser);
621: $sConfirmRecoveryEmailHash = $oUser->getExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash');
622: if (!empty($sConfirmRecoveryEmailHash)) { // email is not confirmed
623: $sRecoveryEmail = '';
624: }
625: }
626: return $sRecoveryEmail;
627: }
628:
629: /**
630: * Creates a recovery link and sends it to recovery email of the user with specified public ID.
631: *
632: * @param string $UserPublicId
633: * @return boolean
634: * @throws \Exception
635: */
636: public function SendPasswordResetEmail($UserPublicId)
637: {
638: $oUser = \Aurora\Modules\Core\Module::Decorator()->GetUserByPublicId($UserPublicId);
639: if ($oUser instanceof User) {
640: $bPrevState = \Aurora\Api::skipCheckUserRole(true);
641: $sPasswordResetHash = $this->generateHash($oUser->Id, $this->getHashModuleName(), __FUNCTION__);
642: $oUser->setExtendedProp(self::GetName() . '::PasswordResetHash', $sPasswordResetHash);
643: \Aurora\Modules\Core\Module::Decorator()->UpdateUserObject($oUser);
644: \Aurora\Api::skipCheckUserRole($bPrevState);
645:
646: $sRecoveryEmail = $oUser->getExtendedProp(self::GetName() . '::RecoveryEmail');
647: $sConfirmRecoveryEmailHash = $oUser->getExtendedProp(self::GetName() . '::ConfirmRecoveryEmailHash');
648: if (!empty($sRecoveryEmail) && empty($sConfirmRecoveryEmailHash)) {
649: return $this->sendPasswordResetMessage($sRecoveryEmail, $sPasswordResetHash);
650: }
651: }
652:
653: throw new \Exception($this->i18N('ERROR_RECOVERY_EMAIL_NOT_FOUND'));
654: }
655:
656: /**
657: * Returns public id of user obtained from the hash.
658: *
659: * @param string $Hash Hash with information about user.
660: * @return string
661: */
662: public function GetUserPublicId($Hash)
663: {
664: Api::checkUserRoleIsAtLeast(\Aurora\System\Enums\UserRole::Anonymous);
665:
666: $oUser = $this->getUserByHash($Hash, $this->getHashModuleName());
667:
668: if ($oUser instanceof User) {
669: return $oUser->PublicId;
670: }
671: return '';
672: }
673:
674: /**
675: * Changes password if hash is valid.
676: * @param string $Hash
677: * @param string $NewPassword
678: * @return boolean
679: * @throws \Aurora\System\Exceptions\ApiException
680: */
681: public function ChangePassword($Hash, $NewPassword)
682: {
683: $bPrevState = Api::skipCheckUserRole(true);
684:
685: $oMin = \Aurora\Modules\Min\Module::Decorator();
686: $mResult = false;
687:
688: if ($oMin && !empty($Hash) && $NewPassword) {
689: $oUser = $this->getUserByHash($Hash, $this->getHashModuleName(), true);
690: $oAccount = null;
691:
692: if ($oUser) {
693: $iAccountId = $oUser->getExtendedProp(self::GetName() . '::RecoveryAccountId');
694: $sAccountType = $oUser->getExtendedProp(self::GetName() . '::RecoveryAccountType');
695:
696: $oAccount = $this->getAccountById($oUser->Id, $iAccountId, $sAccountType);
697: }
698:
699: if ($oUser && $oAccount) {
700: $aArgs = [
701: 'Account' => $oAccount,
702: 'CurrentPassword' => $oAccount->getPassword(),
703: 'NewPassword' => $NewPassword
704: ];
705: $aResponse = [
706: 'AccountPasswordChanged' => false
707: ];
708:
709: $this->broadcastEvent('ChangeAccountPassword', $aArgs, $aResponse);
710:
711: $mResult = $aResponse['AccountPasswordChanged'];
712:
713: if ($mResult) {
714: $oMin->DeleteMinByHash($Hash);
715: Api::UserSession()->DeleteAllAccountSessions($oAccount);
716: }
717: } else {
718: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
719: }
720: } else {
721: throw new \Aurora\System\Exceptions\ApiException(\Aurora\System\Notifications::InvalidInputParameter);
722: }
723:
724: Api::skipCheckUserRole($bPrevState);
725:
726: return $mResult;
727: }
728: /***** public functions might be called with web API *****/
729: }
730: