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