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