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