User   C
last analyzed

Complexity

Total Complexity 57

Size/Duplication

Total Lines 552
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 57
eloc 197
c 4
b 0
f 0
dl 0
loc 552
rs 5.04

37 Methods

Rating   Name   Duplication   Size   Complexity  
A afterRefresh() 0 3 1
A __construct() 0 3 1
A isTokenCurrent() 0 9 2
A generateChangeEmailToken() 0 2 1
A getRandomVerifyString() 0 4 1
A validateAuthKey() 0 3 1
A getExportData() 0 33 1
A getId() 0 3 1
A removeVerifyEmailToken() 0 3 1
A cleanExportRow() 0 21 2
A isVerified() 0 7 3
A findIdentity() 0 3 1
A generateAuthKey() 0 5 1
A removePasswordResetToken() 0 3 1
A rules() 0 8 1
A findIdentityByAccessToken() 0 3 1
A getAuthKey() 0 3 1
A generateVerifyEmailToken() 0 3 1
A getIdHash() 0 8 1
A generatePasswordResetToken() 0 3 1
A removeChangeEmailToken() 0 3 1
A findByEmail() 0 3 1
A getPartnerEmails() 0 5 1
B sendEmailReport() 0 48 7
A validatePassword() 0 5 1
A setPassword() 0 5 1
A sendVerifyEmail() 0 6 1
A isTokenConfirmed() 0 3 2
A sendDeleteNotificationEmail() 0 15 3
A behaviors() 0 8 1
A confirmVerifyEmailToken() 0 3 1
A sendSignupNotificationEmail() 0 6 1
A findByPasswordResetToken() 0 10 2
A findByVerifyEmailToken() 0 17 5
A findByChangeEmailToken() 0 14 3
A afterFind() 0 3 1
A getTimezone() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like User often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use User, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace common\models;
3
4
use yii;
5
use yii\base\NotSupportedException;
6
use yii\db\Query;
7
use yii\web\IdentityInterface;
8
use \common\components\ActiveRecord;
9
use \common\interfaces\UserInterface;
10
use \common\interfaces\UserBehaviorInterface;
11
use \common\interfaces\TimeInterface;
12
13
/**
14
 * User model
15
 *
16
 * @property integer $id
17
 * @property string $password_hash
18
 * @property string $password_reset_token
19
 * @property string $verify_email_token
20
 * @property string $email
21
 * @property string $auth_key
22
 * @property integer $role
23
 * @property integer $status
24
 * @property integer $created_at
25
 * @property integer $updated_at
26
 * @property string $password write-only password
27
 * @property string $timezone
28
 * @property boolean $send_email
29
 * @property integer $email_category
30
 * @property string $partner_email1
31
 * @property string $partner_email2
32
 * @property string $partner_email3
33
 * @property boolean $expose_graph
34
 * @property string $desired_email
35
 * @property string $change_emaiL_token
36
 */
