Issues (2558)

app/Auth.php (1 issue)

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