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