xref: /webtrees/app/Auth.php (revision 9aef375d1d8983f11b518f41ee6f490c9351cbb7)
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 assert;
29use function is_int;
30
31/**
32 * Authentication.
33 */
34class 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    /**
588     * @return array<int,string>
589     */
590    public static function accessLevelNames(): array
591    {
592        return [
593            self::PRIV_PRIVATE => I18N::translate('Show to visitors'),
594            self::PRIV_USER    => I18N::translate('Show to members'),
595            self::PRIV_NONE    => I18N::translate('Show to managers'),
596            self::PRIV_HIDE    => I18N::translate('Hide from everyone'),
597        ];
598    }
599
600    /**
601     * @return array<string,string>
602     */
603    public static function privacyRuleNames(): array
604    {
605        return [
606            'none'         => I18N::translate('Show to visitors'),
607            'privacy'      => I18N::translate('Show to members'),
608            'confidential' => I18N::translate('Show to managers'),
609            'hidden'       => I18N::translate('Hide from everyone'),
610        ];
611    }
612}
613