Completed
Push — master ( 4b5cac...cb75bf )
by Charles
03:12
created

User::sendActivationEmail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
namespace yrc\api\models;
4
5
use app\models\Token;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, yrc\api\models\Token.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
6
7
use Base32\Base32;
8
use OTPHP\TOTP;
9
10
use yii\behaviors\TimestampBehavior;
11
use yii\db\ActiveRecord;
12
use yii\filters\RateLimitInterface;
13
use yii\web\IdentityInterface;
14
use Yii;
15
16
/**
17
 * This is the model class for table "user".
18
 *
19
 * @property integer $id
20
 * @property string $email
21
 * @property strin $username
22
 * @property string $password
23
 * @property string $activation_token
24
 * @property string $reset_token
25
 * @property integer $verified
26
 * @property string $otp_secret
27
 * @property string $otp_enabled
28
 * @property integer $created_at
29
 * @property integer $updated_at
30
 */
31
abstract class User extends ActiveRecord implements IdentityInterface, RateLimitInterface
32
{
33
    /**
34
     * password_hash Algorithm
35
     * @var integer
36
     */
37
    private $passwordHashAlgorithm = PASSWORD_BCRYPT;
38
    
39
    /**
40
     * The rate limit
41
     * @var integer
42
     */
43
    private $rateLimit = 150;
44
45
    /**
46
     * The rate limit window
47
     * @var integer
48
     */
49
    private $rateLimitWindow = 900;
50
    
51
    /**
52
     * password_hash options
53
     * @var array
54
     */
55
    private $passwordHashOptions = [
56
        'cost' => 13,
57
        'memory_cost' => 1<<12,
58
        'time_cost' => 3,
59
        'threads' => 1
60
    ];
61
    
62
    /**
63
     * Overrides init
64
     */
65
    public function init()
66
    {
67
        // self init
68
        parent::init();
69
70
        // Prefer Argon2 if it is available, but fall back to BCRYPT if it isn't
71
        if (defined('PASSWORD_ARGON2')) {
72
            $this->passwordHashAlgorithm = PASSWORD_ARGON2;
73
        }
74
75
        // Lower the bcrypt cost when running tests
76
        if (YII_DEBUG && $this->passwordHashAlgorithm === PASSWORD_BCRYPT) {
77
            $this->passwordHashOptions['cost'] = 10;
78
        }
79
    }
80
81
    /**
82
     * @inheritdoc
83
     */
84
    public function behaviors()
85
    {
86
        return [
87
            TimestampBehavior::className(),
88
        ];
89
    }
90
91
    /**
92
     * @inheritdoc
93
     */
94
    public function getRateLimit($request, $action)
95
    {
96
        return [
97
            $this->rateLimit,
98
            $this->rateLimitWindow
99
        ];
100
    }
101
102
    /**
103
     * @inheritdoc
104
     */
105
    public function loadAllowance($request, $action)
106
    {
107
        $hash = Yii::$app->user->id . $request->getUrl() . $action->id;
108
        $allowance = Yii::$app->cache->get($hash);
109
110
        if ($allowance === false) {
111
            return [
112
                $this->rateLimit,
113
                time()
114
            ];
115
        }
116
117
        return $allowance;
118
    }
119
120
    /**
121
     * @inheritdoc
122
     */
123
    public function saveAllowance($request, $action, $allowance, $timestamp)
124
    {
125
        $hash = Yii::$app->user->id . $request->getUrl() . $action->id;
126
        $allowance = [
127
            $allowance,
128
            $timestamp
129
        ];
130
131
        Yii::$app->cache->set($hash, $allowance, $this->rateLimitWindow);
132
    }
133
    
134
    /**
135
     * @inheritdoc
136
     */
137
    public static function tableName()
138
    {
139
        return 'user';
140
    }
141
142
    /**
143
     * @inheritdoc
144
     */
145
    public function rules()
146
    {
147
        return [
148
            [['password', 'email', 'username'], 'required'],
149
            [['email'], 'email'],
150
            [['password'], 'string', 'length' => [8, 100]],
151
            [['username'], 'string', 'length' => [1, 100]],
152
            [['created_at', 'updated_at', 'otp_enabled', 'verified'], 'integer'],
153
            [['password', 'email'], 'string', 'max' => 255],
154
            [['email'], 'unique'],
155
            [['username'], 'unique'],
156
        ];
157
    }
158
159
    /**
160
     * @inheritdoc
161
     */
162
    public function attributeLabels()
163
    {
164
        return [
165
            'id'                => 'ID',
166
            'email'             => 'Email Address',
167
            'username'          => 'Username',
168
            'password'          => 'Password',
169
            'activation_token'  => 'Activation Token',
170
            'otp_secret'        => 'One Time Password Secret Value',
171
            'otp_enabled'       => 'Is Two Factor Authentication Enabled?',
172
            'verified'          => 'Is the account email verified?',
173
            'created_at'        => 'Created At',
174
            'updated_at'        => 'Last Updated At'
175
        ];
176
    }
177
178
    /**
179
     * Before save occurs
180
     * @return bool
181
     */
182
    public function beforeSave($insert)
183
    {
184
        if (parent::beforeSave($insert)) {
185
            if ($this->isNewRecord || $this->password !== $this->oldAttributes['password']) {
186
                $this->password = password_hash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
187
            }
188
            
189
            return true;
190
        }
191
        
192
        return false;
193
    }
194
    
195
    /**
196
     * Validates the user's password
197
     * @param string $password
198
     * return bool
199
     */
200
    public function validatePassword($password)
201
    {
202
        if (password_verify($password, $this->password)) {
203
            if (password_needs_rehash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions)) {
204
                $this->password = password_hash($password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
205
                
206
                // Allow authentication to continue if we weren't able to update the password, but log the message
207
                if (!$this->save()) {
208
                    Yii::warning('Unable to save newly hashed password for user: ' . $this->id);
209
                }
210
            }
211
212
            return true;
213
        }
214
        
215
        return false;
216
    }
217
218
    /**
219
     * Returns true of OTP is enabled
220
     * @return boolean
221
     */
222
    public function isOTPEnabled()
223
    {
224
        return (bool)$this->otp_enabled;
225
    }
226
    
227
    /**
228
     * Provisions TOTP for the account
229
     * @return boolean|string
230
     */
231
    public function provisionOTP()
232
    {
233
        if ($this->isOTPEnabled() === true) {
234
            return false;
235
        }
236
237
        $secret = \random_bytes(256);
238
        $encodedSecret = Base32::encode($secret);
239
        $totp = new TOTP(
240
            $this->username,
241
            $encodedSecret,
242
            30,             // 30 second window
243
            'sha256',       // SHA256 for the hashing algorithm
244
            6               // 6 digits
245
        );
246
247
        $this->otp_secret = $encodedSecret;
248
249
        if ($this->save()) {
250
            return $totp->getProvisioningUri();
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * Enables OTP
258
     * @return boolean
259
     */
260
    public function enableOTP()
261
    {
262
        if ($this->isOTPEnabled() === true) {
263
            return true;
264
        }
265
266
        if ($this->otp_secret == "") {
267
            return false;
268
        }
269
        
270
        $this->otp_enabled = 1;
271
272
        return $this->save();
273
    }
274
275
    /**
276
     * Disables OTP
277
     * @return boolean
278
     */
279
    public function disableOTP()
280
    {
281
        $this->otp_secret = "";
282
        $this->otp_enabled = 0;
283
284
        return $this->save();
285
    }
286
287
    /**
288
     * Verifies the OTP code
289
     * @param integer $code
290
     * @return boolean
291
     */
292
    public function verifyOTP($code)
293
    {
294
        $totp = new TOTP(
295
            $this->username,
296
            $this->otp_secret,
297
            30,             // 30 second window
298
            'sha256',       // SHA256 for the hashing algorithm
299
            6               // 6 digits
300
        );
301
302
        return $totp->verify($code);
303
    }
304
305
    /**
306
     * Activates the user
307
     * @return boolean
308
     */
309
    public function activate()
310
    {
311
        $this->verified = 1;
312
        return $this->save();
313
    }
314
315
    /**
316
     * Whether or not a user is activated or not
317
     * @return boolean
318
     */
319
    public function isActivated()
320
    {
321
        return (bool)$this->verified;
322
    }
323
324
    /**
325
     * @inheritdoc
326
     */
327
    public static function findIdentity($id)
328
    {
329
        return static::findOne($id);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return static::findOne($id); (array|boolean) is incompatible with the return type declared by the interface yii\web\IdentityInterface::findIdentity of type yii\web\IdentityInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
330
    }
331
    
332
    /**
333
     * @inheritdoc
334
     */
335
    public static function findIdentityByAccessToken($token, $type = null)
336
    {
337
        // Checking of the Token is performed in app\components\filters\auth\HMACSignatureAuth
338
        if ($token === null) {
339
            return null;
340
        }
341
        
342
        return static::findOne(['id' => $token['userId']]);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return static::findOne(a... => $token['userId'])); (array|boolean) is incompatible with the return type declared by the interface yii\web\IdentityInterfac...ndIdentityByAccessToken of type yii\web\IdentityInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
343
    }
344
345
    /**
346
     * @inheritdoc
347
     */
348
    public function getAuthKey() {}
349
350
    /**
351
     * @inheritdoc
352
     */
353
    public function validateAuthKey($authKey)
354
    {
355
        return true;
356
    }
357
358
    /**
359
     * @inheritdoc
360
     */
361
    public function getId()
362
    {
363
        return $this->id;
364
    }
365
366
    /**
367
     * @todo
368
     */
369
    public static function sendActivationEmail($email, $token)
0 ignored issues
show
Unused Code introduced by
The parameter $email is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $token is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
370
    {
371
        return true;
372
    }
373
374
    /**
375
     * @todo
376
     */
377
    public static function sendPasswordResetEmail($email, $token)
0 ignored issues
show
Unused Code introduced by
The parameter $email is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $token is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
378
    {
379
        return true;
380
    }
381
382
    /**
383
     * @todo
384
     */
385
    public static function sendPasswordChangedEmail($email)
0 ignored issues
show
Unused Code introduced by
The parameter $email is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
386
    {
387
        return true;
388
    }
389
}