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