Passed
Pull Request — master (#177)
by Corey
02:57
created

User::sendSignupNotificationEmail()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 6
c 0
b 0
f 0
rs 10
cc 1
nc 1
nop 0
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\QuestionInterface;
12
use \common\interfaces\TimeInterface;
13
14
/**
15
 * User model
16
 *
17
 * @property integer $id
18
 * @property string $password_hash
19
 * @property string $password_reset_token
20
 * @property string $verify_email_token
21
 * @property string $email
22
 * @property string $auth_key
23
 * @property integer $role
24
 * @property integer $status
25
 * @property integer $created_at
26
 * @property integer $updated_at
27
 * @property string $password write-only password
28
 * @property string $timezone
29
 * @property boolean $send_email
30
 * @property integer $email_category
31
 * @property string $partner_email1
32
 * @property string $partner_email2
33
 * @property string $partner_email3
34
 * @property boolean $expose_graph
35
 * @property string $desired_email
36
 * @property string $change_emaiL_token
37
 */
38
class User extends ActiveRecord implements IdentityInterface, UserInterface
39
{
40
  const STATUS_DELETED = 0;
41
  const STATUS_ACTIVE = 10;
42
43
  const ROLE_USER = 10;
44
45
  const CONFIRMED_STRING = '_confirmed';
46
47
  public $user_behavior;
48
  public $question;
49
  public $time;
50
51
  public function __construct(UserBehaviorInterface $user_behavior, QuestionInterface $question, TimeInterface $time, $config = []) {
52
    $this->question = $question;
53
    $this->time = $time;
54
    parent::__construct($config);
55
  }
56
57
  public function afterFind() {
58
    $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...
59
    parent::afterFind();
60
  }
61
62
  public function afterRefresh() {
63
    $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...
64
    parent::afterRefresh();
65
  }
66
67
  //public function afterSave() {
68
  //  $this->time = new \common\components\Time($this->timezone);
69
  //  parent::afterSave();
70
  //}
71
72
  /**
73
   * @inheritdoc
74
   */
75
76
  public function behaviors()
77
  {
78
    return [
79
      'timestamp' => [
80
        'class' => yii\behaviors\TimestampBehavior::class,
81
        'attributes' => [
82
          ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
83
          ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
84
        ],
85
      ],
86
    ];
87
  }
88
89
  /**
90
   * @inheritdoc
91
   */
92
  public function rules()
93
  {
94
    return [
95
      ['status', 'default', 'value' => self::STATUS_ACTIVE],
96
      ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
97
98
      ['role', 'default', 'value' => self::ROLE_USER],
99
      ['role', 'in', 'range' => [self::ROLE_USER]],
100
    ];
101
  }
102
103
  public function getPartnerEmails() {
104
    return [
105
      $this->partner_email1,
106
      $this->partner_email2,
107
      $this->partner_email3,
108
    ];
109
  }
110
111
  /**
112
   * @inheritdoc
113
   */
114
  public static function findIdentity($id)
115
  {
116
    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 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...
117
  }
118
119
  /**
120
   * @inheritdoc
121
   */
122
  public static function findIdentityByAccessToken($token, $type = null)
123
  {
124
    throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
125
  }
126
127
  /**
128
   * Finds user by email
129
   *
130
   * @param  string      $email
131
   * @return static|null
132
   */
133
  public function findByEmail($email)
134
  {
135
    return $this->findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
136
  }
137
138
  /**
139
   * Finds user by password reset token
140
   *
141
   * @param  string      $token password reset token
142
   * @return static|null
143
   */
144
  public function findByPasswordResetToken($token)
145
  {
146
    if(!$this->isTokenCurrent($token)) {
147
      return null;
148
    }
149
150
    return $this->findOne([
151
      'password_reset_token' => $token,
152
      'status' => self::STATUS_ACTIVE,
153
    ]);
154
  }
155
156
  /**
157
   * Finds user by email verification token
158
   *
159
   * @param  string      $token email verification token
160
   * @return static|null
161
   */
162
  public function findByVerifyEmailToken($token)
163
  {
164
    if($this->isTokenConfirmed($token)) return null;
165
166
    $user = $this->findOne([
167
      'verify_email_token' => [$token, $token . self::CONFIRMED_STRING],
168
      'status' => self::STATUS_ACTIVE,
169
    ]);
170
171
    if($user) {
0 ignored issues
show
introduced by
$user is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
172
      if(!$this->isTokenConfirmed($token) &&
173
         !$this->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
174
        return null;
175
      }
176
    }
177
178
    return $user;
179
  }
180
181
  /**
182
   * Finds user by email change token
183
   *
184
   * @param  string      $token email change token
185
   * @return static|null
186
   */
187
  public function findByChangeEmailToken($token)
188
  {
189
    $user = static::findOne([
190
      'change_email_token' => $token,
191
      'status' => self::STATUS_ACTIVE,
192
    ]);
193
194
    if($user) {
0 ignored issues
show
introduced by
$user is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
195
      if(!$user->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
196
        return null;
197
      }
198
    }
199
200
    return $user;
201
  }
202
203
  /**
204
   * Finds out if a token is current or expired
205
   *
206
   * @param  string      $token verification token
207
   * @param  string      $paramPath Yii app param path
208
   * @return boolean
209
   */
210
  public function isTokenCurrent($token, String $paramPath = 'user.passwordResetTokenExpire') {
211
    $expire = \Yii::$app->params[$paramPath];
212
    $parts = explode('_', $token);
213
    $timestamp = (int) end($parts);
214
    if ($timestamp + $expire < time()) {
215
      // token expired
216
      return false;
217
    }
218
    return true;
219
  }
220
221
  /*
222
   * Checks if $token ends with the $match string
223
   *
224
   * @param string    $token verification token (the haystack)
225
   * @param string    $match the needle to search for
226
   */
227
  public function isTokenConfirmed($token = null, String $match = self::CONFIRMED_STRING) {
228
    if(is_null($token)) $token = $this->verify_email_token;
229
    return substr($token, -strlen($match)) === $match;
230
  }
231
232
  /**
233
   * @inheritdoc
234
   */
235
  public function getId()
236
  {
237
    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...
238
  }
239
240
  /**
241
   * @inheritdoc
242
   */
243
  public function getAuthKey()
244
  {
245
    return $this->auth_key;
246
  }
247
248
  public function getTimezone() {
249
    return $this->timezone;
250
  }
251
252
  public function isVerified() {
253
    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...
254
      // for old users who verified their accounts before the addition of
255
      // '_confirmed' to the token
256
      return true;
257
    } else {
258
      return !!$this->verify_email_token && $this->isTokenConfirmed($this->verify_email_token);
259
    }
260
  }
261
262
  /**
263
   * @inheritdoc
264
   */
265
  public function validateAuthKey($authKey)
266
  {
267
    return $this->getAuthKey() === $authKey;
268
  }
269
270
  /**
271
   * Validates password
272
   *
273
   * @param  string  $password password to validate
274
   * @return boolean if password provided is valid for current user
275
   */
276
  public function validatePassword($password)
277
  {
278
    return Yii::$app
279
      ->getSecurity()
280
      ->validatePassword($password, $this->password_hash);
281
  }
282
283
  /**
284
   * Generates password hash from password and sets it to the model
285
   *
286
   * @param string $password
287
   */
288
  public function setPassword($password)
289
  {
290
    $this->password_hash = Yii::$app
291
      ->getSecurity()
292
      ->generatePasswordHash($password);
293
  }
294
295
  /**
296
   * Generates email verification token
297
   */
298
  public function generateVerifyEmailToken()
299
  {
300
    $this->verify_email_token = $this->getRandomVerifyString();
301
  }
302
303
  /**
304
   * Confirms email verification token
305
   */
306
  public function confirmVerifyEmailToken()
307
  {
308
    $this->verify_email_token .= self::CONFIRMED_STRING;
309
  }
310
311
  /**
312
   * Removes email verification token
313
   */
314
  public function removeVerifyEmailToken()
315
  {
316
    $this->verify_email_token = null;
317
  }
318
319
  /**
320
   * Generates email change tokens
321
   */
322
  public function generateChangeEmailToken() {
323
    $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...
324
  }
325
326
  /**
327
   * Removes change email token
328
   */
329
  public function removeChangeEmailToken()
330
  {
331
    $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...
332
  }
333
334
  /**
335
   * Generates "remember me" authentication key
336
   */
337
  public function generateAuthKey()
338
  {
339
    $this->auth_key = Yii::$app
340
      ->getSecurity()
341
      ->generateRandomString();
342
  }
343
344
  /**
345
   * Generates new password reset token
346
   */
347
  public function generatePasswordResetToken()
348
  {
349
    $this->password_reset_token = $this->getRandomVerifyString();
350
  }
351
352
  /**
353
   * Removes password reset token
354
   */
355
  public function removePasswordResetToken()
356
  {
357
    $this->password_reset_token = null;
358
  }
359
360
  /*
361
   * sendEmailReport()
362
   * 
363
   * @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.
364
   * @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)
365
   *
366
   * This is the function that sends email reports. It can send an email report
367
   * for whichever `$date` is passed in. It checks if the user's specified
368
   * criteria are met before it sends any email. It sends email to every
369
   * partner email address the user has set.
370
   */
371
  public function sendEmailReport($date) {
372
    if(!$this->send_email) return false; // no partner emails set
373
    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...
374
375
    $user_behavior = Yii::$container->get(UserBehaviorInterface::class);
376
    $checkins_last_month = $user_behavior->getCheckInBreakdown();
377
378
    // we should only proceed with sending the email if the user
379
    // scored above their set email threshold (User::email_category)
380
    $this_checkin     = $checkins_last_month[$date]; // gets the check-in
381
    if(!$this_checkin)  return false;           // sanity check
382
    $highest_cat_data = end($this_checkin);     // gets the data for the highest category from the check-in
383
    if(!$highest_cat_data) return false;        // another sanity check
384
    $highest_cat_idx  = key($this_checkin); // gets the key of the highest category
385
386
    // if the highest category they reached today was less than
387
    // the category threshold they have set, don't send the email
388
    if($highest_cat_idx < $this->email_category) return false;
389
390
    $graph = Yii::$container
391
      ->get(\common\components\Graph::class)
392
      ->create($checkins_last_month);
393
394
    $user_behaviors = $user_behavior->getByDate(Yii::$app->user->id, $date);
395
    $user_questions = $this->getUserQuestions($date);
396
397
    $messages = [];
398
    foreach($this->getPartnerEmails() as $email) {
399
      if($email) {
400
        $messages[] = Yii::$app->mailer->compose('checkinReport', [
401
          'user'          => $this,
402
          'email'         => $email,
403
          'date'          => $date,
404
          'user_behaviors'  => $user_behaviors,
405
          'questions'     => $user_questions,
406
          'chart_content' => $graph,
407
          'categories'    => \common\models\Category::$categories,
408
          'behaviors_list'  => \common\models\Behavior::$behaviors,
409
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
410
        ->setReplyTo($this->email)
411
        ->setSubject($this->email." has completed a Faster Scale check-in")
412
        ->setTo($email);
413
      }
414
    }
415
416
    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...
417
  }
418
419
  public function getExportData() {
420
    $query = (new Query)
421
      ->select(
422
      'l.id,        
423
       l.date      AS "date",
424
       l.behavior_id AS "behavior_id",
425
       l.category_id AS "category_id",
426
       (SELECT q1.answer
427
        FROM question q1
428
        WHERE q1.question = 1
429
          AND q1.user_behavior_id = l.id) AS "question1",
430
       (SELECT q1.answer
431
        FROM question q1
432
        WHERE q1.question = 2
433
          AND q1.user_behavior_id = l.id) AS "question2",
434
       (SELECT q1.answer
435
        FROM question q1
436
        WHERE q1.question = 3
437
          AND q1.user_behavior_id = l.id) AS "question3"')
438
      ->from('user_behavior_link l')
439
      ->join("LEFT JOIN", "question q", "l.id = q.user_behavior_id")
440
      ->where('l.user_id=:user_id', ["user_id" => Yii::$app->user->id])
441
      ->groupBy('l.id,
442
          l.date,
443
          "question1",
444
          "question2",
445
          "question3"')
446
      ->orderBy('l.date DESC');
447
448
    return $query
449
      ->createCommand()
450
      ->query();
451
452
/* Plaintext Query
453
SELECT l.id,
454
       l.date      AS "date",
455
       l.behavior_id AS "behavior_id",
456
       (SELECT q1.answer
457
        FROM question q1
458
        WHERE q1.question = 1
459
          AND q1.user_behavior_id = l.id) AS "question1",
460
       (SELECT q1.answer
461
        FROM question q1
462
        WHERE q1.question = 2
463
          AND q1.user_behavior_id = l.id) AS "question2",
464
       (SELECT q1.answer
465
        FROM question q1
466
        WHERE q1.question = 3
467
          AND q1.user_behavior_id = l.id) AS "question3"
468
FROM   user_behavior_link l
469
       LEFT JOIN question q
470
         ON l.id = q.user_behavior_id
471
WHERE  l.user_id = 1
472
GROUP  BY l.id,
473
          l.date,
474
          "question1",
475
          "question2",
476
          "question3",
477
ORDER  BY l.date DESC;
478
*/
479
  }
480
481
  public function sendSignupNotificationEmail() {
482
    return \Yii::$app->mailer->compose('signupNotification')
483
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
484
      ->setTo(\Yii::$app->params['adminEmail'])
485
      ->setSubject('A new user has signed up for '.\Yii::$app->name)
486
      ->send();
487
  }
488
489
  public function sendVerifyEmail() {
490
    return \Yii::$app->mailer->compose('verifyEmail', ['user' => $this])
491
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
492
      ->setTo($this->email)
493
      ->setSubject('Please verify your '.\Yii::$app->name .' account')
494
      ->send();
495
  }
496
497
  public function sendDeleteNotificationEmail() {
498
    $messages = [];
499
    foreach(array_merge([$this->email], $this->getPartnerEmails()) as $email) {
500
      if($email) {
501
        $messages[] = Yii::$app->mailer->compose('deleteNotification', [
502
          'user' => $this,
503
          'email' => $email
504
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
505
        ->setReplyTo($this->email)
506
        ->setSubject($this->email." has deleted their The Faster Scale App account")
507
        ->setTo($email);
508
      }
509
    }
510
511
    return Yii::$app->mailer->sendMultiple($messages);
512
  }
513
514
  public function getUserQuestions($local_date = null) {
515
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
516
    $questions = $this->getQuestionData($local_date);
517
    return $this->parseQuestionData($questions);
518
  }
519
520
  public function parseQuestionData($questions) {
521
    if(!$questions) return [];
522
523
    $question_answers = [];
524
    foreach($questions as $question) {
525
      $behavior = $question['behavior'];
526
527
      $question_answers[$behavior['id']]['question'] = [
528
        "id" => $behavior['id'],
529
        "title" => $behavior['name']
530
      ];
531
532
      $question_answers[$behavior['id']]["answers"][] = [
533
        "title" => $this->question::$QUESTIONS[$question['question']],
0 ignored issues
show
Bug introduced by
Accessing QUESTIONS on the interface common\interfaces\QuestionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
534
        "answer" => $question['answer']
535
      ];
536
    }
537
538
    return $question_answers;
539
  }
540
541
  public function getQuestionData($local_date) {
542
    list($start, $end) = $this->time->getUTCBookends($local_date);
543
544
    $questions = $this->question->find()
545
      ->where("user_id=:user_id 
0 ignored issues
show
Bug introduced by
'user_id=:user_id ... AND date < :end_date' of type string is incompatible with the type array expected by parameter $condition of yii\db\QueryInterface::where(). ( Ignorable by Annotation )

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

545
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
546
      AND date > :start_date 
547
      AND date < :end_date", 
548
    [
0 ignored issues
show
Unused Code introduced by
The call to yii\db\QueryInterface::where() has too many arguments starting with array('user_id' => yii::...t, ':end_date' => $end). ( Ignorable by Annotation )

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

548
      ->/** @scrutinizer ignore-call */ where("user_id=:user_id 

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
549
      "user_id" => Yii::$app->user->id, 
550
      ':start_date' => $start, 
551
      ":end_date" => $end
552
    ])
553
    ->asArray()
554
    ->all();
555
556
    $questions = \common\models\UserBehavior::decorate($questions);
557
558
    return $questions;
559
  }
560
561
562
  public function cleanExportData($data) {
563
   $order = array_flip(["date", "behavior", "category", "question1", "question2", "question3"]);
564
565
   $ret = array_map(
566
     function($row) use ($order) {
567
       // change timestamp to local time (for the user)
568
       $row['date'] = $this->time->convertUTCToLocal($row['date'], false);
569
       
570
       // clean up things we don't need
571
       $row['category'] = $row['category']['name'];
572
       $row['behavior'] = $row['behavior']['name'];
573
       unset($row['id']);
574
       unset($row['behavior_id']);
575
       unset($row['category_id']);
576
577
       // sort the array into a sensible order
578
       uksort($row, function($a, $b) use ($order) {
579
        return $order[$a] <=> $order[$b];
580
       });
581
       return $row;
582
     }, 
583
     $data
584
   );
585
   return $ret;
586
  }
587
588
  /*
589
   * getIdHash()
590
   *
591
   * @return String a user-identifying hash
592
   *
593
   * After generating the hash, we run it through a url-safe base64 encoding to
594
   * shorten it. This generated string is currently used as an identifier in
595
   * URLs, so the shorter the better. the url-safe version has been ripped from
596
   * https://secure.php.net/manual/en/function.base64-encode.php#103849
597
   *
598
   * It does NOT take into account the user's email address. The email address
599
   * is changeable by the user. If that was used for this function, the
600
   * returned hash would change when the user updates their email. That would
601
   * obviously not be desirable.
602
   */
603
  public function getIdHash() {
604
    return rtrim(
605
      strtr(
606
        base64_encode(
607
          hash('sha256', $this->id."::".$this->created_at, true)
608
        ),
609
      '+/', '-_'),
610
    '=');
611
  }
612
613
  /*
614
   * getRandomVerifyString()
615
   * 
616
   * @return String a randomly generated string with a timestamp appended
617
   *
618
   * This is generally used for verification purposes: verifying an email, password change, or email address change.
619
   */
620
  private function getRandomVerifyString() {
621
    return Yii::$app
622
      ->getSecurity()
623
      ->generateRandomString() . '_' . time();
624
  }
625
}
626