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

User::getUserQuestions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 2
nc 2
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
  public function __construct(UserBehaviorInterface $user_behavior, TimeInterface $time, $config = []) {
50
    $this->time = $time;
51
    parent::__construct($config);
52
  }
53
54
  public function afterFind() {
55
    $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...
56
    parent::afterFind();
57
  }
58
59
  public function afterRefresh() {
60
    $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...
61
    parent::afterRefresh();
62
  }
63
64
  //public function afterSave() {
65
  //  $this->time = new \common\components\Time($this->timezone);
66
  //  parent::afterSave();
67
  //}
68
69
  /**
70
   * @inheritdoc
71
   */
72
73
  public function behaviors()
74
  {
75
    return [
76
      'timestamp' => [
77
        'class' => yii\behaviors\TimestampBehavior::class,
78
        'attributes' => [
79
          ActiveRecord::EVENT_BEFORE_INSERT => ['created_at', 'updated_at'],
80
          ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
81
        ],
82
      ],
83
    ];
84
  }
85
86
  /**
87
   * @inheritdoc
88
   */
89
  public function rules()
90
  {
91
    return [
92
      ['status', 'default', 'value' => self::STATUS_ACTIVE],
93
      ['status', 'in', 'range' => [self::STATUS_ACTIVE, self::STATUS_DELETED]],
94
95
      ['role', 'default', 'value' => self::ROLE_USER],
96
      ['role', 'in', 'range' => [self::ROLE_USER]],
97
    ];
98
  }
99
100
  public function getPartnerEmails() {
101
    return [
102
      $this->partner_email1,
103
      $this->partner_email2,
104
      $this->partner_email3,
105
    ];
106
  }
107
108
  /**
109
   * @inheritdoc
110
   */
111
  public static function findIdentity($id)
112
  {
113
    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...
114
  }
115
116
  /**
117
   * @inheritdoc
118
   */
119
  public static function findIdentityByAccessToken($token, $type = null)
120
  {
121
    throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
122
  }
123
124
  /**
125
   * Finds user by email
126
   *
127
   * @param  string      $email
128
   * @return static|null
129
   */
130
  public function findByEmail($email)
131
  {
132
    return $this->findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
133
  }
134
135
  /**
136
   * Finds user by password reset token
137
   *
138
   * @param  string      $token password reset token
139
   * @return static|null
140
   */
141
  public function findByPasswordResetToken($token)
142
  {
143
    if(!$this->isTokenCurrent($token)) {
144
      return null;
145
    }
146
147
    return $this->findOne([
148
      'password_reset_token' => $token,
149
      'status' => self::STATUS_ACTIVE,
150
    ]);
151
  }
152
153
  /**
154
   * Finds user by email verification token
155
   *
156
   * @param  string      $token email verification token
157
   * @return static|null
158
   */
159
  public function findByVerifyEmailToken($token)
160
  {
161
    if($this->isTokenConfirmed($token)) return null;
162
163
    $user = $this->findOne([
164
      'verify_email_token' => [$token, $token . self::CONFIRMED_STRING],
165
      'status' => self::STATUS_ACTIVE,
166
    ]);
167
168
    if($user) {
0 ignored issues
show
introduced by
$user is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
169
      if(!$this->isTokenConfirmed($token) &&
170
         !$this->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
171
        return null;
172
      }
173
    }
174
175
    return $user;
176
  }
177
178
  /**
179
   * Finds user by email change token
180
   *
181
   * @param  string      $token email change token
182
   * @return static|null
183
   */
184
  public function findByChangeEmailToken($token)
185
  {
186
    $user = static::findOne([
187
      'change_email_token' => $token,
188
      'status' => self::STATUS_ACTIVE,
189
    ]);
190
191
    if($user) {
0 ignored issues
show
introduced by
$user is of type yii\db\ActiveRecord, thus it always evaluated to true.
Loading history...
192
      if(!$user->isTokenCurrent($token, 'user.verifyAccountTokenExpire')) {
193
        return null;
194
      }
195
    }
196
197
    return $user;
198
  }
199
200
  /**
201
   * Finds out if a token is current or expired
202
   *
203
   * @param  string      $token verification token
204
   * @param  string      $paramPath Yii app param path
205
   * @return boolean
206
   */
