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