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