207
  public function isTokenCurrent($token, String $paramPath = 'user.passwordResetTokenExpire') {
208
    $expire = \Yii::$app->params[$paramPath];
209
    $parts = explode('_', $token);
210
    $timestamp = (int) end($parts);
211
    if ($timestamp + $expire < time()) {
212
      // token expired
213
      return false;
214
    }
215
    return true;
216
  }
217
218
  /*
219
   * Checks if $token ends with the $match string
220
   *
221
   * @param string    $token verification token (the haystack)
222
   * @param string    $match the needle to search for
223
   */
224
  public function isTokenConfirmed($token = null, String $match = self::CONFIRMED_STRING) {
225
    if(is_null($token)) $token = $this->verify_email_token;
226
    return substr($token, -strlen($match)) === $match;
227
  }
228
229
  /**
230
   * @inheritdoc
231
   */
232
  public function getId()
233
  {
234
    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...
235
  }
236
237
  /**
238
   * @inheritdoc
239
   */
240
  public function getAuthKey()
241
  {
242
    return $this->auth_key;
243
  }
244
245
  public function getTimezone() {
246
    return $this->timezone;
247
  }
248
249
  public function isVerified() {
250
    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...
251
      // for old users who verified their accounts before the addition of
252
      // '_confirmed' to the token
253
      return true;
254
    } else {
255
      return !!$this->verify_email_token && $this->isTokenConfirmed($this->verify_email_token);
256
    }
257
  }
258
259
  /**
260
   * @inheritdoc
261
   */
262
  public function validateAuthKey($authKey)
263
  {
264
    return $this->getAuthKey() === $authKey;
265
  }
266
267
  /**
268
   * Validates password
269
   *
270
   * @param  string  $password password to validate
271
   * @return boolean if password provided is valid for current user
272
   */
273
  public function validatePassword($password)
274
  {
275
    return Yii::$app
276
      ->getSecurity()
277
      ->validatePassword($password, $this->password_hash);
278
  }
279
280
  /**
281
   * Generates password hash from password and sets it to the model
282
   *
283
   * @param string $password
284
   */
285
  public function setPassword($password)
286
  {
287
    $this->password_hash = Yii::$app
288
      ->getSecurity()
289
      ->generatePasswordHash($password);
290
  }
291
292
  /**
293
   * Generates email verification token
294
   */
295
  public function generateVerifyEmailToken()
296
  {
297
    $this->verify_email_token = $this->getRandomVerifyString();
298
  }
299
300
  /**
301
   * Confirms email verification token
302
   */
303
  public function confirmVerifyEmailToken()
304
  {
305
    $this->verify_email_token .= self::CONFIRMED_STRING;
306
  }
307
308
  /**
309
   * Removes email verification token
310
   */
311
  public function removeVerifyEmailToken()
312
  {
313
    $this->verify_email_token = null;
314
  }
315
316
  /**
317
   * Generates email change tokens
318
   */
319
  public function generateChangeEmailToken() {
320
    $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...
321
  }
322
323
  /**
324
   * Removes change email token
325
   */
326
  public function removeChangeEmailToken()
327
  {
328
    $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...
329
  }
330
331
  /**
332
   * Generates "remember me" authentication key
333
   */
334
  public function generateAuthKey()
335
  {
336
    $this->auth_key = Yii::$app
337
      ->getSecurity()
338
      ->generateRandomString();
339
  }
340
341
  /**
342
   * Generates new password reset token
343
   */
344
  public function generatePasswordResetToken()
345
  {
346
    $this->password_reset_token = $this->getRandomVerifyString();
347
  }
348
349
  /**
350
   * Removes password reset token
351
   */
352
  public function removePasswordResetToken()
353
  {
354
    $this->password_reset_token = null;
355
  }
356
357
  /*
358
   * sendEmailReport()
359
   * 
360
   * @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.
361
   * @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)
362
   *
363
   * This is the function that sends email reports. It can send an email report
364
   * for whichever `$date` is passed in. It checks if the user's specified
365
   * criteria are met before it sends any email. It sends email to every
366
   * partner email address the user has set.
367
   */
