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