Completed
Pull Request — master (#173)
by Corey
02:55
created

User::sendEmailReport()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 46
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 32
dl 0
loc 46
rs 8.4746
c 0
b 0
f 0
cc 7
nc 7
nop 1
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 = $this->getUserBehaviors($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
       (SELECT q1.answer
426
        FROM question q1
427
        WHERE q1.question = 1
428
          AND q1.user_behavior_id = l.id) AS "question1",
429
       (SELECT q1.answer
430
        FROM question q1
431
        WHERE q1.question = 2
432
          AND q1.user_behavior_id = l.id) AS "question2",
433
       (SELECT q1.answer
434
        FROM question q1
435
        WHERE q1.question = 3
436
          AND q1.user_behavior_id = l.id) AS "question3"')
437
      ->from('user_behavior_link l')
438
      ->join("LEFT JOIN", "question q", "l.id = q.user_behavior_id")
439
      ->where('l.user_id=:user_id', ["user_id" => Yii::$app->user->id])
440
      ->groupBy('l.id,
441
          l.date,
442
          "question1",
443
          "question2",
444
          "question3"')
445
      ->orderBy('l.date DESC');
446
447
    return $query
448
      ->createCommand()
449
      ->query();
450
451
/* Plaintext Query
452
SELECT l.id,
453
       l.date      AS "date",
454
       l.behavior_id AS "behavior_id",
455
       (SELECT q1.answer
456
        FROM question q1
457
        WHERE q1.question = 1
458
          AND q1.user_behavior_id = l.id) AS "question1",
459
       (SELECT q1.answer
460
        FROM question q1
461
        WHERE q1.question = 2
462
          AND q1.user_behavior_id = l.id) AS "question2",
463
       (SELECT q1.answer
464
        FROM question q1
465
        WHERE q1.question = 3
466
          AND q1.user_behavior_id = l.id) AS "question3"
467
FROM   user_behavior_link l
468
       LEFT JOIN question q
469
         ON l.id = q.user_behavior_id
470
WHERE  l.user_id = 1
471
GROUP  BY l.id,
472
          l.date,
473
          "question1",
474
          "question2",
475
          "question3",
476
ORDER  BY l.date DESC;
477
*/
478
  }
479
480
  public function sendSignupNotificationEmail() {
481
    return \Yii::$app->mailer->compose('signupNotification')
482
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
483
      ->setTo(\Yii::$app->params['adminEmail'])
484
      ->setSubject('A new user has signed up for '.\Yii::$app->name)
485
      ->send();
486
  }
487
488
  public function sendVerifyEmail() {
489
    return \Yii::$app->mailer->compose('verifyEmail', ['user' => $this])
490
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
491
      ->setTo($this->email)
492
      ->setSubject('Please verify your '.\Yii::$app->name .' account')
493
      ->send();
494
  }
495
496
  public function sendDeleteNotificationEmail() {
497
    $messages = [];
498
    foreach(array_merge([$this->email], $this->getPartnerEmails()) as $email) {
499
      if($email) {
500
        $messages[] = Yii::$app->mailer->compose('deleteNotification', [
501
          'user' => $this,
502
          'email' => $email
503
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
504
        ->setReplyTo($this->email)
505
        ->setSubject($this->email." has deleted their The Faster Scale App account")
506
        ->setTo($email);
507
      }
508
    }
509
510
    return Yii::$app->mailer->sendMultiple($messages);
511
  }
512
513
  public function getUserQuestions($local_date = null) {
514
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
515
    $questions = $this->getQuestionData($local_date);
516
    return $this->parseQuestionData($questions);
517
  }
518
519
  public function getUserBehaviors($local_date = null) {
520
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
521
522
    $behaviors = $this->getBehaviorData($local_date);
523
    $behaviors = \common\models\UserBehavior::decorateWithCategory($behaviors);
524
    return $this->parseBehaviorData($behaviors);
525
  }
526
527
  public function parseQuestionData($questions) {
528
    if(!$questions) return [];
529
530
    $question_answers = [];
531
    foreach($questions as $question) {
532
      $behavior = $question['behavior'];
533
534
      $question_answers[$behavior['id']]['question'] = [
535
        "id" => $behavior['id'],
536
        "title" => $behavior['name']
537
      ];
538
539
      $question_answers[$behavior['id']]["answers"][] = [
540
        "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...
541
        "answer" => $question['answer']
542
      ];
543
    }
544
545
    return $question_answers;
546
  }
547
 
548
  public function parseBehaviorData($behaviors) {
549
    if(!$behaviors) return [];
550
551
    $opts_by_cat = [];
552
    foreach($behaviors as $behavior) {
553
      $indx = $behavior['behavior']['category_id'];
554
555
      $opts_by_cat[$indx]['category_name'] = $behavior['behavior']['category']['name'];
556
      $opts_by_cat[$indx]['behaviors'][] = [
557
        "id" => $behavior['behavior_id'],
558
        "name"=>$behavior['behavior']['name']];
559
    }
560
561
    return $opts_by_cat;
562
  }
563
564
  public function getQuestionData($local_date) {
565
    list($start, $end) = $this->time->getUTCBookends($local_date);
566
567
    $questions = $this->question->find()
568
      ->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

568
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
569
      AND date > :start_date 
570
      AND date < :end_date", 
571
    [
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

571
      ->/** @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...
572
      "user_id" => Yii::$app->user->id, 
573
      ':start_date' => $start, 
574
      ":end_date" => $end
575
    ])
576
    ->asArray()
577
    ->all();
578
579
    $questions = \common\models\UserBehavior::decorate($questions);
580
581
    return $questions;
582
  }
583
584
  public function getBehaviorData($local_date) {
585
    list($start, $end) = $this->time->getUTCBookends($local_date);
586
587
    $user_behavior = Yii::$container->get(UserBehaviorInterface::class);
588
    return $user_behavior->find()
589
      ->where("user_id=:user_id 
590
      AND date > :start_date 
591
      AND date < :end_date", 
592
    [
593
      "user_id" => Yii::$app->user->id, 
594
      ':start_date' => $start, 
595
      ":end_date" => $end
596
    ])
597
    ->asArray()
598
    ->all();
599
  }
600
601
  public function cleanExportData($data) {
602
   $order = array_flip(["date", "behavior", "category", "question1", "question2", "question3"]);
603
604
   $ret = array_map(
605
     function($row) use ($order) {
606
       // change timestamp to local time (for the user)
607
       $row['date'] = $this->time->convertUTCToLocal($row['date'], false);
608
       
609
       // clean up things we don't need
610
       $row['category'] = $row['behavior']['category']['name'];
611
       $row['behavior'] = $row['behavior']['name'];
612
       unset($row['id']);
613
       unset($row['behavior_id']);
614
615
       // sort the array into a sensible order
616
       uksort($row, function($a, $b) use ($order) {
617
        return $order[$a] <=> $order[$b];
618
       });
619
       return $row;
620
     }, 
621
     $data
622
   );
623
   return $ret;
624
  }
625
626
  /*
627
   * getIdHash()
628
   *
629
   * @return String a user-identifying hash
630
   *
631
   * After generating the hash, we run it through a url-safe base64 encoding to
632
   * shorten it. This generated string is currently used as an identifier in
633
   * URLs, so the shorter the better. the url-safe version has been ripped from
634
   * https://secure.php.net/manual/en/function.base64-encode.php#103849
635
   *
636
   * It does NOT take into account the user's email address. The email address
637
   * is changeable by the user. If that was used for this function, the
638
   * returned hash would change when the user updates their email. That would
639
   * obviously not be desirable.
640
   */
641
  public function getIdHash() {
642
    return rtrim(
643
      strtr(
644
        base64_encode(
645
          hash('sha256', $this->id."::".$this->created_at, true)
646
        ),
647
      '+/', '-_'),
648
    '=');
649
  }
650
651
  /*
652
   * getRandomVerifyString()
653
   * 
654
   * @return String a randomly generated string with a timestamp appended
655
   *
656
   * This is generally used for verification purposes: verifying an email, password change, or email address change.
657
   */
658
  private function getRandomVerifyString() {
659
    return Yii::$app
660
      ->getSecurity()
661
      ->generateRandomString() . '_' . time();
662
  }
663
}
664