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