xref: /webtrees/app/Module/ClippingsCartModule.php (revision b3087c253cf6a932d19775d2730fe656c3008d7f)
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\Encodings\ANSEL;
24use Fisharebest\Webtrees\Encodings\ASCII;
25use Fisharebest\Webtrees\Encodings\UTF16BE;
26use Fisharebest\Webtrees\Encodings\UTF8;
27use Fisharebest\Webtrees\Encodings\Windows1252;
28use Fisharebest\Webtrees\Family;
29use Fisharebest\Webtrees\Gedcom;
30use Fisharebest\Webtrees\GedcomRecord;
31use Fisharebest\Webtrees\Http\RequestHandlers\FamilyPage;
32use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage;
33use Fisharebest\Webtrees\Http\RequestHandlers\LocationPage;
34use Fisharebest\Webtrees\Http\RequestHandlers\MediaPage;
35use Fisharebest\Webtrees\Http\RequestHandlers\NotePage;
36use Fisharebest\Webtrees\Http\RequestHandlers\RepositoryPage;
37use Fisharebest\Webtrees\Http\RequestHandlers\SourcePage;
38use Fisharebest\Webtrees\Http\RequestHandlers\SubmitterPage;
39use Fisharebest\Webtrees\I18N;
40use Fisharebest\Webtrees\Individual;
41use Fisharebest\Webtrees\Location;
42use Fisharebest\Webtrees\Media;
43use Fisharebest\Webtrees\Menu;
44use Fisharebest\Webtrees\Note;
45use Fisharebest\Webtrees\Registry;
46use Fisharebest\Webtrees\Repository;
47use Fisharebest\Webtrees\Services\GedcomExportService;
48use Fisharebest\Webtrees\Services\LinkedRecordService;
49use Fisharebest\Webtrees\Session;
50use Fisharebest\Webtrees\Source;
51use Fisharebest\Webtrees\Submitter;
52use Fisharebest\Webtrees\Tree;
53use Fisharebest\Webtrees\Validator;
54use Illuminate\Support\Collection;
55use Psr\Http\Message\ResponseInterface;
56use Psr\Http\Message\ServerRequestInterface;
57
58use function array_filter;
59use function array_keys;
60use function array_map;
61use function array_search;
62use function assert;
63use function count;
64use function date;
65use function extension_loaded;
66use function in_array;
67use function is_array;
68use function is_string;
69use function preg_match_all;
70use function redirect;
71use function route;
72use function str_replace;
73use function uasort;
74use function view;
75
76use const PREG_SET_ORDER;
77
78/**
79 * Class ClippingsCartModule
80 */
81class ClippingsCartModule extends AbstractModule implements ModuleMenuInterface
82{
83    use ModuleMenuTrait;
84
85    // What to add to the cart?
86    private const ADD_RECORD_ONLY        = 'record';
87    private const ADD_CHILDREN           = 'children';
88    private const ADD_DESCENDANTS        = 'descendants';
89    private const ADD_PARENT_FAMILIES    = 'parents';
90    private const ADD_SPOUSE_FAMILIES    = 'spouses';
91    private const ADD_ANCESTORS          = 'ancestors';
92    private const ADD_ANCESTOR_FAMILIES  = 'families';
93    private const ADD_LINKED_INDIVIDUALS = 'linked';
94
95    // Routes that have a record which can be added to the clipboard
96    private const ROUTES_WITH_RECORDS = [
97        'Family'     => FamilyPage::class,
98        'Individual' => IndividualPage::class,
99        'Media'      => MediaPage::class,
100        'Location'   => LocationPage::class,
101        'Note'       => NotePage::class,
102        'Repository' => RepositoryPage::class,
103        'Source'     => SourcePage::class,
104        'Submitter'  => SubmitterPage::class,
105    ];
106
107    /** @var int The default access level for this module.  It can be changed in the control panel. */
108    protected int $access_level = Auth::PRIV_USER;
109
110    private GedcomExportService $gedcom_export_service;
111
112    private LinkedRecordService $linked_record_service;
113
114    public function __construct(
115        GedcomExportService $gedcom_export_service,
116        LinkedRecordService $linked_record_service
117    ) {
118        $this->gedcom_export_service = $gedcom_export_service;
119        $this->linked_record_service = $linked_record_service;
120    }
121
122    public function description(): string
123    {
124        /* I18N: Description of the “Clippings cart” module */
125        return I18N::translate('Select records from your family tree and save them as a GEDCOM file.');
126    }
127
128    public function defaultMenuOrder(): int
129    {
130        return 6;
131    }
132
133    public function getMenu(Tree $tree): Menu|null
134    {
135        $request = Registry::container()->get(ServerRequestInterface::class);
136        $route   = Validator::attributes($request)->route();
137        $cart    = Session::get('cart');
138        $cart    = is_array($cart) ? $cart : [];
139        $count   = count($cart[$tree->name()] ?? []);
140        $badge   = view('components/badge', ['count' => $count]);
141
142        $submenus = [
143            new Menu($this->title() . ' ' . $badge, route('module', [
144                'module' => $this->name(),
145                'action' => 'Show',
146                'tree'   => $tree->name(),
147            ]), 'menu-clippings-cart', ['rel' => 'nofollow']),
148        ];
149
150        $action = array_search($route->name, self::ROUTES_WITH_RECORDS, true);
151        if ($action !== false) {
152            $xref = $route->attributes['xref'];
153            assert(is_string($xref));
154
155            $add_route = route('module', [
156                'module' => $this->name(),
157                'action' => 'Add' . $action,
158                'xref'   => $xref,
159                'tree'   => $tree->name(),
160            ]);
161
162            $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']);
163        }
164
165        if (!$this->isCartEmpty($tree)) {
166            $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [
167                'module' => $this->name(),
168                'action' => 'Empty',
169                'tree'   => $tree->name(),
170            ]), 'menu-clippings-empty', ['rel' => 'nofollow']);
171
172            $submenus[] = new Menu(I18N::translate('Download'), route('module', [
173                'module' => $this->name(),
174                'action' => 'DownloadForm',
175                'tree'   => $tree->name(),
176            ]), 'menu-clippings-download', ['rel' => 'nofollow']);
177        }
178
179        return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus);
180    }
181
182    public function title(): string
183    {
184        /* I18N: Name of a module */
185        return I18N::translate('Clippings cart');
186    }
187
188    private function isCartEmpty(Tree $tree): bool
189    {
190        $cart     = Session::get('cart');
191        $cart     = is_array($cart) ? $cart : [];
192        $contents = $cart[$tree->name()] ?? [];
193
194        return $contents === [];
195    }
196
197    public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface
198    {
199        $tree = Validator::attributes($request)->tree();
200
201        $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download');
202
203        $download_filenames = [
204            'clippings'                  => 'clippings',
205            'clippings-' . date('Y-m-d') => 'clippings-' . date('Y-m-d'),
206        ];
207
208        return $this->viewResponse('modules/clippings/download', [
209            'download_filenames' => $download_filenames,
210            'module'             => $this->name(),
211            'title'              => $title,
212            'tree'               => $tree,
213            'zip_available'      => extension_loaded('zip'),
214        ]);
215    }
216
217    public function postDownloadAction(ServerRequestInterface $request): ResponseInterface
218    {
219        $tree = Validator::attributes($request)->tree();
220
221        if (Auth::isAdmin()) {
222            $privacy_options = ['none', 'gedadmin', 'user', 'visitor'];
223        } elseif (Auth::isManager($tree)) {
224            $privacy_options = ['gedadmin', 'user', 'visitor'];
225        } elseif (Auth::isMember($tree)) {
226            $privacy_options = ['user', 'visitor'];
227        } else {
228            $privacy_options = ['visitor'];
229        }
230
231        $filename     = Validator::parsedBody($request)->string('filename');
232        $format       = Validator::parsedBody($request)->isInArray(['gedcom', 'zip', 'zipmedia', 'gedzip'])->string('format');
233        $privacy      = Validator::parsedBody($request)->isInArray($privacy_options)->string('privacy');
234        $encoding     = Validator::parsedBody($request)->isInArray([UTF8::NAME, UTF16BE::NAME, ANSEL::NAME, ASCII::NAME, Windows1252::NAME])->string('encoding');
235        $line_endings = Validator::parsedBody($request)->isInArray(['CRLF', 'LF'])->string('line_endings');
236
237        $cart = Session::get('cart');
238        $cart = is_array($cart) ? $cart : [];
239
240        $xrefs = array_keys($cart[$tree->name()] ?? []);
241        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
242
243        $records = new Collection();
244
245        switch ($privacy) {
246            case 'gedadmin':
247                $access_level = Auth::PRIV_NONE;
248                break;
249            case 'user':
250                $access_level = Auth::PRIV_USER;
251                break;
252            case 'visitor':
253                $access_level = Auth::PRIV_PRIVATE;
254                break;
255            case 'none':
256            default:
257                $access_level = Auth::PRIV_HIDE;
258                break;
259        }
260
261        foreach ($xrefs as $xref) {
262            $object = Registry::gedcomRecordFactory()->make($xref, $tree);
263            // The object may have been deleted since we added it to the cart....
264            if ($object instanceof GedcomRecord && $object->canShow($access_level)) {
265                $gedcom = $object->privatizeGedcom($access_level);
266
267                // Remove links to objects that aren't in the cart
268                $patterns = [
269                    '/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(?:\n[2-9].*)*/',
270                    '/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(?:\n[3-9].*)*/',
271                    '/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(?:\n[4-9].*)*/',
272                    '/\n4 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(?:\n[5-9].*)*/',
273                ];
274
275                foreach ($patterns as $pattern) {
276                    preg_match_all($pattern, $gedcom, $matches, PREG_SET_ORDER);
277
278                    foreach ($matches as $match) {
279                        if (!in_array($match[1], $xrefs, true)) {
280                            // Remove the reference to any object that isn't in the cart
281                            $gedcom = str_replace($match[0], '', $gedcom);
282                        }
283                    }
284                }
285
286                $records->add($gedcom);
287            }
288        }
289
290        // We have already applied privacy filtering, so do not do it again.
291        return $this->gedcom_export_service->downloadResponse($tree, false, $encoding, 'none', $line_endings, $filename, $format, $records);
292    }
293
294    public function getEmptyAction(ServerRequestInterface $request): ResponseInterface
295    {
296        $tree = Validator::attributes($request)->tree();
297
298        $cart = Session::get('cart');
299        $cart = is_array($cart) ? $cart : [];
300
301        $cart[$tree->name()] = [];
302        Session::put('cart', $cart);
303
304        $url = route('module', [
305            'module' => $this->name(),
306            'action' => 'Show',
307            'tree'   => $tree->name(),
308        ]);
309
310        return redirect($url);
311    }
312
313    public function postRemoveAction(ServerRequestInterface $request): ResponseInterface
314    {
315        $tree = Validator::attributes($request)->tree();
316        $xref = Validator::queryParams($request)->isXref()->string('xref');
317        $cart = Session::get('cart');
318        $cart = is_array($cart) ? $cart : [];
319
320        unset($cart[$tree->name()][$xref]);
321        Session::put('cart', $cart);
322
323        $url = route('module', [
324            'module' => $this->name(),
325            'action' => 'Show',
326            'tree'   => $tree->name(),
327        ]);
328
329        return redirect($url);
330    }
331
332    public function getShowAction(ServerRequestInterface $request): ResponseInterface
333    {
334        $tree = Validator::attributes($request)->tree();
335
336        return $this->viewResponse('modules/clippings/show', [
337            'module'  => $this->name(),
338            'records' => $this->allRecordsInCart($tree),
339            'title'   => I18N::translate('Family tree clippings cart'),
340            'tree'    => $tree,
341        ]);
342    }
343
344    /**
345     * @return array<GedcomRecord>
346     */
347    private function allRecordsInCart(Tree $tree): array
348    {
349        $cart = Session::get('cart');
350        $cart = is_array($cart) ? $cart : [];
351
352        $xrefs = array_keys($cart[$tree->name()] ?? []);
353        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
354
355        // Fetch all the records in the cart.
356        $records = array_map(static fn (string $xref): GedcomRecord|null => Registry::gedcomRecordFactory()->make($xref, $tree), $xrefs);
357
358        // Some records may have been deleted after they were added to the cart.
359        $records = array_filter($records);
360
361        // Group and sort.
362        uasort($records, static fn (GedcomRecord $x, GedcomRecord $y): int => $x->tag() <=> $y->tag() ?: GedcomRecord::nameComparator()($x, $y));
363
364        return $records;
365    }
366
367    public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface
368    {
369        $tree   = Validator::attributes($request)->tree();
370        $xref   = Validator::queryParams($request)->isXref()->string('xref');
371        $family = Registry::familyFactory()->make($xref, $tree);
372        $family = Auth::checkFamilyAccess($family);
373        $name   = $family->fullName();
374
375        $options = [
376            self::ADD_RECORD_ONLY => $name,
377            /* I18N: %s is a family (husband + wife) */
378            self::ADD_CHILDREN    => I18N::translate('%s and their children', $name),
379            /* I18N: %s is a family (husband + wife) */
380            self::ADD_DESCENDANTS => I18N::translate('%s and their descendants', $name),
381        ];
382
383        $title = I18N::translate('Add %s to the clippings cart', $name);
384
385        return $this->viewResponse('modules/clippings/add-options', [
386            'options' => $options,
387            'record'  => $family,
388            'title'   => $title,
389            'tree'    => $tree,
390        ]);
391    }
392
393    public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface
394    {
395        $tree   = Validator::attributes($request)->tree();
396        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
397        $option = Validator::parsedBody($request)->string('option');
398
399        $family = Registry::familyFactory()->make($xref, $tree);
400        $family = Auth::checkFamilyAccess($family);
401
402        switch ($option) {
403            case self::ADD_RECORD_ONLY:
404                $this->addFamilyToCart($family);
405                break;
406
407            case self::ADD_CHILDREN:
408                $this->addFamilyAndChildrenToCart($family);
409                break;
410
411            case self::ADD_DESCENDANTS:
412                $this->addFamilyAndDescendantsToCart($family);
413                break;
414        }
415
416        return redirect($family->url());
417    }
418
419    protected function addFamilyAndChildrenToCart(Family $family): void
420    {
421        $this->addFamilyToCart($family);
422
423        foreach ($family->children() as $child) {
424            $this->addIndividualToCart($child);
425        }
426    }
427
428    protected function addFamilyAndDescendantsToCart(Family $family): void
429    {
430        $this->addFamilyAndChildrenToCart($family);
431
432        foreach ($family->children() as $child) {
433            foreach ($child->spouseFamilies() as $child_family) {
434                $this->addFamilyAndDescendantsToCart($child_family);
435            }
436        }
437    }
438
439    public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface
440    {
441        $tree       = Validator::attributes($request)->tree();
442        $xref       = Validator::queryParams($request)->isXref()->string('xref');
443        $individual = Registry::individualFactory()->make($xref, $tree);
444        $individual = Auth::checkIndividualAccess($individual);
445        $name       = $individual->fullName();
446
447        if ($individual->sex() === 'F') {
448            $options = [
449                self::ADD_RECORD_ONLY       => $name,
450                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, her parents and siblings', $name),
451                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, her spouses and children', $name),
452                self::ADD_ANCESTORS         => I18N::translate('%s and her ancestors', $name),
453                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, her ancestors and their families', $name),
454                self::ADD_DESCENDANTS       => I18N::translate('%s, her spouses and descendants', $name),
455            ];
456        } else {
457            $options = [
458                self::ADD_RECORD_ONLY       => $name,
459                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, his parents and siblings', $name),
460                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, his spouses and children', $name),
461                self::ADD_ANCESTORS         => I18N::translate('%s and his ancestors', $name),
462                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, his ancestors and their families', $name),
463                self::ADD_DESCENDANTS       => I18N::translate('%s, his spouses and descendants', $name),
464            ];
465        }
466
467        $title = I18N::translate('Add %s to the clippings cart', $name);
468
469        return $this->viewResponse('modules/clippings/add-options', [
470            'options' => $options,
471            'record'  => $individual,
472            'title'   => $title,
473            'tree'    => $tree,
474        ]);
475    }
476
477    public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface
478    {
479        $tree   = Validator::attributes($request)->tree();
480        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
481        $option = Validator::parsedBody($request)->string('option');
482
483        $individual = Registry::individualFactory()->make($xref, $tree);
484        $individual = Auth::checkIndividualAccess($individual);
485
486        switch ($option) {
487            case self::ADD_RECORD_ONLY:
488                $this->addIndividualToCart($individual);
489                break;
490
491            case self::ADD_PARENT_FAMILIES:
492                foreach ($individual->childFamilies() as $family) {
493                    $this->addFamilyAndChildrenToCart($family);
494                }
495                break;
496
497            case self::ADD_SPOUSE_FAMILIES:
498                foreach ($individual->spouseFamilies() as $family) {
499                    $this->addFamilyAndChildrenToCart($family);
500                }
501                break;
502
503            case self::ADD_ANCESTORS:
504                $this->addAncestorsToCart($individual);
505                break;
506
507            case self::ADD_ANCESTOR_FAMILIES:
508                $this->addAncestorFamiliesToCart($individual);
509                break;
510
511            case self::ADD_DESCENDANTS:
512                foreach ($individual->spouseFamilies() as $family) {
513                    $this->addFamilyAndDescendantsToCart($family);
514                }
515                break;
516        }
517
518        return redirect($individual->url());
519    }
520
521    protected function addAncestorsToCart(Individual $individual): void
522    {
523        $this->addIndividualToCart($individual);
524
525        foreach ($individual->childFamilies() as $family) {
526            $this->addFamilyToCart($family);
527
528            foreach ($family->spouses() as $parent) {
529                $this->addAncestorsToCart($parent);
530            }
531        }
532    }
533
534    protected function addAncestorFamiliesToCart(Individual $individual): void
535    {
536        foreach ($individual->childFamilies() as $family) {
537            $this->addFamilyAndChildrenToCart($family);
538
539            foreach ($family->spouses() as $parent) {
540                $this->addAncestorFamiliesToCart($parent);
541            }
542        }
543    }
544
545    public function getAddLocationAction(ServerRequestInterface $request): ResponseInterface
546    {
547        $tree     = Validator::attributes($request)->tree();
548        $xref     = Validator::queryParams($request)->isXref()->string('xref');
549        $location = Registry::locationFactory()->make($xref, $tree);
550        $location = Auth::checkLocationAccess($location);
551        $name     = $location->fullName();
552
553        $options = [
554            self::ADD_RECORD_ONLY => $name,
555        ];
556
557        $title = I18N::translate('Add %s to the clippings cart', $name);
558
559        return $this->viewResponse('modules/clippings/add-options', [
560            'options' => $options,
561            'record'  => $location,
562            'title'   => $title,
563            'tree'    => $tree,
564        ]);
565    }
566
567    public function postAddLocationAction(ServerRequestInterface $request): ResponseInterface
568    {
569        $tree     = Validator::attributes($request)->tree();
570        $xref     = Validator::queryParams($request)->isXref()->string('xref');
571        $location = Registry::locationFactory()->make($xref, $tree);
572        $location = Auth::checkLocationAccess($location);
573
574        $this->addLocationToCart($location);
575
576        return redirect($location->url());
577    }
578
579    public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface
580    {
581        $tree  = Validator::attributes($request)->tree();
582        $xref  = Validator::queryParams($request)->isXref()->string('xref');
583        $media = Registry::mediaFactory()->make($xref, $tree);
584        $media = Auth::checkMediaAccess($media);
585        $name  = $media->fullName();
586
587        $options = [
588            self::ADD_RECORD_ONLY => $name,
589        ];
590
591        $title = I18N::translate('Add %s to the clippings cart', $name);
592
593        return $this->viewResponse('modules/clippings/add-options', [
594            'options' => $options,
595            'record'  => $media,
596            'title'   => $title,
597            'tree'    => $tree,
598        ]);
599    }
600
601    public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface
602    {
603        $tree  = Validator::attributes($request)->tree();
604        $xref  = Validator::queryParams($request)->isXref()->string('xref');
605        $media = Registry::mediaFactory()->make($xref, $tree);
606        $media = Auth::checkMediaAccess($media);
607
608        $this->addMediaToCart($media);
609
610        return redirect($media->url());
611    }
612
613    public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface
614    {
615        $tree = Validator::attributes($request)->tree();
616        $xref = Validator::queryParams($request)->isXref()->string('xref');
617        $note = Registry::noteFactory()->make($xref, $tree);
618        $note = Auth::checkNoteAccess($note);
619        $name = $note->fullName();
620
621        $options = [
622            self::ADD_RECORD_ONLY => $name,
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'  => $note,
630            'title'   => $title,
631            'tree'    => $tree,
632        ]);
633    }
634
635    public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface
636    {
637        $tree = Validator::attributes($request)->tree();
638        $xref = Validator::queryParams($request)->isXref()->string('xref');
639        $note = Registry::noteFactory()->make($xref, $tree);
640        $note = Auth::checkNoteAccess($note);
641
642        $this->addNoteToCart($note);
643
644        return redirect($note->url());
645    }
646
647    public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
648    {
649        $tree       = Validator::attributes($request)->tree();
650        $xref       = Validator::queryParams($request)->isXref()->string('xref');
651        $repository = Registry::repositoryFactory()->make($xref, $tree);
652        $repository = Auth::checkRepositoryAccess($repository);
653        $name       = $repository->fullName();
654
655        $options = [
656            self::ADD_RECORD_ONLY => $name,
657        ];
658
659        $title = I18N::translate('Add %s to the clippings cart', $name);
660
661        return $this->viewResponse('modules/clippings/add-options', [
662            'options' => $options,
663            'record'  => $repository,
664            'title'   => $title,
665            'tree'    => $tree,
666        ]);
667    }
668
669    public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
670    {
671        $tree       = Validator::attributes($request)->tree();
672        $xref       = Validator::queryParams($request)->isXref()->string('xref');
673        $repository = Registry::repositoryFactory()->make($xref, $tree);
674        $repository = Auth::checkRepositoryAccess($repository);
675
676        $this->addRepositoryToCart($repository);
677
678        foreach ($this->linked_record_service->linkedSources($repository) as $source) {
679            $this->addSourceToCart($source);
680        }
681
682        return redirect($repository->url());
683    }
684
685    public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface
686    {
687        $tree   = Validator::attributes($request)->tree();
688        $xref   = Validator::queryParams($request)->isXref()->string('xref');
689        $source = Registry::sourceFactory()->make($xref, $tree);
690        $source = Auth::checkSourceAccess($source);
691        $name   = $source->fullName();
692
693        $options = [
694            self::ADD_RECORD_ONLY        => $name,
695            self::ADD_LINKED_INDIVIDUALS => I18N::translate('%s and the individuals that reference it.', $name),
696        ];
697
698        $title = I18N::translate('Add %s to the clippings cart', $name);
699
700        return $this->viewResponse('modules/clippings/add-options', [
701            'options' => $options,
702            'record'  => $source,
703            'title'   => $title,
704            'tree'    => $tree,
705        ]);
706    }
707
708    public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface
709    {
710        $tree   = Validator::attributes($request)->tree();
711        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
712        $option = Validator::parsedBody($request)->string('option');
713
714        $source = Registry::sourceFactory()->make($xref, $tree);
715        $source = Auth::checkSourceAccess($source);
716
717        $this->addSourceToCart($source);
718
719        if ($option === self::ADD_LINKED_INDIVIDUALS) {
720            foreach ($this->linked_record_service->linkedIndividuals($source) as $individual) {
721                $this->addIndividualToCart($individual);
722            }
723            foreach ($this->linked_record_service->linkedFamilies($source) as $family) {
724                $this->addFamilyToCart($family);
725            }
726        }
727
728        return redirect($source->url());
729    }
730
731    public function getAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
732    {
733        $tree      = Validator::attributes($request)->tree();
734        $xref      = Validator::queryParams($request)->isXref()->string('xref');
735        $submitter = Registry::submitterFactory()->make($xref, $tree);
736        $submitter = Auth::checkSubmitterAccess($submitter);
737        $name      = $submitter->fullName();
738
739        $options = [
740            self::ADD_RECORD_ONLY => $name,
741        ];
742
743        $title = I18N::translate('Add %s to the clippings cart', $name);
744
745        return $this->viewResponse('modules/clippings/add-options', [
746            'options' => $options,
747            'record'  => $submitter,
748            'title'   => $title,
749            'tree'    => $tree,
750        ]);
751    }
752
753    public function postAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
754    {
755        $tree      = Validator::attributes($request)->tree();
756        $xref      = Validator::queryParams($request)->isXref()->string('xref');
757        $submitter = Registry::submitterFactory()->make($xref, $tree);
758        $submitter = Auth::checkSubmitterAccess($submitter);
759
760        $this->addSubmitterToCart($submitter);
761
762        return redirect($submitter->url());
763    }
764
765    protected function addFamilyToCart(Family $family): void
766    {
767        $cart = Session::get('cart');
768        $cart = is_array($cart) ? $cart : [];
769
770        $tree = $family->tree()->name();
771        $xref = $family->xref();
772
773        if (($cart[$tree][$xref] ?? false) === false) {
774            $cart[$tree][$xref] = true;
775
776            Session::put('cart', $cart);
777
778            foreach ($family->spouses() as $spouse) {
779                $this->addIndividualToCart($spouse);
780            }
781
782            $this->addLocationLinksToCart($family);
783            $this->addMediaLinksToCart($family);
784            $this->addNoteLinksToCart($family);
785            $this->addSourceLinksToCart($family);
786            $this->addSubmitterLinksToCart($family);
787        }
788    }
789
790    protected function addIndividualToCart(Individual $individual): void
791    {
792        $cart = Session::get('cart');
793        $cart = is_array($cart) ? $cart : [];
794
795        $tree = $individual->tree()->name();
796        $xref = $individual->xref();
797
798        if (($cart[$tree][$xref] ?? false) === false) {
799            $cart[$tree][$xref] = true;
800
801            Session::put('cart', $cart);
802
803            $this->addLocationLinksToCart($individual);
804            $this->addMediaLinksToCart($individual);
805            $this->addNoteLinksToCart($individual);
806            $this->addSourceLinksToCart($individual);
807        }
808    }
809
810    protected function addLocationToCart(Location $location): void
811    {
812        $cart = Session::get('cart');
813        $cart = is_array($cart) ? $cart : [];
814
815        $tree = $location->tree()->name();
816        $xref = $location->xref();
817
818        if (($cart[$tree][$xref] ?? false) === false) {
819            $cart[$tree][$xref] = true;
820
821            Session::put('cart', $cart);
822
823            $this->addLocationLinksToCart($location);
824            $this->addMediaLinksToCart($location);
825            $this->addNoteLinksToCart($location);
826            $this->addSourceLinksToCart($location);
827        }
828    }
829
830    protected function addLocationLinksToCart(GedcomRecord $record): void
831    {
832        preg_match_all('/\n\d _LOC @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
833
834        foreach ($matches[1] as $xref) {
835            $location = Registry::locationFactory()->make($xref, $record->tree());
836
837            if ($location instanceof Location && $location->canShow()) {
838                $this->addLocationToCart($location);
839            }
840        }
841    }
842
843    protected function addMediaToCart(Media $media): void
844    {
845        $cart = Session::get('cart');
846        $cart = is_array($cart) ? $cart : [];
847
848        $tree = $media->tree()->name();
849        $xref = $media->xref();
850
851        if (($cart[$tree][$xref] ?? false) === false) {
852            $cart[$tree][$xref] = true;
853
854            Session::put('cart', $cart);
855
856            $this->addNoteLinksToCart($media);
857        }
858    }
859
860    protected function addMediaLinksToCart(GedcomRecord $record): void
861    {
862        preg_match_all('/\n\d OBJE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
863
864        foreach ($matches[1] as $xref) {
865            $media = Registry::mediaFactory()->make($xref, $record->tree());
866
867            if ($media instanceof Media && $media->canShow()) {
868                $this->addMediaToCart($media);
869            }
870        }
871    }
872
873    protected function addNoteToCart(Note $note): void
874    {
875        $cart = Session::get('cart');
876        $cart = is_array($cart) ? $cart : [];
877
878        $tree = $note->tree()->name();
879        $xref = $note->xref();
880
881        if (($cart[$tree][$xref] ?? false) === false) {
882            $cart[$tree][$xref] = true;
883
884            Session::put('cart', $cart);
885        }
886    }
887
888    protected function addNoteLinksToCart(GedcomRecord $record): void
889    {
890        preg_match_all('/\n\d NOTE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
891
892        foreach ($matches[1] as $xref) {
893            $note = Registry::noteFactory()->make($xref, $record->tree());
894
895            if ($note instanceof Note && $note->canShow()) {
896                $this->addNoteToCart($note);
897            }
898        }
899    }
900
901    protected function addSourceToCart(Source $source): void
902    {
903        $cart = Session::get('cart');
904        $cart = is_array($cart) ? $cart : [];
905
906        $tree = $source->tree()->name();
907        $xref = $source->xref();
908
909        if (($cart[$tree][$xref] ?? false) === false) {
910            $cart[$tree][$xref] = true;
911
912            Session::put('cart', $cart);
913
914            $this->addNoteLinksToCart($source);
915            $this->addRepositoryLinksToCart($source);
916        }
917    }
918
919    protected function addSourceLinksToCart(GedcomRecord $record): void
920    {
921        preg_match_all('/\n\d SOUR @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
922
923        foreach ($matches[1] as $xref) {
924            $source = Registry::sourceFactory()->make($xref, $record->tree());
925
926            if ($source instanceof Source && $source->canShow()) {
927                $this->addSourceToCart($source);
928            }
929        }
930    }
931
932    protected function addRepositoryToCart(Repository $repository): void
933    {
934        $cart = Session::get('cart');
935        $cart = is_array($cart) ? $cart : [];
936
937        $tree = $repository->tree()->name();
938        $xref = $repository->xref();
939
940        if (($cart[$tree][$xref] ?? false) === false) {
941            $cart[$tree][$xref] = true;
942
943            Session::put('cart', $cart);
944
945            $this->addNoteLinksToCart($repository);
946        }
947    }
948
949    protected function addRepositoryLinksToCart(GedcomRecord $record): void
950    {
951        preg_match_all('/\n\d REPO @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
952
953        foreach ($matches[1] as $xref) {
954            $repository = Registry::repositoryFactory()->make($xref, $record->tree());
955
956            if ($repository instanceof Repository && $repository->canShow()) {
957                $this->addRepositoryToCart($repository);
958            }
959        }
960    }
961
962    protected function addSubmitterToCart(Submitter $submitter): void
963    {
964        $cart = Session::get('cart');
965        $cart = is_array($cart) ? $cart : [];
966        $tree = $submitter->tree()->name();
967        $xref = $submitter->xref();
968
969        if (($cart[$tree][$xref] ?? false) === false) {
970            $cart[$tree][$xref] = true;
971
972            Session::put('cart', $cart);
973
974            $this->addNoteLinksToCart($submitter);
975        }
976    }
977
978    protected function addSubmitterLinksToCart(GedcomRecord $record): void
979    {
980        preg_match_all('/\n\d SUBM @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
981
982        foreach ($matches[1] as $xref) {
983            $submitter = Registry::submitterFactory()->make($xref, $record->tree());
984
985            if ($submitter instanceof Submitter && $submitter->canShow()) {
986                $this->addSubmitterToCart($submitter);
987            }
988        }
989    }
990}
991