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