xref: /webtrees/app/Auth.php (revision 5f52b64188c4f443eebeb985bb53134753a7560f)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 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
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees;
21
22use Fisharebest\Webtrees\Contracts\UserInterface;
23use Fisharebest\Webtrees\Http\Exceptions\HttpAccessDeniedException;
24use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
25use Fisharebest\Webtrees\Module\ModuleInterface;
26use Fisharebest\Webtrees\Services\UserService;
27
28use function is_int;
29
30/**
31 * Authentication.
32 */
33class Auth
34{
35    // Privacy constants
36    public const PRIV_PRIVATE = 2; // Allows visitors to view the item
37    public const PRIV_USER    = 1; // Allows members to access the item
38    public const PRIV_NONE    = 0; // Allows managers to access the item
39    public const 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