Auth::isMember()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Fisharebest\Webtrees\Contracts\UserInterface;
23
use Fisharebest\Webtrees\Http\Exceptions\HttpAccessDeniedException;
24
use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
25
use Fisharebest\Webtrees\Module\ModuleInterface;
26
use Fisharebest\Webtrees\Services\UserService;
27
28
use function assert;
29
use function is_int;
30
31
/**
32
 * Authentication.
33
 */
34
class Auth
35
{
36
    // Privacy constants
37
    public const PRIV_PRIVATE = 2; // Allows visitors to view the item
38
    public const PRIV_USER    = 1; // Allows members to access the item
39
    public const PRIV_NONE    = 0; // Allows managers to access the item
40
    public const PRIV_HIDE    = -1; // Hide the item to all users
41
42
    /**
43
     * Are we currently logged in?
44
     *
45
     * @return bool
46
     */
47
    public static function check(): bool
48
    {
49
        return self::id() !== null;
50
    }
51
52
    /**
53
     * Is the specified/current user an administrator?
54
     *
55
     * @param UserInterface|null $user
56
     *
57
     * @return bool
58
     */
59
    public static function isAdmin(?UserInterface $user = null): bool
60
    {
61
        $user ??= self::user();
62
63
        return $user->getPreference(UserInterface::PREF_IS_ADMINISTRATOR) === '1';
64
    }
65
66
    /**
67
     * Is the specified/current user a manager of a tree?
68
     *
69
     * @param Tree               $tree
70
     * @param UserInterface|null $user
71
     *
72
     * @return bool
73
     */
74
    public static function isManager(Tree $tree, ?UserInterface $user = null): bool
75
    {
76
        $user ??= self::user();
77
78
        return self::isAdmin($user) || $tree->getUserPreference($user, UserInterface::PREF_TREE_ROLE) === UserInterface::ROLE_MANAGER;
79
    }
80
81
    /**
82
     * Is the specified/current user a moderator of a tree?
83
     *
84
     * @param Tree               $tree
85
     * @param UserInterface|null $user
86
     *
87
     * @return bool
88
     */
89
    public static function isModerator(Tree $tree, ?UserInterface $user = null): bool
90
    {
91
        $user ??= self::user();
92
93
        return
94
            self::isManager($tree, $user) ||
95
            $tree->getUserPreference($user, UserInterface::PREF_TREE_ROLE) === UserInterface::ROLE_MODERATOR;
96
    }
97
98
    /**
99
     * Is the specified/current user an editor of a tree?
100
     *
101
     * @param Tree               $tree
102
     * @param UserInterface|null $user
103
     *
104
     * @return bool
105
     */
106
    public static function isEditor(Tree $tree, ?UserInterface $user = null): bool
107
    {
108
        $user ??= self::user();
109
110
        return
111
            self::isModerator($tree, $user) ||
112
            $tree->getUserPreference($user, UserInterface::PREF_TREE_ROLE) === UserInterface::ROLE_EDITOR;
113
    }
114
115
    /**
116
     * Is the specified/current user a member of a tree?
117
     *
118
     * @param Tree               $tree
119
     * @param UserInterface|null $user
120
     *
121
     * @return bool
122
     */
123
    public static function isMember(Tree $tree, ?UserInterface $user = null): bool
124
    {
125
        $user ??= self::user();
126
127
        return
128
            self::isEditor($tree, $user) ||
129
            $tree->getUserPreference($user, UserInterface::PREF_TREE_ROLE) === UserInterface::ROLE_MEMBER;
130
    }
131
132
    /**
133
     * What is the specified/current user's access level within a tree?
134
     *
135
     * @param Tree               $tree
136
     * @param UserInterface|null $user
137
     *
138
     * @return int
139
     */
140
    public static function accessLevel(Tree $tree, ?UserInterface $user = null): int
141
    {
142
        $user ??= self::user();
143
144
        if (self::isManager($tree, $user)) {
145
            return self::PRIV_NONE;
146
        }
147
148
        if (self::isMember($tree, $user)) {
149
            return self::PRIV_USER;
150
        }
151
152
        return self::PRIV_PRIVATE;
153
    }
154
155
    /**
156
     * The ID of the authenticated user, from the current session.
157
     *
158
     * @return int|null
159
     */
160
    public static function id(): ?int
161
    {
162
        $wt_user = Session::get('wt_user');
163
164
        return is_int($wt_user) ? $wt_user : null;
165
    }
166
167
    /**
168
     * The authenticated user, from the current session.
169
     *
170
     * @return UserInterface
171
     */
172
    public static function user(): UserInterface
173
    {
174
        $user_service = app(UserService::class);
175
        assert($user_service instanceof UserService);
176
177
        return $user_service->find(self::id()) ?? new GuestUser();
178
    }
179
180
    /**
181
     * Login directly as an explicit user - for masquerading.
182
     *
183
     * @param UserInterface $user
184
     *
185
     * @return void
186
     */
187
    public static function login(UserInterface $user): void
188
    {
189
        Session::regenerate();
190
        Session::put('wt_user', $user->id());
191
    }
192
193
    /**
194
     * End the session for the current user.
195
     *
196
     * @return void
197
     */
198
    public static function logout(): void
199
    {
200
        Session::regenerate(true);
201
    }
202
203
    /**
204
     * @template T of ModuleInterface
205
     *
206
     * @param ModuleInterface $module
207
     * @param class-string<T> $interface
208
     * @param Tree            $tree
209
     * @param UserInterface   $user
210
     *
211
     * @return void
212
     */
213
    public static function checkComponentAccess(ModuleInterface $module, string $interface, Tree $tree, UserInterface $user): void
214
    {
215
        if ($module->accessLevel($tree, $interface) < self::accessLevel($tree, $user)) {
216
            throw new HttpAccessDeniedException();
217
        }
218
    }
219
220
    /**
221
     * @param Family|null $family
222
     * @param bool        $edit
223
     *
224
     * @return Family
225
     * @throws HttpNotFoundException
226
     * @throws HttpAccessDeniedException
227
     */
228
    public static function checkFamilyAccess(?Family $family, bool $edit = false): Family
229
    {
230
        $message = I18N::translate('This family does not exist or you do not have permission to view it.');
231
232
        if ($family === null) {
233
            throw new HttpNotFoundException($message);
234
        }
235
236
        if ($edit && $family->canEdit()) {
237
            $family->lock();
238
239
            return $family;
240
        }
241
242
        if ($family->canShow()) {
243
            return $family;
244
        }
245
246
        throw new HttpAccessDeniedException($message);
247
    }
248
249
    /**
250
     * @param Header|null $header
251
     * @param bool        $edit
252
     *
253
     * @return Header
254
     * @throws HttpNotFoundException
255
     * @throws HttpAccessDeniedException
256
     */
257
    public static function checkHeaderAccess(?Header $header, bool $edit = false): Header
258
    {
259
        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
260
261
        if ($header === null) {
262
            throw new HttpNotFoundException($message);
263
        }
264
265
        if ($edit && $header->canEdit()) {
266
            $header->lock();
267
268
            return $header;
269
        }
270
271
        if ($header->canShow()) {
272
            return $header;
273
        }
274
275
        throw new HttpAccessDeniedException($message);
276
    }
277
278
    /**
279
     * @param Individual|null $individual
280
     * @param bool            $edit
281
     * @param bool            $chart For some charts, we can show private records
282
     *
283
     * @return Individual
284
     * @throws HttpNotFoundException
285
     * @throws HttpAccessDeniedException
286
     */
287
    public static function checkIndividualAccess(?Individual $individual, bool $edit = false, bool $chart = false): Individual
288
    {
289
        $message = I18N::translate('This individual does not exist or you do not have permission to view it.');
290
291
        if ($individual === null) {
292
            throw new HttpNotFoundException($message);
293
        }
294
295
        if ($edit && $individual->canEdit()) {
296
            $individual->lock();
297
298
            return $individual;
299
        }
300
301
        if ($chart && $individual->tree()->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
302
            return $individual;
303
        }
304
305
        if ($individual->canShow()) {
306
            return $individual;
307
        }
308
309
        throw new HttpAccessDeniedException($message);
310
    }
311
312
    /**
313
     * @param Location|null $location
314
     * @param bool          $edit
315
     *
316
     * @return Location
317
     * @throws HttpNotFoundException
318
     * @throws HttpAccessDeniedException
319
     */
320
    public static function checkLocationAccess(?Location $location, bool $edit = false): Location
321
    {
322
        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
323
324
        if ($location === null) {
325
            throw new HttpNotFoundException($message);
326
        }
327
328
        if ($edit && $location->canEdit()) {
329
            $location->lock();
330
331
            return $location;
332
        }
333
334
        if ($location->canShow()) {
335
            return $location;
336
        }
337
338
        throw new HttpAccessDeniedException($message);
339
    }
340
341
    /**
342
     * @param Media|null $media
343
     * @param bool       $edit
344
     *
345
     * @return Media
346
     * @throws HttpNotFoundException
347
     * @throws HttpAccessDeniedException
348
     */
349
    public static function checkMediaAccess(?Media $media, bool $edit = false): Media
350
    {
351
        $message = I18N::translate('This media object does not exist or you do not have permission to view it.');
352
353
        if ($media === null) {
354
            throw new HttpNotFoundException($message);
355
        }
356
357
        if ($edit && $media->canEdit()) {
358
            $media->lock();
359
360
            return $media;
361
        }
362
363
        if ($media->canShow()) {
364
            return $media;
365
        }
366
367
        throw new HttpAccessDeniedException($message);
368
    }
369
370
    /**
371
     * @param Note|null $note
372
     * @param bool      $edit
373
     *
374
     * @return Note
375
     * @throws HttpNotFoundException
376
     * @throws HttpAccessDeniedException
377
     */
378
    public static function checkNoteAccess(?Note $note, bool $edit = false): Note
379
    {
380
        $message = I18N::translate('This note does not exist or you do not have permission to view it.');
381
382
        if ($note === null) {
383
            throw new HttpNotFoundException($message);
384
        }
385
386
        if ($edit && $note->canEdit()) {
387
            $note->lock();
388
389
            return $note;
390
        }
391
392
        if ($note->canShow()) {
393
            return $note;
394
        }
395
396
        throw new HttpAccessDeniedException($message);
397
    }
398
399
    /**
400
     * @param SharedNote|null $shared_note
401
     * @param bool            $edit
402
     *
403
     * @return SharedNote
404
     * @throws HttpNotFoundException
405
     * @throws HttpAccessDeniedException
406
     */
407
    public static function checkSharedNoteAccess(?SharedNote $shared_note, bool $edit = false): SharedNote
408
    {
409
        $message = I18N::translate('This note does not exist or you do not have permission to view it.');
410
411
        if ($shared_note === null) {
412
            throw new HttpNotFoundException($message);
413
        }
414
415
        if ($edit && $shared_note->canEdit()) {
416
            $shared_note->lock();
417
418
            return $shared_note;
419
        }
420
421
        if ($shared_note->canShow()) {
422
            return $shared_note;
423
        }
424
425
        throw new HttpAccessDeniedException($message);
426
    }
427
428
    /**
429
     * @param GedcomRecord|null $record
430
     * @param bool              $edit
431
     *
432
     * @return GedcomRecord
433
     * @throws HttpNotFoundException
434
     * @throws HttpAccessDeniedException
435
     */
436
    public static function checkRecordAccess(?GedcomRecord $record, bool $edit = false): GedcomRecord
437
    {
438
        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
439
440
        if ($record === null) {
441
            throw new HttpNotFoundException($message);
442
        }
443
444
        if ($edit && $record->canEdit()) {
445
            $record->lock();
446
447
            return $record;
448
        }
449
450
        if ($record->canShow()) {
451
            return $record;
452
        }
453
454
        throw new HttpAccessDeniedException($message);
455
    }
456
457
    /**
458
     * @param Repository|null $repository
459
     * @param bool            $edit
460
     *
461
     * @return Repository
462
     * @throws HttpNotFoundException
463
     * @throws HttpAccessDeniedException
464
     */
465
    public static function checkRepositoryAccess(?Repository $repository, bool $edit = false): Repository
466
    {
467
        $message = I18N::translate('This repository does not exist or you do not have permission to view it.');
468
469
        if ($repository === null) {
470
            throw new HttpNotFoundException($message);
471
        }
472
473
        if ($edit && $repository->canEdit()) {
474
            $repository->lock();
475
476
            return $repository;
477
        }
478
479
        if ($repository->canShow()) {
480
            return $repository;
481
        }
482
483
        throw new HttpAccessDeniedException($message);
484
    }
485
486
    /**
487
     * @param Source|null $source
488
     * @param bool        $edit
489
     *
490
     * @return Source
491
     * @throws HttpNotFoundException
492
     * @throws HttpAccessDeniedException
493
     */
494
    public static function checkSourceAccess(?Source $source, bool $edit = false): Source
495
    {
496
        $message = I18N::translate('This source does not exist or you do not have permission to view it.');
497
498
        if ($source === null) {
499
            throw new HttpNotFoundException($message);
500
        }
501
502
        if ($edit && $source->canEdit()) {
503
            $source->lock();
504
505
            return $source;
506
        }
507
508
        if ($source->canShow()) {
509
            return $source;
510
        }
511
512
        throw new HttpAccessDeniedException($message);
513
    }
514
515
    /**
516
     * @param Submitter|null $submitter
517
     * @param bool           $edit
518
     *
519
     * @return Submitter
520
     * @throws HttpNotFoundException
521
     * @throws HttpAccessDeniedException
522
     */
523
    public static function checkSubmitterAccess(?Submitter $submitter, bool $edit = false): Submitter
524
    {
525
        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
526
527
        if ($submitter === null) {
528
            throw new HttpNotFoundException($message);
529
        }
530
531
        if ($edit && $submitter->canEdit()) {
532
            $submitter->lock();
533
534
            return $submitter;
535
        }
536
537
        if ($submitter->canShow()) {
538
            return $submitter;
539
        }
540
541
        throw new HttpAccessDeniedException($message);
542
    }
543
544
    /**
545
     * @param Submission|null $submission
546
     * @param bool            $edit
547
     *
548
     * @return Submission
549
     * @throws HttpNotFoundException
550
     * @throws HttpAccessDeniedException
551
     */
552
    public static function checkSubmissionAccess(?Submission $submission, bool $edit = false): Submission
553
    {
554
        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
555
556
        if ($submission === null) {
557
            throw new HttpNotFoundException($message);
558
        }
559
560
        if ($edit && $submission->canEdit()) {
561
            $submission->lock();
562
563
            return $submission;
564
        }
565
566
        if ($submission->canShow()) {
567
            return $submission;
568
        }
569
570
        throw new HttpAccessDeniedException($message);
571
    }
572
573
    /**
574
     * @param Tree          $tree
575
     * @param UserInterface $user
576
     *
577
     * @return bool
578
     */
579
    public static function canUploadMedia(Tree $tree, UserInterface $user): bool
580
    {
581
        return
582
            self::isEditor($tree, $user) &&
583
            self::accessLevel($tree, $user) <= (int) $tree->getPreference('MEDIA_UPLOAD');
584
    }
585
586
    /**
587
     * @return array<int,string>
588
     */
589
    public static function accessLevelNames(): array
590
    {
591
        return [
592
            self::PRIV_PRIVATE => I18N::translate('Show to visitors'),
593
            self::PRIV_USER    => I18N::translate('Show to members'),
594
            self::PRIV_NONE    => I18N::translate('Show to managers'),
595
            self::PRIV_HIDE    => I18N::translate('Hide from everyone'),
596
        ];
597
    }
598
599
    /**
600
     * @return array<string,string>
601
     */
602
    public static function privacyRuleNames(): array
603
    {
604
        return [
605
            'none'         => I18N::translate('Show to visitors'),
606
            'privacy'      => I18N::translate('Show to members'),
607
            'confidential' => I18N::translate('Show to managers'),
608
            'hidden'       => I18N::translate('Hide from everyone'),
609
        ];
610
    }
611
}
612