xref: /webtrees/app/Module/ShareAnniversaryModule.php (revision 66ecd01738ea7f89ee5b1944d4d969facd03cb4e)
1354a9dbaSGreg Roach<?php
2354a9dbaSGreg Roach
3354a9dbaSGreg Roach/**
4354a9dbaSGreg Roach * webtrees: online genealogy
5354a9dbaSGreg Roach * Copyright (C) 2021 webtrees development team
6354a9dbaSGreg Roach * This program is free software: you can redistribute it and/or modify
7354a9dbaSGreg Roach * it under the terms of the GNU General Public License as published by
8354a9dbaSGreg Roach * the Free Software Foundation, either version 3 of the License, or
9354a9dbaSGreg Roach * (at your option) any later version.
10354a9dbaSGreg Roach * This program is distributed in the hope that it will be useful,
11354a9dbaSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12354a9dbaSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13354a9dbaSGreg Roach * GNU General Public License for more details.
14354a9dbaSGreg Roach * You should have received a copy of the GNU General Public License
15354a9dbaSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16354a9dbaSGreg Roach */
17354a9dbaSGreg Roach
18354a9dbaSGreg Roachdeclare(strict_types=1);
19354a9dbaSGreg Roach
20354a9dbaSGreg Roachnamespace Fisharebest\Webtrees\Module;
21354a9dbaSGreg Roach
22354a9dbaSGreg Roachuse Aura\Router\RouterContainer;
23354a9dbaSGreg Roachuse Fisharebest\Webtrees\Auth;
24354a9dbaSGreg Roachuse Fisharebest\Webtrees\Date\GregorianDate;
25354a9dbaSGreg Roachuse Fisharebest\Webtrees\Fact;
26354a9dbaSGreg Roachuse Fisharebest\Webtrees\Family;
27354a9dbaSGreg Roachuse Fisharebest\Webtrees\GedcomRecord;
2881b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
29354a9dbaSGreg Roachuse Fisharebest\Webtrees\I18N;
30354a9dbaSGreg Roachuse Fisharebest\Webtrees\Individual;
31354a9dbaSGreg Roachuse Fisharebest\Webtrees\Registry;
32354a9dbaSGreg Roachuse Fisharebest\Webtrees\Tree;
33354a9dbaSGreg Roachuse Illuminate\Support\Collection;
34354a9dbaSGreg Roachuse Psr\Http\Message\ResponseInterface;
35354a9dbaSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
36354a9dbaSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
37354a9dbaSGreg Roachuse Sabre\VObject\Component\VCalendar;
38354a9dbaSGreg Roach
39354a9dbaSGreg Roachuse function app;
40354a9dbaSGreg Roachuse function assert;
41354a9dbaSGreg Roachuse function response;
42354a9dbaSGreg Roachuse function route;
43354a9dbaSGreg Roachuse function strip_tags;
44354a9dbaSGreg Roachuse function view;
45354a9dbaSGreg Roach
46354a9dbaSGreg Roach/**
47354a9dbaSGreg Roach * Class ShareAnniversaryModule
48354a9dbaSGreg Roach */
49354a9dbaSGreg Roachclass ShareAnniversaryModule extends AbstractModule implements ModuleShareInterface, RequestHandlerInterface
50354a9dbaSGreg Roach{
51354a9dbaSGreg Roach    use ModuleShareTrait;
52354a9dbaSGreg Roach
53354a9dbaSGreg Roach    protected const INDIVIDUAL_EVENTS = ['BIRT', 'DEAT'];
54354a9dbaSGreg Roach    protected const FAMILY_EVENTS     = ['MARR'];
55354a9dbaSGreg Roach
56354a9dbaSGreg Roach    protected const ROUTE_URL = '/tree/{tree}/anniversary-ics/{xref}/{fact_id}';
57354a9dbaSGreg Roach
58354a9dbaSGreg Roach    /**
59354a9dbaSGreg Roach     * Initialization.
60354a9dbaSGreg Roach     *
61354a9dbaSGreg Roach     * @return void
62354a9dbaSGreg Roach     */
63354a9dbaSGreg Roach    public function boot(): void
64354a9dbaSGreg Roach    {
65354a9dbaSGreg Roach        $router_container = app(RouterContainer::class);
66354a9dbaSGreg Roach        assert($router_container instanceof RouterContainer);
67354a9dbaSGreg Roach
68354a9dbaSGreg Roach        $router_container->getMap()
69354a9dbaSGreg Roach            ->get(static::class, static::ROUTE_URL, $this);
70354a9dbaSGreg Roach    }
71354a9dbaSGreg Roach
72354a9dbaSGreg Roach    /**
73354a9dbaSGreg Roach     * How should this module be identified in the control panel, etc.?
74354a9dbaSGreg Roach     *
75354a9dbaSGreg Roach     * @return string
76354a9dbaSGreg Roach     */
77354a9dbaSGreg Roach    public function title(): string
78354a9dbaSGreg Roach    {
79354a9dbaSGreg Roach        return I18N::translate('Share the anniversary of an event');
80354a9dbaSGreg Roach    }
81354a9dbaSGreg Roach
82354a9dbaSGreg Roach    /**
83354a9dbaSGreg Roach     * A sentence describing what this module does.
84354a9dbaSGreg Roach     *
85354a9dbaSGreg Roach     * @return string
86354a9dbaSGreg Roach     */
87354a9dbaSGreg Roach    public function description(): string
88354a9dbaSGreg Roach    {
89354a9dbaSGreg Roach        return I18N::translate('Download a .ICS file containing an anniversary');
90354a9dbaSGreg Roach    }
91354a9dbaSGreg Roach
92354a9dbaSGreg Roach    /**
93354a9dbaSGreg Roach     * HTML to include in the share links page.
94354a9dbaSGreg Roach     *
95354a9dbaSGreg Roach     * @param GedcomRecord $record
96354a9dbaSGreg Roach     *
97354a9dbaSGreg Roach     * @return string
98354a9dbaSGreg Roach     */
99354a9dbaSGreg Roach    public function share(GedcomRecord $record): string
100354a9dbaSGreg Roach    {
101354a9dbaSGreg Roach        if ($record instanceof Individual) {
102354a9dbaSGreg Roach            $facts = $record->facts(static::INDIVIDUAL_EVENTS, true)
103354a9dbaSGreg Roach                ->merge($record->spouseFamilies()->map(fn (Family $family): Collection => $family->facts(static::FAMILY_EVENTS, true)));
104354a9dbaSGreg Roach        } elseif ($record instanceof Family) {
105354a9dbaSGreg Roach            $facts = $record->facts(static::FAMILY_EVENTS, true);
106354a9dbaSGreg Roach        } else {
107354a9dbaSGreg Roach            return '';
108354a9dbaSGreg Roach        }
109354a9dbaSGreg Roach
110354a9dbaSGreg Roach        // iCalendar only supports exact Gregorian dates.
111354a9dbaSGreg Roach        $facts = $facts
112354a9dbaSGreg Roach            ->flatten()
113354a9dbaSGreg Roach            ->filter(fn (Fact $fact): bool => $fact->date()->isOK())
114354a9dbaSGreg Roach            ->filter(fn (Fact $fact): bool => $fact->date()->qual1 === '')
115354a9dbaSGreg Roach            ->filter(fn (Fact $fact): bool => $fact->date()->minimumDate() instanceof GregorianDate)
116138837acSGreg Roach            ->filter(fn (Fact $fact): bool => $fact->date()->minimumJulianDay() === $fact->date()->maximumJulianDay())
117354a9dbaSGreg Roach            ->mapWithKeys(fn (Fact $fact): array => [
118354a9dbaSGreg Roach                route(static::class, ['tree' => $record->tree()->name(), 'xref' => $fact->record()->xref(), 'fact_id' => $fact->id()]) =>
119*66ecd017SGreg Roach                    $fact->label() . ' — ' . $fact->date()->display(),
120354a9dbaSGreg Roach            ]);
121354a9dbaSGreg Roach
122354a9dbaSGreg Roach        if ($facts->isNotEmpty()) {
123354a9dbaSGreg Roach            $url = route(static::class, ['tree' => $record->tree()->name(), 'xref' => $record->xref()]);
124354a9dbaSGreg Roach
125354a9dbaSGreg Roach            return view('modules/share-anniversary/share', [
126354a9dbaSGreg Roach                'facts'  => $facts,
127354a9dbaSGreg Roach                'record' => $record,
128354a9dbaSGreg Roach                'url'    => $url,
129354a9dbaSGreg Roach            ]);
130354a9dbaSGreg Roach        }
131354a9dbaSGreg Roach
132354a9dbaSGreg Roach        return '';
133354a9dbaSGreg Roach    }
134354a9dbaSGreg Roach
135354a9dbaSGreg Roach    /**
136354a9dbaSGreg Roach     * @param ServerRequestInterface $request
137354a9dbaSGreg Roach     *
138354a9dbaSGreg Roach     * @return ResponseInterface
139354a9dbaSGreg Roach     */
140354a9dbaSGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
141354a9dbaSGreg Roach    {
142354a9dbaSGreg Roach        $tree = $request->getAttribute('tree');
143354a9dbaSGreg Roach        assert($tree instanceof Tree);
144354a9dbaSGreg Roach
145354a9dbaSGreg Roach        $xref    = $request->getAttribute('xref');
146354a9dbaSGreg Roach        $fact_id = $request->getAttribute('fact_id');
147354a9dbaSGreg Roach
148354a9dbaSGreg Roach        $record = Registry::gedcomRecordFactory()->make($xref, $tree);
149354a9dbaSGreg Roach        $record = Auth::checkRecordAccess($record);
150354a9dbaSGreg Roach
151354a9dbaSGreg Roach        $fact = $record->facts()
152354a9dbaSGreg Roach            ->filter(fn (Fact $fact): bool => $fact->id() === $fact_id)
153354a9dbaSGreg Roach            ->first();
154354a9dbaSGreg Roach
155354a9dbaSGreg Roach        if ($fact instanceof Fact) {
156354a9dbaSGreg Roach            $date             = $fact->date()->minimumDate()->format('%Y%m%d');
157354a9dbaSGreg Roach            $vcalendar        = new VCalendar();
158354a9dbaSGreg Roach            $vevent           = $vcalendar->add('VEVENT');
159354a9dbaSGreg Roach            $dtstart          = $vevent->add('DTSTART', $date);
160354a9dbaSGreg Roach            $dtstart['VALUE'] = 'DATE';
161354a9dbaSGreg Roach            $vevent->add('RRULE', 'FREQ=YEARLY');
162354a9dbaSGreg Roach            $vevent->add('SUMMARY', strip_tags($record->fullName()) . ' — ' . $fact->label());
163354a9dbaSGreg Roach
164354a9dbaSGreg Roach            return response($vcalendar->serialize())
165354a9dbaSGreg Roach                ->withHeader('Content-Type', 'text/calendar')
166354a9dbaSGreg Roach                ->withHeader('Content-Disposition', 'attachment; filename="' . $fact->id() . '.ics');
167354a9dbaSGreg Roach        }
168354a9dbaSGreg Roach
169354a9dbaSGreg Roach        throw new HttpNotFoundException();
170354a9dbaSGreg Roach    }
171354a9dbaSGreg Roach}
172