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