Completed
Pull Request — master (#163)
by Corey
03:37
created

User::parseBehaviorData()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

456
    /** @scrutinizer ignore-call */ 
457
    $behaviors = $this->user_behavior::decorateWithCategory($behaviors);
Loading history...
457
    return $this->parseBehaviorData($behaviors);
458
  }
459
460
  public function parseQuestionData($questions) {
461
    if(!$questions) return [];
462
463
    $question_answers = [];
464
    foreach($questions as $question) {
465
      $behavior = $question['behavior'];
466
467
      $question_answers[$behavior['id']]['question'] = [
468
        "id" => $behavior['id'],
469
        "title" => $behavior['name']
470
      ];
471
472
      $question_answers[$behavior['id']]["answers"][] = [
473
        "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...
474
        "answer" => $question['answer']
475
      ];
476
    }
477
478
    return $question_answers;
479
  }
480
 
481
  public function parseBehaviorData($behaviors) {
482
    if(!$behaviors) return [];
483
484
    $opts_by_cat = [];
485
    foreach($behaviors as $behavior) {
486
      $indx = $behavior['behavior']['category_id'];
487
488
      $opts_by_cat[$indx]['category_name'] = $behavior['behavior']['category']['name'];
489
      $opts_by_cat[$indx]['behaviors'][] = [
490
        "id" => $behavior['behavior_id'],
491
        "name"=>$behavior['behavior']['name']];
492
    }
493
494
    return $opts_by_cat;
495
  }
496
497
  public function getQuestionData($local_date) {
498
    list($start, $end) = $this->time->getUTCBookends($local_date);
499
500
    $questions = $this->question->find()
501
      ->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

501
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
502
      AND date > :start_date 
503
      AND date < :end_date", 
504
    [
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

504
      ->/** @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...
505
      "user_id" => Yii::$app->user->id, 
506
      ':start_date' => $start, 
507
      ":end_date" => $end
508
    ])
509
    ->asArray()
510
    ->all();
511
512
    $questions = $this->user_behavior::decorate($questions);
0 ignored issues
show
Bug introduced by
The method decorate() does not exist on common\interfaces\UserBehaviorInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to common\interfaces\UserBehaviorInterface. ( Ignorable by Annotation )

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

512
    /** @scrutinizer ignore-call */ 
513
    $questions = $this->user_behavior::decorate($questions);
Loading history...
513
514
    return $questions;
515
  }
516
517
  public function getBehaviorData($local_date) {
518
    list($start, $end) = $this->time->getUTCBookends($local_date);
519
520
    return $this->user_behavior->find()
521
      ->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

521
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
522
      AND date > :start_date 
523
      AND date < :end_date", 
524
    [
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

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