Completed
Pull Request — master (#163)
by Corey
02:33
created

User::isPartnerEnabled()   A

Complexity

Conditions 6
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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

535
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
536
      AND date > :start_date 
537
      AND date < :end_date", 
538
    [
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

538
      ->/** @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...
539
      "user_id" => Yii::$app->user->id, 
540
      ':start_date' => $start, 
541
      ":end_date" => $end
542
    ])
543
    ->asArray()
544
    ->all();
545
546
    $questions = $this->user_behavior::decorate($questions);
0 ignored issues
show
Bug introduced by
The call to common\interfaces\UserBe...orInterface::decorate() has too few arguments starting with with_category. ( Ignorable by Annotation )

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

546
    /** @scrutinizer ignore-call */ 
547
    $questions = $this->user_behavior::decorate($questions);

This check compares calls to functions or methods with their respective definitions. If the call has less 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...
547
548
    return $questions;
549
  }
550
551
  public function getBehaviorData($local_date) {
552
    list($start, $end) = $this->time->getUTCBookends($local_date);
553
554
    return $this->user_behavior->find()
555
      ->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

555
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
556
      AND date > :start_date 
557
      AND date < :end_date", 
558
    [
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

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