xref: /webtrees/app/Http/RequestHandlers/IndividualPage.php (revision 449b311ecf65f677a2595e1e29f712d11ef22f34)
1852ede8cSGreg Roach<?php
2852ede8cSGreg Roach
3852ede8cSGreg Roach/**
4852ede8cSGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6852ede8cSGreg Roach * This program is free software: you can redistribute it and/or modify
7852ede8cSGreg Roach * it under the terms of the GNU General Public License as published by
8852ede8cSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9852ede8cSGreg Roach * (at your option) any later version.
10852ede8cSGreg Roach * This program is distributed in the hope that it will be useful,
11852ede8cSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12852ede8cSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13852ede8cSGreg Roach * GNU General Public License for more details.
14852ede8cSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16852ede8cSGreg Roach */
17852ede8cSGreg Roach
18852ede8cSGreg Roachdeclare(strict_types=1);
19852ede8cSGreg Roach
20852ede8cSGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers;
21852ede8cSGreg Roach
22e5d858f5SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
23054771e9SGreg Roachuse Fisharebest\Webtrees\Age;
24852ede8cSGreg Roachuse Fisharebest\Webtrees\Auth;
25852ede8cSGreg Roachuse Fisharebest\Webtrees\Date;
26852ede8cSGreg Roachuse Fisharebest\Webtrees\Http\ViewResponseTrait;
27852ede8cSGreg Roachuse Fisharebest\Webtrees\I18N;
28852ede8cSGreg Roachuse Fisharebest\Webtrees\Individual;
29852ede8cSGreg Roachuse Fisharebest\Webtrees\Media;
30852ede8cSGreg Roachuse Fisharebest\Webtrees\MediaFile;
31853f2b8aSGreg Roachuse Fisharebest\Webtrees\Module\ModuleShareInterface;
32852ede8cSGreg Roachuse Fisharebest\Webtrees\Module\ModuleSidebarInterface;
33852ede8cSGreg Roachuse Fisharebest\Webtrees\Module\ModuleTabInterface;
346b9cb339SGreg Roachuse Fisharebest\Webtrees\Registry;
35852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\ClipboardService;
36852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\ModuleService;
37852ede8cSGreg Roachuse Fisharebest\Webtrees\Services\UserService;
38b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
39852ede8cSGreg Roachuse Illuminate\Support\Collection;
40852ede8cSGreg Roachuse Psr\Http\Message\ResponseInterface;
41852ede8cSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
42852ede8cSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
43852ede8cSGreg Roach
442406e0e0SGreg Roachuse function array_map;
452406e0e0SGreg Roachuse function date;
46852ede8cSGreg Roachuse function e;
47852ede8cSGreg Roachuse function explode;
482406e0e0SGreg Roachuse function implode;
49852ede8cSGreg Roachuse function redirect;
50ca2c8695SGreg Roachuse function strip_tags;
512406e0e0SGreg Roachuse function strtoupper;
52ca2c8695SGreg Roachuse function trim;
532406e0e0SGreg Roach
54852ede8cSGreg Roach/**
55852ede8cSGreg Roach * Show an individual's page.
56852ede8cSGreg Roach */
57852ede8cSGreg Roachclass IndividualPage implements RequestHandlerInterface
58852ede8cSGreg Roach{
59852ede8cSGreg Roach    use ViewResponseTrait;
60852ede8cSGreg Roach
61c4943cffSGreg Roach    private ClipboardService $clipboard_service;
62852ede8cSGreg Roach
63c4943cffSGreg Roach    private ModuleService $module_service;
64852ede8cSGreg Roach
65c4943cffSGreg Roach    private UserService $user_service;
66852ede8cSGreg Roach
67852ede8cSGreg Roach    /**
68852ede8cSGreg Roach     * @param ClipboardService $clipboard_service
69852ede8cSGreg Roach     * @param ModuleService    $module_service
70852ede8cSGreg Roach     * @param UserService      $user_service
71852ede8cSGreg Roach     */
72828e3b20SGreg Roach    public function __construct(
73828e3b20SGreg Roach        ClipboardService $clipboard_service,
74828e3b20SGreg Roach        ModuleService $module_service,
75828e3b20SGreg Roach        UserService $user_service
76828e3b20SGreg Roach    ) {
77852ede8cSGreg Roach        $this->clipboard_service = $clipboard_service;
78852ede8cSGreg Roach        $this->module_service    = $module_service;
79852ede8cSGreg Roach        $this->user_service      = $user_service;
80852ede8cSGreg Roach    }
81852ede8cSGreg Roach
82852ede8cSGreg Roach    /**
83852ede8cSGreg Roach     * @param ServerRequestInterface $request
84852ede8cSGreg Roach     *
85852ede8cSGreg Roach     * @return ResponseInterface
86852ede8cSGreg Roach     */
87852ede8cSGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
88852ede8cSGreg Roach    {
89b55cbc6bSGreg Roach        $tree       = Validator::attributes($request)->tree();
90b55cbc6bSGreg Roach        $xref       = Validator::attributes($request)->isXref()->string('xref');
91b55cbc6bSGreg Roach        $slug       = Validator::attributes($request)->string('slug', '');
926b9cb339SGreg Roach        $individual = Registry::individualFactory()->make($xref, $tree);
93852ede8cSGreg Roach        $individual = Auth::checkIndividualAccess($individual);
94852ede8cSGreg Roach
95852ede8cSGreg Roach        // Redirect to correct xref/slug
96b55cbc6bSGreg Roach        if ($individual->xref() !== $xref || Registry::slugFactory()->make($individual) !== $slug) {
97e5d858f5SGreg Roach            return redirect($individual->url(), StatusCodeInterface::STATUS_MOVED_PERMANENTLY);
98852ede8cSGreg Roach        }
99852ede8cSGreg Roach
100852ede8cSGreg Roach        // What images are linked to this individual
101852ede8cSGreg Roach        $individual_media = new Collection();
102852ede8cSGreg Roach        foreach ($individual->facts(['OBJE']) as $fact) {
103852ede8cSGreg Roach            $media_object = $fact->target();
104852ede8cSGreg Roach            if ($media_object instanceof Media) {
105852ede8cSGreg Roach                $media_file = $media_object->firstImageFile();
106852ede8cSGreg Roach                if ($media_file instanceof MediaFile) {
107852ede8cSGreg Roach                    $individual_media->add($media_file);
108852ede8cSGreg Roach                }
109852ede8cSGreg Roach            }
110852ede8cSGreg Roach        }
111852ede8cSGreg Roach
112852ede8cSGreg Roach        // If this individual is linked to a user account, show the link
113852ede8cSGreg Roach        if (Auth::isAdmin()) {
114852ede8cSGreg Roach            $users = $this->user_service->findByIndividual($individual);
115828e3b20SGreg Roach        } else {
116828e3b20SGreg Roach            $users = new Collection();
117852ede8cSGreg Roach        }
118852ede8cSGreg Roach
119828e3b20SGreg Roach        $shares = $this->module_service
120828e3b20SGreg Roach            ->findByInterface(ModuleShareInterface::class)
121853f2b8aSGreg Roach            ->map(fn (ModuleShareInterface $module) => $module->share($individual))
122853f2b8aSGreg Roach            ->filter();
123853f2b8aSGreg Roach
124852ede8cSGreg Roach        return $this->viewResponse('individual-page', [
125a5fd6d7cSGreg Roach            'age'              => $this->ageString($individual),
1265416d6ddSGreg Roach            'can_upload_media' => Auth::canUploadMedia($tree, Auth::user()),
12769cdf014SGreg Roach            'clipboard_facts'  => $this->clipboard_service->pastableFacts($individual),
128852ede8cSGreg Roach            'individual_media' => $individual_media,
1292406e0e0SGreg Roach            'meta_description' => $this->metaDescription($individual),
130852ede8cSGreg Roach            'meta_robots'      => 'index,follow',
1310f5fd22fSGreg Roach            'record'           => $individual,
132853f2b8aSGreg Roach            'shares'           => $shares,
133852ede8cSGreg Roach            'sidebars'         => $this->getSidebars($individual),
134852ede8cSGreg Roach            'tabs'             => $this->getTabs($individual),
135852ede8cSGreg Roach            'significant'      => $this->significant($individual),
1365e6816beSGreg Roach            'title'            => $individual->fullName() . ' ' . $individual->lifespan(),
137852ede8cSGreg Roach            'tree'             => $tree,
138828e3b20SGreg Roach            'users'            => $users,
13915c4f62cSGreg Roach        ])->withHeader('Link', '<' . $individual->url() . '>; rel="canonical"');
140852ede8cSGreg Roach    }
141852ede8cSGreg Roach
142852ede8cSGreg Roach    /**
1432406e0e0SGreg Roach     * @param Individual $individual
1442406e0e0SGreg Roach     *
1452406e0e0SGreg Roach     * @return string
1462406e0e0SGreg Roach     */
147a5fd6d7cSGreg Roach    private function ageString(Individual $individual): string
148a5fd6d7cSGreg Roach    {
149a5fd6d7cSGreg Roach        if ($individual->isDead()) {
150a5fd6d7cSGreg Roach            // If dead, show age at death
151a5fd6d7cSGreg Roach            $age = (string) new Age($individual->getBirthDate(), $individual->getDeathDate());
152a5fd6d7cSGreg Roach
153a5fd6d7cSGreg Roach            if ($age === '') {
154a5fd6d7cSGreg Roach                return '';
155a5fd6d7cSGreg Roach            }
156a5fd6d7cSGreg Roach
157a5fd6d7cSGreg Roach            switch ($individual->sex()) {
158a5fd6d7cSGreg Roach                case 'M':
159a5fd6d7cSGreg Roach                    /* I18N: The age of an individual at a given date */
160a5fd6d7cSGreg Roach                    return I18N::translateContext('Male', '(aged %s)', $age);
161a5fd6d7cSGreg Roach                case 'F':
162a5fd6d7cSGreg Roach                    /* I18N: The age of an individual at a given date */
163a5fd6d7cSGreg Roach                    return I18N::translateContext('Female', '(aged %s)', $age);
164a5fd6d7cSGreg Roach                default:
165a5fd6d7cSGreg Roach                    /* I18N: The age of an individual at a given date */
166a5fd6d7cSGreg Roach                    return I18N::translate('(aged %s)', $age);
167a5fd6d7cSGreg Roach            }
168a5fd6d7cSGreg Roach        }
169a5fd6d7cSGreg Roach
170a5fd6d7cSGreg Roach        // If living, show age today
171a5fd6d7cSGreg Roach        $today = new Date(strtoupper(date('d M Y')));
172a5fd6d7cSGreg Roach        $age   = (string) new Age($individual->getBirthDate(), $today);
173a5fd6d7cSGreg Roach
174a5fd6d7cSGreg Roach        if ($age === '') {
175a5fd6d7cSGreg Roach            return '';
176a5fd6d7cSGreg Roach        }
177a5fd6d7cSGreg Roach
178a5fd6d7cSGreg Roach        /* I18N: The current age of a living individual */
179a5fd6d7cSGreg Roach        return I18N::translate('(age %s)', $age);
180a5fd6d7cSGreg Roach    }
181a5fd6d7cSGreg Roach
182a5fd6d7cSGreg Roach    /**
183a5fd6d7cSGreg Roach     * @param Individual $individual
184a5fd6d7cSGreg Roach     *
185a5fd6d7cSGreg Roach     * @return string
186a5fd6d7cSGreg Roach     */
1872406e0e0SGreg Roach    private function metaDescription(Individual $individual): string
1882406e0e0SGreg Roach    {
1892406e0e0SGreg Roach        $meta_facts = [];
1902406e0e0SGreg Roach
1912406e0e0SGreg Roach        $birth_date  = $individual->getBirthDate();
1922406e0e0SGreg Roach        $birth_place = $individual->getBirthPlace();
1932406e0e0SGreg Roach
1942406e0e0SGreg Roach        if ($birth_date->isOK() || $birth_place->id() !== 0) {
1952406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Birth') . ' ' .
19666ecd017SGreg Roach                $birth_date->display() . ' ' .
1972406e0e0SGreg Roach                $birth_place->placeName();
1982406e0e0SGreg Roach        }
1992406e0e0SGreg Roach
2002406e0e0SGreg Roach        $death_date  = $individual->getDeathDate();
2012406e0e0SGreg Roach        $death_place = $individual->getDeathPlace();
2022406e0e0SGreg Roach
2032406e0e0SGreg Roach        if ($death_date->isOK() || $death_place->id() !== 0) {
2042406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Death') . ' ' .
20566ecd017SGreg Roach                $death_date->display() . ' ' .
2062406e0e0SGreg Roach                $death_place->placeName();
2072406e0e0SGreg Roach        }
2082406e0e0SGreg Roach
2092406e0e0SGreg Roach        foreach ($individual->childFamilies() as $family) {
2102406e0e0SGreg Roach            $meta_facts[] = I18N::translate('Parents') . ' ' . $family->fullName();
2112406e0e0SGreg Roach        }
2122406e0e0SGreg Roach
2132406e0e0SGreg Roach        foreach ($individual->spouseFamilies() as $family) {
2142406e0e0SGreg Roach            $spouse = $family->spouse($individual);
2152406e0e0SGreg Roach            if ($spouse instanceof Individual) {
2162406e0e0SGreg Roach                $meta_facts[] = I18N::translate('Spouse') . ' ' . $spouse->fullName();
2172406e0e0SGreg Roach            }
2182406e0e0SGreg Roach
219*f25fc0f9SGreg Roach            $child_names = $family->children()->map(static fn (Individual $individual): string => e($individual->getAllNames()[0]['givn']))->implode(', ');
2202406e0e0SGreg Roach
2212406e0e0SGreg Roach            if ($child_names !== '') {
2222406e0e0SGreg Roach                $meta_facts[] = I18N::translate('Children') . ' ' . $child_names;
2232406e0e0SGreg Roach            }
2242406e0e0SGreg Roach        }
2252406e0e0SGreg Roach
226ca2c8695SGreg Roach        $meta_facts = array_map(static fn (string $x): string => strip_tags($x), $meta_facts);
227ca2c8695SGreg Roach        $meta_facts = array_map(static fn (string $x): string => trim($x), $meta_facts);
2282406e0e0SGreg Roach
2292406e0e0SGreg Roach        return implode(', ', $meta_facts);
2302406e0e0SGreg Roach    }
2312406e0e0SGreg Roach
2322406e0e0SGreg Roach    /**
233852ede8cSGreg Roach     * Which tabs should we show on this individual's page.
234852ede8cSGreg Roach     * We don't show empty tabs.
235852ede8cSGreg Roach     *
236852ede8cSGreg Roach     * @param Individual $individual
237852ede8cSGreg Roach     *
23836779af1SGreg Roach     * @return Collection<int,ModuleSidebarInterface>
239852ede8cSGreg Roach     */
240852ede8cSGreg Roach    public function getSidebars(Individual $individual): Collection
241852ede8cSGreg Roach    {
242828e3b20SGreg Roach        return $this->module_service
243828e3b20SGreg Roach            ->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
244*f25fc0f9SGreg Roach            ->filter(static fn (ModuleSidebarInterface $sidebar): bool => $sidebar->hasSidebarContent($individual));
245852ede8cSGreg Roach    }
246852ede8cSGreg Roach
247852ede8cSGreg Roach    /**
248852ede8cSGreg Roach     * Which tabs should we show on this individual's page.
249852ede8cSGreg Roach     * We don't show empty tabs.
250852ede8cSGreg Roach     *
251852ede8cSGreg Roach     * @param Individual $individual
252852ede8cSGreg Roach     *
25336779af1SGreg Roach     * @return Collection<int,ModuleTabInterface>
254852ede8cSGreg Roach     */
255852ede8cSGreg Roach    public function getTabs(Individual $individual): Collection
256852ede8cSGreg Roach    {
257828e3b20SGreg Roach        return $this->module_service
258828e3b20SGreg Roach            ->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
259*f25fc0f9SGreg Roach            ->filter(static fn (ModuleTabInterface $tab): bool => $tab->hasTabContent($individual));
260852ede8cSGreg Roach    }
261852ede8cSGreg Roach
262852ede8cSGreg Roach    /**
263852ede8cSGreg Roach     * What are the significant elements of this page?
264852ede8cSGreg Roach     * The layout will need them to generate URLs for charts and reports.
265852ede8cSGreg Roach     *
266852ede8cSGreg Roach     * @param Individual $individual
267852ede8cSGreg Roach     *
268ac701fbdSGreg Roach     * @return object
269852ede8cSGreg Roach     */
270ac701fbdSGreg Roach    private function significant(Individual $individual): object
271852ede8cSGreg Roach    {
272852ede8cSGreg Roach        [$surname] = explode(',', $individual->sortName());
273852ede8cSGreg Roach
274852ede8cSGreg Roach        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
275852ede8cSGreg Roach
276852ede8cSGreg Roach        return (object) [
277852ede8cSGreg Roach            'family'     => $family,
278852ede8cSGreg Roach            'individual' => $individual,
279852ede8cSGreg Roach            'surname'    => $surname,
280852ede8cSGreg Roach        ];
281852ede8cSGreg Roach    }
282852ede8cSGreg Roach}
283