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