Test Failed
Push — master ( cf3dbd...f03535 )
by Jean-Christophe
27:09
created

generateEmailValidationUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 2
Metric Value
eloc 6
c 2
b 0
f 2
dl 0
loc 7
ccs 0
cts 6
cp 0
rs 10
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Ubiquity\controllers\auth;
4
5
use Ubiquity\utils\base\UDateTime;
6
use Ubiquity\utils\flash\FlashMessage;
7
use Ubiquity\utils\http\UCookie;
8
use Ubiquity\utils\http\USession;
9
use Ubiquity\utils\http\URequest;
10
use Ubiquity\cache\CacheManager;
11
12
/**
13
 * 
14
 * Ubiquity\controllers\auth$AuthControllerValidationTrait
15
 * This class is part of Ubiquity
16
 * @author jc
17
 * @version 1.0.0
18
 * 
19
 * @property bool $_invalid
20
 *
21
 */
22
trait AuthControllerValidationTrait {
23
24
	private static $TWO_FA_KEY='2FA-infos';
25
26
	protected static $TOKENS_VALIDATE_EMAIL='email.validation';
27
28
	protected static $TOKENS_RECOVERY_ACCOUNT='account.recovery';
29
30
	abstract protected function twoFABadCodeMessage(FlashMessage $fMessage);
31
	
32
	abstract protected function fMessage(FlashMessage $fMessage, $id = null):string;
33
	
34
	abstract protected function _getFiles(): AuthFiles;
35
	
36
	abstract protected function getBaseUrl():string;
37
	
38
	abstract protected function authLoadView($viewName, $vars = [ ]):void;
39
	
40
	abstract protected function twoFAMessage(FlashMessage $fMessage);
41
	
42
	abstract protected function useAjax():bool;
43
	
44
	abstract public function _getBodySelector():string;
45
	
46
	abstract protected function generate2FACode():string;
47
	
48
	abstract public function _getUserSessionKey():string;
49
	
50
	abstract protected function onConnect($connected);
51
	
52
	abstract protected function initializeAuth();
53
	
54
	abstract protected function finalizeAuth();
55
	
56
	abstract protected function onBad2FACode():void;
57
	
58
	abstract protected function _send2FACode(string $code,$connected):void;
59
	
60
	abstract protected function newTwoFACodeMessage(FlashMessage $fMessage);
61
	
62
	abstract protected function emailValidationDuration():\DateInterval;
63
	
64
	abstract protected function _sendEmailValidation(string $email,string $validationURL,string $expire):void;
65
	
66
	abstract protected function emailValidationSuccess(FlashMessage $fMessage);
67
	
68
	abstract protected function emailValidationError(FlashMessage $fMessage);
69
	
70
	abstract protected function towFACodePrefix():string;
71
72
	abstract protected function twoFACodeDuration():\DateInterval;
73
74
	abstract protected function getAuthTokensEmailValidation():AuthTokens;
75
76
	abstract protected function isValidEmailForRecovery(string $email):bool;
77
78
	abstract protected function recoveryEmailSendMessage(FlashMessage $fMessage);
79
80
	abstract protected function _sendEmailAccountRecovery(string $email,string $url,string $expire):bool;
81
82
	abstract protected function recoveryEmailErrorMessage(FlashMessage $fMessage);
83
84
	abstract protected function accountRecoveryDuration():\DateInterval;
85
86
	abstract protected function getAuthTokensAccountRecovery():AuthTokens;
87
88
	abstract protected function passwordResetAction(string $email,string $newPasswordHash):bool;
89
90
	abstract protected function resetPasswordSuccessMessage(FlashMessage $fMessage);
91
92
	abstract protected function resetPasswordErrorMessage(FlashMessage $fMessage);
93
94
	/**
95
	 * @noRoute
96
	 */
97
	#[\Ubiquity\attributes\items\router\NoRoute]
98
	public function bad2FACode():void{
99
		$this->confirm();
100
		$fMessage = new FlashMessage ( 'Invalid 2FA code!', 'Two Factor Authentification', 'warning', 'warning circle' );
101
		$this->twoFABadCodeMessage( $fMessage );
102
		$message = $this->fMessage ( $fMessage, 'bad-code' );
103
		$this->authLoadView ( $this->_getFiles ()->getViewBadTwoFACode(), [ '_message' => $message,'url' => $this->getBaseUrl ().'/sendNew2FACode','bodySelector' => '#bad-two-fa','_btCaption' => 'Send new code' ] );
104
	}
105
106
	/**
107
	 * @noRoute
108
	 */
109
	#[\Ubiquity\attributes\items\router\NoRoute]
110
	public function confirm(){
111
		$fMessage = new FlashMessage( 'Enter the rescue code and validate.', 'Two factor Authentification', 'info', 'key' );
112
		$this->twoFAMessage ( $fMessage );
113
		$message = $this->fMessage ( $fMessage );
114
		if($this->useAjax()){
115
			$frm=$this->jquery->semantic()->htmlForm('frm-valid-code');
116
			$frm->addExtraFieldRule('code','empty');
117
			$frm->setValidationParams(['inline'=>true,'on'=>'blur']);
118
		}
119
		$this->authLoadView ( $this->_getFiles ()->getViewStepTwo(), [ '_message' => $message,'submitURL' => $this->getBaseUrl ().'/submitCode','bodySelector' => $this->_getBodySelector(),'prefix'=>$this->towFACodePrefix() ] );
120
	}
121
	
122
	protected function save2FACode():array{
123
		$code=$this->generate2FACode();
124
		$expire=(new \DateTime())->add($this->twoFACodeDuration());
125
		$codeInfos=USession::get(self::$TWO_FA_KEY,compact('code','expire'));
126
		USession::set(self::$TWO_FA_KEY,$codeInfos);
127
		return $codeInfos;
128
	}
129
	
130
	/**
131
	 * Submits the 2FA code in post request.
132
	 * 
133
	 * @post
134
	 */
135
	#[\Ubiquity\attributes\items\router\Post]
136
	public function submitCode(){
137
		if(URequest::isPost() && USession::exists(self::$TWO_FA_KEY)){
138
			$twoFAInfos=USession::get(self::$TWO_FA_KEY);
139
			$expired=$twoFAInfos['expire']<new \DateTime();
140
			if(!$expired && $this->check2FACode($twoFAInfos['code'],URequest::post('code'))){
1 ignored issue
show
Bug introduced by
It seems like Ubiquity\utils\http\URequest::post('code') can also be of type null; however, parameter $userInput of Ubiquity\controllers\aut...onTrait::check2FACode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

140
			if(!$expired && $this->check2FACode($twoFAInfos['code'],/** @scrutinizer ignore-type */ URequest::post('code'))){
Loading history...
141
				$this->onConnect(USession::get($this->_getUserSessionKey().'-2FA'));
142
			}
143
			else{
144
				$this->_invalid=true;
145
				$this->initializeAuth();
146
				$this->onBad2FACode();
147
				$this->finalizeAuth();
148
			}
149
		}
150
	}
151
152
	/**
153
	 * @noRoute
154
	 */
155
	#[\Ubiquity\attributes\items\router\NoRoute]
156
	public function send2FACode(){
157
		$codeInfos=$this->save2FACode();
158
		$this->_send2FACode($codeInfos['code'], USession::get($this->_getUserSessionKey().'-2FA'));
159
	}
160
	
161
	public function sendNew2FACode(){
162
		$this->send2FACode();
163
		$fMessage = new FlashMessage ( 'A new code was submited.', 'Two factor Authentification', 'success', 'key' );
164
		$this->newTwoFACodeMessage ( $fMessage );
165
		echo $this->fMessage ( $fMessage );
166
	}
167
	
168
	protected function generateEmailValidationUrl($email):array {
169
		$duration=$this->emailValidationDuration();
170
		$tokens=$this->getAuthTokensEmailValidation();
171
		$d=new \DateTime();
172
		$dExpire=$d->add($duration);
173
		$key=$tokens->store(['email'=>$email]);
174
		return ['url'=>$key.'/'.\md5($email),'expire'=>$dExpire];
175
	}
176
	
177
	protected function prepareEmailValidation(string $email){
178
		$data=$this->generateEmailValidationUrl($email);
179
		$validationURL=$this->getBaseUrl().'/checkEmail/'.$data['url'];
180
		$this->_sendEmailValidation($email, $validationURL,UDateTime::elapsed($data['expire']));
181
	}
182
	
183
	/**
184
	 * To override
185
	 * Checks an email.
186
	 *
187
	 * @param string $mail
188
	 * @return bool
189
	 */
190
	protected function validateEmail(string $mail):bool{
191
		return true;
192
	}
193
194
	/**
195
	 * To override for a more secure 2FA code.
196
	 * @param string $secret
197
	 * @param string $userInput
198
	 * @return bool
199
	 */
200
	protected function check2FACode(string $secret,string $userInput):bool{
201
		return $secret===$userInput;
202
	}
203
	
204
	/**
205
	 * Route for email validation checking when creating a new account.
206
	 * @param string $key
207
	 * @param string $hashMail
208
	 */
209
	public function checkEmail(string $key,string $hashMail){
210
		$isValid=false;
211
		$tokens=$this->getAuthTokensEmailValidation();
212
		if($tokens->exists($key)){
213
			if(!$tokens->expired($key)){
214
				$data=$tokens->fetch($key);
215
				$email=$data['email'];
216
				if(\md5($email)===$hashMail && $this->validateEmail($email)){
217
					$fMessage = new FlashMessage ( "Your email <b>$email</b> has been validated.", 'Account creation', 'success', 'user' );
218
					$this->emailValidationSuccess($fMessage);
219
					$isValid=true;
220
				}
221
				$msg='This validation link is not valid!';
222
			}else{
223
				$msg='This validation link is no longer active!';
224
			}
225
		}
226
		if(!$isValid){
227
			$fMessage = new FlashMessage ( $msg??'This validation link is not valid!', 'Account creation', 'error', 'user' );
228
			$this->emailValidationError($fMessage);
229
		}
230
		echo $this->fMessage($fMessage);
231
	}
232
233
	public function recoveryInit(){
234
		$fMessage = new FlashMessage( 'Enter the email associated with your account to receive a password reset link.', 'Account recovery', 'info', 'user' );
235
		$this->recoveryInitMessage ( $fMessage );
0 ignored issues
show
Bug introduced by
The method recoveryInitMessage() does not exist on Ubiquity\controllers\aut...ntrollerValidationTrait. Did you maybe mean recoveryInit()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

235
		$this->/** @scrutinizer ignore-call */ 
236
         recoveryInitMessage ( $fMessage );

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
236
		$message = $this->fMessage ( $fMessage );
237
		if($this->useAjax()){
238
			$frm=$this->jquery->semantic()->htmlForm('frm-account-recovery');
239
			$frm->addExtraFieldRules('email',['empty','email']);
240
			$frm->setValidationParams(['inline'=>true,'on'=>'blur']);
241
		}
242
		$this->authLoadView ( $this->_getFiles ()->getViewInitRecovery(), [ '_message' => $message,'submitURL' => $this->getBaseUrl ().'/recoveryInfo','bodySelector' => $this->_getBodySelector()] );
243
	}
244
245
	/**
246
	 * @post
247
	 */
248
	#[\Ubiquity\attributes\items\router\Post]
249
	public function recoveryInfo(){
250
		if(URequest::isPost()){
251
			if($this->isValidEmailForRecovery($email=URequest::filterPost('email',FILTER_VALIDATE_EMAIL))) {
1 ignored issue
show
Bug introduced by
It seems like $email = Ubiquity\utils\...\FILTER_VALIDATE_EMAIL) can also be of type null; however, parameter $email of Ubiquity\controllers\aut...ValidEmailForRecovery() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

251
			if($this->isValidEmailForRecovery(/** @scrutinizer ignore-type */ $email=URequest::filterPost('email',FILTER_VALIDATE_EMAIL))) {
Loading history...
252
				$this->prepareEmailAccountRecovery($email);
1 ignored issue
show
Bug introduced by
It seems like $email can also be of type null; however, parameter $email of Ubiquity\controllers\aut...eEmailAccountRecovery() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

252
				$this->prepareEmailAccountRecovery(/** @scrutinizer ignore-type */ $email);
Loading history...
253
				$fMessage = new FlashMessage (sprintf('A password reset email has been sent to <b>%s</b>.<br>You can only use this link temporarily, from the same machine, on this browser.',$email), 'Account recovery', 'success', 'email');
254
				$this->recoveryEmailSendMessage($fMessage);
255
			}else{
256
				$fMessage = new FlashMessage (sprintf('No account is associated with the email address <b>%s</b>.<br><a href="%s" data-target="%s">Try again.</a>.',$email,$this->getBaseUrl().'/recoveryInit',$this->_getBodySelector()), 'Account recovery', 'error', 'user');
257
				$this->recoveryEmailErrorMessage($fMessage);
258
			}
259
			echo $this->fMessage ( $fMessage );
260
		}
261
	}
262
263
	public function recovery(string $key,string $hashMail) {
264
		$tokens = $this->getAuthTokensAccountRecovery();
265
		if ($tokens->exists($key)) {
266
			if (!$tokens->expired($key)) {
267
				$data = $tokens->fetch($key);
268
				if(\is_array($data)) {
269
					$email = $data['email'];
270
					if (\md5($email) === $hashMail && $this->validateEmail($email)) {
271
						$fMessage = new FlashMessage ("Enter a new password associated to the account <b>$email</b>.", 'Account recovery', 'success', 'user');
272
						$this->emailAccountRecoverySuccess($fMessage);
0 ignored issues
show
Bug introduced by
It seems like emailAccountRecoverySuccess() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

272
						$this->/** @scrutinizer ignore-call */ 
273
             emailAccountRecoverySuccess($fMessage);
Loading history...
273
						$message=$this->fMessage($fMessage);
274
						if($this->useAjax()) {
275
							$frm = $this->_addFrmAjaxBehavior('frm-account-recovery');
0 ignored issues
show
Bug introduced by
It seems like _addFrmAjaxBehavior() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

275
							/** @scrutinizer ignore-call */ 
276
       $frm = $this->_addFrmAjaxBehavior('frm-account-recovery');
Loading history...
276
							$passwordInputName = $this->_getPasswordInputName();
0 ignored issues
show
Bug introduced by
It seems like _getPasswordInputName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

276
							/** @scrutinizer ignore-call */ 
277
       $passwordInputName = $this->_getPasswordInputName();
Loading history...
277
							$frm->addExtraFieldRules($passwordInputName . '-conf', ['empty', "match[$passwordInputName]"]);
278
						}
279
						$this->authLoadView ( $this->_getFiles ()->getViewRecovery(), [ 'key'=>$key,'email'=>$email,'_message' => $message,'submitURL' => $this->getBaseUrl ().'/recoverySubmit','bodySelector' => $this->_getBodySelector(),'passwordInputName' => $this->_getPasswordInputName (),'passwordLabel' => $this->passwordLabel (),'passwordConfLabel'=>$this->passwordConfLabel()] );
0 ignored issues
show
Bug introduced by
It seems like passwordConfLabel() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

279
						$this->authLoadView ( $this->_getFiles ()->getViewRecovery(), [ 'key'=>$key,'email'=>$email,'_message' => $message,'submitURL' => $this->getBaseUrl ().'/recoverySubmit','bodySelector' => $this->_getBodySelector(),'passwordInputName' => $this->_getPasswordInputName (),'passwordLabel' => $this->passwordLabel (),'passwordConfLabel'=>$this->/** @scrutinizer ignore-call */ passwordConfLabel()] );
Loading history...
Bug introduced by
It seems like passwordLabel() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

279
						$this->authLoadView ( $this->_getFiles ()->getViewRecovery(), [ 'key'=>$key,'email'=>$email,'_message' => $message,'submitURL' => $this->getBaseUrl ().'/recoverySubmit','bodySelector' => $this->_getBodySelector(),'passwordInputName' => $this->_getPasswordInputName (),'passwordLabel' => $this->/** @scrutinizer ignore-call */ passwordLabel (),'passwordConfLabel'=>$this->passwordConfLabel()] );
Loading history...
280
						return ;
281
					}
282
				}
283
				$msg = 'This recovery link was not generated on this device!';
284
			} else {
285
				$msg = 'This recovery link is no longer active!';
286
			}
287
		}
288
		$fMessage = new FlashMessage ($msg ?? 'This account recovery link is not valid!', 'Account recovery', 'error', 'user');
289
		$this->emailAccountRecoveryError($fMessage);
0 ignored issues
show
Bug introduced by
It seems like emailAccountRecoveryError() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

289
		$this->/** @scrutinizer ignore-call */ 
290
         emailAccountRecoveryError($fMessage);
Loading history...
290
		echo $this->fMessage($fMessage);
291
	}
292
293
	protected function generateEmailAccountRecoveryUrl($email):array {
294
		$duration=$this->accountRecoveryDuration();
295
		$tokens=$this->getAuthTokensAccountRecovery();
296
		$d=new \DateTime();
297
		$dExpire=$d->add($duration);
298
		$key=$tokens->store(['email'=>$email]);
299
		return ['url'=>$key.'/'.\md5($email),'expire'=>$dExpire];
300
	}
301
302
	protected function prepareEmailAccountRecovery(string $email){
303
		$data=$this->generateEmailAccountRecoveryUrl($email);
304
		$validationURL=$this->getBaseUrl().'/recovery/'.$data['url'];
305
		$this->_sendEmailAccountRecovery($email, $validationURL,UDateTime::elapsed($data['expire']));
306
	}
307
308
	/**
309
	 * @post
310
	 */
311
	#[\Ubiquity\attributes\items\router\Post]
312
	public function recoverySubmit(){
313
		if(URequest::isPost() && URequest::has('key')){
314
			$tokens = $this->getAuthTokensAccountRecovery();
315
			$key=URequest::post('key');
316
			if ($tokens->exists($key)) {
1 ignored issue
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $token of Ubiquity\controllers\auth\AuthTokens::exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

316
			if ($tokens->exists(/** @scrutinizer ignore-type */ $key)) {
Loading history...
317
				if(!$tokens->expired($key)){
1 ignored issue
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $token of Ubiquity\controllers\auth\AuthTokens::expired() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

317
				if(!$tokens->expired(/** @scrutinizer ignore-type */ $key)){
Loading history...
318
					$data=$tokens->fetch($key);
319
					$email=$data['email'];
320
					if($email===URequest::post('email')){
321
						if($this->passwordResetAction($email,URequest::password_hash('password'))){
322
							$fMessage = new FlashMessage ("Your password has been updated correctly for the account associated with <b>$email</b>.", 'Account recovery', 'success', 'user');
323
							$this->resetPasswordSuccessMessage($fMessage);
324
							echo $this->info(true);
0 ignored issues
show
Bug introduced by
It seems like info() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

324
							echo $this->/** @scrutinizer ignore-call */ info(true);
Loading history...
325
							$isValid=true;
326
						}else{
327
							$msg='An error occurs when updating your password!';
328
						}
329
					}
330
				}else{
331
					$msg='This account recovery link is expired!';
332
				}
333
				$tokens->remove($key);
1 ignored issue
show
Bug introduced by
It seems like $key can also be of type null; however, parameter $token of Ubiquity\controllers\auth\AuthTokens::remove() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

333
				$tokens->remove(/** @scrutinizer ignore-type */ $key);
Loading history...
334
			}
335
			if(!$isValid){
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $isValid does not seem to be defined for all execution paths leading up to this point.
Loading history...
336
				$fMessage = new FlashMessage ($msg, 'Account recovery', 'error', 'user');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $msg does not seem to be defined for all execution paths leading up to this point.
Loading history...
337
				$this->resetPasswordErrorMessage($fMessage);
338
			}
339
			echo $this->fMessage($fMessage);
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $fMessage does not seem to be defined for all execution paths leading up to this point.
Loading history...
340
		}
341
	}
342
}
343
344