xref: /webtrees/app/Module/ClippingsCartModule.php (revision 6232931fa0cebfba2f226855a8d2e05aa0780d5c)
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    /**
115     * @param GedcomExportService $gedcom_export_service
116     * @param LinkedRecordService $linked_record_service
117     */
118    public function __construct(
119        GedcomExportService $gedcom_export_service,
120        LinkedRecordService $linked_record_service
121    ) {
122        $this->gedcom_export_service = $gedcom_export_service;
123        $this->linked_record_service = $linked_record_service;
124    }
125
126    /**
127     * A sentence describing what this module does.
128     *
129     * @return string
130     */
131    public function description(): string
132    {
133        /* I18N: Description of the “Clippings cart” module */
134        return I18N::translate('Select records from your family tree and save them as a GEDCOM file.');
135    }
136
137    /**
138     * The default position for this menu.  It can be changed in the control panel.
139     *
140     * @return int
141     */
142    public function defaultMenuOrder(): int
143    {
144        return 6;
145    }
146
147    /**
148     * A menu, to be added to the main application menu.
149     *
150     * @param Tree $tree
151     *
152     * @return Menu|null
153     */
154    public function getMenu(Tree $tree): ?Menu
155    {
156        $request = Registry::container()->get(ServerRequestInterface::class);
157        $route   = Validator::attributes($request)->route();
158        $cart    = Session::get('cart');
159        $cart    = is_array($cart) ? $cart : [];
160        $count   = count($cart[$tree->name()] ?? []);
161        $badge   = view('components/badge', ['count' => $count]);
162
163        $submenus = [
164            new Menu($this->title() . ' ' . $badge, route('module', [
165                'module' => $this->name(),
166                'action' => 'Show',
167                'tree'   => $tree->name(),
168            ]), 'menu-clippings-cart', ['rel' => 'nofollow']),
169        ];
170
171        $action = array_search($route->name, self::ROUTES_WITH_RECORDS, true);
172        if ($action !== false) {
173            $xref = $route->attributes['xref'];
174            assert(is_string($xref));
175
176            $add_route = route('module', [
177                'module' => $this->name(),
178                'action' => 'Add' . $action,
179                'xref'   => $xref,
180                'tree'   => $tree->name(),
181            ]);
182
183            $submenus[] = new Menu(I18N::translate('Add to the clippings cart'), $add_route, 'menu-clippings-add', ['rel' => 'nofollow']);
184        }
185
186        if (!$this->isCartEmpty($tree)) {
187            $submenus[] = new Menu(I18N::translate('Empty the clippings cart'), route('module', [
188                'module' => $this->name(),
189                'action' => 'Empty',
190                'tree'   => $tree->name(),
191            ]), 'menu-clippings-empty', ['rel' => 'nofollow']);
192
193            $submenus[] = new Menu(I18N::translate('Download'), route('module', [
194                'module' => $this->name(),
195                'action' => 'DownloadForm',
196                'tree'   => $tree->name(),
197            ]), 'menu-clippings-download', ['rel' => 'nofollow']);
198        }
199
200        return new Menu($this->title(), '#', 'menu-clippings', ['rel' => 'nofollow'], $submenus);
201    }
202
203    /**
204     * How should this module be identified in the control panel, etc.?
205     *
206     * @return string
207     */
208    public function title(): string
209    {
210        /* I18N: Name of a module */
211        return I18N::translate('Clippings cart');
212    }
213
214    /**
215     * @param Tree $tree
216     *
217     * @return bool
218     */
219    private function isCartEmpty(Tree $tree): bool
220    {
221        $cart     = Session::get('cart');
222        $cart     = is_array($cart) ? $cart : [];
223        $contents = $cart[$tree->name()] ?? [];
224
225        return $contents === [];
226    }
227
228    /**
229     * @param ServerRequestInterface $request
230     *
231     * @return ResponseInterface
232     */
233    public function getDownloadFormAction(ServerRequestInterface $request): ResponseInterface
234    {
235        $tree = Validator::attributes($request)->tree();
236
237        $title = I18N::translate('Family tree clippings cart') . ' — ' . I18N::translate('Download');
238
239        $download_filenames = [
240            'clippings'                  => 'clippings',
241            'clippings-' . date('Y-m-d') => 'clippings-' . date('Y-m-d'),
242        ];
243
244        return $this->viewResponse('modules/clippings/download', [
245            'download_filenames' => $download_filenames,
246            'module'             => $this->name(),
247            'title'              => $title,
248            'tree'               => $tree,
249            'zip_available'      => extension_loaded('zip'),
250        ]);
251    }
252
253    /**
254     * @param ServerRequestInterface $request
255     *
256     * @return ResponseInterface
257     */
258    public function postDownloadAction(ServerRequestInterface $request): ResponseInterface
259    {
260        $tree = Validator::attributes($request)->tree();
261
262        if (Auth::isAdmin()) {
263            $privacy_options = ['none', 'gedadmin', 'user', 'visitor'];
264        } elseif (Auth::isManager($tree)) {
265            $privacy_options = ['gedadmin', 'user', 'visitor'];
266        } elseif (Auth::isMember($tree)) {
267            $privacy_options = ['user', 'visitor'];
268        } else {
269            $privacy_options = ['visitor'];
270        }
271
272        $filename     = Validator::parsedBody($request)->string('filename');
273        $format       = Validator::parsedBody($request)->isInArray(['gedcom', 'zip', 'zipmedia', 'gedzip'])->string('format');
274        $privacy      = Validator::parsedBody($request)->isInArray($privacy_options)->string('privacy');
275        $encoding     = Validator::parsedBody($request)->isInArray([UTF8::NAME, UTF16BE::NAME, ANSEL::NAME, ASCII::NAME, Windows1252::NAME])->string('encoding');
276        $line_endings = Validator::parsedBody($request)->isInArray(['CRLF', 'LF'])->string('line_endings');
277
278        $cart = Session::get('cart');
279        $cart = is_array($cart) ? $cart : [];
280
281        $xrefs = array_keys($cart[$tree->name()] ?? []);
282        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
283
284        $records = new Collection();
285
286        switch ($privacy) {
287            case 'gedadmin':
288                $access_level = Auth::PRIV_NONE;
289                break;
290            case 'user':
291                $access_level = Auth::PRIV_USER;
292                break;
293            case 'visitor':
294                $access_level = Auth::PRIV_PRIVATE;
295                break;
296            case 'none':
297            default:
298                $access_level = Auth::PRIV_HIDE;
299                break;
300        }
301
302        foreach ($xrefs as $xref) {
303            $object = Registry::gedcomRecordFactory()->make($xref, $tree);
304            // The object may have been deleted since we added it to the cart....
305            if ($object instanceof GedcomRecord) {
306                $record = $object->privatizeGedcom($access_level);
307                // Remove links to objects that aren't in the cart
308                preg_match_all('/\n1 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[2-9].*)*/', $record, $matches, PREG_SET_ORDER);
309                foreach ($matches as $match) {
310                    if (!in_array($match[1], $xrefs, true)) {
311                        $record = str_replace($match[0], '', $record);
312                    }
313                }
314                preg_match_all('/\n2 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[3-9].*)*/', $record, $matches, PREG_SET_ORDER);
315                foreach ($matches as $match) {
316                    if (!in_array($match[1], $xrefs, true)) {
317                        $record = str_replace($match[0], '', $record);
318                    }
319                }
320                preg_match_all('/\n3 ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@(\n[4-9].*)*/', $record, $matches, PREG_SET_ORDER);
321                foreach ($matches as $match) {
322                    if (!in_array($match[1], $xrefs, true)) {
323                        $record = str_replace($match[0], '', $record);
324                    }
325                }
326
327                $records->add($record);
328            }
329        }
330
331        // We have already applied privacy filtering, so do not do it again.
332        return $this->gedcom_export_service->downloadResponse($tree, false, $encoding, 'none', $line_endings, $filename, $format, $records);
333    }
334
335    /**
336     * @param ServerRequestInterface $request
337     *
338     * @return ResponseInterface
339     */
340    public function getEmptyAction(ServerRequestInterface $request): ResponseInterface
341    {
342        $tree = Validator::attributes($request)->tree();
343
344        $cart = Session::get('cart');
345        $cart = is_array($cart) ? $cart : [];
346
347        $cart[$tree->name()] = [];
348        Session::put('cart', $cart);
349
350        $url = route('module', [
351            'module' => $this->name(),
352            'action' => 'Show',
353            'tree'   => $tree->name(),
354        ]);
355
356        return redirect($url);
357    }
358
359    /**
360     * @param ServerRequestInterface $request
361     *
362     * @return ResponseInterface
363     */
364    public function postRemoveAction(ServerRequestInterface $request): ResponseInterface
365    {
366        $tree = Validator::attributes($request)->tree();
367        $xref = Validator::queryParams($request)->isXref()->string('xref');
368        $cart = Session::get('cart');
369        $cart = is_array($cart) ? $cart : [];
370
371        unset($cart[$tree->name()][$xref]);
372        Session::put('cart', $cart);
373
374        $url = route('module', [
375            'module' => $this->name(),
376            'action' => 'Show',
377            'tree'   => $tree->name(),
378        ]);
379
380        return redirect($url);
381    }
382
383    /**
384     * @param ServerRequestInterface $request
385     *
386     * @return ResponseInterface
387     */
388    public function getShowAction(ServerRequestInterface $request): ResponseInterface
389    {
390        $tree = Validator::attributes($request)->tree();
391
392        return $this->viewResponse('modules/clippings/show', [
393            'module'  => $this->name(),
394            'records' => $this->allRecordsInCart($tree),
395            'title'   => I18N::translate('Family tree clippings cart'),
396            'tree'    => $tree,
397        ]);
398    }
399
400    /**
401     * Get all the records in the cart.
402     *
403     * @param Tree $tree
404     *
405     * @return array<GedcomRecord>
406     */
407    private function allRecordsInCart(Tree $tree): array
408    {
409        $cart = Session::get('cart');
410        $cart = is_array($cart) ? $cart : [];
411
412        $xrefs = array_keys($cart[$tree->name()] ?? []);
413        $xrefs = array_map('strval', $xrefs); // PHP converts numeric keys to integers.
414
415        // Fetch all the records in the cart.
416        $records = array_map(static function (string $xref) use ($tree): ?GedcomRecord {
417            return Registry::gedcomRecordFactory()->make($xref, $tree);
418        }, $xrefs);
419
420        // Some records may have been deleted after they were added to the cart.
421        $records = array_filter($records);
422
423        // Group and sort.
424        uasort($records, static function (GedcomRecord $x, GedcomRecord $y): int {
425            return $x->tag() <=> $y->tag() ?: GedcomRecord::nameComparator()($x, $y);
426        });
427
428        return $records;
429    }
430
431    /**
432     * @param ServerRequestInterface $request
433     *
434     * @return ResponseInterface
435     */
436    public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface
437    {
438        $tree   = Validator::attributes($request)->tree();
439        $xref   = Validator::queryParams($request)->isXref()->string('xref');
440        $family = Registry::familyFactory()->make($xref, $tree);
441        $family = Auth::checkFamilyAccess($family);
442        $name   = $family->fullName();
443
444        $options = [
445            self::ADD_RECORD_ONLY => $name,
446            /* I18N: %s is a family (husband + wife) */
447            self::ADD_CHILDREN    => I18N::translate('%s and their children', $name),
448            /* I18N: %s is a family (husband + wife) */
449            self::ADD_DESCENDANTS => I18N::translate('%s and their descendants', $name),
450        ];
451
452        $title = I18N::translate('Add %s to the clippings cart', $name);
453
454        return $this->viewResponse('modules/clippings/add-options', [
455            'options' => $options,
456            'record'  => $family,
457            'title'   => $title,
458            'tree'    => $tree,
459        ]);
460    }
461
462    /**
463     * @param ServerRequestInterface $request
464     *
465     * @return ResponseInterface
466     */
467    public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface
468    {
469        $tree   = Validator::attributes($request)->tree();
470        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
471        $option = Validator::parsedBody($request)->string('option');
472
473        $family = Registry::familyFactory()->make($xref, $tree);
474        $family = Auth::checkFamilyAccess($family);
475
476        switch ($option) {
477            case self::ADD_RECORD_ONLY:
478                $this->addFamilyToCart($family);
479                break;
480
481            case self::ADD_CHILDREN:
482                $this->addFamilyAndChildrenToCart($family);
483                break;
484
485            case self::ADD_DESCENDANTS:
486                $this->addFamilyAndDescendantsToCart($family);
487                break;
488        }
489
490        return redirect($family->url());
491    }
492
493
494    /**
495     * @param Family $family
496     *
497     * @return void
498     */
499    protected function addFamilyAndChildrenToCart(Family $family): void
500    {
501        $this->addFamilyToCart($family);
502
503        foreach ($family->children() as $child) {
504            $this->addIndividualToCart($child);
505        }
506    }
507
508    /**
509     * @param Family $family
510     *
511     * @return void
512     */
513    protected function addFamilyAndDescendantsToCart(Family $family): void
514    {
515        $this->addFamilyAndChildrenToCart($family);
516
517        foreach ($family->children() as $child) {
518            foreach ($child->spouseFamilies() as $child_family) {
519                $this->addFamilyAndDescendantsToCart($child_family);
520            }
521        }
522    }
523
524    /**
525     * @param ServerRequestInterface $request
526     *
527     * @return ResponseInterface
528     */
529    public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface
530    {
531        $tree       = Validator::attributes($request)->tree();
532        $xref       = Validator::queryParams($request)->isXref()->string('xref');
533        $individual = Registry::individualFactory()->make($xref, $tree);
534        $individual = Auth::checkIndividualAccess($individual);
535        $name       = $individual->fullName();
536
537        if ($individual->sex() === 'F') {
538            $options = [
539                self::ADD_RECORD_ONLY       => $name,
540                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, her parents and siblings', $name),
541                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, her spouses and children', $name),
542                self::ADD_ANCESTORS         => I18N::translate('%s and her ancestors', $name),
543                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, her ancestors and their families', $name),
544                self::ADD_DESCENDANTS       => I18N::translate('%s, her spouses and descendants', $name),
545            ];
546        } else {
547            $options = [
548                self::ADD_RECORD_ONLY       => $name,
549                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, his parents and siblings', $name),
550                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, his spouses and children', $name),
551                self::ADD_ANCESTORS         => I18N::translate('%s and his ancestors', $name),
552                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, his ancestors and their families', $name),
553                self::ADD_DESCENDANTS       => I18N::translate('%s, his spouses and descendants', $name),
554            ];
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'  => $individual,
562            'title'   => $title,
563            'tree'    => $tree,
564        ]);
565    }
566
567    /**
568     * @param ServerRequestInterface $request
569     *
570     * @return ResponseInterface
571     */
572    public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface
573    {
574        $tree   = Validator::attributes($request)->tree();
575        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
576        $option = Validator::parsedBody($request)->string('option');
577
578        $individual = Registry::individualFactory()->make($xref, $tree);
579        $individual = Auth::checkIndividualAccess($individual);
580
581        switch ($option) {
582            case self::ADD_RECORD_ONLY:
583                $this->addIndividualToCart($individual);
584                break;
585
586            case self::ADD_PARENT_FAMILIES:
587                foreach ($individual->childFamilies() as $family) {
588                    $this->addFamilyAndChildrenToCart($family);
589                }
590                break;
591
592            case self::ADD_SPOUSE_FAMILIES:
593                foreach ($individual->spouseFamilies() as $family) {
594                    $this->addFamilyAndChildrenToCart($family);
595                }
596                break;
597
598            case self::ADD_ANCESTORS:
599                $this->addAncestorsToCart($individual);
600                break;
601
602            case self::ADD_ANCESTOR_FAMILIES:
603                $this->addAncestorFamiliesToCart($individual);
604                break;
605
606            case self::ADD_DESCENDANTS:
607                foreach ($individual->spouseFamilies() as $family) {
608                    $this->addFamilyAndDescendantsToCart($family);
609                }
610                break;
611        }
612
613        return redirect($individual->url());
614    }
615
616    /**
617     * @param Individual $individual
618     *
619     * @return void
620     */
621    protected function addAncestorsToCart(Individual $individual): void
622    {
623        $this->addIndividualToCart($individual);
624
625        foreach ($individual->childFamilies() as $family) {
626            $this->addFamilyToCart($family);
627
628            foreach ($family->spouses() as $parent) {
629                $this->addAncestorsToCart($parent);
630            }
631        }
632    }
633
634    /**
635     * @param Individual $individual
636     *
637     * @return void
638     */
639    protected function addAncestorFamiliesToCart(Individual $individual): void
640    {
641        foreach ($individual->childFamilies() as $family) {
642            $this->addFamilyAndChildrenToCart($family);
643
644            foreach ($family->spouses() as $parent) {
645                $this->addAncestorFamiliesToCart($parent);
646            }
647        }
648    }
649
650    /**
651     * @param ServerRequestInterface $request
652     *
653     * @return ResponseInterface
654     */
655    public function getAddLocationAction(ServerRequestInterface $request): ResponseInterface
656    {
657        $tree     = Validator::attributes($request)->tree();
658        $xref     = Validator::queryParams($request)->isXref()->string('xref');
659        $location = Registry::locationFactory()->make($xref, $tree);
660        $location = Auth::checkLocationAccess($location);
661        $name     = $location->fullName();
662
663        $options = [
664            self::ADD_RECORD_ONLY => $name,
665        ];
666
667        $title = I18N::translate('Add %s to the clippings cart', $name);
668
669        return $this->viewResponse('modules/clippings/add-options', [
670            'options' => $options,
671            'record'  => $location,
672            'title'   => $title,
673            'tree'    => $tree,
674        ]);
675    }
676
677    /**
678     * @param ServerRequestInterface $request
679     *
680     * @return ResponseInterface
681     */
682    public function postAddLocationAction(ServerRequestInterface $request): ResponseInterface
683    {
684        $tree     = Validator::attributes($request)->tree();
685        $xref     = Validator::queryParams($request)->isXref()->string('xref');
686        $location = Registry::locationFactory()->make($xref, $tree);
687        $location = Auth::checkLocationAccess($location);
688
689        $this->addLocationToCart($location);
690
691        return redirect($location->url());
692    }
693
694    /**
695     * @param ServerRequestInterface $request
696     *
697     * @return ResponseInterface
698     */
699    public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface
700    {
701        $tree  = Validator::attributes($request)->tree();
702        $xref  = Validator::queryParams($request)->isXref()->string('xref');
703        $media = Registry::mediaFactory()->make($xref, $tree);
704        $media = Auth::checkMediaAccess($media);
705        $name  = $media->fullName();
706
707        $options = [
708            self::ADD_RECORD_ONLY => $name,
709        ];
710
711        $title = I18N::translate('Add %s to the clippings cart', $name);
712
713        return $this->viewResponse('modules/clippings/add-options', [
714            'options' => $options,
715            'record'  => $media,
716            'title'   => $title,
717            'tree'    => $tree,
718        ]);
719    }
720
721    /**
722     * @param ServerRequestInterface $request
723     *
724     * @return ResponseInterface
725     */
726    public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface
727    {
728        $tree  = Validator::attributes($request)->tree();
729        $xref  = Validator::queryParams($request)->isXref()->string('xref');
730        $media = Registry::mediaFactory()->make($xref, $tree);
731        $media = Auth::checkMediaAccess($media);
732
733        $this->addMediaToCart($media);
734
735        return redirect($media->url());
736    }
737
738    /**
739     * @param ServerRequestInterface $request
740     *
741     * @return ResponseInterface
742     */
743    public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface
744    {
745        $tree = Validator::attributes($request)->tree();
746        $xref = Validator::queryParams($request)->isXref()->string('xref');
747        $note = Registry::noteFactory()->make($xref, $tree);
748        $note = Auth::checkNoteAccess($note);
749        $name = $note->fullName();
750
751        $options = [
752            self::ADD_RECORD_ONLY => $name,
753        ];
754
755        $title = I18N::translate('Add %s to the clippings cart', $name);
756
757        return $this->viewResponse('modules/clippings/add-options', [
758            'options' => $options,
759            'record'  => $note,
760            'title'   => $title,
761            'tree'    => $tree,
762        ]);
763    }
764
765    /**
766     * @param ServerRequestInterface $request
767     *
768     * @return ResponseInterface
769     */
770    public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface
771    {
772        $tree = Validator::attributes($request)->tree();
773        $xref = Validator::queryParams($request)->isXref()->string('xref');
774        $note = Registry::noteFactory()->make($xref, $tree);
775        $note = Auth::checkNoteAccess($note);
776
777        $this->addNoteToCart($note);
778
779        return redirect($note->url());
780    }
781
782    /**
783     * @param ServerRequestInterface $request
784     *
785     * @return ResponseInterface
786     */
787    public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
788    {
789        $tree       = Validator::attributes($request)->tree();
790        $xref       = Validator::queryParams($request)->isXref()->string('xref');
791        $repository = Registry::repositoryFactory()->make($xref, $tree);
792        $repository = Auth::checkRepositoryAccess($repository);
793        $name       = $repository->fullName();
794
795        $options = [
796            self::ADD_RECORD_ONLY => $name,
797        ];
798
799        $title = I18N::translate('Add %s to the clippings cart', $name);
800
801        return $this->viewResponse('modules/clippings/add-options', [
802            'options' => $options,
803            'record'  => $repository,
804            'title'   => $title,
805            'tree'    => $tree,
806        ]);
807    }
808
809    /**
810     * @param ServerRequestInterface $request
811     *
812     * @return ResponseInterface
813     */
814    public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
815    {
816        $tree       = Validator::attributes($request)->tree();
817        $xref       = Validator::queryParams($request)->isXref()->string('xref');
818        $repository = Registry::repositoryFactory()->make($xref, $tree);
819        $repository = Auth::checkRepositoryAccess($repository);
820
821        $this->addRepositoryToCart($repository);
822
823        foreach ($this->linked_record_service->linkedSources($repository) as $source) {
824            $this->addSourceToCart($source);
825        }
826
827        return redirect($repository->url());
828    }
829
830    /**
831     * @param ServerRequestInterface $request
832     *
833     * @return ResponseInterface
834     */
835    public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface
836    {
837        $tree   = Validator::attributes($request)->tree();
838        $xref   = Validator::queryParams($request)->isXref()->string('xref');
839        $source = Registry::sourceFactory()->make($xref, $tree);
840        $source = Auth::checkSourceAccess($source);
841        $name   = $source->fullName();
842
843        $options = [
844            self::ADD_RECORD_ONLY        => $name,
845            self::ADD_LINKED_INDIVIDUALS => I18N::translate('%s and the individuals that reference it.', $name),
846        ];
847
848        $title = I18N::translate('Add %s to the clippings cart', $name);
849
850        return $this->viewResponse('modules/clippings/add-options', [
851            'options' => $options,
852            'record'  => $source,
853            'title'   => $title,
854            'tree'    => $tree,
855        ]);
856    }
857
858    /**
859     * @param ServerRequestInterface $request
860     *
861     * @return ResponseInterface
862     */
863    public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface
864    {
865        $tree   = Validator::attributes($request)->tree();
866        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
867        $option = Validator::parsedBody($request)->string('option');
868
869        $source = Registry::sourceFactory()->make($xref, $tree);
870        $source = Auth::checkSourceAccess($source);
871
872        $this->addSourceToCart($source);
873
874        if ($option === self::ADD_LINKED_INDIVIDUALS) {
875            foreach ($this->linked_record_service->linkedIndividuals($source) as $individual) {
876                $this->addIndividualToCart($individual);
877            }
878            foreach ($this->linked_record_service->linkedFamilies($source) as $family) {
879                $this->addFamilyToCart($family);
880            }
881        }
882
883        return redirect($source->url());
884    }
885
886    /**
887     * @param ServerRequestInterface $request
888     *
889     * @return ResponseInterface
890     */
891    public function getAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
892    {
893        $tree      = Validator::attributes($request)->tree();
894        $xref      = Validator::queryParams($request)->isXref()->string('xref');
895        $submitter = Registry::submitterFactory()->make($xref, $tree);
896        $submitter = Auth::checkSubmitterAccess($submitter);
897        $name      = $submitter->fullName();
898
899        $options = [
900            self::ADD_RECORD_ONLY => $name,
901        ];
902
903        $title = I18N::translate('Add %s to the clippings cart', $name);
904
905        return $this->viewResponse('modules/clippings/add-options', [
906            'options' => $options,
907            'record'  => $submitter,
908            'title'   => $title,
909            'tree'    => $tree,
910        ]);
911    }
912
913    /**
914     * @param ServerRequestInterface $request
915     *
916     * @return ResponseInterface
917     */
918    public function postAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
919    {
920        $tree      = Validator::attributes($request)->tree();
921        $xref      = Validator::queryParams($request)->isXref()->string('xref');
922        $submitter = Registry::submitterFactory()->make($xref, $tree);
923        $submitter = Auth::checkSubmitterAccess($submitter);
924
925        $this->addSubmitterToCart($submitter);
926
927        return redirect($submitter->url());
928    }
929
930    /**
931     * @param Family $family
932     */
933    protected function addFamilyToCart(Family $family): void
934    {
935        $cart = Session::get('cart');
936        $cart = is_array($cart) ? $cart : [];
937
938        $tree = $family->tree()->name();
939        $xref = $family->xref();
940
941        if (($cart[$tree][$xref] ?? false) === false) {
942            $cart[$tree][$xref] = true;
943
944            Session::put('cart', $cart);
945
946            foreach ($family->spouses() as $spouse) {
947                $this->addIndividualToCart($spouse);
948            }
949
950            $this->addLocationLinksToCart($family);
951            $this->addMediaLinksToCart($family);
952            $this->addNoteLinksToCart($family);
953            $this->addSourceLinksToCart($family);
954            $this->addSubmitterLinksToCart($family);
955        }
956    }
957
958    /**
959     * @param Individual $individual
960     */
961    protected function addIndividualToCart(Individual $individual): void
962    {
963        $cart = Session::get('cart');
964        $cart = is_array($cart) ? $cart : [];
965
966        $tree = $individual->tree()->name();
967        $xref = $individual->xref();
968
969        if (($cart[$tree][$xref] ?? false) === false) {
970            $cart[$tree][$xref] = true;
971
972            Session::put('cart', $cart);
973
974            $this->addLocationLinksToCart($individual);
975            $this->addMediaLinksToCart($individual);
976            $this->addNoteLinksToCart($individual);
977            $this->addSourceLinksToCart($individual);
978        }
979    }
980
981    /**
982     * @param Location $location
983     */
984    protected function addLocationToCart(Location $location): void
985    {
986        $cart = Session::get('cart');
987        $cart = is_array($cart) ? $cart : [];
988
989        $tree = $location->tree()->name();
990        $xref = $location->xref();
991
992        if (($cart[$tree][$xref] ?? false) === false) {
993            $cart[$tree][$xref] = true;
994
995            Session::put('cart', $cart);
996
997            $this->addLocationLinksToCart($location);
998            $this->addMediaLinksToCart($location);
999            $this->addNoteLinksToCart($location);
1000            $this->addSourceLinksToCart($location);
1001        }
1002    }
1003
1004    /**
1005     * @param GedcomRecord $record
1006     */
1007    protected function addLocationLinksToCart(GedcomRecord $record): void
1008    {
1009        preg_match_all('/\n\d _LOC @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1010
1011        foreach ($matches[1] as $xref) {
1012            $location = Registry::locationFactory()->make($xref, $record->tree());
1013
1014            if ($location instanceof Location && $location->canShow()) {
1015                $this->addLocationToCart($location);
1016            }
1017        }
1018    }
1019
1020    /**
1021     * @param Media $media
1022     */
1023    protected function addMediaToCart(Media $media): void
1024    {
1025        $cart = Session::get('cart');
1026        $cart = is_array($cart) ? $cart : [];
1027
1028        $tree = $media->tree()->name();
1029        $xref = $media->xref();
1030
1031        if (($cart[$tree][$xref] ?? false) === false) {
1032            $cart[$tree][$xref] = true;
1033
1034            Session::put('cart', $cart);
1035
1036            $this->addNoteLinksToCart($media);
1037        }
1038    }
1039
1040    /**
1041     * @param GedcomRecord $record
1042     */
1043    protected function addMediaLinksToCart(GedcomRecord $record): void
1044    {
1045        preg_match_all('/\n\d OBJE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1046
1047        foreach ($matches[1] as $xref) {
1048            $media = Registry::mediaFactory()->make($xref, $record->tree());
1049
1050            if ($media instanceof Media && $media->canShow()) {
1051                $this->addMediaToCart($media);
1052            }
1053        }
1054    }
1055
1056    /**
1057     * @param Note $note
1058     */
1059    protected function addNoteToCart(Note $note): void
1060    {
1061        $cart = Session::get('cart');
1062        $cart = is_array($cart) ? $cart : [];
1063
1064        $tree = $note->tree()->name();
1065        $xref = $note->xref();
1066
1067        if (($cart[$tree][$xref] ?? false) === false) {
1068            $cart[$tree][$xref] = true;
1069
1070            Session::put('cart', $cart);
1071        }
1072    }
1073
1074    /**
1075     * @param GedcomRecord $record
1076     */
1077    protected function addNoteLinksToCart(GedcomRecord $record): void
1078    {
1079        preg_match_all('/\n\d NOTE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1080
1081        foreach ($matches[1] as $xref) {
1082            $note = Registry::noteFactory()->make($xref, $record->tree());
1083
1084            if ($note instanceof Note && $note->canShow()) {
1085                $this->addNoteToCart($note);
1086            }
1087        }
1088    }
1089
1090    /**
1091     * @param Source $source
1092     */
1093    protected function addSourceToCart(Source $source): void
1094    {
1095        $cart = Session::get('cart');
1096        $cart = is_array($cart) ? $cart : [];
1097
1098        $tree = $source->tree()->name();
1099        $xref = $source->xref();
1100
1101        if (($cart[$tree][$xref] ?? false) === false) {
1102            $cart[$tree][$xref] = true;
1103
1104            Session::put('cart', $cart);
1105
1106            $this->addNoteLinksToCart($source);
1107            $this->addRepositoryLinksToCart($source);
1108        }
1109    }
1110
1111    /**
1112     * @param GedcomRecord $record
1113     */
1114    protected function addSourceLinksToCart(GedcomRecord $record): void
1115    {
1116        preg_match_all('/\n\d SOUR @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1117
1118        foreach ($matches[1] as $xref) {
1119            $source = Registry::sourceFactory()->make($xref, $record->tree());
1120
1121            if ($source instanceof Source && $source->canShow()) {
1122                $this->addSourceToCart($source);
1123            }
1124        }
1125    }
1126
1127    /**
1128     * @param Repository $repository
1129     */
1130    protected function addRepositoryToCart(Repository $repository): void
1131    {
1132        $cart = Session::get('cart');
1133        $cart = is_array($cart) ? $cart : [];
1134
1135        $tree = $repository->tree()->name();
1136        $xref = $repository->xref();
1137
1138        if (($cart[$tree][$xref] ?? false) === false) {
1139            $cart[$tree][$xref] = true;
1140
1141            Session::put('cart', $cart);
1142
1143            $this->addNoteLinksToCart($repository);
1144        }
1145    }
1146
1147    /**
1148     * @param GedcomRecord $record
1149     */
1150    protected function addRepositoryLinksToCart(GedcomRecord $record): void
1151    {
1152        preg_match_all('/\n\d REPO @(' . Gedcom::REGEX_XREF . '@)/', $record->gedcom(), $matches);
1153
1154        foreach ($matches[1] as $xref) {
1155            $repository = Registry::repositoryFactory()->make($xref, $record->tree());
1156
1157            if ($repository instanceof Repository && $repository->canShow()) {
1158                $this->addRepositoryToCart($repository);
1159            }
1160        }
1161    }
1162
1163    /**
1164     * @param Submitter $submitter
1165     */
1166    protected function addSubmitterToCart(Submitter $submitter): void
1167    {
1168        $cart = Session::get('cart');
1169        $cart = is_array($cart) ? $cart : [];
1170        $tree = $submitter->tree()->name();
1171        $xref = $submitter->xref();
1172
1173        if (($cart[$tree][$xref] ?? false) === false) {
1174            $cart[$tree][$xref] = true;
1175
1176            Session::put('cart', $cart);
1177
1178            $this->addNoteLinksToCart($submitter);
1179        }
1180    }
1181
1182    /**
1183     * @param GedcomRecord $record
1184     */
1185    protected function addSubmitterLinksToCart(GedcomRecord $record): void
1186    {
1187        preg_match_all('/\n\d SUBM @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1188
1189        foreach ($matches[1] as $xref) {
1190            $submitter = Registry::submitterFactory()->make($xref, $record->tree());
1191
1192            if ($submitter instanceof Submitter && $submitter->canShow()) {
1193                $this->addSubmitterToCart($submitter);
1194            }
1195        }
1196    }
1197}
1198