Completed
Pull Request — master (#178)
by Corey
06:02
created

User::parseQuestionData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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