xref: /webtrees/app/Module/ClippingsCartModule.php (revision 1bdbe4be8b89ea7a9cca535712d11b03e44ed743)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 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 Aura\Router\Route;
23use Fisharebest\Webtrees\Auth;
24use Fisharebest\Webtrees\Family;
25use Fisharebest\Webtrees\Gedcom;
26use Fisharebest\Webtrees\GedcomRecord;
27use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage;
28use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage;
29use Fisharebest\Webtrees\Http\RequestHandlers\LocationPage;
30use Fisharebest\Webtrees\Http\RequestHandlers\MediaPage;
31use Fisharebest\Webtrees\Http\RequestHandlers\NotePage;
32use Fisharebest\Webtrees\Http\RequestHandlers\RepositoryPage;
33use Fisharebest\Webtrees\Http\RequestHandlers\SourcePage;
34use Fisharebest\Webtrees\Http\RequestHandlers\SubmitterPage;
35use Fisharebest\Webtrees\I18N;
36use Fisharebest\Webtrees\Individual;
37use Fisharebest\Webtrees\Location;
38use Fisharebest\Webtrees\Media;
39use Fisharebest\Webtrees\Menu;
40use Fisharebest\Webtrees\Note;
41use Fisharebest\Webtrees\Registry;
42use Fisharebest\Webtrees\Repository;
43use Fisharebest\Webtrees\Services\GedcomExportService;
44use Fisharebest\Webtrees\Services\UserService;
45use Fisharebest\Webtrees\Session;
46use Fisharebest\Webtrees\Source;
47use Fisharebest\Webtrees\Submitter;
48use Fisharebest\Webtrees\Tree;
49use Illuminate\Support\Collection;
50use League\Flysystem\Filesystem;
51use League\Flysystem\ZipArchive\ZipArchiveAdapter;
52use Psr\Http\Message\ResponseFactoryInterface;
53use Psr\Http\Message\ResponseInterface;
54use Psr\Http\Message\ServerRequestInterface;
55use Psr\Http\Message\StreamFactoryInterface;
56use RuntimeException;
57
58use function app;
59use function array_filter;
60use function array_keys;
61use function array_map;
62use function array_search;
63use function assert;
64use function fopen;
65use function in_array;
66use function is_string;
67use function preg_match_all;
68use function redirect;
69use function rewind;
70use function route;
71use function str_replace;
72use function stream_get_meta_data;
73use function tmpfile;
74use function uasort;
75use function view;
76
77use const PREG_SET_ORDER;
78
79/**
80 * Class ClippingsCartModule
81 */
82class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
83{
84    use ModuleMenuTrait;
85
86    // Routes that have a record which can be added to the clipboard
87    private const ROUTES_WITH_RECORDS = [
88        'Family'     => FamilyPage::class,
89        'Individual' => IndividualPage::class,
90        'Media'      => MediaPage::class,
91        'Location'   => LocationPage::class,
92        'Note'       => NotePage::class,
93        'Repository' => RepositoryPage::class,
94        'Source'     => SourcePage::class,
95        'Submitter'  => SubmitterPage::class,
96    ];
97
98    /** @var int The default access level for this module.  It can be changed in the control panel. */
99    protected $access_level = Auth::PRIV_USER;
100
101    /** @var GedcomExportService */
102    private $gedcom_export_service;
103
104    /** @var UserService */
105    private $user_service;
106
107    /**
108     * ClippingsCartModule constructor.
109     *
110     * @param GedcomExportService $gedcom_export_service
111     * @param UserService         $user_service
112     */
113    public function __construct(GedcomExportService $gedcom_export_service, UserService $user_service)
114    {
115        $this->gedcom_export_service = $gedcom_export_service;
116        $this->user_service          = $user_service;
117    }
118
119    /**
120     * A sentence describing what this module does.
121     *
122     * @return string
123     */
124    public function description(): string
125    {
126        /* I18N: Description of the “Clippings cart” module */
127        return I18N::translate('Select records from your family tree and save them as a GEDCOM file.');
128    }
129
130    /**
131     * The default position for this menu.  It can be changed in the control panel.
132     *
133     * @return int
134     */
135    public function defaultMenuOrder(): int
136    {
137        return 6;
138    }
139
140    /**
141     * A menu, to be added to the main application menu.
142     *
143     * @param Tree $tree
144     *
145     * @return Menu|null
146     */
147    public function getMenu(Tree $tree): ?Menu
148    {
149        /** @var ServerRequestInterface $request */
150        $request = app(ServerRequestInterface::class);
151
152        $route = $request->getAttribute('route');
153        assert($route instanceof Route);
154
155        $cart  = Session::get('cart', []);
156        $count = count($cart[$tree->name()] ?? []);
157        $badge = view('components/badge', ['count' => $count]);
158
159        $submenus = [
160            new Menu($this->title() . ' ' . $badge, route('module', [
161                'module' => $this->name(),
162                'action' => 'Show',
163                'tree'   => $tree->name(),
164            ]), 'menu-clippings-cart', ['rel' => 'nofollow']),
165        ];
166
167        $action = array_search($route->name, self::ROUTES_WITH_RECORDS, true);
168        if ($action !== false) {
169            $xref = $route->attributes['xref'];
170            assert(is_string($xref));
171
172            $add_route = route('module', [
173                'module' => $this->name(),
174                'action' => 'Add' . $action,
175                'xref'   => $xref,
176                'tree'   => $tree->name(),
177            ]);
178
179            $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']);
180        }
181
182        if (!$this->isCartEmpty($tree)) {
183            $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [
184                'module' => $this->name(),
185                'action' => 'Empty',
186                'tree'   => $tree->name(),
187            ]), 'menu-clippings-empty', ['rel' => 'nofollow']);
188
189            $submenus[] = new Menu(I18N::translate('Download'), route('module', [
190                'module' => $this->name(),
191                'action' => 'DownloadForm',
192                'tree'   => $tree->name(),
193            ]), 'menu-clippings-download', ['rel' => 'nofollow']);
194        }
195
196        return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus);
197    }
198
199    /**
200     * How should this module be identified in the control panel, etc.?
201     *
202     * @return string
203     */
204    public function title(): string
205    {
206        /* I18N: Name of a module */
207        return I18N::translate('Clippings cart');
208    }
209
210    /**
211     * @param Tree $tree
212     *
213     * @return bool
214     */
215    private function isCartEmpty(Tree $tree): bool
216    {
217        $cart     = Session::get('cart', []);
218        $contents = $cart[$tree->name()] ?? [];
219
220        return $contents === [];
221    }
222
223    /**
224     * @param ServerRequestInterface $request
225     *
226     * @return ResponseInterface
227     */
228    public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface
229    {
230        $tree = $request->getAttribute('tree');
231        assert($tree instanceof Tree);
232
233        $user  = $request->getAttribute('user');
234        $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download');
235
236        return $this->viewResponse('modules/clippings/download', [
237            'is_manager' => Auth::isManager($tree, $user),
238            'is_member'  => Auth::isMember($tree, $user),
239            'module'     => $this->name(),
240            'title'      => $title,
241            'tree'       => $tree,
242        ]);
243    }
244
245    /**
246     * @param ServerRequestInterface $request
247     *
248     * @return ResponseInterface
249     */
250    public function postDownloadAction(ServerRequestInterface $request): ResponseInterface
251    {
252        $tree = $request->getAttribute('tree');
253        assert($tree instanceof Tree);
254
255        $data_filesystem = Registry::filesystem()->data();
256
257        $params = (array) $request->getParsedBody();
258
259        $privatize_export = $params['privatize_export'] ?? 'none';
260
261        if ($privatize_export === 'none' && !Auth::isManager($tree)) {
262            $privatize_export = 'member';
263        }
264
265        if ($privatize_export === 'gedadmin' && !Auth::isManager($tree)) {
266            $privatize_export = 'member';
267        }
268
269        if ($privatize_export === 'user' && !Auth::isMember($tree)) {
270            $privatize_export = 'visitor';
271        }
272
273        $convert = (bool) ($params['convert'] ?? false);
274
275        $cart = Session::get('cart', []);
276
277        $xrefs = array_keys($cart[$tree->name()] ?? []);
278        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
279
280        // Create a new/empty .ZIP file
281        $temp_zip_file  = stream_get_meta_data(tmpfile())['uri'];
282        $zip_adapter    = new ZipArchiveAdapter($temp_zip_file);
283        $zip_filesystem = new Filesystem($zip_adapter);
284
285        $media_filesystem = $tree->mediaFilesystem($data_filesystem);
286
287        // Media file prefix
288        $path = $tree->getPreference('MEDIA_DIRECTORY');
289
290        $encoding = $convert ? 'ANSI' : 'UTF-8';
291
292        $records = new Collection();
293
294        switch ($privatize_export) {
295            case 'gedadmin':
296                $access_level = Auth::PRIV_NONE;
297                break;
298            case 'user':
299                $access_level = Auth::PRIV_USER;
300                break;
301            case 'visitor':
302                $access_level = Auth::PRIV_PRIVATE;
303                break;
304            case 'none':
305            default:
306                $access_level = Auth::PRIV_HIDE;
307                break;
308        }
309
310        foreach ($xrefs as $xref) {
311            $object = Registry::gedcomRecordFactory()->make($xref, $tree);
312            // The object may have been deleted since we added it to the cart....
313            if ($object instanceof GedcomRecord) {
314                $record = $object->privatizeGedcom($access_level);
315                // Remove links to objects that aren't in the cart
316                preg_match_all('/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[2-9].*)*/', $record, $matches, PREG_SET_ORDER);
317                foreach ($matches as $match) {
318                    if (!in_array($match[1], $xrefs, true)) {
319                        $record = str_replace($match[0], '', $record);
320                    }
321                }
322                preg_match_all('/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[3-9].*)*/', $record, $matches, PREG_SET_ORDER);
323                foreach ($matches as $match) {
324                    if (!in_array($match[1], $xrefs, true)) {
325                        $record = str_replace($match[0], '', $record);
326                    }
327                }
328                preg_match_all('/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[4-9].*)*/', $record, $matches, PREG_SET_ORDER);
329                foreach ($matches as $match) {
330                    if (!in_array($match[1], $xrefs, true)) {
331                        $record = str_replace($match[0], '', $record);
332                    }
333                }
334
335                if ($object instanceof Individual || $object instanceof Family) {
336                    $records->add($record . "\n1 SOUR @WEBTREES@\n2 PAGE " . $object->url());
337                } elseif ($object instanceof Source) {
338                    $records->add($record . "\n1 NOTE " . $object->url());
339                } elseif ($object instanceof Media) {
340                    // Add the media files to the archive
341                    foreach ($object->mediaFiles() as $media_file) {
342                        $from = $media_file->filename();
343                        $to   = $path . $media_file->filename();
344                        if (!$media_file->isExternal() && $media_filesystem->has($from) && !$zip_filesystem->has($to)) {
345                            $zip_filesystem->writeStream($to, $media_filesystem->readStream($from));
346                        }
347                    }
348                    $records->add($record);
349                } else {
350                    $records->add($record);
351                }
352            }
353        }
354
355        $base_url = $request->getAttribute('base_url');
356
357        // Create a source, to indicate the source of the data.
358        $record = "0 @WEBTREES@ SOUR\n1 TITL " . $base_url;
359        $author = $this->user_service->find((int) $tree->getPreference('CONTACT_USER_ID'));
360        if ($author !== null) {
361            $record .= "\n1 AUTH " . $author->realName();
362        }
363        $records->add($record);
364
365        $stream = fopen('php://temp', 'wb+');
366
367        if ($stream === false) {
368            throw new RuntimeException('Failed to create temporary stream');
369        }
370
371        // We have already applied privacy filtering, so do not do it again.
372        $this->gedcom_export_service->export($tree, $stream, false, $encoding, Auth::PRIV_HIDE, $path, $records);
373        rewind($stream);
374
375        // Finally add the GEDCOM file to the .ZIP file.
376        $zip_filesystem->writeStream('clippings.ged', $stream);
377
378        // Need to force-close ZipArchive filesystems.
379        $zip_adapter->getArchive()->close();
380
381        // Use a stream, so that we do not have to load the entire file into memory.
382        $stream = app(StreamFactoryInterface::class)->createStreamFromFile($temp_zip_file);
383
384        /** @var ResponseFactoryInterface $response_factory */
385        $response_factory = app(ResponseFactoryInterface::class);
386
387        return $response_factory->createResponse()
388            ->withBody($stream)
389            ->withHeader('Content-Type', 'application/zip')
390            ->withHeader('Content-Disposition', 'attachment; filename="clippings.zip');
391    }
392
393    /**
394     * @param ServerRequestInterface $request
395     *
396     * @return ResponseInterface
397     */
398    public function getEmptyAction(ServerRequestInterface $request): ResponseInterface
399    {
400        $tree = $request->getAttribute('tree');
401        assert($tree instanceof Tree);
402
403        $cart                = Session::get('cart', []);
404        $cart[$tree->name()] = [];
405        Session::put('cart', $cart);
406
407        $url = route('module', [
408            'module' => $this->name(),
409            'action' => 'Show',
410            'tree'   => $tree->name(),
411        ]);
412
413        return redirect($url);
414    }
415
416    /**
417     * @param ServerRequestInterface $request
418     *
419     * @return ResponseInterface
420     */
421    public function postRemoveAction(ServerRequestInterface $request): ResponseInterface
422    {
423        $tree = $request->getAttribute('tree');
424        assert($tree instanceof Tree);
425
426        $xref = $request->getQueryParams()['xref'] ?? '';
427
428        $cart = Session::get('cart', []);
429        unset($cart[$tree->name()][$xref]);
430        Session::put('cart', $cart);
431
432        $url = route('module', [
433            'module' => $this->name(),
434            'action' => 'Show',
435            'tree'   => $tree->name(),
436        ]);
437
438        return redirect($url);
439    }
440
441    /**
442     * @param ServerRequestInterface $request
443     *
444     * @return ResponseInterface
445     */
446    public function getShowAction(ServerRequestInterface $request): ResponseInterface
447    {
448        $tree = $request->getAttribute('tree');
449        assert($tree instanceof Tree);
450
451        return $this->viewResponse('modules/clippings/show', [
452            'module'  => $this->name(),
453            'records' => $this->allRecordsInCart($tree),
454            'title'   => I18N::translate('Family tree clippings cart'),
455            'tree'    => $tree,
456        ]);
457    }
458
459    /**
460     * Get all the records in the cart.
461     *
462     * @param Tree $tree
463     *
464     * @return GedcomRecord[]
465     */
466    private function allRecordsInCart(Tree $tree): array
467    {
468        $cart = Session::get('cart', []);
469
470        $xrefs = array_keys($cart[$tree->name()] ?? []);
471        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
472
473        // Fetch all the records in the cart.
474        $records = array_map(static function (string $xref) use ($tree): ?GedcomRecord {
475            return Registry::gedcomRecordFactory()->make($xref, $tree);
476        }, $xrefs);
477
478        // Some records may have been deleted after they were added to the cart.
479        $records = array_filter($records);
480
481        // Group and sort.
482        uasort($records, static function (GedcomRecord $x, GedcomRecord $y): int {
483            return $x->tag() <=> $y->tag() ?: GedcomRecord::nameComparator()($x, $y);
484        });
485
486        return $records;
487    }
488
489    /**
490     * @param ServerRequestInterface $request
491     *
492     * @return ResponseInterface
493     */
494    public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface
495    {
496        $tree = $request->getAttribute('tree');
497        assert($tree instanceof Tree);
498
499        $xref = $request->getQueryParams()['xref'] ?? '';
500
501        $family = Registry::familyFactory()->make($xref, $tree);
502        $family = Auth::checkFamilyAccess($family);
503        $name   = $family->fullName();
504
505        $options = [
506            'record'      => $name,
507            /* I18N: %s is a family (husband + wife) */
508            'members'     => I18N::translate('%s and their children', $name),
509            /* I18N: %s is a family (husband + wife) */
510            'descendants' => I18N::translate('%s and their descendants', $name),
511        ];
512
513        $title = I18N::translate('Add %s to the clippings cart', $name);
514
515        return $this->viewResponse('modules/clippings/add-options', [
516            'options' => $options,
517            'record'  => $family,
518            'title'   => $title,
519            'tree'    => $tree,
520        ]);
521    }
522
523    /**
524     * @param ServerRequestInterface $request
525     *
526     * @return ResponseInterface
527     */
528    public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface
529    {
530        $tree = $request->getAttribute('tree');
531        assert($tree instanceof Tree);
532
533        $params = (array) $request->getParsedBody();
534
535        $xref   = $params['xref'] ?? '';
536        $option = $params['option'] ?? '';
537
538        $family = Registry::familyFactory()->make($xref, $tree);
539        $family = Auth::checkFamilyAccess($family);
540
541        switch ($option) {
542            case 'self':
543                $this->addFamilyToCart($family);
544                break;
545
546            case 'members':
547                $this->addFamilyAndChildrenToCart($family);
548                break;
549
550            case 'descendants':
551                $this->addFamilyAndDescendantsToCart($family);
552                break;
553        }
554
555        return redirect($family->url());
556    }
557
558
559    /**
560     * @param Family $family
561     *
562     * @return void
563     */
564    protected function addFamilyAndChildrenToCart(Family $family): void
565    {
566        $this->addFamilyToCart($family);
567
568        foreach ($family->children() as $child) {
569            $this->addIndividualToCart($child);
570        }
571    }
572
573    /**
574     * @param Family $family
575     *
576     * @return void
577     */
578    protected function addFamilyAndDescendantsToCart(Family $family): void
579    {
580        $this->addFamilyAndChildrenToCart($family);
581
582        foreach ($family->children() as $child) {
583            foreach ($child->spouseFamilies() as $child_family) {
584                $this->addFamilyAndDescendantsToCart($child_family);
585            }
586        }
587    }
588
589    /**
590     * @param ServerRequestInterface $request
591     *
592     * @return ResponseInterface
593     */
594    public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface
595    {
596        $tree = $request->getAttribute('tree');
597        assert($tree instanceof Tree);
598
599        $xref = $request->getQueryParams()['xref'] ?? '';
600
601        $individual = Registry::individualFactory()->make($xref, $tree);
602        $individual = Auth::checkIndividualAccess($individual);
603        $name       = $individual->fullName();
604
605        if ($individual->sex() === 'F') {
606            $options = [
607                'record'            => $name,
608                'parents'           => I18N::translate('%s, her parents and siblings', $name),
609                'spouses'           => I18N::translate('%s, her spouses and children', $name),
610                'ancestors'         => I18N::translate('%s and her ancestors', $name),
611                'ancestor_families' => I18N::translate('%s, her ancestors and their families', $name),
612                'descendants'       => I18N::translate('%s, her spouses and descendants', $name),
613            ];
614        } else {
615            $options = [
616                'record'            => $name,
617                'parents'           => I18N::translate('%s, his parents and siblings', $name),
618                'spouses'           => I18N::translate('%s, his spouses and children', $name),
619                'ancestors'         => I18N::translate('%s and his ancestors', $name),
620                'ancestor_families' => I18N::translate('%s, his ancestors and their families', $name),
621                'descendants'       => I18N::translate('%s, his spouses and descendants', $name),
622            ];
623        }
624
625        $title = I18N::translate('Add %s to the clippings cart', $name);
626
627        return $this->viewResponse('modules/clippings/add-options', [
628            'options' => $options,
629            'record'  => $individual,
630            'title'   => $title,
631            'tree'    => $tree,
632        ]);
633    }
634
635    /**
636     * @param ServerRequestInterface $request
637     *
638     * @return ResponseInterface
639     */
640    public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface
641    {
642        $tree = $request->getAttribute('tree');
643        assert($tree instanceof Tree);
644
645        $params = (array) $request->getParsedBody();
646
647        $xref   = $params['xref'] ?? '';
648        $option = $params['option'] ?? '';
649
650        $individual = Registry::individualFactory()->make($xref, $tree);
651        $individual = Auth::checkIndividualAccess($individual);
652
653        switch ($option) {
654            case 'self':
655                $this->addIndividualToCart($individual);
656                break;
657
658            case 'parents':
659                foreach ($individual->childFamilies() as $family) {
660                    $this->addFamilyAndChildrenToCart($family);
661                }
662                break;
663
664            case 'spouses':
665                foreach ($individual->spouseFamilies() as $family) {
666                    $this->addFamilyAndChildrenToCart($family);
667                }
668                break;
669
670            case 'ancestors':
671                $this->addAncestorsToCart($individual);
672                break;
673
674            case 'ancestor_families':
675                $this->addAncestorFamiliesToCart($individual);
676                break;
677
678            case 'descendants':
679                foreach ($individual->spouseFamilies() as $family) {
680                    $this->addFamilyAndDescendantsToCart($family);
681                }
682                break;
683        }
684
685        return redirect($individual->url());
686    }
687
688    /**
689     * @param Individual $individual
690     *
691     * @return void
692     */
693    protected function addAncestorsToCart(Individual $individual): void
694    {
695        $this->addIndividualToCart($individual);
696
697        foreach ($individual->childFamilies() as $family) {
698            $this->addFamilyToCart($family);
699
700            foreach ($family->spouses() as $parent) {
701                $this->addAncestorsToCart($parent);
702            }
703        }
704    }
705
706    /**
707     * @param Individual $individual
708     *
709     * @return void
710     */
711    protected function addAncestorFamiliesToCart(Individual $individual): void
712    {
713        foreach ($individual->childFamilies() as $family) {
714            $this->addFamilyAndChildrenToCart($family);
715
716            foreach ($family->spouses() as $parent) {
717                $this->addAncestorFamiliesToCart($parent);
718            }
719        }
720    }
721
722    /**
723     * @param ServerRequestInterface $request
724     *
725     * @return ResponseInterface
726     */
727    public function getAddLocationAction(ServerRequestInterface $request): ResponseInterface
728    {
729        $tree = $request->getAttribute('tree');
730        assert($tree instanceof Tree);
731
732        $xref = $request->getQueryParams()['xref'] ?? '';
733
734        $location = Registry::locationFactory()->make($xref, $tree);
735        $location = Auth::checkLocationAccess($location);
736        $name     = $location->fullName();
737
738        $options = [
739            'record' => $name,
740        ];
741
742        $title = I18N::translate('Add %s to the clippings cart', $name);
743
744        return $this->viewResponse('modules/clippings/add-options', [
745            'options' => $options,
746            'record'  => $location,
747            'title'   => $title,
748            'tree'    => $tree,
749        ]);
750    }
751
752    /**
753     * @param ServerRequestInterface $request
754     *
755     * @return ResponseInterface
756     */
757    public function postAddLocationAction(ServerRequestInterface $request): ResponseInterface
758    {
759        $tree = $request->getAttribute('tree');
760        assert($tree instanceof Tree);
761
762        $xref = $request->getQueryParams()['xref'] ?? '';
763
764        $location = Registry::locationFactory()->make($xref, $tree);
765        $location = Auth::checkLocationAccess($location);
766
767        $this->addLocationToCart($location);
768
769        return redirect($location->url());
770    }
771
772    /**
773     * @param ServerRequestInterface $request
774     *
775     * @return ResponseInterface
776     */
777    public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface
778    {
779        $tree = $request->getAttribute('tree');
780        assert($tree instanceof Tree);
781
782        $xref = $request->getQueryParams()['xref'] ?? '';
783
784        $media = Registry::mediaFactory()->make($xref, $tree);
785        $media = Auth::checkMediaAccess($media);
786        $name  = $media->fullName();
787
788        $options = [
789            'record' => $name,
790        ];
791
792        $title = I18N::translate('Add %s to the clippings cart', $name);
793
794        return $this->viewResponse('modules/clippings/add-options', [
795            'options' => $options,
796            'record'  => $media,
797            'title'   => $title,
798            'tree'    => $tree,
799        ]);
800    }
801
802    /**
803     * @param ServerRequestInterface $request
804     *
805     * @return ResponseInterface
806     */
807    public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface
808    {
809        $tree = $request->getAttribute('tree');
810        assert($tree instanceof Tree);
811
812        $xref = $request->getQueryParams()['xref'] ?? '';
813
814        $media = Registry::mediaFactory()->make($xref, $tree);
815        $media = Auth::checkMediaAccess($media);
816
817        $this->addMediaToCart($media);
818
819        return redirect($media->url());
820    }
821
822    /**
823     * @param ServerRequestInterface $request
824     *
825     * @return ResponseInterface
826     */
827    public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface
828    {
829        $tree = $request->getAttribute('tree');
830        assert($tree instanceof Tree);
831
832        $xref = $request->getQueryParams()['xref'] ?? '';
833
834        $note = Registry::noteFactory()->make($xref, $tree);
835        $note = Auth::checkNoteAccess($note);
836        $name = $note->fullName();
837
838        $options = [
839            'record' => $name,
840        ];
841
842        $title = I18N::translate('Add %s to the clippings cart', $name);
843
844        return $this->viewResponse('modules/clippings/add-options', [
845            'options' => $options,
846            'record'  => $note,
847            'title'   => $title,
848            'tree'    => $tree,
849        ]);
850    }
851
852    /**
853     * @param ServerRequestInterface $request
854     *
855     * @return ResponseInterface
856     */
857    public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface
858    {
859        $tree = $request->getAttribute('tree');
860        assert($tree instanceof Tree);
861
862        $xref = $request->getQueryParams()['xref'] ?? '';
863
864        $note = Registry::noteFactory()->make($xref, $tree);
865        $note = Auth::checkNoteAccess($note);
866
867        $this->addNoteToCart($note);
868
869        return redirect($note->url());
870    }
871
872    /**
873     * @param ServerRequestInterface $request
874     *
875     * @return ResponseInterface
876     */
877    public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
878    {
879        $tree = $request->getAttribute('tree');
880        assert($tree instanceof Tree);
881
882        $xref = $request->getQueryParams()['xref'] ?? '';
883
884        $repository = Registry::repositoryFactory()->make($xref, $tree);
885        $repository = Auth::checkRepositoryAccess($repository);
886        $name       = $repository->fullName();
887
888        $options = [
889            'record' => $name,
890        ];
891
892        $title = I18N::translate('Add %s to the clippings cart', $name);
893
894        return $this->viewResponse('modules/clippings/add-options', [
895            'options' => $options,
896            'record'  => $repository,
897            'title'   => $title,
898            'tree'    => $tree,
899        ]);
900    }
901
902    /**
903     * @param ServerRequestInterface $request
904     *
905     * @return ResponseInterface
906     */
907    public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
908    {
909        $tree = $request->getAttribute('tree');
910        assert($tree instanceof Tree);
911
912        $xref = $request->getQueryParams()['xref'] ?? '';
913
914        $repository = Registry::repositoryFactory()->make($xref, $tree);
915        $repository = Auth::checkRepositoryAccess($repository);
916
917        $this->addRepositoryToCart($repository);
918
919        foreach ($repository->linkedSources('REPO') as $source) {
920            $this->addSourceToCart($source);
921        }
922
923        return redirect($repository->url());
924    }
925
926    /**
927     * @param ServerRequestInterface $request
928     *
929     * @return ResponseInterface
930     */
931    public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface
932    {
933        $tree = $request->getAttribute('tree');
934        assert($tree instanceof Tree);
935
936        $xref = $request->getQueryParams()['xref'] ?? '';
937
938        $source = Registry::sourceFactory()->make($xref, $tree);
939        $source = Auth::checkSourceAccess($source);
940        $name   = $source->fullName();
941
942        $options = [
943            'record' => $name,
944            'linked' => I18N::translate('%s and the individuals that reference it.', $name),
945        ];
946
947        $title = I18N::translate('Add %s to the clippings cart', $name);
948
949        return $this->viewResponse('modules/clippings/add-options', [
950            'options' => $options,
951            'record'  => $source,
952            'title'   => $title,
953            'tree'    => $tree,
954        ]);
955    }
956
957    /**
958     * @param ServerRequestInterface $request
959     *
960     * @return ResponseInterface
961     */
962    public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface
963    {
964        $tree = $request->getAttribute('tree');
965        assert($tree instanceof Tree);
966
967        $params = (array) $request->getParsedBody();
968
969        $xref   = $params['xref'] ?? '';
970        $option = $params['option'] ?? '';
971
972        $source = Registry::sourceFactory()->make($xref, $tree);
973        $source = Auth::checkSourceAccess($source);
974
975        $this->addSourceToCart($source);
976
977        if ($option === 'linked') {
978            foreach ($source->linkedIndividuals('SOUR') as $individual) {
979                $this->addIndividualToCart($individual);
980            }
981            foreach ($source->linkedFamilies('SOUR') as $family) {
982                $this->addFamilyToCart($family);
983            }
984        }
985
986        return redirect($source->url());
987    }
988
989    /**
990     * @param ServerRequestInterface $request
991     *
992     * @return ResponseInterface
993     */
994    public function getAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
995    {
996        $tree = $request->getAttribute('tree');
997        assert($tree instanceof Tree);
998
999        $xref = $request->getQueryParams()['xref'] ?? '';
1000
1001        $submitter = Registry::submitterFactory()->make($xref, $tree);
1002        $submitter = Auth::checkSubmitterAccess($submitter);
1003        $name      = $submitter->fullName();
1004
1005        $options = [
1006            'record' => $name,
1007        ];
1008
1009        $title = I18N::translate('Add %s to the clippings cart', $name);
1010
1011        return $this->viewResponse('modules/clippings/add-options', [
1012            'options' => $options,
1013            'record'  => $submitter,
1014            'title'   => $title,
1015            'tree'    => $tree,
1016        ]);
1017    }
1018
1019    /**
1020     * @param ServerRequestInterface $request
1021     *
1022     * @return ResponseInterface
1023     */
1024    public function postAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
1025    {
1026        $tree = $request->getAttribute('tree');
1027        assert($tree instanceof Tree);
1028
1029        $xref = $request->getQueryParams()['xref'] ?? '';
1030
1031        $submitter = Registry::submitterFactory()->make($xref, $tree);
1032        $submitter = Auth::checkSubmitterAccess($submitter);
1033
1034        $this->addSubmitterToCart($submitter);
1035
1036        return redirect($submitter->url());
1037    }
1038
1039    /**
1040     * @param Family $family
1041     */
1042    protected function addFamilyToCart(Family $family): void
1043    {
1044        $cart = Session::get('cart', []);
1045        $tree = $family->tree()->name();
1046        $xref = $family->xref();
1047
1048        if (($cart[$tree][$xref] ?? false) === false) {
1049            $cart[$tree][$xref] = true;
1050
1051            Session::put('cart', $cart);
1052
1053            foreach ($family->spouses() as $spouse) {
1054                $this->addIndividualToCart($spouse);
1055            }
1056
1057            $this->addLocationLinksToCart($family);
1058            $this->addMediaLinksToCart($family);
1059            $this->addNoteLinksToCart($family);
1060            $this->addSourceLinksToCart($family);
1061            $this->addSubmitterLinksToCart($family);
1062        }
1063    }
1064
1065    /**
1066     * @param Individual $individual
1067     */
1068    protected function addIndividualToCart(Individual $individual): void
1069    {
1070        $cart = Session::get('cart', []);
1071        $tree = $individual->tree()->name();
1072        $xref = $individual->xref();
1073
1074        if (($cart[$tree][$xref] ?? false) === false) {
1075            $cart[$tree][$xref] = true;
1076
1077            Session::put('cart', $cart);
1078
1079            $this->addLocationLinksToCart($individual);
1080            $this->addMediaLinksToCart($individual);
1081            $this->addNoteLinksToCart($individual);
1082            $this->addSourceLinksToCart($individual);
1083        }
1084    }
1085
1086    /**
1087     * @param Location $location
1088     */
1089    protected function addLocationToCart(Location $location): void
1090    {
1091        $cart = Session::get('cart', []);
1092        $tree = $location->tree()->name();
1093        $xref = $location->xref();
1094
1095        if (($cart[$tree][$xref] ?? false) === false) {
1096            $cart[$tree][$xref] = true;
1097
1098            Session::put('cart', $cart);
1099
1100            $this->addLocationLinksToCart($location);
1101            $this->addMediaLinksToCart($location);
1102            $this->addNoteLinksToCart($location);
1103            $this->addSourceLinksToCart($location);
1104        }
1105    }
1106
1107    /**
1108     * @param GedcomRecord $record
1109     */
1110    protected function addLocationLinksToCart(GedcomRecord $record): void
1111    {
1112        preg_match_all('/\n\d _LOC @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1113
1114        foreach ($matches[1] as $xref) {
1115            $location = Registry::locationFactory()->make($xref, $record->tree());
1116
1117            if ($location instanceof Location && $location->canShow()) {
1118                $this->addLocationToCart($location);
1119            }
1120        }
1121    }
1122
1123    /**
1124     * @param Media $media
1125     */
1126    protected function addMediaToCart(Media $media): void
1127    {
1128        $cart = Session::get('cart', []);
1129        $tree = $media->tree()->name();
1130        $xref = $media->xref();
1131
1132        if (($cart[$tree][$xref] ?? false) === false) {
1133            $cart[$tree][$xref] = true;
1134
1135            Session::put('cart', $cart);
1136
1137            $this->addNoteLinksToCart($media);
1138        }
1139    }
1140
1141    /**
1142     * @param GedcomRecord $record
1143     */
1144    protected function addMediaLinksToCart(GedcomRecord $record): void
1145    {
1146        preg_match_all('/\n\d OBJE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1147
1148        foreach ($matches[1] as $xref) {
1149            $media = Registry::mediaFactory()->make($xref, $record->tree());
1150
1151            if ($media instanceof Media && $media->canShow()) {
1152                $this->addMediaToCart($media);
1153            }
1154        }
1155    }
1156
1157    /**
1158     * @param Note $note
1159     */
1160    protected function addNoteToCart(Note $note): void
1161    {
1162        $cart = Session::get('cart', []);
1163        $tree = $note->tree()->name();
1164        $xref = $note->xref();
1165
1166        if (($cart[$tree][$xref] ?? false) === false) {
1167            $cart[$tree][$xref] = true;
1168
1169            Session::put('cart', $cart);
1170        }
1171    }
1172
1173    /**
1174     * @param GedcomRecord $record
1175     */
1176    protected function addNoteLinksToCart(GedcomRecord $record): void
1177    {
1178        preg_match_all('/\n\d NOTE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1179
1180        foreach ($matches[1] as $xref) {
1181            $note = Registry::noteFactory()->make($xref, $record->tree());
1182
1183            if ($note instanceof Note && $note->canShow()) {
1184                $this->addNoteToCart($note);
1185            }
1186        }
1187    }
1188
1189    /**
1190     * @param Source $source
1191     */
1192    protected function addSourceToCart(Source $source): void
1193    {
1194        $cart = Session::get('cart', []);
1195        $tree = $source->tree()->name();
1196        $xref = $source->xref();
1197
1198        if (($cart[$tree][$xref] ?? false) === false) {
1199            $cart[$tree][$xref] = true;
1200
1201            Session::put('cart', $cart);
1202
1203            $this->addNoteLinksToCart($source);
1204            $this->addRepositoryLinksToCart($source);
1205        }
1206    }
1207
1208    /**
1209     * @param GedcomRecord $record
1210     */
1211    protected function addSourceLinksToCart(GedcomRecord $record): void
1212    {
1213        preg_match_all('/\n\d SOUR @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1214
1215        foreach ($matches[1] as $xref) {
1216            $source = Registry::sourceFactory()->make($xref, $record->tree());
1217
1218            if ($source instanceof Source && $source->canShow()) {
1219                $this->addSourceToCart($source);
1220            }
1221        }
1222    }
1223
1224    /**
1225     * @param Repository $repository
1226     */
1227    protected function addRepositoryToCart(Repository $repository): void
1228    {
1229        $cart = Session::get('cart', []);
1230        $tree = $repository->tree()->name();
1231        $xref = $repository->xref();
1232
1233        if (($cart[$tree][$xref] ?? false) === false) {
1234            $cart[$tree][$xref] = true;
1235
1236            Session::put('cart', $cart);
1237
1238            $this->addNoteLinksToCart($repository);
1239        }
1240    }
1241
1242    /**
1243     * @param GedcomRecord $record
1244     */
1245    protected function addRepositoryLinksToCart(GedcomRecord $record): void
1246    {
1247        preg_match_all('/\n\d REPO @(' . Gedcom::REGEX_XREF . '@)/', $record->gedcom(), $matches);
1248
1249        foreach ($matches[1] as $xref) {
1250            $repository = Registry::repositoryFactory()->make($xref, $record->tree());
1251
1252            if ($repository instanceof Repository && $repository->canShow()) {
1253                $this->addRepositoryToCart($repository);
1254            }
1255        }
1256    }
1257
1258    /**
1259     * @param Submitter $submitter
1260     */
1261    protected function addSubmitterToCart(Submitter $submitter): void
1262    {
1263        $cart = Session::get('cart', []);
1264        $tree = $submitter->tree()->name();
1265        $xref = $submitter->xref();
1266
1267        if (($cart[$tree][$xref] ?? false) === false) {
1268            $cart[$tree][$xref] = true;
1269
1270            Session::put('cart', $cart);
1271
1272            $this->addNoteLinksToCart($submitter);
1273        }
1274    }
1275
1276    /**
1277     * @param GedcomRecord $record
1278     */
1279    protected function addSubmitterLinksToCart(GedcomRecord $record): void
1280    {
1281        preg_match_all('/\n\d SUBM @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1282
1283        foreach ($matches[1] as $xref) {
1284            $submitter = Registry::submitterFactory()->make($xref, $record->tree());
1285
1286            if ($submitter instanceof Submitter && $submitter->canShow()) {
1287                $this->addSubmitterToCart($submitter);
1288            }
1289        }
1290    }
1291}
1292