Passed
Push — master ( 391857...ec1759 )
by Jonathan
11:52
created

User::scopeWithRoleInDomain()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 20
rs 9.5555
cc 5
nc 2
nop 3
1
<?php
2
3
namespace Uccello\Core\Models;
4
5
use Illuminate\Notifications\Notifiable;
6
use Illuminate\Foundation\Auth\User as Authenticatable;
7
use Illuminate\Database\Eloquent\SoftDeletes;
8
use Illuminate\Support\Collection;
9
use Illuminate\Support\Facades\Auth;
10
use Illuminate\Support\Facades\Cache;
11
use Spatie\Searchable\Searchable;
0 ignored issues
show
Bug introduced by
The type Spatie\Searchable\Searchable was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
use Spatie\Searchable\SearchResult;
0 ignored issues
show
Bug introduced by
The type Spatie\Searchable\SearchResult was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use Uccello\Core\Support\Traits\UccelloModule;
14
use Uccello\Core\Models\Group;
15
16
class User extends Authenticatable implements Searchable
17
{
18
    use SoftDeletes;
19
    use Notifiable;
20
    use UccelloModule;
21
22
    /**
23
     * The table associated with the model.
24
     *
25
     * @var string
26
     */
27
    protected $table = 'users';
28
29
    /**
30
     * The attributes that should be mutated to dates.
31
     *
32
     * @var array
33
     */
34
    protected $dates = [ 'deleted_at' ];
35
36
    /**
37
     * The attributes that should be casted to native types.
38
     *
39
     * @var array
40
     */
41
    protected $casts = [
42
        'avatar' => 'object',
43
    ];
44
45
    /**
46
     * The attributes that are mass assignable.
47
     *
48
     * @var array
49
     */
50
    protected $fillable = [
51
        'username',
52
        'name',
53
        'email',
54
        'password',
55
        'is_admin',
56
        'domain_id'
57
    ];
58
59
    /**
60
     * The attributes that should be hidden for arrays.
61
     *
62
     * @var array
63
     */
64
    protected $hidden = [
65
        'password', 'remember_token',
66
    ];
67
68
    /**
69
     * The accessors to append to the model's array form.
70
     *
71
     * @var array
72
     */
73
    protected $appends = [
74
        'recordLabel',
75
        'uuid',
76
    ];
77
78
    public $searchableType = 'user';
79
80
    public $searchableColumns = [
81
        'username',
82
        'name',
83
        'email'
84
    ];
85
86
    public function getSearchResult(): SearchResult
87
    {
88
        return new SearchResult(
89
            $this,
90
            $this->recordLabel
91
        );
92
    }
93
94
    /**
95
     * Scope a query to only include users with role in domain.
96
     *
97
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
98
     * @param  \Uccello\Core\Models\Domain|null $domain
99
     *
100
     *
101
     * @return \Illuminate\Database\Eloquent\Builder
102
     */
103
    public function scopeWithRoleInDomain($builder, ?Domain $domain, $withDescendants = false)
104
    {
105
        if (!$domain) {
106
            $domain = Domain::first();
107
        }
108
109
        // Check if user is admin or if he has at least a role on the domain
110
        // or on descendants domains if withDescendants option is on
111
        return $builder->where('is_admin',true)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $builder->where('...ion(...) { /* ... */ }) also could return the type Illuminate\Database\Query\Builder which is incompatible with the documented return type Illuminate\Database\Eloquent\Builder.
Loading history...
112
            ->orWhereIn('id', function ($query) use ($domain, $withDescendants) {
113
                $privilegesTable = env('UCCELLO_TABLE_PREFIX', 'uccello_').'privileges';
114
115
                $query->select('user_id')
116
                    ->from($privilegesTable);
117
118
                if (Auth::user() && Auth::user()->canSeeDescendantsRecords($domain) && $withDescendants) {
0 ignored issues
show
Bug introduced by
The method canSeeDescendantsRecords() does not exist on Illuminate\Contracts\Auth\Authenticatable. It seems like you code against a sub-type of Illuminate\Contracts\Auth\Authenticatable such as Illuminate\Foundation\Auth\User. ( Ignorable by Annotation )

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

118
                if (Auth::user() && Auth::user()->/** @scrutinizer ignore-call */ canSeeDescendantsRecords($domain) && $withDescendants) {
Loading history...
119
                    $domainsIds = $domain->findDescendants()->pluck('id');
120
                    $query->whereIn('domain_id', $domainsIds);
121
                } else {
122
                    $query->where('domain_id', $domain->id);
123
                }
124
            });
125
        }
126
127
    public function domain()
128
    {
129
        return $this->belongsTo(Domain::class);
130
    }
131
132
    public function lastDomain()
133
    {
134
        return $this->belongsTo(Domain::class);
135
    }
136
137
    public function privileges()
138
    {
139
        return $this->hasMany(Privilege::class);
140
    }
141
142
    public function menus()
143
    {
144
        return $this->hasMany(Menu::class);
145
    }
146
147
    public function groups()
148
    {
149
        return $this->belongsToMany(Group::class, 'uccello_rl_groups_users');
150
    }
151
152
    public function userSettings()
153
    {
154
        return $this->hasOne(UserSettings::class, 'user_id');
155
    }
156
157
    /**
158
     * Returns record label
159
     *
160
     * @return string
161
     */
162
    public function getRecordLabelAttribute() : string
163
    {
164
        return trim($this->name) ?? $this->username;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Uccello\Core\Models\User. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
165
    }
166
167
    /**
168
     * Get avatar type
169
     *
170
     * @return string
171
     */
172
    public function getAvatarTypeAttribute() : string
173
    {
174
        return $this->avatar->type ?? 'initials';
0 ignored issues
show
Bug introduced by
The property type does not exist on string.
Loading history...
175
    }
176
177
    /**
178
     * Returns initals generated from the user name
179
     *
180
     * @return string
181
     */
182
    public function getInitialsAttribute() : string
183
    {
184
        $initials = "";
185
186
        $words = explode(" ", strtoupper($this->name));
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Uccello\Core\Models\User. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
187
188
        $i = 0;
189
        foreach ($words as $w) {
190
            $initials .= $w[0];
191
            $i++;
192
193
            if ($i === 3) { // Maximum: 3 letters
194
                break;
195
            }
196
        }
197
198
        return $initials;
199
    }
200
201
    /**
202
     * Returns the image to use as the user avatar
203
     *
204
     * @return string
205
     */
206
    public function getImageAttribute() : string
207
    {
208
        $image = 'vendor/uccello/uccello/images/user-no-image.png';
209
210
        if ($this->avatarType === 'gravatar') {
211
            $image = 'https://www.gravatar.com/avatar/' . md5($this->email) . '?d=mm';
0 ignored issues
show
Bug introduced by
The property email does not seem to exist on Uccello\Core\Models\User. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
212
213
        } elseif ($this->avatarType === 'image' && !empty($this->avatar->path)) {
0 ignored issues
show
Bug introduced by
The property path does not exist on string.
Loading history...
214
            $image = $this->avatar->path;
215
        }
216
217
        return $image;
218
    }
219
220
    /**
221
     * Returns user settings
222
     *
223
     * @return \stdClass;
224
     */
225
    public function getSettingsAttribute()
226
    {
227
        return $this->userSettings->data ?? new \stdClass;
0 ignored issues
show
Bug introduced by
The property data does not seem to exist on Uccello\Core\Models\UserSettings. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
228
    }
229
230
    /**
231
     * Searches a settings by key and returns the current value
232
     *
233
     * @param string $key
234
     * @param mixed $defaultValue
235
     * @return \stdClass|null;
236
     */
237
    public function getSettings($key, $defaultValue=null) {
238
        return $this->userSettings->data->{$key} ?? $defaultValue;
0 ignored issues
show
Bug introduced by
The property data does not seem to exist on Uccello\Core\Models\UserSettings. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
239
    }
240
241
    /**
242
     * Returns user's roles on a domain
243
     *
244
     * @param \Uccello\Core\Models\Domain $domain
245
     * @return \Illuminate\Support\Collection
246
     */
247
    public function rolesOnDomain($domain) : Collection
248
    {
249
        // return Cache::remember('user_'.$this->id.'_domain_'.$domain->slug.'_roles', 600, function () use($domain) {
250
            $roles = collect();
251
252
            if (config('uccello.roles.display_ancestors_roles')) {
253
                $treeDomainsIds = $domain->findAncestors()->pluck('id');
254
            } else {
255
                $treeDomainsIds = collect([ $domain->id ]);
256
            }
257
258
            foreach ($treeDomainsIds as $treeDomainId) {
259
                $_domain = Domain::find($treeDomainId);
260
                foreach ($this->privileges->where('domain_id', $_domain->id) as $privilege) {
261
                    $roles[ ] = $privilege->role;
262
                }
263
            }
264
265
            return $roles;
266
        // });
267
268
    }
269
270
    /**
271
     * Returns ids of user's roles on a domain
272
     *
273
     * @param \Uccello\Core\Models\Domain $domain
274
     * @return \Illuminate\Support\Collection
275
     */
276
    public function subordonateRolesIdsOnDomain($domain) : Collection
277
    {
278
        $roles = $this->rolesOnDomain($domain);
279
280
        $subordonateRoles = collect();
281
        foreach ($roles as $role) {
282
            $subordonateRoles = $subordonateRoles->merge($role->findDescendants()->pluck('id'));
283
        }
284
285
        return $subordonateRoles;
286
    }
287
288
    /**
289
     * Check if the user has at least a role on a domain
290
     *
291
     * @param \Uccello\Core\Models\Domain $domain
292
     * @return boolean
293
     */
294
    public function hasRoleOnDomain($domain) : bool {
295
        if ($this->is_admin) {
296
            return true;
297
        }
298
299
        return $this->rolesOnDomain($domain)->count() > 0;
300
    }
301
302
    /**
303
     * Check if the user has at least a role on a domain or its descendants
304
     *
305
     * @param \Uccello\Core\Models\Domain $domain
306
     * @return boolean
307
     */
308
    public function hasRoleOnDescendantDomain(Domain $domain) : bool {
309
        if ($this->is_admin) {
310
            return true;
311
        }
312
313
        $hasRole = false;
314
315
        $descendants = Cache::remember('domain_'.$domain->slug.'_descendants', 600, function () use($domain) {
316
            return $domain->findDescendants()->get();
317
        });
318
319
        foreach ($descendants as $descendant) {
320
            if ($this->hasRoleOnDomain($descendant)) {
321
                $hasRole = true;
322
                break;
323
            }
324
        }
325
326
        return $hasRole;
327
    }
328
329
    /**
330
     * Returns all user capabilities on a module in a domain.
331
     * If the user has a capability in one of the parents of a domain, he also has it in that domain.
332
     *
333
     * @param \Uccello\Core\Models\Domain $domain
334
     * @param \Uccello\Core\Models\Module $module
335
     * @return \Illuminate\Support\Collection
336
     */
337
    public function capabilitiesOnModule(Domain $domain, Module $module) : Collection
338
    {
339
        $keyName = 'user_'.$this->id.'_'.$domain->slug.'_'.$module->name.'_capabilities';
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Uccello\Core\Models\Module. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
340
341
        return Cache::remember($keyName, 600, function () use($domain, $module) {
342
            $capabilities = collect();
343
344
            // Get the domain and all its parents
345
            $domainParents = $domain->findAncestors()->get();
346
347
            // Get user privileges on each domain
348
            foreach ($domainParents as $_domain) {
349
                $privileges = $this->privileges->where('domain_id', $_domain->id);
350
351
                foreach ($privileges as $privilege) {
352
353
                    foreach ($privilege->role->profiles as $profile) {
354
                        $capabilities = $capabilities->merge($profile->capabilitiesOnModule($module));
355
                    }
356
                }
357
            }
358
359
            return $capabilities;
360
        });
361
    }
362
363
    /**
364
     * Checks if the user has a capability on a module in a domain.
365
     *
366
     * @param string $capabilityName
367
     * @param \Uccello\Core\Models\Domain $domain
368
     * @param \Uccello\Core\Models\Module $module
369
     * @return boolean
370
     */
371
    public function hasCapabilityOnModule(string $capabilityName, Domain $domain, Module $module) : bool
372
    {
373
        $capability = capability($capabilityName);
374
375
        $userCapabilities = $this->capabilitiesOnModule($domain, $module);
376
377
        return $this->is_admin || $userCapabilities->contains($capability);
378
    }
379
380
    /**
381
     * Checks if the user can access to settings panel.
382
     * Checks if the user has at least one admin capability on admin modules in a domain.
383
     *
384
     * @param \Uccello\Core\Models\Domain|null $domain
385
     * @return boolean
386
     */
387
    public function canAccessToSettingsPanel(?Domain $domain) : bool
388
    {
389
        if (empty($domain)) {
390
            $domain = Domain::first();
391
        }
392
393
        $keyName = 'user_'.$this->id.'_'.$domain->slug.'_can_access_to_settings_panel';
394
395
        return Cache::remember($keyName, 600, function () use($domain) {
396
397
            $hasCapability = false;
398
399
            foreach (Module::all() as $module) {
400
                if ($module->isAdminModule() === true && $this->canAdmin($domain, $module)) {
401
                    $hasCapability = true;
402
                    break;
403
                }
404
            }
405
406
            return $hasCapability;
407
        });
408
    }
409
410
    /**
411
     * Checks if the user can admin a module in a domain.
412
     *
413
     * @param \Uccello\Core\Models\Domain $domain
414
     * @param \Uccello\Core\Models\Module $module
415
     * @return boolean
416
     */
417
    public function canAdmin(Domain $domain, Module $module) : bool
418
    {
419
        return $this->hasCapabilityOnModule('admin', $domain, $module);
420
    }
421
422
    /**
423
     * Checks if the user can create a module in a domain.
424
     *
425
     * @param \Uccello\Core\Models\Domain $domain
426
     * @param \Uccello\Core\Models\Module $module
427
     * @return boolean
428
     */
429
    public function canCreate(Domain $domain, Module $module) : bool
430
    {
431
        return $this->hasCapabilityOnModule('create', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
432
    }
433
434
    /**
435
     * Checks if the user can retrieve a module in a domain.
436
     *
437
     * @param \Uccello\Core\Models\Domain $domain
438
     * @param \Uccello\Core\Models\Module $module
439
     * @return boolean
440
     */
441
    public function canRetrieve(Domain $domain, Module $module) : bool
442
    {
443
        return $this->hasCapabilityOnModule('retrieve', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
444
    }
445
446
    /**
447
     * Checks if the user can update a module in a domain.
448
     *
449
     * @param \Uccello\Core\Models\Domain $domain
450
     * @param \Uccello\Core\Models\Module $module
451
     * @return boolean
452
     */
453
    public function canUpdate(Domain $domain, Module $module) : bool
454
    {
455
        return $this->hasCapabilityOnModule('update', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
456
    }
457
458
    /**
459
     * Checks if the user can delete a module in a domain.
460
     *
461
     * @param \Uccello\Core\Models\Domain $domain
462
     * @param \Uccello\Core\Models\Module $module
463
     * @return boolean
464
     */
465
    public function canDelete(Domain $domain, Module $module) : bool
466
    {
467
        return $this->hasCapabilityOnModule('delete', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
468
    }
469
470
    /**
471
     * Checks if the user can create by API a module in a domain.
472
     *
473
     * @param \Uccello\Core\Models\Domain $domain
474
     * @param \Uccello\Core\Models\Module $module
475
     * @return boolean
476
     */
477
    public function canCreateByApi(Domain $domain, Module $module) : bool
478
    {
479
        return $this->hasCapabilityOnModule('api-create', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
480
    }
481
482
    /**
483
     * Checks if the user can retrieve by API a module in a domain.
484
     *
485
     * @param \Uccello\Core\Models\Domain $domain
486
     * @param \Uccello\Core\Models\Module $module
487
     * @return boolean
488
     */
489
    public function canRetrieveByApi(Domain $domain, Module $module) : bool
490
    {
491
        return $this->hasCapabilityOnModule('api-retrieve', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
492
    }
493
494
    /**
495
     * Checks if the user can update by API a module in a domain.
496
     *
497
     * @param \Uccello\Core\Models\Domain $domain
498
     * @param \Uccello\Core\Models\Module $module
499
     * @return boolean
500
     */
501
    public function canUpdateByApi(Domain $domain, Module $module) : bool
502
    {
503
        return $this->hasCapabilityOnModule('api-update', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
504
    }
505
506
    /**
507
     * Checks if the user can delete by API a module in a domain.
508
     *
509
     * @param \Uccello\Core\Models\Domain $domain
510
     * @param \Uccello\Core\Models\Module $module
511
     * @return boolean
512
     */
513
    public function canDeleteByApi(Domain $domain, Module $module) : bool
514
    {
515
        return $this->hasCapabilityOnModule('api-delete', $domain, $module) || ($module->isAdminModule() && $this->canAdmin($domain, $module));
516
    }
517
518
    /**
519
     * Checks if the user has almost a role allowing to view data transversally
520
     *
521
     * @param \Uccello\Core\Models\Domain $domain
522
     * @return boolean
523
     */
524
    public function canSeeDescendantsRecords(Domain $domain) : bool
525
    {
526
        $allowed = false;
527
528
        if ($this->is_admin) {
529
            $allowed = true;
530
        } else {
531
            $roles = $this->rolesOnDomain($domain);
532
            foreach ($roles as $role) {
533
                if ($role->see_descendants_records) {
534
                    $allowed = true;
535
                    break;
536
                }
537
            }
538
        }
539
540
        return $allowed;
541
    }
542
543
    public function getAllowedGroupUuids()
544
    {
545
        // Use cache
546
        $allowedGroups = Cache::rememberForever(
547
            'allowed_groups_for_' . ($this->is_admin ? 'admin' : $this->getKey()),
548
            function () {
549
                return $this->getAllowedGroupUuidsProcess();
550
            }
551
        );
552
553
        return $allowedGroups;
554
    }
555
556
    public function getAllowedGroupsAndUsers($addUsers = true)
557
    {
558
        // Use cache
559
        $allowedGroupsAndUsers = Cache::rememberForever(
560
            'allowed_group_users_for_' . ($addUsers ? 'u_' : '') . ($this->is_admin ? 'admin' : $this->getKey()),
561
            function () use ($addUsers) {
562
                return $this->getAllowedGroupsAndUsersProcess($addUsers);
563
            }
564
        );
565
566
        return $allowedGroupsAndUsers;
567
    }
568
569
    protected function getAllowedGroupUuidsProcess()
570
    {
571
        $allowedUserUuids = collect([$this->uuid]);
572
573
        if ($this->is_admin) {
574
            $groups = Group::all();
575
        } else {
576
            $groups = [];
577
            $users = [];
578
579
            foreach ($this->groups as $group) {
580
                $groups[$group->uuid] = $group;
581
            };
582
583
            $this->addRecursiveChildrenGroups($groups, $users, $groups, false);
584
585
            $groups = collect($groups);
586
        }
587
588
        foreach ($groups as $uuid => $group) {
589
            $allowedUserUuids[] = $uuid;
590
        }
591
592
        return $allowedUserUuids;
593
    }
594
595
    protected function addRecursiveChildrenGroups(&$groups, &$users, $searchGroups, $addUsers = false)
596
    {
597
        foreach ($searchGroups as $uuid => $searchGroup) {
598
            $searchChildrenGroups = [];
599
600
            foreach ($searchGroup->childrenGroups as $childrenGroup) {
601
                if (empty($groups[$childrenGroup->uuid])) {
602
                    $groups[$childrenGroup->uuid] = $childrenGroup;
603
                    $searchChildrenGroups[$childrenGroup->uuid] = $childrenGroup;
604
                }
605
606
                if($addUsers)
607
                {
608
                    foreach ($childrenGroup->users as $user) {
609
                        if (empty($users[$user->uuid])) {
610
                            $users[$user->uuid] = $user;
611
                        }
612
                    }
613
                }
614
            }
615
616
            $this->addRecursiveChildrenGroups($groups, $users, $searchChildrenGroups, $addUsers);
617
        }
618
    }
619
620
    protected function getAllowedGroupsAndUsersProcess($addUsers = true)
621
    {
622
        $allowedUserUuids = collect([[
623
            'uuid' => $this->uuid,
624
            'recordLabel' => uctrans('me', $this->module)
625
        ]]);
626
627
        // if ($this->is_admin) {
628
            $groups = Group::orderBy('name')->get();
629
            $users  = \App\User::orderBy('name')->get();
0 ignored issues
show
Bug introduced by
The type App\User was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
630
        // } else {
631
        //     $groups = [];
632
        //     $users = [];
633
634
        //     foreach ($this->groups as $group) {
635
        //         $groups[$group->uuid] = $group;
636
637
        //         if($addUsers)
638
        //         {
639
        //             foreach ($group->users as $user) {
640
        //                 if (empty($users[$user->uuid])) {
641
        //                     $users[$user->uuid] = $user;
642
        //                 }
643
        //             }
644
        //         }
645
        //     };
646
647
        //     $this->addRecursiveChildrenGroups($groups, $users, $groups, $addUsers);
648
649
        //     $groups = collect($groups)->sortBy('name');
650
        //     $users  = collect($users)->sortBy('name');
651
        // }
652
653
        foreach ($groups as $uuid => $group) {
654
            $allowedUserUuids[] = [
655
                'uuid' => $group->uuid,
656
                'recordLabel' => $group->recordLabel
657
            ];
658
        }
659
660
        foreach ($users as $uuid => $user) {
661
            if($user->getKey() != $this->getKey()) {
662
                $allowedUserUuids[] = [
663
                    'uuid' => $user->uuid,
664
                    'recordLabel' => $user->recordLabel
665
                ];
666
            }
667
        }
668
669
        return $allowedUserUuids;
670
    }
671
}
672