37
class User extends ActiveRecord implements IdentityInterface, UserInterface
38
{
39
  const STATUS_DELETED = 0;
40
  const STATUS_ACTIVE = 10;
41
42
  const ROLE_USER = 10;
43
44
  const CONFIRMED_STRING = '_confirmed';
45
46
  public $user_behavior;
47
  public $time;
48
49
  private $export_order = [
50
    "date" => 0,
51
    "behavior" => 1,
52
    "category" => 2,
53
    "question1" => 3,
54
    "question2" => 4,
55
    "question3" => 5,
56
  ];
57
58
  public function __construct(UserBehaviorInterface $user_behavior, TimeInterface $time, $config = []) {
59
    $this->time = $time;
60
    parent::__construct($config);
61
  }
62
63
  public function afterFind() {
64
    $this->time->timezone = $this->timezone;
0 ignored issues
show
Bug introduced by
Accessing timezone on the interface common\interfaces\TimeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
65
    parent::afterFind();
66
  }
67
68
  public function afterRefresh() {
69
    $this->time->timezone = $this->timezone;
0 ignored issues
show
Bug introduced by
Accessing timezone on the interface common\interfaces\TimeInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
70
    parent::afterRefresh();
71
  }
72
73
  //public function afterSave() {
74
  //  $this->time = new \common\components\Time($this->timezone);
75
  //  parent::afterSave();
76
  //}
77
78
  /**
79
   * @inheritdoc
80
   * @codeCoverageIgnore
81
   */
82
83
  public function behaviors()
84
  {
85
    return [
86
      'timestamp' => [
87
        'class' => yii\behaviors\TimestampBehavior::class,
88
        'attributes' => [
89
          ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
90
          ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
91
        ],
92
      ],
93
    ];
94
  }
95
96
  /**
97
   * @inheritdoc
98
   */
99
  public function rules()
100
  {
101
    return [
102
      ['status', 'default', 'value' => self::STATUS_ACTIVE],
103
      ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
104
105
      ['role', 'default', 'value' => self::ROLE_USER],
106
      ['role', 'in', 'range' => [self::ROLE_USER]],
107
    ];
108
  }
109
110
  public function getPartnerEmails() {
111
    return [
112
      $this->partner_email1,
113
      $this->partner_email2,
114
      $this->partner_email3,
115
    ];
116
  }
117
118
  /**
119
   * @inheritdoc
120
   * @codeCoverageIgnore
121
   */
122
  public static function findIdentity($id)
123
  {
124
    return static::findOne($id);
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::findOne($id) returns the type yii\db\ActiveRecord which is incompatible with the return type mandated by yii\web\IdentityInterface::findIdentity() of null|yii\web\IdentityInterface.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
125
  }
126
127
  /**
128
   * @inheritdoc
129
   * @codeCoverageIgnore
130
   */
131
  public static function findIdentityByAccessToken($token, $type = null)
132
  {
133
    throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
134
  }
135
136
  /**
137
   * Finds user by email
138
   *
139
   * @param  string      $email
140
   * @return static|null
141
   */
142
  public function findByEmail($email)
143
  {
144
    return $this->find()->where(['email' => $email, 'status' => self::STATUS_ACTIVE])->one();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->find()->wh...:STATUS_ACTIVE))->one() also could return the type array which is incompatible with the documented return type common\models\User|null.
Loading history...
145
  }
146
147
  /**
148
   * Finds user by password reset token
149
   *
150
   * @param  string      $token password reset token
151
   * @return static|null
152
   */
153
  public function findByPasswordResetToken($token)
154
  {
155
    if(!$this->isTokenCurrent($token)) {
156
      return null;
157
    }
158
159
    return $this->find()->where([
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->find()->wh...:STATUS_ACTIVE))->one() also could return the type array which is incompatible with the documented return type common\models\User|null.
Loading history...
160
      'password_reset_token' => $token,
161
      'status' => self::STATUS_ACTIVE,
162
    ])->one();
163
  }
164
165
  /**
166
   * Finds user by email verification token
167
   *
168
   * @param  string      $token email verification token
169
   * @return static|null
170
   */
171
  public function findByVerifyEmailToken($token)
172
  {
173
    if($this->isTokenConfirmed($token)) return null;
174
175
    $user = $this->find()->where([
176
      'verify_email_token' => [$token, $token . self::CONFIRMED_STRING],
177
      'status' => self::STATUS_ACTIVE,
178
    ])->one();
179
180
    if($user) {
181
      if(!$this->isTokenConfirmed($token) &&
182
         !$this->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
183
        return null;
184
      }
185
    }
186
187
    return $user;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user also could return the type array which is incompatible with the documented return type common\models\User|null.
Loading history...
188
  }
189
190
  /**
191
   * Finds user by email change token
192
   *
193
   * @param  string      $token email change token
194
   * @return static|null
195
   */
196
  public function findByChangeEmailToken($token)
197
  {
198
    $user = static::find()->where([
199
      'change_email_token' => $token,
200
      'status' => self::STATUS_ACTIVE,
201
    ])->one();
202
203
    if($user) {
204
      if(!$user->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
205
        return null;
206
      }
207
    }
208
209
    return $user;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $user also could return the type array which is incompatible with the documented return type common\models\User|null.
Loading history...
210
  }
211
212
  /**
213
   * Finds out if a token is current or expired
214
   *
215
   * @param  string      $token verification token
216
   * @param  string      $paramPath Yii app param path
217
   * @return boolean
218
   */
219
  public function isTokenCurrent($token, String $paramPath = 'user.passwordResetTokenExpire') {
220
    $expire = \Yii::$app->params[$paramPath];
221
    $parts = explode('_', $token);
222
    $timestamp = (int) end($parts);
223
    if ($timestamp + $expire < time()) {
224
      // token expired
225
      return false;
226
    }
227
    return true;
228
  }
