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