xref: /webtrees/app/Module/ClippingsCartModule.php (revision f25fc0f929f69ab8124cf0cecde45e457db7574a)
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 fn(string $xref): ?GedcomRecord => Registry::gedcomRecordFactory()->make($xref, $tree), $xrefs);
417
418        // Some records may have been deleted after they were added to the cart.
419        $records = array_filter($records);
420
421        // Group and sort.
422        uasort($records, static fn(GedcomRecord $x, GedcomRecord $y): int => $x->tag() <=> $y->tag() ?: GedcomRecord::nameComparator()($x, $y));
423
424        return $records;
425    }
426
427    /**
428     * @param ServerRequestInterface $request
429     *
430     * @return ResponseInterface
431     */
432    public function getAddFamilyAction(ServerRequestInterface $request): ResponseInterface
433    {
434        $tree   = Validator::attributes($request)->tree();
435        $xref   = Validator::queryParams($request)->isXref()->string('xref');
436        $family = Registry::familyFactory()->make($xref, $tree);
437        $family = Auth::checkFamilyAccess($family);
438        $name   = $family->fullName();
439
440        $options = [
441            self::ADD_RECORD_ONLY => $name,
442            /* I18N: %s is a family (husband + wife) */
443            self::ADD_CHILDREN    => I18N::translate('%s and their children', $name),
444            /* I18N: %s is a family (husband + wife) */
445            self::ADD_DESCENDANTS => I18N::translate('%s and their descendants', $name),
446        ];
447
448        $title = I18N::translate('Add %s to the clippings cart', $name);
449
450        return $this->viewResponse('modules/clippings/add-options', [
451            'options' => $options,
452            'record'  => $family,
453            'title'   => $title,
454            'tree'    => $tree,
455        ]);
456    }
457
458    /**
459     * @param ServerRequestInterface $request
460     *
461     * @return ResponseInterface
462     */
463    public function postAddFamilyAction(ServerRequestInterface $request): ResponseInterface
464    {
465        $tree   = Validator::attributes($request)->tree();
466        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
467        $option = Validator::parsedBody($request)->string('option');
468
469        $family = Registry::familyFactory()->make($xref, $tree);
470        $family = Auth::checkFamilyAccess($family);
471
472        switch ($option) {
473            case self::ADD_RECORD_ONLY:
474                $this->addFamilyToCart($family);
475                break;
476
477            case self::ADD_CHILDREN:
478                $this->addFamilyAndChildrenToCart($family);
479                break;
480
481            case self::ADD_DESCENDANTS:
482                $this->addFamilyAndDescendantsToCart($family);
483                break;
484        }
485
486        return redirect($family->url());
487    }
488
489
490    /**
491     * @param Family $family
492     *
493     * @return void
494     */
495    protected function addFamilyAndChildrenToCart(Family $family): void
496    {
497        $this->addFamilyToCart($family);
498
499        foreach ($family->children() as $child) {
500            $this->addIndividualToCart($child);
501        }
502    }
503
504    /**
505     * @param Family $family
506     *
507     * @return void
508     */
509    protected function addFamilyAndDescendantsToCart(Family $family): void
510    {
511        $this->addFamilyAndChildrenToCart($family);
512
513        foreach ($family->children() as $child) {
514            foreach ($child->spouseFamilies() as $child_family) {
515                $this->addFamilyAndDescendantsToCart($child_family);
516            }
517        }
518    }
519
520    /**
521     * @param ServerRequestInterface $request
522     *
523     * @return ResponseInterface
524     */
525    public function getAddIndividualAction(ServerRequestInterface $request): ResponseInterface
526    {
527        $tree       = Validator::attributes($request)->tree();
528        $xref       = Validator::queryParams($request)->isXref()->string('xref');
529        $individual = Registry::individualFactory()->make($xref, $tree);
530        $individual = Auth::checkIndividualAccess($individual);
531        $name       = $individual->fullName();
532
533        if ($individual->sex() === 'F') {
534            $options = [
535                self::ADD_RECORD_ONLY       => $name,
536                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, her parents and siblings', $name),
537                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, her spouses and children', $name),
538                self::ADD_ANCESTORS         => I18N::translate('%s and her ancestors', $name),
539                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, her ancestors and their families', $name),
540                self::ADD_DESCENDANTS       => I18N::translate('%s, her spouses and descendants', $name),
541            ];
542        } else {
543            $options = [
544                self::ADD_RECORD_ONLY       => $name,
545                self::ADD_PARENT_FAMILIES   => I18N::translate('%s, his parents and siblings', $name),
546                self::ADD_SPOUSE_FAMILIES   => I18N::translate('%s, his spouses and children', $name),
547                self::ADD_ANCESTORS         => I18N::translate('%s and his ancestors', $name),
548                self::ADD_ANCESTOR_FAMILIES => I18N::translate('%s, his ancestors and their families', $name),
549                self::ADD_DESCENDANTS       => I18N::translate('%s, his spouses and descendants', $name),
550            ];
551        }
552
553        $title = I18N::translate('Add %s to the clippings cart', $name);
554
555        return $this->viewResponse('modules/clippings/add-options', [
556            'options' => $options,
557            'record'  => $individual,
558            'title'   => $title,
559            'tree'    => $tree,
560        ]);
561    }
562
563    /**
564     * @param ServerRequestInterface $request
565     *
566     * @return ResponseInterface
567     */
568    public function postAddIndividualAction(ServerRequestInterface $request): ResponseInterface
569    {
570        $tree   = Validator::attributes($request)->tree();
571        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
572        $option = Validator::parsedBody($request)->string('option');
573
574        $individual = Registry::individualFactory()->make($xref, $tree);
575        $individual = Auth::checkIndividualAccess($individual);
576
577        switch ($option) {
578            case self::ADD_RECORD_ONLY:
579                $this->addIndividualToCart($individual);
580                break;
581
582            case self::ADD_PARENT_FAMILIES:
583                foreach ($individual->childFamilies() as $family) {
584                    $this->addFamilyAndChildrenToCart($family);
585                }
586                break;
587
588            case self::ADD_SPOUSE_FAMILIES:
589                foreach ($individual->spouseFamilies() as $family) {
590                    $this->addFamilyAndChildrenToCart($family);
591                }
592                break;
593
594            case self::ADD_ANCESTORS:
595                $this->addAncestorsToCart($individual);
596                break;
597
598            case self::ADD_ANCESTOR_FAMILIES:
599                $this->addAncestorFamiliesToCart($individual);
600                break;
601
602            case self::ADD_DESCENDANTS:
603                foreach ($individual->spouseFamilies() as $family) {
604                    $this->addFamilyAndDescendantsToCart($family);
605                }
606                break;
607        }
608
609        return redirect($individual->url());
610    }
611
612    /**
613     * @param Individual $individual
614     *
615     * @return void
616     */
617    protected function addAncestorsToCart(Individual $individual): void
618    {
619        $this->addIndividualToCart($individual);
620
621        foreach ($individual->childFamilies() as $family) {
622            $this->addFamilyToCart($family);
623
624            foreach ($family->spouses() as $parent) {
625                $this->addAncestorsToCart($parent);
626            }
627        }
628    }
629
630    /**
631     * @param Individual $individual
632     *
633     * @return void
634     */
635    protected function addAncestorFamiliesToCart(Individual $individual): void
636    {
637        foreach ($individual->childFamilies() as $family) {
638            $this->addFamilyAndChildrenToCart($family);
639
640            foreach ($family->spouses() as $parent) {
641                $this->addAncestorFamiliesToCart($parent);
642            }
643        }
644    }
645
646    /**
647     * @param ServerRequestInterface $request
648     *
649     * @return ResponseInterface
650     */
651    public function getAddLocationAction(ServerRequestInterface $request): ResponseInterface
652    {
653        $tree     = Validator::attributes($request)->tree();
654        $xref     = Validator::queryParams($request)->isXref()->string('xref');
655        $location = Registry::locationFactory()->make($xref, $tree);
656        $location = Auth::checkLocationAccess($location);
657        $name     = $location->fullName();
658
659        $options = [
660            self::ADD_RECORD_ONLY => $name,
661        ];
662
663        $title = I18N::translate('Add %s to the clippings cart', $name);
664
665        return $this->viewResponse('modules/clippings/add-options', [
666            'options' => $options,
667            'record'  => $location,
668            'title'   => $title,
669            'tree'    => $tree,
670        ]);
671    }
672
673    /**
674     * @param ServerRequestInterface $request
675     *
676     * @return ResponseInterface
677     */
678    public function postAddLocationAction(ServerRequestInterface $request): ResponseInterface
679    {
680        $tree     = Validator::attributes($request)->tree();
681        $xref     = Validator::queryParams($request)->isXref()->string('xref');
682        $location = Registry::locationFactory()->make($xref, $tree);
683        $location = Auth::checkLocationAccess($location);
684
685        $this->addLocationToCart($location);
686
687        return redirect($location->url());
688    }
689
690    /**
691     * @param ServerRequestInterface $request
692     *
693     * @return ResponseInterface
694     */
695    public function getAddMediaAction(ServerRequestInterface $request): ResponseInterface
696    {
697        $tree  = Validator::attributes($request)->tree();
698        $xref  = Validator::queryParams($request)->isXref()->string('xref');
699        $media = Registry::mediaFactory()->make($xref, $tree);
700        $media = Auth::checkMediaAccess($media);
701        $name  = $media->fullName();
702
703        $options = [
704            self::ADD_RECORD_ONLY => $name,
705        ];
706
707        $title = I18N::translate('Add %s to the clippings cart', $name);
708
709        return $this->viewResponse('modules/clippings/add-options', [
710            'options' => $options,
711            'record'  => $media,
712            'title'   => $title,
713            'tree'    => $tree,
714        ]);
715    }
716
717    /**
718     * @param ServerRequestInterface $request
719     *
720     * @return ResponseInterface
721     */
722    public function postAddMediaAction(ServerRequestInterface $request): ResponseInterface
723    {
724        $tree  = Validator::attributes($request)->tree();
725        $xref  = Validator::queryParams($request)->isXref()->string('xref');
726        $media = Registry::mediaFactory()->make($xref, $tree);
727        $media = Auth::checkMediaAccess($media);
728
729        $this->addMediaToCart($media);
730
731        return redirect($media->url());
732    }
733
734    /**
735     * @param ServerRequestInterface $request
736     *
737     * @return ResponseInterface
738     */
739    public function getAddNoteAction(ServerRequestInterface $request): ResponseInterface
740    {
741        $tree = Validator::attributes($request)->tree();
742        $xref = Validator::queryParams($request)->isXref()->string('xref');
743        $note = Registry::noteFactory()->make($xref, $tree);
744        $note = Auth::checkNoteAccess($note);
745        $name = $note->fullName();
746
747        $options = [
748            self::ADD_RECORD_ONLY => $name,
749        ];
750
751        $title = I18N::translate('Add %s to the clippings cart', $name);
752
753        return $this->viewResponse('modules/clippings/add-options', [
754            'options' => $options,
755            'record'  => $note,
756            'title'   => $title,
757            'tree'    => $tree,
758        ]);
759    }
760
761    /**
762     * @param ServerRequestInterface $request
763     *
764     * @return ResponseInterface
765     */
766    public function postAddNoteAction(ServerRequestInterface $request): ResponseInterface
767    {
768        $tree = Validator::attributes($request)->tree();
769        $xref = Validator::queryParams($request)->isXref()->string('xref');
770        $note = Registry::noteFactory()->make($xref, $tree);
771        $note = Auth::checkNoteAccess($note);
772
773        $this->addNoteToCart($note);
774
775        return redirect($note->url());
776    }
777
778    /**
779     * @param ServerRequestInterface $request
780     *
781     * @return ResponseInterface
782     */
783    public function getAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
784    {
785        $tree       = Validator::attributes($request)->tree();
786        $xref       = Validator::queryParams($request)->isXref()->string('xref');
787        $repository = Registry::repositoryFactory()->make($xref, $tree);
788        $repository = Auth::checkRepositoryAccess($repository);
789        $name       = $repository->fullName();
790
791        $options = [
792            self::ADD_RECORD_ONLY => $name,
793        ];
794
795        $title = I18N::translate('Add %s to the clippings cart', $name);
796
797        return $this->viewResponse('modules/clippings/add-options', [
798            'options' => $options,
799            'record'  => $repository,
800            'title'   => $title,
801            'tree'    => $tree,
802        ]);
803    }
804
805    /**
806     * @param ServerRequestInterface $request
807     *
808     * @return ResponseInterface
809     */
810    public function postAddRepositoryAction(ServerRequestInterface $request): ResponseInterface
811    {
812        $tree       = Validator::attributes($request)->tree();
813        $xref       = Validator::queryParams($request)->isXref()->string('xref');
814        $repository = Registry::repositoryFactory()->make($xref, $tree);
815        $repository = Auth::checkRepositoryAccess($repository);
816
817        $this->addRepositoryToCart($repository);
818
819        foreach ($this->linked_record_service->linkedSources($repository) as $source) {
820            $this->addSourceToCart($source);
821        }
822
823        return redirect($repository->url());
824    }
825
826    /**
827     * @param ServerRequestInterface $request
828     *
829     * @return ResponseInterface
830     */
831    public function getAddSourceAction(ServerRequestInterface $request): ResponseInterface
832    {
833        $tree   = Validator::attributes($request)->tree();
834        $xref   = Validator::queryParams($request)->isXref()->string('xref');
835        $source = Registry::sourceFactory()->make($xref, $tree);
836        $source = Auth::checkSourceAccess($source);
837        $name   = $source->fullName();
838
839        $options = [
840            self::ADD_RECORD_ONLY        => $name,
841            self::ADD_LINKED_INDIVIDUALS => I18N::translate('%s and the individuals that reference it.', $name),
842        ];
843
844        $title = I18N::translate('Add %s to the clippings cart', $name);
845
846        return $this->viewResponse('modules/clippings/add-options', [
847            'options' => $options,
848            'record'  => $source,
849            'title'   => $title,
850            'tree'    => $tree,
851        ]);
852    }
853
854    /**
855     * @param ServerRequestInterface $request
856     *
857     * @return ResponseInterface
858     */
859    public function postAddSourceAction(ServerRequestInterface $request): ResponseInterface
860    {
861        $tree   = Validator::attributes($request)->tree();
862        $xref   = Validator::parsedBody($request)->isXref()->string('xref');
863        $option = Validator::parsedBody($request)->string('option');
864
865        $source = Registry::sourceFactory()->make($xref, $tree);
866        $source = Auth::checkSourceAccess($source);
867
868        $this->addSourceToCart($source);
869
870        if ($option === self::ADD_LINKED_INDIVIDUALS) {
871            foreach ($this->linked_record_service->linkedIndividuals($source) as $individual) {
872                $this->addIndividualToCart($individual);
873            }
874            foreach ($this->linked_record_service->linkedFamilies($source) as $family) {
875                $this->addFamilyToCart($family);
876            }
877        }
878
879        return redirect($source->url());
880    }
881
882    /**
883     * @param ServerRequestInterface $request
884     *
885     * @return ResponseInterface
886     */
887    public function getAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
888    {
889        $tree      = Validator::attributes($request)->tree();
890        $xref      = Validator::queryParams($request)->isXref()->string('xref');
891        $submitter = Registry::submitterFactory()->make($xref, $tree);
892        $submitter = Auth::checkSubmitterAccess($submitter);
893        $name      = $submitter->fullName();
894
895        $options = [
896            self::ADD_RECORD_ONLY => $name,
897        ];
898
899        $title = I18N::translate('Add %s to the clippings cart', $name);
900
901        return $this->viewResponse('modules/clippings/add-options', [
902            'options' => $options,
903            'record'  => $submitter,
904            'title'   => $title,
905            'tree'    => $tree,
906        ]);
907    }
908
909    /**
910     * @param ServerRequestInterface $request
911     *
912     * @return ResponseInterface
913     */
914    public function postAddSubmitterAction(ServerRequestInterface $request): ResponseInterface
915    {
916        $tree      = Validator::attributes($request)->tree();
917        $xref      = Validator::queryParams($request)->isXref()->string('xref');
918        $submitter = Registry::submitterFactory()->make($xref, $tree);
919        $submitter = Auth::checkSubmitterAccess($submitter);
920
921        $this->addSubmitterToCart($submitter);
922
923        return redirect($submitter->url());
924    }
925
926    /**
927     * @param Family $family
928     */
929    protected function addFamilyToCart(Family $family): void
930    {
931        $cart = Session::get('cart');
932        $cart = is_array($cart) ? $cart : [];
933
934        $tree = $family->tree()->name();
935        $xref = $family->xref();
936
937        if (($cart[$tree][$xref] ?? false) === false) {
938            $cart[$tree][$xref] = true;
939
940            Session::put('cart', $cart);
941
942            foreach ($family->spouses() as $spouse) {
943                $this->addIndividualToCart($spouse);
944            }
945
946            $this->addLocationLinksToCart($family);
947            $this->addMediaLinksToCart($family);
948            $this->addNoteLinksToCart($family);
949            $this->addSourceLinksToCart($family);
950            $this->addSubmitterLinksToCart($family);
951        }
952    }
953
954    /**
955     * @param Individual $individual
956     */
957    protected function addIndividualToCart(Individual $individual): void
958    {
959        $cart = Session::get('cart');
960        $cart = is_array($cart) ? $cart : [];
961
962        $tree = $individual->tree()->name();
963        $xref = $individual->xref();
964
965        if (($cart[$tree][$xref] ?? false) === false) {
966            $cart[$tree][$xref] = true;
967
968            Session::put('cart', $cart);
969
970            $this->addLocationLinksToCart($individual);
971            $this->addMediaLinksToCart($individual);
972            $this->addNoteLinksToCart($individual);
973            $this->addSourceLinksToCart($individual);
974        }
975    }
976
977    /**
978     * @param Location $location
979     */
980    protected function addLocationToCart(Location $location): void
981    {
982        $cart = Session::get('cart');
983        $cart = is_array($cart) ? $cart : [];
984
985        $tree = $location->tree()->name();
986        $xref = $location->xref();
987
988        if (($cart[$tree][$xref] ?? false) === false) {
989            $cart[$tree][$xref] = true;
990
991            Session::put('cart', $cart);
992
993            $this->addLocationLinksToCart($location);
994            $this->addMediaLinksToCart($location);
995            $this->addNoteLinksToCart($location);
996            $this->addSourceLinksToCart($location);
997        }
998    }
999
1000    /**
1001     * @param GedcomRecord $record
1002     */
1003    protected function addLocationLinksToCart(GedcomRecord $record): void
1004    {
1005        preg_match_all('/\n\d _LOC @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1006
1007        foreach ($matches[1] as $xref) {
1008            $location = Registry::locationFactory()->make($xref, $record->tree());
1009
1010            if ($location instanceof Location && $location->canShow()) {
1011                $this->addLocationToCart($location);
1012            }
1013        }
1014    }
1015
1016    /**
1017     * @param Media $media
1018     */
1019    protected function addMediaToCart(Media $media): void
1020    {
1021        $cart = Session::get('cart');
1022        $cart = is_array($cart) ? $cart : [];
1023
1024        $tree = $media->tree()->name();
1025        $xref = $media->xref();
1026
1027        if (($cart[$tree][$xref] ?? false) === false) {
1028            $cart[$tree][$xref] = true;
1029
1030            Session::put('cart', $cart);
1031
1032            $this->addNoteLinksToCart($media);
1033        }
1034    }
1035
1036    /**
1037     * @param GedcomRecord $record
1038     */
1039    protected function addMediaLinksToCart(GedcomRecord $record): void
1040    {
1041        preg_match_all('/\n\d OBJE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1042
1043        foreach ($matches[1] as $xref) {
1044            $media = Registry::mediaFactory()->make($xref, $record->tree());
1045
1046            if ($media instanceof Media && $media->canShow()) {
1047                $this->addMediaToCart($media);
1048            }
1049        }
1050    }
1051
1052    /**
1053     * @param Note $note
1054     */
1055    protected function addNoteToCart(Note $note): void
1056    {
1057        $cart = Session::get('cart');
1058        $cart = is_array($cart) ? $cart : [];
1059
1060        $tree = $note->tree()->name();
1061        $xref = $note->xref();
1062
1063        if (($cart[$tree][$xref] ?? false) === false) {
1064            $cart[$tree][$xref] = true;
1065
1066            Session::put('cart', $cart);
1067        }
1068    }
1069
1070    /**
1071     * @param GedcomRecord $record
1072     */
1073    protected function addNoteLinksToCart(GedcomRecord $record): void
1074    {
1075        preg_match_all('/\n\d NOTE @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1076
1077        foreach ($matches[1] as $xref) {
1078            $note = Registry::noteFactory()->make($xref, $record->tree());
1079
1080            if ($note instanceof Note && $note->canShow()) {
1081                $this->addNoteToCart($note);
1082            }
1083        }
1084    }
1085
1086    /**
1087     * @param Source $source
1088     */
1089    protected function addSourceToCart(Source $source): void
1090    {
1091        $cart = Session::get('cart');
1092        $cart = is_array($cart) ? $cart : [];
1093
1094        $tree = $source->tree()->name();
1095        $xref = $source->xref();
1096
1097        if (($cart[$tree][$xref] ?? false) === false) {
1098            $cart[$tree][$xref] = true;
1099
1100            Session::put('cart', $cart);
1101
1102            $this->addNoteLinksToCart($source);
1103            $this->addRepositoryLinksToCart($source);
1104        }
1105    }
1106
1107    /**
1108     * @param GedcomRecord $record
1109     */
1110    protected function addSourceLinksToCart(GedcomRecord $record): void
1111    {
1112        preg_match_all('/\n\d SOUR @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1113
1114        foreach ($matches[1] as $xref) {
1115            $source = Registry::sourceFactory()->make($xref, $record->tree());
1116
1117            if ($source instanceof Source && $source->canShow()) {
1118                $this->addSourceToCart($source);
1119            }
1120        }
1121    }
1122
1123    /**
1124     * @param Repository $repository
1125     */
1126    protected function addRepositoryToCart(Repository $repository): void
1127    {
1128        $cart = Session::get('cart');
1129        $cart = is_array($cart) ? $cart : [];
1130
1131        $tree = $repository->tree()->name();
1132        $xref = $repository->xref();
1133
1134        if (($cart[$tree][$xref] ?? false) === false) {
1135            $cart[$tree][$xref] = true;
1136
1137            Session::put('cart', $cart);
1138
1139            $this->addNoteLinksToCart($repository);
1140        }
1141    }
1142
1143    /**
1144     * @param GedcomRecord $record
1145     */
1146    protected function addRepositoryLinksToCart(GedcomRecord $record): void
1147    {
1148        preg_match_all('/\n\d REPO @(' . Gedcom::REGEX_XREF . '@)/', $record->gedcom(), $matches);
1149
1150        foreach ($matches[1] as $xref) {
1151            $repository = Registry::repositoryFactory()->make($xref, $record->tree());
1152
1153            if ($repository instanceof Repository && $repository->canShow()) {
1154                $this->addRepositoryToCart($repository);
1155            }
1156        }
1157    }
1158
1159    /**
1160     * @param Submitter $submitter
1161     */
1162    protected function addSubmitterToCart(Submitter $submitter): void
1163    {
1164        $cart = Session::get('cart');
1165        $cart = is_array($cart) ? $cart : [];
1166        $tree = $submitter->tree()->name();
1167        $xref = $submitter->xref();
1168
1169        if (($cart[$tree][$xref] ?? false) === false) {
1170            $cart[$tree][$xref] = true;
1171
1172            Session::put('cart', $cart);
1173
1174            $this->addNoteLinksToCart($submitter);
1175        }
1176    }
1177
1178    /**
1179     * @param GedcomRecord $record
1180     */
1181    protected function addSubmitterLinksToCart(GedcomRecord $record): void
1182    {
1183        preg_match_all('/\n\d SUBM @(' . Gedcom::REGEX_XREF . ')@/', $record->gedcom(), $matches);
1184
1185        foreach ($matches[1] as $xref) {
1186            $submitter = Registry::submitterFactory()->make($xref, $record->tree());
1187
1188            if ($submitter instanceof Submitter && $submitter->canShow()) {
1189                $this->addSubmitterToCart($submitter);
1190            }
1191        }
1192    }
1193}
1194