229
230
  /*
231
   * Checks if $token ends with the $match string
232
   *
233
   * @param string    $token verification token (the haystack)
234
   * @param string    $match the needle to search for
235
   */
236
  public function isTokenConfirmed($token = null, String $match = self::CONFIRMED_STRING) {
237
    if(is_null($token)) $token = $this->verify_email_token;
238
    return substr($token, -strlen($match)) === $match;
239
  }
240
241
  /**
242
   * @inheritdoc
243
   * @codeCoverageIgnore
244
   */
245
  public function getId()
246
  {
247
    return $this->getPrimaryKey();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getPrimaryKey() also could return the type array which is incompatible with the return type mandated by yii\web\IdentityInterface::getId() of integer|string.
Loading history...
248
  }
249
250
  /**
251
   * @inheritdoc
252
   * @codeCoverageIgnore
253
   */
254
  public function getAuthKey()
255
  {
256
    return $this->auth_key;
257
  }
258
259
  public function getTimezone() {
260
    return $this->timezone;
261
  }
262
263
  public function isVerified() {
264
    if(is_null($this->verify_email_token)) {
0 ignored issues
show
introduced by
The condition is_null($this->verify_email_token) is always false.
Loading history...
265
      // for old users who verified their accounts before the addition of
266
      // '_confirmed' to the token
267
      return true;
268
    } else {
269
      return !!$this->verify_email_token && $this->isTokenConfirmed($this->verify_email_token);
270
    }
271
  }
272
273
  /**
274
   * @inheritdoc
275
   */
276
  public function validateAuthKey($authKey)
277
  {
278
    return $this->getAuthKey() === $authKey;
279
  }
280
281
  /**
282
   * Validates password
283
   *
284
   * @param  string  $password password to validate
285
   * @return boolean if password provided is valid for current user
286
   */
287
  public function validatePassword($password)
288
  {
289
    return Yii::$app
290
      ->getSecurity()
291
      ->validatePassword($password, $this->password_hash);
292
  }
293
294
  /**
295
   * Generates password hash from password and sets it to the model
296
   *
297
   * @param string $password
298
   */
299
  public function setPassword($password)
300
  {
301
    $this->password_hash = Yii::$app
302
      ->getSecurity()
303
      ->generatePasswordHash($password);
304
  }
305
306
  /**
307
   * Generates email verification token
308
   */
309
  public function generateVerifyEmailToken()
310
  {
311
    $this->verify_email_token = $this->getRandomVerifyString();
312
  }
313
314
  /**
315
   * Confirms email verification token
316
   */
317
  public function confirmVerifyEmailToken()
318
  {
319
    $this->verify_email_token .= self::CONFIRMED_STRING;
320
  }
321
322
  /**
323
   * Removes email verification token
324
   */
325
  public function removeVerifyEmailToken()
326
  {
327
    $this->verify_email_token = null;
328
  }
329
330
  /**
331
   * Generates email change tokens
332
   */
333
  public function generateChangeEmailToken() {
334
    $this->change_email_token = $this->getRandomVerifyString();
0 ignored issues
show
Bug Best Practice introduced by
The property change_email_token does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
335
  }
336
337
  /**
338
   * Removes change email token
339
   */
340
  public function removeChangeEmailToken()
341
  {
342
    $this->change_email_token = null;
0 ignored issues
show
Bug Best Practice introduced by
The property change_email_token does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
343
  }
344
345
  /**
346
   * Generates "remember me" authentication key
347
   */
348
  public function generateAuthKey()
349
  {
350
    $this->auth_key = Yii::$app
351
      ->getSecurity()
352
      ->generateRandomString();
353
  }
354
355
  /**
356
   * Generates new password reset token
357
   */
358
  public function generatePasswordResetToken()
359
  {
360
    $this->password_reset_token = $this->getRandomVerifyString();
361
  }
362
363
  /**
364
   * Removes password reset token
365
   */
366
  public function removePasswordResetToken()
367
  {
368
    $this->password_reset_token = null;
369
  }
