Completed
Push — master ( ebefd5...98eed5 )
by Charles
06:43
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
    public function beforeValidate()
179
    {
180
        if (parent::beforeValidate()) {
181
            $this->username = \strtolower($this->username);
182
            return true;
183
        }
184
185
        return false;
186
    }
187
188
    /**
189
     * Before save occurs
190
     * @return bool
191
     */
192
    public function beforeSave($insert)
193
    {
194
        if (parent::beforeSave($insert)) {
195
            if ($this->isNewRecord || $this->password !== $this->oldAttributes['password']) {
196
                $this->password = password_hash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
197
            }
198
            
199
            return true;
200
        }
201
        
202
        return false;
203
    }
204
    
205
    /**
206
     * Validates the user's password
207
     * @param string $password
208
     * return bool
209
     */
210
    public function validatePassword($password)
211
    {
212
        if (password_verify($password, $this->password)) {
213
            if (password_needs_rehash($this->password, $this->passwordHashAlgorithm, $this->passwordHashOptions)) {
214
                $this->password = password_hash($password, $this->passwordHashAlgorithm, $this->passwordHashOptions);
215
                
216
                // Allow authentication to continue if we weren't able to update the password, but log the message
217
                if (!$this->save()) {
218
                    Yii::warning('Unable to save newly hashed password for user: ' . $this->id);
219
                }
220
            }
221
222
            return true;
223
        }
224
        
225
        return false;
226
    }
227
228
    /**
229
     * Returns true of OTP is enabled
230
     * @return boolean
231
     */
232
    public function isOTPEnabled()
233
    {
234
        return (bool)$this->otp_enabled;
235
    }
236
    
237
    /**
238
     * Provisions TOTP for the account
239
     * @return boolean|string
240
     */
241
    public function provisionOTP()
242
    {
243
        if ($this->isOTPEnabled() === true) {
244
            return false;
245
        }
246
247
        $secret = \random_bytes(256);
248
        $encodedSecret = Base32::encode($secret);
249
        $totp = new TOTP(
250
            $this->username,
251
            $encodedSecret,
252
            30,             // 30 second window
253
            'sha256',       // SHA256 for the hashing algorithm
254
            6               // 6 digits
255
        );
256
257
        $this->otp_secret = $encodedSecret;
258
259
        if ($this->save()) {
260
            return $totp->getProvisioningUri();
261
        }
262
263
        return false;
264
    }
265
266
    /**
267
     * Enables OTP
268
     * @return boolean
269
     */
270
    public function enableOTP()
271
    {
272
        if ($this->isOTPEnabled() === true) {
273
            return true;
274
        }
275
276
        if ($this->otp_secret == "") {
277
            return false;
278
        }
279
        
280
        $this->otp_enabled = 1;
281
282
        return $this->save();
283
    }
284
285
    /**
286
     * Disables OTP
287
     * @return boolean
288
     */
289
    public function disableOTP()
290
    {
291
        $this->otp_secret = "";
292
        $this->otp_enabled = 0;
293
294
        return $this->save();
295
    }
296
297
    /**
298
     * Verifies the OTP code
299
     * @param integer $code
300
     * @return boolean
301
     */
302
    public function verifyOTP($code)
303
    {
304
        $totp = new TOTP(
305
            $this->username,
306
            $this->otp_secret,
307
            30,             // 30 second window
308
            'sha256',       // SHA256 for the hashing algorithm
309
            6               // 6 digits
310
        );
311
312
        return $totp->verify($code);
313
    }
314
315
    /**
316
     * Activates the user
317
     * @return boolean
318
     */
319
    public function activate()
320
    {
321
        $this->verified = 1;
322
        return $this->save();
323
    }
324
325
    /**
326
     * Whether or not a user is activated or not
327
     * @return boolean
328
     */
329
    public function isActivated()
330
    {
331
        return (bool)$this->verified;
332
    }
333
334
    /**
335
     * @inheritdoc
336
     */
337
    public static function findIdentity($id)
338
    {
339
        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...
340
    }
341
    
342
    /**
343
     * @inheritdoc
344
     */
345
    public static function findIdentityByAccessToken($token, $type = null)
346
    {
347
        // Checking of the Token is performed in app\components\filters\auth\HMACSignatureAuth
348
        if ($token === null) {
349
            return null;
350
        }
351
        
352
        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...
353
    }
354
355
    /**
356
     * @inheritdoc
357
     */
358
    public function getAuthKey() {}
359
360
    /**
361
     * @inheritdoc
362
     */
363
    public function validateAuthKey($authKey)
364
    {
365
        return true;
366
    }
367
368
    /**
369
     * @inheritdoc
370
     */
371
    public function getId()
372
    {
373
        return $this->id;
374
    }
375
}