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