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\Module; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Date\GregorianDate; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\Family; 26use Fisharebest\Webtrees\GedcomRecord; 27use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException; 28use Fisharebest\Webtrees\I18N; 29use Fisharebest\Webtrees\Individual; 30use Fisharebest\Webtrees\Registry; 31use Fisharebest\Webtrees\Validator; 32use Illuminate\Support\Collection; 33use Psr\Http\Message\ResponseInterface; 34use Psr\Http\Message\ServerRequestInterface; 35use Psr\Http\Server\RequestHandlerInterface; 36use Sabre\VObject\Component\VCalendar; 37 38use function response; 39use function route; 40use function strip_tags; 41use function view; 42 43/** 44 * Class ShareAnniversaryModule 45 */ 46class ShareAnniversaryModule extends AbstractModule implements ModuleShareInterface, RequestHandlerInterface 47{ 48 use ModuleShareTrait; 49 50 protected const array INDIVIDUAL_EVENTS = ['BIRT', 'DEAT']; 51 protected const array FAMILY_EVENTS = ['MARR']; 52 53 protected const string ROUTE_URL = '/tree/{tree}/anniversary-ics/{xref}/{fact_id}'; 54 55 /** 56 * Initialization. 57 * 58 * @return void 59 */ 60 public function boot(): void 61 { 62 Registry::routeFactory()->routeMap() 63 ->get(static::class, static::ROUTE_URL, $this); 64 } 65 66 /** 67 * How should this module be identified in the control panel, etc.? 68 * 69 * @return string 70 */ 71 public function title(): string 72 { 73 return I18N::translate('Share the anniversary of an event'); 74 } 75 76 public function description(): string 77 { 78 return I18N::translate('Download a .ICS file containing an anniversary'); 79 } 80 81 /** 82 * HTML to include in the share links page. 83 * 84 * @param GedcomRecord $record 85 * 86 * @return string 87 */ 88 public function share(GedcomRecord $record): string 89 { 90 if ($record instanceof Individual) { 91 $facts = $record->facts(static::INDIVIDUAL_EVENTS, true) 92 ->merge($record->spouseFamilies()->map(fn (Family $family): Collection => $family->facts(static::FAMILY_EVENTS, true))); 93 } elseif ($record instanceof Family) { 94 $facts = $record->facts(static::FAMILY_EVENTS, true); 95 } else { 96 return ''; 97 } 98 99 // iCalendar only supports exact Gregorian dates. 100 $facts = $facts 101 ->flatten() 102 ->filter(fn (Fact $fact): bool => $fact->date()->isOK()) 103 ->filter(fn (Fact $fact): bool => $fact->date()->qual1 === '') 104 ->filter(fn (Fact $fact): bool => $fact->date()->minimumDate() instanceof GregorianDate) 105 ->filter(fn (Fact $fact): bool => $fact->date()->minimumJulianDay() === $fact->date()->maximumJulianDay()) 106 ->mapWithKeys(fn (Fact $fact): array => [ 107 route(static::class, ['tree' => $record->tree()->name(), 'xref' => $fact->record()->xref(), 'fact_id' => $fact->id()]) => 108 $fact->label() . ' — ' . $fact->date()->display(), 109 ]); 110 111 if ($facts->isNotEmpty()) { 112 $url = route(static::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()]); 113 114 return view('modules/share-anniversary/share', [ 115 'facts' => $facts, 116 'record' => $record, 117 'url' => $url, 118 ]); 119 } 120 121 return ''; 122 } 123 124 /** 125 * @param ServerRequestInterface $request 126 * 127 * @return ResponseInterface 128 */ 129 public function handle(ServerRequestInterface $request): ResponseInterface 130 { 131 $tree = Validator::attributes($request)->tree(); 132 $xref = Validator::attributes($request)->isXref()->string('xref'); 133 $fact_id = Validator::attributes($request)->string('fact_id'); 134 $record = Registry::gedcomRecordFactory()->make($xref, $tree); 135 $record = Auth::checkRecordAccess($record); 136 137 $fact = $record->facts()->first(fn (Fact $fact): bool => $fact->id() === $fact_id); 138 139 if ($fact instanceof Fact) { 140 $vcalendar = new VCalendar([ 141 'VEVENT' => [ 142 'DTSTART' => $fact->date()->minimumDate()->format('%Y%m%d'), 143 'RRULE' => 'FREQ=YEARLY', 144 'SUMMARY' => strip_tags($record->fullName()) . ' — ' . $fact->label(), 145 ], 146 ]); 147 148 return response($vcalendar->serialize()) 149 ->withHeader('content-type', 'text/calendar') 150 ->withHeader('content-disposition', 'attachment; filename="' . $fact->id() . '.ics'); 151 } 152 153 throw new HttpNotFoundException(); 154 } 155} 156