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