xref: /webtrees/app/Http/RequestHandlers/IndividualPage.php (revision 9f9acdbc09170c04c1e150c36ee57d49027e314a)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22use Fig\Http\Message\StatusCodeInterface;
23use Fisharebest\Webtrees\Age;
24use Fisharebest\Webtrees\Auth;
25use Fisharebest\Webtrees\Date;
26use Fisharebest\Webtrees\Fact;
27use Fisharebest\Webtrees\Factory;
28use Fisharebest\Webtrees\Http\ViewResponseTrait;
29use Fisharebest\Webtrees\I18N;
30use Fisharebest\Webtrees\Individual;
31use Fisharebest\Webtrees\Media;
32use Fisharebest\Webtrees\MediaFile;
33use Fisharebest\Webtrees\Module\ModuleSidebarInterface;
34use Fisharebest\Webtrees\Module\ModuleTabInterface;
35use Fisharebest\Webtrees\Services\ClipboardService;
36use Fisharebest\Webtrees\Services\ModuleService;
37use Fisharebest\Webtrees\Services\UserService;
38use Fisharebest\Webtrees\Tree;
39use Illuminate\Support\Collection;
40use Psr\Http\Message\ResponseInterface;
41use Psr\Http\Message\ServerRequestInterface;
42use Psr\Http\Server\RequestHandlerInterface;
43use stdClass;
44
45use function array_map;
46use function assert;
47use function date;
48use function e;
49use function explode;
50use function implode;
51use function is_string;
52use function redirect;
53use function route;
54use function strtoupper;
55use function view;
56
57/**
58 * Show an individual's page.
59 */
60class IndividualPage implements RequestHandlerInterface
61{
62    use ViewResponseTrait;
63
64    /** @var ClipboardService */
65    private $clipboard_service;
66
67    /** @var ModuleService */
68    private $module_service;
69
70    /** @var UserService */
71    private $user_service;
72
73    /**
74     * IndividualPage constructor.
75     *
76     * @param ClipboardService $clipboard_service
77     * @param ModuleService    $module_service
78     * @param UserService      $user_service
79     */
80    public function __construct(ClipboardService $clipboard_service, ModuleService $module_service, UserService $user_service)
81    {
82        $this->clipboard_service = $clipboard_service;
83        $this->module_service    = $module_service;
84        $this->user_service      = $user_service;
85    }
86
87    /**
88     * @param ServerRequestInterface $request
89     *
90     * @return ResponseInterface
91     */
92    public function handle(ServerRequestInterface $request): ResponseInterface
93    {
94        $tree = $request->getAttribute('tree');
95        assert($tree instanceof Tree);
96
97        $xref = $request->getAttribute('xref');
98        assert(is_string($xref));
99
100        $individual = Factory::individual()->make($xref, $tree);
101        $individual = Auth::checkIndividualAccess($individual);
102
103        // Redirect to correct xref/slug
104        if ($individual->xref() !== $xref || $request->getAttribute('slug') !== $individual->slug()) {
105            return redirect($individual->url(), StatusCodeInterface::STATUS_MOVED_PERMANENTLY);
106        }
107
108        // What is (was) the age of the individual
109        $bdate = $individual->getBirthDate();
110        $ddate = $individual->getDeathDate();
111
112        if ($individual->isDead()) {
113            // If dead, show age at death
114            $age = (new Age($bdate, $ddate))->ageAtEvent(false);
115        } else {
116            // If living, show age today
117            $today = strtoupper(date('d M Y'));
118            $age   = (new Age($bdate, new Date($today)))->ageAtEvent(true);
119        }
120
121        // What images are linked to this individual
122        $individual_media = new Collection();
123        foreach ($individual->facts(['OBJE']) as $fact) {
124            $media_object = $fact->target();
125            if ($media_object instanceof Media) {
126                $media_file = $media_object->firstImageFile();
127                if ($media_file instanceof MediaFile) {
128                    $individual_media->add($media_file);
129                }
130            }
131        }
132
133        $name_records = $individual->facts(['NAME'])->map(static function (Fact $fact): string {
134            return view('individual-name', ['fact' => $fact]);
135        });
136
137        $sex_records = $individual->facts(['SEX'])->map(static function (Fact $fact): string {
138            return view('individual-sex', ['fact' => $fact]);
139        });
140
141        // If this individual is linked to a user account, show the link
142        $user_link = '';
143        if (Auth::isAdmin()) {
144            $users = $this->user_service->findByIndividual($individual);
145            foreach ($users as $user) {
146                $user_link = ' —  <a href="' . e(route('admin-users', ['filter' => $user->email()])) . '">' . e($user->userName()) . '</a>';
147            }
148        }
149
150        return $this->viewResponse('individual-page', [
151            'age'              => $age,
152            'clipboard_facts'  => $this->clipboard_service->pastableFacts($individual, new Collection()),
153            'individual'       => $individual,
154            'individual_media' => $individual_media,
155            'meta_description' => $this->metaDescription($individual),
156            'meta_robots'      => 'index,follow',
157            'name_records'     => $name_records,
158            'sex_records'      => $sex_records,
159            'sidebars'         => $this->getSidebars($individual),
160            'tabs'             => $this->getTabs($individual),
161            'significant'      => $this->significant($individual),
162            'title'            => $individual->fullName() . ' ' . $individual->lifespan(),
163            'tree'             => $tree,
164            'user_link'        => $user_link,
165        ]);
166    }
167
168    /**
169     * @param Individual $individual
170     *
171     * @return string
172     */
173    private function metaDescription(Individual $individual): string
174    {
175        $meta_facts = [];
176
177        $birth_date  = $individual->getBirthDate();
178        $birth_place = $individual->getBirthPlace();
179
180        if ($birth_date->isOK() || $birth_place->id() !== 0) {
181            $meta_facts[] = I18N::translate('Birth') . ' ' .
182                $birth_date->display(false, null, false) . ' ' .
183                $birth_place->placeName();
184        }
185
186        $death_date  = $individual->getDeathDate();
187        $death_place = $individual->getDeathPlace();
188
189        if ($death_date->isOK() || $death_place->id() !== 0) {
190            $meta_facts[] = I18N::translate('Death') . ' ' .
191                $death_date->display(false, null, false) . ' ' .
192                $death_place->placeName();
193        }
194
195        foreach ($individual->childFamilies() as $family) {
196            $meta_facts[] = I18N::translate('Parents') . ' ' . $family->fullName();
197        }
198
199        foreach ($individual->spouseFamilies() as $family) {
200            $spouse = $family->spouse($individual);
201            if ($spouse instanceof Individual) {
202                $meta_facts[] = I18N::translate('Spouse') . ' ' . $spouse->fullName();
203            }
204
205            $child_names = $family->children()->map(static function (Individual $individual): string {
206                return e($individual->getAllNames()[0]['givn']);
207            })->implode(', ');
208
209
210            if ($child_names !== '') {
211                $meta_facts[] = I18N::translate('Children') . ' ' . $child_names;
212            }
213        }
214
215        $meta_facts = array_map('strip_tags', $meta_facts);
216        $meta_facts = array_map('trim', $meta_facts);
217
218        return implode(', ', $meta_facts);
219    }
220
221    /**
222     * Which tabs should we show on this individual's page.
223     * We don't show empty tabs.
224     *
225     * @param Individual $individual
226     *
227     * @return Collection<ModuleSidebarInterface>
228     */
229    public function getSidebars(Individual $individual): Collection
230    {
231        return $this->module_service->findByComponent(ModuleSidebarInterface::class, $individual->tree(), Auth::user())
232            ->filter(static function (ModuleSidebarInterface $sidebar) use ($individual): bool {
233                return $sidebar->hasSidebarContent($individual);
234            });
235    }
236
237    /**
238     * Which tabs should we show on this individual's page.
239     * We don't show empty tabs.
240     *
241     * @param Individual $individual
242     *
243     * @return Collection<ModuleTabInterface>
244     */
245    public function getTabs(Individual $individual): Collection
246    {
247        return $this->module_service->findByComponent(ModuleTabInterface::class, $individual->tree(), Auth::user())
248            ->filter(static function (ModuleTabInterface $tab) use ($individual): bool {
249                return $tab->hasTabContent($individual);
250            });
251    }
252
253    /**
254     * What are the significant elements of this page?
255     * The layout will need them to generate URLs for charts and reports.
256     *
257     * @param Individual $individual
258     *
259     * @return stdClass
260     */
261    private function significant(Individual $individual): stdClass
262    {
263        [$surname] = explode(',', $individual->sortName());
264
265        $family = $individual->childFamilies()->merge($individual->spouseFamilies())->first();
266
267        return (object) [
268            'family'     => $family,
269            'individual' => $individual,
270            'surname'    => $surname,
271        ];
272    }
273}
274