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