2014-05-27 17:09:08 -04:00
< ? php
/**
2016-07-21 11:07:57 -04:00
* @ copyright Copyright ( c ) 2016 , ownCloud , Inc .
*
2019-12-03 13:57:53 -05:00
* @ author Arthur Schiwon < blizzz @ arthur - schiwon . de >
2015-03-26 06:44:34 -04:00
* @ author Bernhard Posselt < dev @ bernhard - posselt . com >
2017-11-06 09:56:42 -05:00
* @ author Bjoern Schiessle < bjoern @ schiessle . org >
2020-03-31 04:49:10 -04:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2019-12-03 13:57:53 -05:00
* @ author Daniel Kesselberg < mail @ danielkesselberg . de >
2016-07-21 11:07:57 -04:00
* @ author Joas Schilling < coding @ schilljs . com >
2016-05-26 13:56:05 -04:00
* @ author Julius Haertl < jus @ bitgrid . net >
2019-12-03 13:57:53 -05:00
* @ author Julius Härtl < jus @ bitgrid . net >
2016-05-26 13:56:05 -04:00
* @ author Lukas Reschke < lukas @ statuscode . ch >
2015-03-26 06:44:34 -04:00
* @ author Morris Jobke < hey @ morrisjobke . de >
2019-12-03 13:57:53 -05:00
* @ author Rémy Jacquin < remy @ remyj . fr >
* @ author Robin Appelman < robin @ icewind . nl >
2016-07-21 11:07:57 -04:00
* @ author Roeland Jago Douma < roeland @ famdouma . nl >
2015-03-26 06:44:34 -04:00
* @ author Thomas Müller < thomas . mueller @ tmit . eu >
* @ author Victor Dubiniuk < dubiniuk @ owncloud . com >
*
* @ license AGPL - 3.0
*
* This code is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , version 3 ,
* as published by the Free Software Foundation .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License , version 3 ,
2019-12-03 13:57:53 -05:00
* along with this program . If not , see < http :// www . gnu . org / licenses />
2015-03-26 06:44:34 -04:00
*
2014-05-27 17:09:08 -04:00
*/
2016-01-20 04:42:19 -05:00
namespace OC\Core\Controller ;
2014-05-27 17:09:08 -04:00
2022-03-21 04:49:45 -04:00
use Exception ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Controller ;
2017-04-14 07:42:40 -04:00
use OCP\AppFramework\Http\JSONResponse ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Http\TemplateResponse ;
2022-03-21 04:49:45 -04:00
use OCP\AppFramework\Services\IInitialState ;
2017-04-07 16:42:43 -04:00
use OCP\Defaults ;
2018-08-10 10:12:34 -04:00
use OCP\Encryption\IEncryptionModule ;
2016-09-29 10:38:29 -04:00
use OCP\Encryption\IManager ;
2022-03-21 04:26:55 -04:00
use OCP\EventDispatcher\IEventDispatcher ;
2021-06-29 19:20:33 -04:00
use OCP\HintException ;
2019-11-22 14:52:10 -05:00
use OCP\IConfig ;
use OCP\IL10N ;
use OCP\IRequest ;
use OCP\IURLGenerator ;
2017-03-28 14:39:36 -04:00
use OCP\IUser ;
2014-10-20 13:05:48 -04:00
use OCP\IUserManager ;
2015-02-12 10:03:51 -05:00
use OCP\Mail\IMailer ;
2021-08-13 09:53:17 -04:00
use OCP\Security\VerificationToken\IVerificationToken ;
2022-10-17 08:36:24 -04:00
use OCP\Security\VerificationToken\InvalidTokenException ;
use OC\Authentication\TwoFactorAuth\Manager ;
use OC\Core\Events\BeforePasswordResetEvent ;
use OC\Core\Events\PasswordResetEvent ;
use OC\Core\Exception\ResetPasswordException ;
use OC\Security\RateLimiting\Exception\RateLimitExceededException ;
use OC\Security\RateLimiting\Limiter ;
2022-04-12 11:55:01 -04:00
use Psr\Log\LoggerInterface ;
2021-06-29 19:20:33 -04:00
use function array_filter ;
use function count ;
2019-07-09 05:58:14 -04:00
use function reset ;
2014-05-27 17:09:08 -04:00
2014-10-20 13:05:48 -04:00
/**
* Class LostController
*
2014-10-24 07:41:44 -04:00
* Successfully changing a password will emit the post_passwordReset hook .
*
2016-01-20 04:42:19 -05:00
* @ package OC\Core\Controller
2014-10-20 13:05:48 -04:00
*/
2014-05-27 17:09:08 -04:00
class LostController extends Controller {
2022-04-12 11:55:01 -04:00
protected IURLGenerator $urlGenerator ;
protected IUserManager $userManager ;
protected Defaults $defaults ;
protected IL10N $l10n ;
protected string $from ;
protected IManager $encryptionManager ;
protected IConfig $config ;
protected IMailer $mailer ;
private LoggerInterface $logger ;
private Manager $twoFactorManager ;
2022-03-21 04:49:45 -04:00
private IInitialState $initialState ;
2022-04-12 11:55:01 -04:00
private IVerificationToken $verificationToken ;
2022-03-21 04:26:55 -04:00
private IEventDispatcher $eventDispatcher ;
2022-10-17 08:36:24 -04:00
private Limiter $limiter ;
2022-03-21 04:26:55 -04:00
2021-08-13 09:53:17 -04:00
public function __construct (
2022-04-12 11:55:01 -04:00
string $appName ,
2021-08-13 09:53:17 -04:00
IRequest $request ,
IURLGenerator $urlGenerator ,
IUserManager $userManager ,
Defaults $defaults ,
IL10N $l10n ,
IConfig $config ,
2022-03-21 04:49:45 -04:00
string $defaultMailAddress ,
2021-08-13 09:53:17 -04:00
IManager $encryptionManager ,
IMailer $mailer ,
2022-04-12 11:55:01 -04:00
LoggerInterface $logger ,
2021-08-13 09:53:17 -04:00
Manager $twoFactorManager ,
2022-03-21 04:49:45 -04:00
IInitialState $initialState ,
2022-03-21 04:26:55 -04:00
IVerificationToken $verificationToken ,
2022-10-17 08:36:24 -04:00
IEventDispatcher $eventDispatcher ,
Limiter $limiter
2021-08-13 09:53:17 -04:00
) {
2014-05-27 17:09:08 -04:00
parent :: __construct ( $appName , $request );
$this -> urlGenerator = $urlGenerator ;
2014-06-06 10:36:10 -04:00
$this -> userManager = $userManager ;
2014-05-28 13:13:07 -04:00
$this -> defaults = $defaults ;
$this -> l10n = $l10n ;
2016-09-29 10:38:29 -04:00
$this -> from = $defaultMailAddress ;
$this -> encryptionManager = $encryptionManager ;
2014-06-06 10:36:10 -04:00
$this -> config = $config ;
2015-02-12 10:03:51 -05:00
$this -> mailer = $mailer ;
2019-01-14 15:05:52 -05:00
$this -> logger = $logger ;
2019-01-28 10:12:06 -05:00
$this -> twoFactorManager = $twoFactorManager ;
2022-03-21 04:49:45 -04:00
$this -> initialState = $initialState ;
2021-08-13 09:53:17 -04:00
$this -> verificationToken = $verificationToken ;
2022-03-21 04:26:55 -04:00
$this -> eventDispatcher = $eventDispatcher ;
2022-10-17 08:36:24 -04:00
$this -> limiter = $limiter ;
2014-05-27 17:09:08 -04:00
}
/**
2014-06-06 10:36:10 -04:00
* Someone wants to reset their password :
*
2014-05-27 17:09:08 -04:00
* @ PublicPage
* @ NoCSRFRequired
*/
2022-04-12 11:55:01 -04:00
public function resetform ( string $token , string $userId ) : TemplateResponse {
2016-05-19 07:23:12 -04:00
try {
$this -> checkPasswordResetToken ( $token , $userId );
2022-03-21 04:49:45 -04:00
} catch ( Exception $e ) {
2021-09-10 16:40:10 -04:00
if ( $this -> config -> getSystemValue ( 'lost_password_link' , '' ) !== 'disabled'
|| ( $e instanceof InvalidTokenException
&& ! in_array ( $e -> getCode (), [ InvalidTokenException :: TOKEN_NOT_FOUND , InvalidTokenException :: USER_UNKNOWN ]))
) {
return new TemplateResponse (
'core' , 'error' , [
" errors " => [[ " error " => $e -> getMessage ()]]
],
TemplateResponse :: RENDER_AS_GUEST
);
}
return new TemplateResponse ( 'core' , 'error' , [
'errors' => [[ 'error' => $this -> l10n -> t ( 'Password reset is disabled' )]]
],
TemplateResponse :: RENDER_AS_GUEST
2016-05-19 07:23:12 -04:00
);
}
2022-03-21 04:49:45 -04:00
$this -> initialState -> provideInitialState ( 'resetPasswordUser' , $userId );
$this -> initialState -> provideInitialState ( 'resetPasswordTarget' ,
2019-07-25 11:04:33 -04:00
$this -> urlGenerator -> linkToRouteAbsolute ( 'core.lost.setPassword' , [ 'userId' => $userId , 'token' => $token ])
);
2016-05-19 07:23:12 -04:00
2014-06-06 10:36:10 -04:00
return new TemplateResponse (
2016-01-20 04:42:19 -05:00
'core' ,
2019-07-25 11:04:33 -04:00
'login' ,
[],
2014-06-06 10:36:10 -04:00
'guest'
);
2014-05-27 17:09:08 -04:00
}
2014-06-06 10:36:10 -04:00
2016-05-19 07:23:12 -04:00
/**
2022-03-21 04:49:45 -04:00
* @ throws Exception
2016-05-19 07:23:12 -04:00
*/
2021-08-13 09:53:17 -04:00
protected function checkPasswordResetToken ( string $token , string $userId ) : void {
2016-08-28 08:22:29 -04:00
try {
2021-09-10 13:06:50 -04:00
$user = $this -> userManager -> get ( $userId );
$this -> verificationToken -> check ( $token , $user , 'lostpassword' , $user ? $user -> getEMailAddress () : '' , true );
2021-08-13 09:53:17 -04:00
} catch ( InvalidTokenException $e ) {
$error = $e -> getCode () === InvalidTokenException :: TOKEN_EXPIRED
? $this -> l10n -> t ( 'Could not reset password because the token is expired' )
: $this -> l10n -> t ( 'Could not reset password because the token is invalid' );
2022-03-21 04:49:45 -04:00
throw new Exception ( $error , ( int ) $e -> getCode (), $e );
2016-05-19 07:23:12 -04:00
}
}
2022-04-12 11:55:01 -04:00
private function error ( string $message , array $additional = []) : array {
2020-03-26 04:30:18 -04:00
return array_merge ([ 'status' => 'error' , 'msg' => $message ], $additional );
2014-06-06 10:51:58 -04:00
}
2022-04-12 11:55:01 -04:00
private function success ( array $data = []) : array {
2020-10-05 09:12:57 -04:00
return array_merge ( $data , [ 'status' => 'success' ]);
2014-06-06 10:51:58 -04:00
}
2014-05-28 13:13:07 -04:00
/**
* @ PublicPage
2017-04-12 14:32:48 -04:00
* @ BruteForceProtection ( action = passwordResetEmail )
2017-04-22 02:12:54 -04:00
* @ AnonRateThrottle ( limit = 10 , period = 300 )
2014-05-28 13:13:07 -04:00
*/
2022-04-12 11:55:01 -04:00
public function email ( string $user ) : JSONResponse {
2017-05-11 10:46:43 -04:00
if ( $this -> config -> getSystemValue ( 'lost_password_link' , '' ) !== '' ) {
return new JSONResponse ( $this -> error ( $this -> l10n -> t ( 'Password reset is disabled' )));
}
2017-12-22 09:22:49 -05:00
\OCP\Util :: emitHook (
'\OCA\Files_Sharing\API\Server2Server' ,
'preLoginNameUsedAsUserName' ,
[ 'uid' => & $user ]
);
2014-06-06 10:36:10 -04:00
// FIXME: use HTTP error codes
2014-05-28 13:13:07 -04:00
try {
2014-10-20 13:05:48 -04:00
$this -> sendEmail ( $user );
2019-07-26 09:21:41 -04:00
} catch ( ResetPasswordException $e ) {
2019-01-14 15:05:52 -05:00
// Ignore the error since we do not want to leak this info
2019-07-26 09:21:41 -04:00
$this -> logger -> warning ( 'Could not send password reset email: ' . $e -> getMessage ());
2022-03-21 04:49:45 -04:00
} catch ( Exception $e ) {
2022-04-12 11:55:01 -04:00
$this -> logger -> error ( $e -> getMessage (), [ 'exception' => $e ]);
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2017-04-14 07:42:40 -04:00
$response = new JSONResponse ( $this -> success ());
$response -> throttle ();
return $response ;
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2014-05-28 13:13:07 -04:00
/**
* @ PublicPage
*/
2022-04-12 11:55:01 -04:00
public function setPassword ( string $token , string $userId , string $password , bool $proceed ) : array {
2016-09-29 10:38:29 -04:00
if ( $this -> encryptionManager -> isEnabled () && ! $proceed ) {
2018-08-10 10:12:34 -04:00
$encryptionModules = $this -> encryptionManager -> getEncryptionModules ();
foreach ( $encryptionModules as $module ) {
/** @var IEncryptionModule $instance */
$instance = call_user_func ( $module [ 'callback' ]);
// this way we can find out whether per-user keys are used or a system wide encryption key
if ( $instance -> needDetailedAccessList ()) {
2020-03-26 04:30:18 -04:00
return $this -> error ( '' , [ 'encryption' => true ]);
2018-08-10 10:12:34 -04:00
}
}
2014-10-20 13:05:48 -04:00
}
2014-05-28 13:13:07 -04:00
try {
2016-05-19 07:23:12 -04:00
$this -> checkPasswordResetToken ( $token , $userId );
2014-06-06 10:51:58 -04:00
$user = $this -> userManager -> get ( $userId );
2014-06-06 10:36:10 -04:00
2022-03-21 04:26:55 -04:00
$this -> eventDispatcher -> dispatchTyped ( new BeforePasswordResetEvent ( $user , $password ));
2020-03-26 04:30:18 -04:00
\OC_Hook :: emit ( '\OC\Core\LostPassword\Controller\LostController' , 'pre_passwordReset' , [ 'uid' => $userId , 'password' => $password ]);
2017-01-02 15:24:37 -05:00
2023-01-04 05:23:43 -05:00
if ( strlen ( $password ) > IUserManager :: MAX_PASSWORD_LENGTH ) {
2023-01-03 10:36:01 -05:00
throw new HintException ( 'Password too long' , $this -> l10n -> t ( 'Password is too long. Maximum allowed length is 469 characters.' ));
}
2014-07-24 06:50:39 -04:00
if ( ! $user -> setPassword ( $password )) {
2022-03-21 04:49:45 -04:00
throw new Exception ();
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2022-03-21 04:26:55 -04:00
$this -> eventDispatcher -> dispatchTyped ( new PasswordResetEvent ( $user , $password ));
2020-03-26 04:30:18 -04:00
\OC_Hook :: emit ( '\OC\Core\LostPassword\Controller\LostController' , 'post_passwordReset' , [ 'uid' => $userId , 'password' => $password ]);
2014-10-24 07:41:44 -04:00
2019-01-28 10:12:06 -05:00
$this -> twoFactorManager -> clearTwoFactorPending ( $userId );
2016-08-23 09:01:38 -04:00
$this -> config -> deleteUserValue ( $userId , 'core' , 'lostpassword' );
2017-07-24 06:17:53 -04:00
@ \OC :: $server -> getUserSession () -> unsetMagicInCookie ();
2020-04-10 08:19:56 -04:00
} catch ( HintException $e ) {
2018-05-20 06:51:50 -04:00
return $this -> error ( $e -> getHint ());
2022-03-21 04:49:45 -04:00
} catch ( Exception $e ) {
2014-06-06 10:51:58 -04:00
return $this -> error ( $e -> getMessage ());
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2018-06-19 11:02:20 -04:00
return $this -> success ([ 'user' => $userId ]);
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2014-10-20 13:05:48 -04:00
/**
2019-07-26 09:21:41 -04:00
* @ throws ResetPasswordException
* @ throws \OCP\PreConditionNotMetException
2014-10-20 13:05:48 -04:00
*/
2022-04-12 11:55:01 -04:00
protected function sendEmail ( string $input ) : void {
2017-03-28 14:39:36 -04:00
$user = $this -> findUserByIdOrMail ( $input );
$email = $user -> getEMailAddress ();
2014-06-06 10:36:10 -04:00
2014-05-28 13:13:07 -04:00
if ( empty ( $email )) {
2019-07-26 09:21:41 -04:00
throw new ResetPasswordException ( 'Could not send reset e-mail since there is no email for username ' . $input );
2014-05-28 13:13:07 -04:00
}
2014-06-06 10:36:10 -04:00
2022-10-17 08:36:24 -04:00
try {
$this -> limiter -> registerUserRequest ( 'lostpasswordemail' , 5 , 1800 , $user );
} catch ( RateLimitExceededException $e ) {
throw new ResetPasswordException ( 'Could not send reset e-mail, 5 of them were already sent in the last 30 minutes' , 0 , $e );
}
2016-08-28 08:22:29 -04:00
// Generate the token. It is stored encrypted in the database with the
// secret being the users' email address appended with the system secret.
// This makes the token automatically invalidate once the user changes
// their email address.
2021-08-13 09:53:17 -04:00
$token = $this -> verificationToken -> create ( $user , 'lostpassword' , $email );
2014-10-20 13:05:48 -04:00
2020-03-26 04:30:18 -04:00
$link = $this -> urlGenerator -> linkToRouteAbsolute ( 'core.lost.resetform' , [ 'userId' => $user -> getUID (), 'token' => $token ]);
2014-06-06 10:36:10 -04:00
2017-09-04 09:07:19 -04:00
$emailTemplate = $this -> mailer -> createEMailTemplate ( 'core.ResetPassword' , [
2017-08-24 12:02:37 -04:00
'link' => $link ,
]);
2017-04-11 18:24:58 -04:00
2017-09-15 04:59:11 -04:00
$emailTemplate -> setSubject ( $this -> l10n -> t ( '%s password reset' , [ $this -> defaults -> getName ()]));
2017-04-11 18:24:58 -04:00
$emailTemplate -> addHeader ();
$emailTemplate -> addHeading ( $this -> l10n -> t ( 'Password reset' ));
$emailTemplate -> addBodyText (
2018-02-15 06:18:51 -05:00
htmlspecialchars ( $this -> l10n -> t ( 'Click the following button to reset your password. If you have not requested the password reset, then ignore this email.' )),
2017-04-11 18:24:58 -04:00
$this -> l10n -> t ( 'Click the following link to reset your password. If you have not requested the password reset, then ignore this email.' )
);
$emailTemplate -> addBodyButton (
2018-02-15 06:18:51 -05:00
htmlspecialchars ( $this -> l10n -> t ( 'Reset your password' )),
2017-04-11 18:24:58 -04:00
$link ,
false
);
$emailTemplate -> addFooter ();
2014-06-06 10:36:10 -04:00
2014-05-28 13:13:07 -04:00
try {
2015-02-12 10:03:51 -05:00
$message = $this -> mailer -> createMessage ();
2021-02-18 06:38:43 -05:00
$message -> setTo ([ $email => $user -> getDisplayName ()]);
2015-02-12 10:03:51 -05:00
$message -> setFrom ([ $this -> from => $this -> defaults -> getName ()]);
2017-09-15 05:01:21 -04:00
$message -> useTemplate ( $emailTemplate );
2015-02-12 10:03:51 -05:00
$this -> mailer -> send ( $message );
2022-03-21 04:49:45 -04:00
} catch ( Exception $e ) {
2019-07-26 09:21:41 -04:00
// Log the exception and continue
2022-04-12 11:55:01 -04:00
$this -> logger -> error ( $e -> getMessage (), [ 'app' => 'core' , 'exception' => $e ]);
2014-05-28 13:13:07 -04:00
}
}
2014-05-27 17:09:08 -04:00
2017-03-28 14:39:36 -04:00
/**
2019-07-26 09:21:41 -04:00
* @ throws ResetPasswordException
2017-03-28 14:39:36 -04:00
*/
2022-04-12 11:55:01 -04:00
protected function findUserByIdOrMail ( string $input ) : IUser {
2017-03-28 14:39:36 -04:00
$user = $this -> userManager -> get ( $input );
if ( $user instanceof IUser ) {
2017-08-18 07:03:40 -04:00
if ( ! $user -> isEnabled ()) {
2022-08-21 07:16:23 -04:00
throw new ResetPasswordException ( 'User ' . $user -> getUID () . ' is disabled' );
2017-08-18 07:03:40 -04:00
}
2017-03-28 14:39:36 -04:00
return $user ;
}
2017-08-18 07:03:40 -04:00
2019-07-09 05:58:14 -04:00
$users = array_filter ( $this -> userManager -> getByEmail ( $input ), function ( IUser $user ) {
2018-08-18 10:51:59 -04:00
return $user -> isEnabled ();
});
2019-07-09 05:58:14 -04:00
if ( count ( $users ) === 1 ) {
return reset ( $users );
2017-03-28 14:39:36 -04:00
}
2022-08-19 12:30:32 -04:00
throw new ResetPasswordException ( 'Could not find user ' . $input );
2017-03-28 14:39:36 -04:00
}
2014-05-27 17:09:08 -04:00
}