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