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