1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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 INDIVIDUAL_EVENTS = ['BIRT', 'DEAT']; 51 protected const FAMILY_EVENTS = ['MARR']; 52 53 protected const 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 /** 77 * A sentence describing what this module does. 78 * 79 * @return string 80 */ 81 public function description(): string 82 { 83 return I18N::translate('Download a .ICS file containing an anniversary'); 84 } 85 86 /** 87 * HTML to include in the share links page. 88 * 89 * @param GedcomRecord $record 90 * 91 * @return string 92 */ 93 public function share(GedcomRecord $record): string 94 { 95 if ($record instanceof Individual) { 96 $facts = $record->facts(static::INDIVIDUAL_EVENTS, true) 97 ->merge($record->spouseFamilies()->map(fn (Family $family): Collection => $family->facts(static::FAMILY_EVENTS, true))); 98 } elseif ($record instanceof Family) { 99 $facts = $record->facts(static::FAMILY_EVENTS, true); 100 } else { 101 return ''; 102 } 103 104 // iCalendar only supports exact Gregorian dates. 105 $facts = $facts 106 ->flatten() 107 ->filter(fn (Fact $fact): bool => $fact->date()->isOK()) 108 ->filter(fn (Fact $fact): bool => $fact->date()->qual1 === '') 109 ->filter(fn (Fact $fact): bool => $fact->date()->minimumDate() instanceof GregorianDate) 110 ->filter(fn (Fact $fact): bool => $fact->date()->minimumJulianDay() === $fact->date()->maximumJulianDay()) 111 ->mapWithKeys(fn (Fact $fact): array => [ 112 route(static::class, ['tree' => $record->tree()->name(), 'xref' => $fact->record()->xref(), 'fact_id' => $fact->id()]) => 113 $fact->label() . ' — ' . $fact->date()->display(), 114 ]); 115 116 if ($facts->isNotEmpty()) { 117 $url = route(static::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()]); 118 119 return view('modules/share-anniversary/share', [ 120 'facts' => $facts, 121 'record' => $record, 122 'url' => $url, 123 ]); 124 } 125 126 return ''; 127 } 128 129 /** 130 * @param ServerRequestInterface $request 131 * 132 * @return ResponseInterface 133 */ 134 public function handle(ServerRequestInterface $request): ResponseInterface 135 { 136 $tree = Validator::attributes($request)->tree(); 137 $xref = Validator::attributes($request)->isXref()->string('xref'); 138 $fact_id = Validator::attributes($request)->string('fact_id'); 139 $record = Registry::gedcomRecordFactory()->make($xref, $tree); 140 $record = Auth::checkRecordAccess($record); 141 142 $fact = $record->facts() 143 ->filter(fn (Fact $fact): bool => $fact->id() === $fact_id) 144 ->first(); 145 146 if ($fact instanceof Fact) { 147 $date = $fact->date()->minimumDate()->format('%Y%m%d'); 148 $vcalendar = new VCalendar(); 149 $vevent = $vcalendar->add('VEVENT'); 150 $dtstart = $vevent->add('DTSTART', $date); 151 $dtstart['VALUE'] = 'DATE'; 152 $vevent->add('RRULE', 'FREQ=YEARLY'); 153 $vevent->add('SUMMARY', strip_tags($record->fullName()) . ' — ' . $fact->label()); 154 155 return response($vcalendar->serialize()) 156 ->withHeader('content-type', 'text/calendar') 157 ->withHeader('content-disposition', 'attachment; filename="' . $fact->id() . '.ics'); 158 } 159 160 throw new HttpNotFoundException(); 161 } 162} 163