370
371
  /*
372
   * sendEmailReport()
373
   * 
374
   * @param $date String a date string in YYYY-mm-dd format. The desired check-in date to send an email report of. Normally just today.
375
   * @return boolean whether or not it succeeds. It will return false if the user's specified criteria are not met (or if the user did not select any behaviors for the given day)
376
   *
377
   * This is the function that sends email reports. It can send an email report
378
   * for whichever `$date` is passed in. It checks if the user's specified
379
   * criteria are met before it sends any email. It sends email to every
380
   * partner email address the user has set.
381
   */
382
  public function sendEmailReport($date) {
383
    if(!$this->send_email) return false; // no partner emails set
384
    list($start, $end) = $this->time->getUTCBookends($date);
0 ignored issues
show
Comprehensibility Best Practice introduced by
This list assign is not used and could be removed.
Loading history...
385
386
    $user_behavior = Yii::$container->get(UserBehaviorInterface::class);
387
    $checkins_last_month = $user_behavior->getCheckInBreakdown();
388
389
    // we should only proceed with sending the email if the user
390
    // scored above their set email threshold (User::email_category)
391
    $this_checkin     = $checkins_last_month[$date]; // gets the check-in
392
    if(!$this_checkin)  return false;                // sanity check
393
    $highest_cat_data = end($this_checkin);          // gets the data for the highest category from the check-in
394
    if(!$highest_cat_data) return false;             // another sanity check
395
    $highest_cat_idx  = key($this_checkin);          // gets the key of the highest category
396
397
    // if the highest category they reached today was less than
398
    // the category threshold they have set, don't send the email
399
    if($highest_cat_idx < $this->email_category) return false;
400
401
    $user_behaviors = $user_behavior->getByDate(Yii::$app->user->id, $date);
402
403
    $question = Yii::$container->get(\common\interfaces\QuestionInterface::class);
404
    $user_questions = $question->getByUser(Yii::$app->user->id, $date);
405
406
    $graph = Yii::$container
407
      ->get(\common\components\Graph::class)
408
      ->create($checkins_last_month);
409
410
    $messages = [];
411
    foreach($this->getPartnerEmails() as $email) {
412
      if($email) {
413
        $messages[] = Yii::$app->mailer->compose('checkinReport', [
414
          'user'           => $this,
415
          'email'          => $email,
416
          'date'           => $date,
417
          'user_behaviors' => $user_behaviors,
418
          'questions'      => $user_questions,
419
          'chart_content'  => $graph,
420
          'categories'     => \common\models\Category::$categories,
421
          'behaviors_list' => \common\models\Behavior::$behaviors,
422
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
423
        ->setReplyTo($this->email)
424
        ->setSubject($this->email." has completed a Faster Scale check-in")
425
        ->setTo($email);
426
      }
427
    }
428
429
    return Yii::$app->mailer->sendMultiple($messages);
0 ignored issues
show
Bug Best Practice introduced by
The expression return yii::app->mailer->sendMultiple($messages) returns the type integer which is incompatible with the documented return type boolean.
Loading history...
430
  }
431
432
  public function getExportData() {
433
    $query = (new Query)
434
      ->select(
435
      'l.id,        
436
       l.date      AS "date",
437
       l.custom_behavior AS "custom_behavior",
438
       l.behavior_id AS "behavior_id",
439
       l.category_id AS "category_id",
440
       (SELECT q1.answer
441
        FROM question q1
442
        WHERE q1.question = 1
443
          AND q1.user_behavior_id = l.id) AS "question1",
444
       (SELECT q1.answer
445
        FROM question q1
446
        WHERE q1.question = 2
447
          AND q1.user_behavior_id = l.id) AS "question2",
448
       (SELECT q1.answer
449
        FROM question q1
450
        WHERE q1.question = 3
451
          AND q1.user_behavior_id = l.id) AS "question3"')
452
      ->from('user_behavior_link l')
453
      ->join("LEFT JOIN", "question q", "l.id = q.user_behavior_id")
454
      ->where('l.user_id=:user_id', ["user_id" => Yii::$app->user->id])
455
      ->groupBy('l.id,
456
          l.date,
457
          "question1",
458
          "question2",
459
          "question3"')
460
      ->orderBy('l.date DESC');
461
462
    return $query
463
      ->createCommand()
464
      ->query();
465
466
/* Plaintext Query
467
SELECT l.id,
468
       l.date      AS "date",
469
       l.custom_behavior AS "custom_behavior",
470
       l.behavior_id AS "behavior_id",
471
       (SELECT q1.answer
472
        FROM question q1
473
        WHERE q1.question = 1
474
          AND q1.user_behavior_id = l.id) AS "question1",
475
       (SELECT q1.answer
476
        FROM question q1
477
        WHERE q1.question = 2
478
          AND q1.user_behavior_id = l.id) AS "question2",
479
       (SELECT q1.answer
480
        FROM question q1
481
        WHERE q1.question = 3
482
          AND q1.user_behavior_id = l.id) AS "question3"
483
FROM   user_behavior_link l
484
       LEFT JOIN question q
485
         ON l.id = q.user_behavior_id
486
WHERE  l.user_id = 1
487
GROUP  BY l.id,
488
          l.date,
489
          l.custom_behavior,
490
          "question1",
491
          "question2",
492
          "question3",
493
ORDER  BY l.date DESC;
494
*/
495
  }
496
497
  public function sendSignupNotificationEmail() {
498
    return \Yii::$app->mailer->compose('signupNotification')
499
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
500
      ->setTo(\Yii::$app->params['adminEmail'])
501
      ->setSubject('A new user has signed up for '.\Yii::$app->name)
502
      ->send();
503
  }
504
505
  public function sendVerifyEmail() {
506
    return \Yii::$app->mailer->compose('verifyEmail', ['user' => $this])
507
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
508
      ->setTo($this->email)
509
      ->setSubject('Please verify your '.\Yii::$app->name .' account')
510
      ->send();
511
  }
512
513
  public function sendDeleteNotificationEmail() {
514
    $messages = [];
515
    foreach(array_merge([$this->email], $this->getPartnerEmails()) as $email) {
516
      if($email) {
517
        $messages[] = Yii::$app->mailer->compose('deleteNotification', [
518
          'user' => $this,
519
          'email' => $email
520
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
521
        ->setReplyTo($this->email)
522
        ->setSubject($this->email." has deleted their The Faster Scale App account")
523
        ->setTo($email);
524
      }
525
    }
526
527
    return Yii::$app->mailer->sendMultiple($messages);
528
  }
529
530
  public function cleanExportRow($row) {
531
   // change timestamp to local time (for the user)
532
   $row['date'] = $this->time->convertUTCToLocal($row['date'], false);
533
534
   // clean up things we don't need
535
   $row['category'] = $row['category']['name'];
536
   if(array_key_exists('behavior', $row)) {
537
     $row['behavior'] = $row['behavior']['name'];
538
   } else {
539
     $row['behavior'] = $row['custom_behavior'];
540
   }
541
   unset($row['id']);
542
   unset($row['behavior_id']);
543
   unset($row['category_id']);
544
   unset($row['custom_behavior']);
545
546
   // sort the array into a sensible order
547
   uksort($row, function($a, $b) {
548
     return $this->export_order[$a] <=> $this->export_order[$b];
549
   });
550
   return $row;
551
  }
552
553
  /*
554
   * getIdHash()
555
   *
556
   * @return String a user-identifying hash
557
   *
558
   * After generating the hash, we run it through a url-safe base64 encoding to
559
   * shorten it. This generated string is currently used as an identifier in
560
   * URLs, so the shorter the better. the url-safe version has been ripped from
561
   * https://secure.php.net/manual/en/function.base64-encode.php#103849
562
   *
563
   * It does NOT take into account the user's email address. The email address
564
   * is changeable by the user. If that was used for this function, the
565
   * returned hash would change when the user updates their email. That would
566
   * obviously not be desirable.
567
   */
568
  public function getIdHash() {
569
    return rtrim(
570
      strtr(
571
        base64_encode(
572
          hash('sha256', $this->id."::".$this->created_at, true)
573
        ),
574
      '+/', '-_'),
575
    '=');
576
  }
577
578
  /*
579
   * getRandomVerifyString()
580
   * 
581
   * @return String a randomly generated string with a timestamp appended
582
   *
583
   * This is generally used for verification purposes: verifying an email, password change, or email address change.
584
   */
585
  private function getRandomVerifyString() {
586
    return Yii::$app
587
      ->getSecurity()
588
      ->generateRandomString() . '_' . time();
589
  }
590
}
591