xref: /webtrees/app/Auth.php (revision b11cdcd45131b1585d66693fab363cfeb18c51a4)
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     * @param ModuleInterface $module
205     * @param string          $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