Completed
Pull Request — master (#163)
by Corey
02:42
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 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 $question;
48
  public $time;
49
50
  public function __construct(UserBehaviorInterface $user_behavior, QuestionInterface $question, TimeInterface $time, $config = []) {
51
    $this->user_behavior = $user_behavior;
52
    $this->question = $question;
53
    $this->time = $time;
54
    parent::__construct($config);
55
  }
56
57
  public function afterFind() {
58
    $this->time = new \common\components\Time($this->timezone);
59
    parent::afterFind();
60
  }
61
62
  public function afterRefresh() {
63
    $this->time = new \common\components\Time($this->timezone);
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
  public function sendEmailReport($date) {
361
    if(!$this->send_email) return false; // no partner emails set
362
    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...
363
364
    $checkins_last_month = $this->user_behavior->getCheckInBreakdown();
0 ignored issues
show
Bug introduced by
The call to common\interfaces\UserBe...::getCheckinBreakdown() has too few arguments starting with period. ( Ignorable by Annotation )

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

364
    /** @scrutinizer ignore-call */ 
365
    $checkins_last_month = $this->user_behavior->getCheckInBreakdown();

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...
365
    $graph = Yii::$container
366
      ->get(\common\components\Graph::class)
367
      ->create($checkins_last_month);
368
369
    $user_behaviors   = $this->getUserBehaviors($date);
370
    $user_questions = $this->getUserQuestions($date);
371
372
    $messages = [];
373
    foreach($this->getPartnerEmails() as $email) {
374
      if($email) {
375
        $messages[] = Yii::$app->mailer->compose('checkinReport', [
376
          'user'          => $this,
377
          'email'         => $email,
378
          'date'          => $date,
379
          'user_behaviors'  => $user_behaviors,
380
          'questions'     => $user_questions,
381
          'chart_content' => $graph,
382
          'categories'    => \common\models\Category::$categories,
383
          'behaviors_list'  => \common\models\Behavior::$behaviors,
384
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
385
        ->setReplyTo($this->email)
386
        ->setSubject($this->email." has completed a Faster Scale check-in")
387
        ->setTo($email);
388
      }
389
    }
390
391
    return Yii::$app->mailer->sendMultiple($messages);
392
  }
393
394
  public function getExportData() {
395
    $query = (new Query)
396
      ->select(
397
      'l.id,        
398
       l.date      AS "date",
399
       l.behavior_id AS "behavior_id",
400
       (SELECT q1.answer
401
        FROM question q1
402
        WHERE q1.question = 1
403
          AND q1.user_behavior_id = l.id) AS "question1",
404
       (SELECT q1.answer
405
        FROM question q1
406
        WHERE q1.question = 2
407
          AND q1.user_behavior_id = l.id) AS "question2",
408
       (SELECT q1.answer
409
        FROM question q1
410
        WHERE q1.question = 3
411
          AND q1.user_behavior_id = l.id) AS "question3"')
412
      ->from('user_behavior_link l')
413
      ->join("LEFT JOIN", "question q", "l.id = q.user_behavior_id")
414
      ->where('l.user_id=:user_id', ["user_id" => Yii::$app->user->id])
415
      ->groupBy('l.id,
416
          l.date,
417
          "question1",
418
          "question2",
419
          "question3"')
420
      ->orderBy('l.date DESC');
421
422
    return $query
423
      ->createCommand()
424
      ->query();
425
426
/* Plaintext Query
427
SELECT l.id,
428
       l.date      AS "date",
429
       l.behavior_id AS "behavior_id",
430
       (SELECT q1.answer
431
        FROM question q1
432
        WHERE q1.question = 1
433
          AND q1.user_behavior_id = l.id) AS "question1",
434
       (SELECT q1.answer
435
        FROM question q1
436
        WHERE q1.question = 2
437
          AND q1.user_behavior_id = l.id) AS "question2",
438
       (SELECT q1.answer
439
        FROM question q1
440
        WHERE q1.question = 3
441
          AND q1.user_behavior_id = l.id) AS "question3"
442
FROM   user_behavior_link l
443
       LEFT JOIN question q
444
         ON l.id = q.user_behavior_id
445
WHERE  l.user_id = 1
446
GROUP  BY l.id,
447
          l.date,
448
          "question1",
449
          "question2",
450
          "question3",
451
ORDER  BY l.date DESC;
452
*/
453
  }
454
455
  public function sendSignupNotificationEmail() {
456
    return \Yii::$app->mailer->compose('signupNotification')
457
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
458
      ->setTo(\Yii::$app->params['adminEmail'])
459
      ->setSubject('A new user has signed up for '.\Yii::$app->name)
460
      ->send();
461
  }
462
463
  public function sendVerifyEmail() {
464
    return \Yii::$app->mailer->compose('verifyEmail', ['user' => $this])
465
      ->setFrom([\Yii::$app->params['supportEmail'] => \Yii::$app->name])
466
      ->setTo($this->email)
467
      ->setSubject('Please verify your '.\Yii::$app->name .' account')
468
      ->send();
469
  }
470
471
  public function sendDeleteNotificationEmail() {
472
    $messages = [];
473
    foreach(array_merge([$this->email], $this->getPartnerEmails()) as $email) {
474
      if($email) {
475
        $messages[] = Yii::$app->mailer->compose('deleteNotification', [
476
          'user' => $this,
477
          'email' => $email
478
        ])->setFrom([Yii::$app->params['supportEmail'] => Yii::$app->name])
479
        ->setReplyTo($this->email)
480
        ->setSubject($this->email." has deleted their The Faster Scale App account")
481
        ->setTo($email);
482
      }
483
    }
484
485
    return Yii::$app->mailer->sendMultiple($messages);
486
  }
487
488
  public function getUserQuestions($local_date = null) {
489
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
490
    $questions = $this->getQuestionData($local_date);
491
    return $this->parseQuestionData($questions);
492
  }
493
494
  public function getUserBehaviors($local_date = null) {
495
    if(is_null($local_date)) $local_date = $this->time->getLocalDate();
496
497
    $behaviors = $this->getBehaviorData($local_date);
498
    $behaviors = $this->user_behavior::decorateWithCategory($behaviors);
499
    return $this->parseBehaviorData($behaviors);
500
  }
501
502
  public function parseQuestionData($questions) {
503
    if(!$questions) return [];
504
505
    $question_answers = [];
506
    foreach($questions as $question) {
507
      $behavior = $question['behavior'];
508
509
      $question_answers[$behavior['id']]['question'] = [
510
        "id" => $behavior['id'],
511
        "title" => $behavior['name']
512
      ];
513
514
      $question_answers[$behavior['id']]["answers"][] = [
515
        "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...
516
        "answer" => $question['answer']
517
      ];
518
    }
519
520
    return $question_answers;
521
  }
522
 
523
  public function parseBehaviorData($behaviors) {
524
    if(!$behaviors) return [];
525
526
    $opts_by_cat = [];
527
    foreach($behaviors as $behavior) {
528
      $indx = $behavior['behavior']['category_id'];
529
530
      $opts_by_cat[$indx]['category_name'] = $behavior['behavior']['category']['name'];
531
      $opts_by_cat[$indx]['behaviors'][] = [
532
        "id" => $behavior['behavior_id'],
533
        "name"=>$behavior['behavior']['name']];
534
    }
535
536
    return $opts_by_cat;
537
  }
538
539
  public function getQuestionData($local_date) {
540
    list($start, $end) = $this->time->getUTCBookends($local_date);
541
542
    $questions = $this->question->find()
543
      ->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

543
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
544
      AND date > :start_date 
545
      AND date < :end_date", 
546
    [
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

546
      ->/** @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...
547
      "user_id" => Yii::$app->user->id, 
548
      ':start_date' => $start, 
549
      ":end_date" => $end
550
    ])
551
    ->asArray()
552
    ->all();
553
554
    $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

554
    /** @scrutinizer ignore-call */ 
555
    $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...
555
556
    return $questions;
557
  }
558
559
  public function getBehaviorData($local_date) {
560
    list($start, $end) = $this->time->getUTCBookends($local_date);
561
562
    return $this->user_behavior->find()
563
      ->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

563
      ->where(/** @scrutinizer ignore-type */ "user_id=:user_id 
Loading history...
564
      AND date > :start_date 
565
      AND date < :end_date", 
566
    [
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

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