xref: /webtrees/app/Auth.php (revision 87d151aa1b9399ea87ad77b43872224555f1d926)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2022 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 $user = null): bool
59    {
60        $user = $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 $user = null): bool
74    {
75        $user = $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 $user = null): bool
89    {
90        $user = $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 $user = null): bool
106    {
107        $user = $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 $user = null): bool
123    {
124        $user = $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 $user = null): int
140    {
141        $user = $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     * @return int|null
158     */
159    public static function id(): ?int
160    {
161        $wt_user = Session::get('wt_user');
162
163        return is_int($wt_user) ? $wt_user : null;
164    }
165
166    /**
167     * The authenticated user, from the current session.
168     *
169     * @return UserInterface
170     */
171    public static function user(): UserInterface
172    {
173        $user_service = app(UserService::class);
174        assert($user_service instanceof UserService);
175
176        return $user_service->find(self::id()) ?? new GuestUser();
177    }
178
179    /**
180     * Login directly as an explicit user - for masquerading.
181     *
182     * @param UserInterface $user
183     *
184     * @return void
185     */
186    public static function login(UserInterface $user): void
187    {
188        Session::regenerate();
189        Session::put('wt_user', $user->id());
190    }
191
192    /**
193     * End the session for the current user.
194     *
195     * @return void
196     */
197    public static function logout(): void
198    {
199        Session::regenerate(true);
200    }
201
202    /**
203     * @param ModuleInterface $module
204     * @param string          $interface
205     * @param Tree            $tree
206     * @param UserInterface   $user
207     *
208     * @return void
209     */
210    public static function checkComponentAccess(ModuleInterface $module, string $interface, Tree $tree, UserInterface $user): void
211    {
212        if ($module->accessLevel($tree, $interface) < self::accessLevel($tree, $user)) {
213            throw new HttpAccessDeniedException();
214        }
215    }
216
217    /**
218     * @param Family|null $family
219     * @param bool        $edit
220     *
221     * @return Family
222     * @throws HttpNotFoundException
223     * @throws HttpAccessDeniedException
224     */
225    public static function checkFamilyAccess(?Family $family, bool $edit = false): Family
226    {
227        $message = I18N::translate('This family does not exist or you do not have permission to view it.');
228
229        if ($family === null) {
230            throw new HttpNotFoundException($message);
231        }
232
233        if ($edit && $family->canEdit()) {
234            $family->lock();
235
236            return $family;
237        }
238
239        if ($family->canShow()) {
240            return $family;
241        }
242
243        throw new HttpAccessDeniedException($message);
244    }
245
246    /**
247     * @param Header|null $header
248     * @param bool        $edit
249     *
250     * @return Header
251     * @throws HttpNotFoundException
252     * @throws HttpAccessDeniedException
253     */
254    public static function checkHeaderAccess(?Header $header, bool $edit = false): Header
255    {
256        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
257
258        if ($header === null) {
259            throw new HttpNotFoundException($message);
260        }
261
262        if ($edit && $header->canEdit()) {
263            $header->lock();
264
265            return $header;
266        }
267
268        if ($header->canShow()) {
269            return $header;
270        }
271
272        throw new HttpAccessDeniedException($message);
273    }
274
275    /**
276     * @param Individual|null $individual
277     * @param bool            $edit
278     * @param bool            $chart For some charts, we can show private records
279     *
280     * @return Individual
281     * @throws HttpNotFoundException
282     * @throws HttpAccessDeniedException
283     */
284    public static function checkIndividualAccess(?Individual $individual, bool $edit = false, bool $chart = false): Individual
285    {
286        $message = I18N::translate('This individual does not exist or you do not have permission to view it.');
287
288        if ($individual === null) {
289            throw new HttpNotFoundException($message);
290        }
291
292        if ($edit && $individual->canEdit()) {
293            $individual->lock();
294
295            return $individual;
296        }
297
298        if ($chart && $individual->tree()->getPreference('SHOW_PRIVATE_RELATIONSHIPS') === '1') {
299            return $individual;
300        }
301
302        if ($individual->canShow()) {
303            return $individual;
304        }
305
306        throw new HttpAccessDeniedException($message);
307    }
308
309    /**
310     * @param Location|null $location
311     * @param bool          $edit
312     *
313     * @return Location
314     * @throws HttpNotFoundException
315     * @throws HttpAccessDeniedException
316     */
317    public static function checkLocationAccess(?Location $location, bool $edit = false): Location
318    {
319        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
320
321        if ($location === null) {
322            throw new HttpNotFoundException($message);
323        }
324
325        if ($edit && $location->canEdit()) {
326            $location->lock();
327
328            return $location;
329        }
330
331        if ($location->canShow()) {
332            return $location;
333        }
334
335        throw new HttpAccessDeniedException($message);
336    }
337
338    /**
339     * @param Media|null $media
340     * @param bool       $edit
341     *
342     * @return Media
343     * @throws HttpNotFoundException
344     * @throws HttpAccessDeniedException
345     */
346    public static function checkMediaAccess(?Media $media, bool $edit = false): Media
347    {
348        $message = I18N::translate('This media object does not exist or you do not have permission to view it.');
349
350        if ($media === null) {
351            throw new HttpNotFoundException($message);
352        }
353
354        if ($edit && $media->canEdit()) {
355            $media->lock();
356
357            return $media;
358        }
359
360        if ($media->canShow()) {
361            return $media;
362        }
363
364        throw new HttpAccessDeniedException($message);
365    }
366
367    /**
368     * @param Note|null $note
369     * @param bool      $edit
370     *
371     * @return Note
372     * @throws HttpNotFoundException
373     * @throws HttpAccessDeniedException
374     */
375    public static function checkNoteAccess(?Note $note, bool $edit = false): Note
376    {
377        $message = I18N::translate('This note does not exist or you do not have permission to view it.');
378
379        if ($note === null) {
380            throw new HttpNotFoundException($message);
381        }
382
383        if ($edit && $note->canEdit()) {
384            $note->lock();
385
386            return $note;
387        }
388
389        if ($note->canShow()) {
390            return $note;
391        }
392
393        throw new HttpAccessDeniedException($message);
394    }
395
396    /**
397     * @param SharedNote|null $shared_note
398     * @param bool            $edit
399     *
400     * @return SharedNote
401     * @throws HttpNotFoundException
402     * @throws HttpAccessDeniedException
403     */
404    public static function checkSharedNoteAccess(?SharedNote $shared_note, bool $edit = false): SharedNote
405    {
406        $message = I18N::translate('This note does not exist or you do not have permission to view it.');
407
408        if ($shared_note === null) {
409            throw new HttpNotFoundException($message);
410        }
411
412        if ($edit && $shared_note->canEdit()) {
413            $shared_note->lock();
414
415            return $shared_note;
416        }
417
418        if ($shared_note->canShow()) {
419            return $shared_note;
420        }
421
422        throw new HttpAccessDeniedException($message);
423    }
424
425    /**
426     * @param GedcomRecord|null $record
427     * @param bool              $edit
428     *
429     * @return GedcomRecord
430     * @throws HttpNotFoundException
431     * @throws HttpAccessDeniedException
432     */
433    public static function checkRecordAccess(?GedcomRecord $record, bool $edit = false): GedcomRecord
434    {
435        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
436
437        if ($record === null) {
438            throw new HttpNotFoundException($message);
439        }
440
441        if ($edit && $record->canEdit()) {
442            $record->lock();
443
444            return $record;
445        }
446
447        if ($record->canShow()) {
448            return $record;
449        }
450
451        throw new HttpAccessDeniedException($message);
452    }
453
454    /**
455     * @param Repository|null $repository
456     * @param bool            $edit
457     *
458     * @return Repository
459     * @throws HttpNotFoundException
460     * @throws HttpAccessDeniedException
461     */
462    public static function checkRepositoryAccess(?Repository $repository, bool $edit = false): Repository
463    {
464        $message = I18N::translate('This repository does not exist or you do not have permission to view it.');
465
466        if ($repository === null) {
467            throw new HttpNotFoundException($message);
468        }
469
470        if ($edit && $repository->canEdit()) {
471            $repository->lock();
472
473            return $repository;
474        }
475
476        if ($repository->canShow()) {
477            return $repository;
478        }
479
480        throw new HttpAccessDeniedException($message);
481    }
482
483    /**
484     * @param Source|null $source
485     * @param bool        $edit
486     *
487     * @return Source
488     * @throws HttpNotFoundException
489     * @throws HttpAccessDeniedException
490     */
491    public static function checkSourceAccess(?Source $source, bool $edit = false): Source
492    {
493        $message = I18N::translate('This source does not exist or you do not have permission to view it.');
494
495        if ($source === null) {
496            throw new HttpNotFoundException($message);
497        }
498
499        if ($edit && $source->canEdit()) {
500            $source->lock();
501
502            return $source;
503        }
504
505        if ($source->canShow()) {
506            return $source;
507        }
508
509        throw new HttpAccessDeniedException($message);
510    }
511
512    /*
513     * @param Submitter|null $submitter
514     * @param bool           $edit
515     *
516     * @return Submitter
517     * @throws HttpFoundException
518     * @throws HttpDeniedException
519     */
520    public static function checkSubmitterAccess(?Submitter $submitter, bool $edit = false): Submitter
521    {
522        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
523
524        if ($submitter === null) {
525            throw new HttpNotFoundException($message);
526        }
527
528        if ($edit && $submitter->canEdit()) {
529            $submitter->lock();
530
531            return $submitter;
532        }
533
534        if ($submitter->canShow()) {
535            return $submitter;
536        }
537
538        throw new HttpAccessDeniedException($message);
539    }
540
541    /*
542     * @param Submission|null $submission
543     * @param bool            $edit
544     *
545     * @return Submission
546     * @throws HttpNotFoundException
547     * @throws HttpAccessDeniedException
548     */
549    public static function checkSubmissionAccess(?Submission $submission, bool $edit = false): Submission
550    {
551        $message = I18N::translate('This record does not exist or you do not have permission to view it.');
552
553        if ($submission === null) {
554            throw new HttpNotFoundException($message);
555        }
556
557        if ($edit && $submission->canEdit()) {
558            $submission->lock();
559
560            return $submission;
561        }
562
563        if ($submission->canShow()) {
564            return $submission;
565        }
566
567        throw new HttpAccessDeniedException($message);
568    }
569
570    /**
571     * @param Tree          $tree
572     * @param UserInterface $user
573     *
574     * @return bool
575     */
576    public static function canUploadMedia(Tree $tree, UserInterface $user): bool
577    {
578        return
579            self::isEditor($tree, $user) &&
580            self::accessLevel($tree, $user) <= (int) $tree->getPreference('MEDIA_UPLOAD');
581    }
582
583
584    /**
585     * @return array<int,string>
586     */
587    public static function accessLevelNames(): array
588    {
589        return [
590            self::PRIV_PRIVATE => I18N::translate('Show to visitors'),
591            self::PRIV_USER    => I18N::translate('Show to members'),
592            self::PRIV_NONE    => I18N::translate('Show to managers'),
593            self::PRIV_HIDE    => I18N::translate('Hide from everyone'),
594        ];
595    }
596
597    /**
598     * @return array<string,string>
599     */
600    public static function privacyRuleNames(): array
601    {
602        return [
603            'none'         => I18N::translate('Show to visitors'),
604            'privacy'      => I18N::translate('Show to members'),
605            'confidential' => I18N::translate('Show to managers'),
606            'hidden'       => I18N::translate('Hide from everyone'),
607        ];
608    }
609}
610