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