368
  public function sendEmailReport($date) {
369
    if(!$this->send_email) return false; // no partner emails set
370
    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...
371
372
    $user_behavior = Yii::$container->get(UserBehaviorInterface::class);
373
    $checkins_last_month = $user_behavior->getCheckInBreakdown();
374
375
    // we should only proceed with sending the email if the user
376
    // scored above their set email threshold (User::email_category)
377
    $this_checkin     = $checkins_last_month[$date]; // gets the check-in
378
    if(!$this_checkin)  return false;                // sanity check
379
    $highest_cat_data = end($this_checkin);          // gets the data for the highest category from the check-in
380
    if(!$highest_cat_data) return false;             // another sanity check
381
    $highest_cat_idx  = key($this_checkin);          // gets the key of the highest category
382
383
    // if the highest category they reached today was less than
384
    // the category threshold they have set, don't send the email
385
    if($highest_cat_idx < $this->email_category) return false;
386
387
    $user_behaviors = $user_behavior->getByDate(Yii::$app->user->id, $date);
388
389
    $question = Yii::$container->get(\common\interfaces\QuestionInterface::class);
390
    $user_questions = $question->getByUser(Yii::$app->user->id, $date);
391
392
    $graph = Yii::$container
393
      ->get(\common\components\Graph::class)
394
      ->create($checkins_last_month);
395
396
    $messages = [];
397
    foreach($this->getPartnerEmails() as $email) {
398
      if($email) {
399
        $messages[] = Yii::$app->mailer->compose('checkinReport', [
400
          'user'           => $this,
401
          'email'          => $email,
402
          'date'           => $date,
403
          'user_behaviors' => $user_behaviors,
404
          'questions'      => $user_questions,
405
          'chart_content'  => $graph,
406
          'categories'     => \common\models\Category::$categories,
407
          'behaviors_list' => \common\models\Behavior::$behaviors,
408
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
409
        ->setReplyTo($this->email)
410
        ->setSubject($this->email." has completed a Faster Scale check-in")
411
        ->setTo($email);
412
      }
413
    }
414
415
    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...
416
  }
417
418
  public function getExportData() {
419
    $query = (new Query)
420
      ->select(
421
      'l.id,        
422
       l.date      AS "date",
423
       l.behavior_id AS "behavior_id",
424
       l.category_id AS "category_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 cleanExportData($data) {
514
    var_dump( $data);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($data) looks like debug code. Are you sure you do not want to remove it?
Loading history...
515
    exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
516
   $order = array_flip(["date", "behavior", "category", "question1", "question2", "question3"]);
0 ignored issues
show
Unused Code introduced by
$order = array_flip(arra...estion2', 'question3')) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
517
518
   $ret = array_map(
519
     function($row) use ($order) {
520
       // change timestamp to local time (for the user)
521
       $row['date'] = $this->time->convertUTCToLocal($row['date'], false);
522
       
523
       // clean up things we don't need
524
       $row['category'] = $row['category']['name'];
525
       $row['behavior'] = $row['behavior']['name'];
526
       unset($row['id']);
527
       unset($row['behavior_id']);
528
       unset($row['category_id']);
529
530
       // sort the array into a sensible order
531
       uksort($row, function($a, $b) use ($order) {
532
        return $order[$a] <=> $order[$b];
533
       });
534
       return $row;
535
     }, 
536
     $data
537
   );
538
   return $ret;
539
  }
540
541
  /*
542
   * getIdHash()
543
   *
544
   * @return String a user-identifying hash
545
   *
546
   * After generating the hash, we run it through a url-safe base64 encoding to
547
   * shorten it. This generated string is currently used as an identifier in
548
   * URLs, so the shorter the better. the url-safe version has been ripped from
549
   * https://secure.php.net/manual/en/function.base64-encode.php#103849
550
   *
551
   * It does NOT take into account the user's email address. The email address
552
   * is changeable by the user. If that was used for this function, the
553
   * returned hash would change when the user updates their email. That would
554
   * obviously not be desirable.
555
   */
556
  public function getIdHash() {
557
    return rtrim(
558
      strtr(
559
        base64_encode(
560
          hash('sha256', $this->id."::".$this->created_at, true)
561
        ),
562
      '+/', '-_'),
563
    '=');
564
  }
565
566
  /*
567
   * getRandomVerifyString()
568
   * 
569
   * @return String a randomly generated string with a timestamp appended
570
   *
571
   * This is generally used for verification purposes: verifying an email, password change, or email address change.
572
   */
573
  private function getRandomVerifyString() {
574
    return Yii::$app
575
      ->getSecurity()
576
      ->generateRandomString() . '_' . time();
577
  }
578
}
579